|
18 | 18 | useSymfony\Component\Console\Formatter\OutputFormatterStyle;
|
19 | 19 | useSymfony\Component\Console\Input\InputInterface;
|
20 | 20 | useSymfony\Component\Console\Input\StreamableInputInterface;
|
| 21 | +useSymfony\Component\Console\Output\ConsoleOutput; |
21 | 22 | useSymfony\Component\Console\Output\ConsoleOutputInterface;
|
22 | 23 | useSymfony\Component\Console\Output\ConsoleSectionOutput;
|
23 | 24 | useSymfony\Component\Console\Output\OutputInterface;
|
| 25 | +useSymfony\Component\Console\Output\StreamOutput; |
24 | 26 | useSymfony\Component\Console\Question\ChoiceQuestion;
|
25 | 27 | useSymfony\Component\Console\Question\Question;
|
| 28 | +useSymfony\Component\Console\Style\SymfonyStyle; |
26 | 29 | useSymfony\Component\Console\Terminal;
|
27 | 30 |
|
28 | 31 | usefunctionSymfony\Component\String\s;
|
|
34 | 37 | */
|
35 | 38 | class QuestionHelperextends Helper
|
36 | 39 | {
|
| 40 | +privateconstKEY_ALT_B ="\033b"; |
| 41 | +privateconstKEY_ALT_F ="\033f"; |
| 42 | +privateconstKEY_ARROW_LEFT ="\033[D"; |
| 43 | +privateconstKEY_ARROW_RIGHT ="\033[C"; |
| 44 | +privateconstKEY_BACKSPACE ="\177"; |
| 45 | +privateconstKEY_CTRL_A ="\001"; |
| 46 | +privateconstKEY_CTRL_B ="\002"; |
| 47 | +privateconstKEY_CTRL_E ="\005"; |
| 48 | +privateconstKEY_CTRL_F ="\006"; |
| 49 | +privateconstKEY_CTRL_H ="\010"; |
| 50 | +privateconstKEY_CTRL_ARROW_LEFT ="\033[1;5D"; |
| 51 | +privateconstKEY_CTRL_ARROW_RIGHT ="\033[1;5C"; |
| 52 | +privateconstKEY_CTRL_SHIFT_ARROW_LEFT ="\033[1;6D"; |
| 53 | +privateconstKEY_CTRL_SHIFT_ARROW_RIGHT ="\033[1;6C"; |
| 54 | +privateconstKEY_DELETE ="\033[3~"; |
| 55 | +privateconstKEY_END ="\033[F"; |
| 56 | +privateconstKEY_ENTER ="\n"; |
| 57 | +privateconstKEY_HOME ="\033[H"; |
| 58 | + |
37 | 59 | /**
|
38 | 60 | * @var resource|null
|
39 | 61 | */
|
@@ -129,7 +151,7 @@ private function doAsk(OutputInterface $output, Question $question): mixed
|
129 | 151 | stream_set_blocking($inputStream,true);
|
130 | 152 | }
|
131 | 153 |
|
132 |
| -$ret =$this->readInput($inputStream,$question); |
| 154 | +$ret =$this->readInput($inputStream,$question,$output); |
133 | 155 |
|
134 | 156 | if (!$isBlocked) {
|
135 | 157 | stream_set_blocking($inputStream,false);
|
@@ -522,13 +544,12 @@ private function isInteractiveInput($inputStream): bool
|
522 | 544 | * @param resource $inputStream The handler resource
|
523 | 545 | * @param Question $question The question being asked
|
524 | 546 | */
|
525 |
| -privatefunctionreadInput($inputStream,Question$question):string|false |
| 547 | +privatefunctionreadInput($inputStream,Question$question,OutputInterface$output):string|false |
526 | 548 | {
|
527 | 549 | if (!$question->isMultiline()) {
|
528 | 550 | $cp =$this->setIOCodepage();
|
529 |
| -$ret =fgets($inputStream,4096); |
530 | 551 |
|
531 |
| -return$this->resetIOCodepage($cp,$ret); |
| 552 | +return$this->resetIOCodepage($cp,$this->handleCliInput($inputStream,$output)); |
532 | 553 | }
|
533 | 554 |
|
534 | 555 | $multiLineStreamReader =$this->cloneInputStream($inputStream);
|
@@ -609,4 +630,164 @@ private function cloneInputStream($inputStream)
|
609 | 630 |
|
610 | 631 | return$cloneStream;
|
611 | 632 | }
|
| 633 | + |
| 634 | +/** |
| 635 | + * @param resource $inputStream The handler resource |
| 636 | + */ |
| 637 | +privatefunctionhandleCliInput($inputStream,OutputInterface$output):string|false |
| 638 | + { |
| 639 | +if (!Terminal::hasSttyAvailable() ||'/' !== \DIRECTORY_SEPARATOR) { |
| 640 | +returnfgets($inputStream,4096); |
| 641 | + } |
| 642 | + |
| 643 | +// Memory not supported for stream_select |
| 644 | +$isStdin ='php://stdin' === (stream_get_meta_data($inputStream)['uri'] ??null); |
| 645 | +// Check for stdout and stderr because helpers are using stderr by default |
| 646 | +$isOutputSupported =$outputinstanceof StreamOutput ?\in_array(stream_get_meta_data($output->getStream())['uri'] ??null, ['php://stdout','php://stderr','php://output']) : |
| 647 | + ($outputinstanceof SymfonyStyle &&$output->getOutput()instanceof StreamOutput &&\in_array(stream_get_meta_data($output->getOutput()->getStream())['uri'] ??null, ['php://stdout','php://stderr','php://output'])); |
| 648 | +$sttyMode =shell_exec('stty -g'); |
| 649 | +// Disable icanon (so we can fread each keypress) |
| 650 | +shell_exec('stty -icanon -echo'); |
| 651 | + |
| 652 | +if ($isOutputSupported) { |
| 653 | +$originalOutput =$output; |
| 654 | +// This is needed for the input handling, when a question is in a section because then the inout is handled after the section |
| 655 | +// Verbosity level is set to normal to see the input because using quiet would not show in input |
| 656 | +$output =newConsoleOutput(); |
| 657 | + } |
| 658 | + |
| 659 | +$cursor =newCursor($output); |
| 660 | +$startXPos =$cursor->getCurrentPosition()[0]; |
| 661 | +$pressedKey =false; |
| 662 | +$ret = []; |
| 663 | +$currentInputXPos =0; |
| 664 | + |
| 665 | +while (!feof($inputStream) &&self::KEY_ENTER !==$pressedKey) { |
| 666 | +$read = [$inputStream]; |
| 667 | +$write =$except =null; |
| 668 | +while ($isStdin &&0 === @stream_select($read,$write,$except,0,100)) { |
| 669 | +// Give signal handlers a chance to run |
| 670 | +$read = [$inputStream]; |
| 671 | + } |
| 672 | +$pressedKey =fread($inputStream,1); |
| 673 | + |
| 674 | +if ((false ===$pressedKey ||0 ===\ord($pressedKey)) &&empty($ret)) { |
| 675 | +// Reset stty so it behaves normally again |
| 676 | +shell_exec('stty'.$sttyMode); |
| 677 | + |
| 678 | +returnfalse; |
| 679 | + } |
| 680 | + |
| 681 | +$unreadBytes =stream_get_meta_data($inputStream)['unread_bytes']; |
| 682 | +if ("\033" ===$pressedKey &&0 <$unreadBytes) { |
| 683 | +$pressedKey .=fread($inputStream,1); |
| 684 | +if (91 ===\ord($pressedKey[1]) &&1 <$unreadBytes) { |
| 685 | +// Ctrl keys / key combinations need at least 3 chars |
| 686 | +$pressedKey .=fread($inputStream,1); |
| 687 | +if (isset($pressedKey[2]) &&51 ===\ord($pressedKey[2]) &&2 <$unreadBytes) { |
| 688 | +// Del needs 4 chars |
| 689 | +$pressedKey .=fread($inputStream,1); |
| 690 | + } |
| 691 | +if (isset($pressedKey[2]) &&49 ===\ord($pressedKey[2]) &&2 <$unreadBytes) { |
| 692 | +// Ctrl + arrow left/right needs 6 chars |
| 693 | +$pressedKey .=fread($inputStream,3); |
| 694 | + } |
| 695 | + } |
| 696 | + }elseif ("\303" ===$pressedKey &&0 <$unreadBytes) { |
| 697 | +// Special chars need 2 chars |
| 698 | +$pressedKey .=fread($inputStream,1); |
| 699 | + } |
| 700 | + |
| 701 | +switch (true) { |
| 702 | +caseself::KEY_ARROW_LEFT ===$pressedKey &&$currentInputXPos >0: |
| 703 | +caseself::KEY_CTRL_B ===$pressedKey &&$currentInputXPos >0: |
| 704 | +$cursor->moveLeft(); |
| 705 | + --$currentInputXPos; |
| 706 | +break; |
| 707 | +caseself::KEY_ARROW_RIGHT ===$pressedKey &&$currentInputXPos <\count($ret): |
| 708 | +caseself::KEY_CTRL_F ===$pressedKey &&$currentInputXPos <\count($ret): |
| 709 | +$cursor->moveRight(); |
| 710 | + ++$currentInputXPos; |
| 711 | +break; |
| 712 | +caseself::KEY_CTRL_ARROW_LEFT ===$pressedKey &&$currentInputXPos >0: |
| 713 | +caseself::KEY_ALT_B ===$pressedKey &&$currentInputXPos >0: |
| 714 | +caseself::KEY_CTRL_SHIFT_ARROW_LEFT ===$pressedKey &&$currentInputXPos >0: |
| 715 | +// Move word left |
| 716 | +do { |
| 717 | +$cursor->moveLeft(); |
| 718 | + --$currentInputXPos; |
| 719 | + }while ($currentInputXPos >0 && (1 <\strlen($ret[$currentInputXPos -1]) ||preg_match('/\w/',$ret[$currentInputXPos -1]))); |
| 720 | +break; |
| 721 | +caseself::KEY_CTRL_ARROW_RIGHT ===$pressedKey &&$currentInputXPos <\count($ret): |
| 722 | +caseself::KEY_ALT_F ===$pressedKey &&$currentInputXPos <\count($ret): |
| 723 | +caseself::KEY_CTRL_SHIFT_ARROW_RIGHT ===$pressedKey &&$currentInputXPos <\count($ret): |
| 724 | +// Move word right |
| 725 | +do { |
| 726 | +$cursor->moveRight(); |
| 727 | + ++$currentInputXPos; |
| 728 | + }while ($currentInputXPos <\count($ret) && (1 <\strlen($ret[$currentInputXPos]) ||preg_match('/\w/',$ret[$currentInputXPos]))); |
| 729 | +break; |
| 730 | +caseself::KEY_CTRL_H ===$pressedKey &&$currentInputXPos >0: |
| 731 | +caseself::KEY_BACKSPACE ===$pressedKey &&$currentInputXPos >0: |
| 732 | +array_splice($ret,$currentInputXPos -1,1); |
| 733 | +$cursor->moveToColumn($startXPos); |
| 734 | +if ($isOutputSupported) { |
| 735 | +$output->write(implode('',$ret)); |
| 736 | + } |
| 737 | +$cursor->clearLineAfter() |
| 738 | + ->moveToColumn(($currentInputXPos +$startXPos) -1); |
| 739 | + --$currentInputXPos; |
| 740 | +break; |
| 741 | +caseself::KEY_DELETE ===$pressedKey &&$currentInputXPos <\count($ret): |
| 742 | +array_splice($ret,$currentInputXPos,1); |
| 743 | +$cursor->moveToColumn($startXPos); |
| 744 | +if ($isOutputSupported) { |
| 745 | +$output->write(implode('',$ret)); |
| 746 | + } |
| 747 | +$cursor->clearLineAfter() |
| 748 | + ->moveToColumn($currentInputXPos +$startXPos); |
| 749 | +break; |
| 750 | +caseself::KEY_HOME ===$pressedKey: |
| 751 | +caseself::KEY_CTRL_A ===$pressedKey: |
| 752 | +$cursor->moveToColumn($startXPos); |
| 753 | +$currentInputXPos =0; |
| 754 | +break; |
| 755 | +caseself::KEY_END ===$pressedKey: |
| 756 | +caseself::KEY_CTRL_E ===$pressedKey: |
| 757 | +$cursor->moveToColumn($startXPos +\count($ret)); |
| 758 | +$currentInputXPos =\count($ret); |
| 759 | +break; |
| 760 | +case !preg_match('@[[:cntrl:]]@',$pressedKey): |
| 761 | +if ($currentInputXPos >=0 &&$currentInputXPos <\count($ret)) { |
| 762 | +array_splice($ret,$currentInputXPos,0,$pressedKey); |
| 763 | +$cursor->moveToColumn($startXPos); |
| 764 | +if ($isOutputSupported) { |
| 765 | +$output->write(implode('',$ret)); |
| 766 | + } |
| 767 | +$cursor->clearLineAfter() |
| 768 | + ->moveToColumn($currentInputXPos +$startXPos +1); |
| 769 | + }else { |
| 770 | +$ret[] =$pressedKey; |
| 771 | +if ($isOutputSupported) { |
| 772 | +$output->write($pressedKey); |
| 773 | + } |
| 774 | + } |
| 775 | + ++$currentInputXPos; |
| 776 | +break; |
| 777 | +default: |
| 778 | +break; |
| 779 | + } |
| 780 | + } |
| 781 | + |
| 782 | +if ($isOutputSupported) { |
| 783 | +// Clear the output to write it to the original output |
| 784 | +$cursor->moveToColumn($startXPos)->clearLineAfter(); |
| 785 | +$originalOutput->writeln(implode('',$ret)); |
| 786 | + } |
| 787 | + |
| 788 | +// Reset stty so it behaves normally again |
| 789 | +shell_exec('stty'.$sttyMode); |
| 790 | + |
| 791 | +returnimplode('',$ret); |
| 792 | + } |
612 | 793 | }
|