|
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 | privatestaticbool$stty =true;
|
38 | 60 | privatestaticbool$stdinIsInteractive;
|
39 | 61 |
|
@@ -122,7 +144,7 @@ private function doAsk($inputStream, OutputInterface $output, Question $question
|
122 | 144 | stream_set_blocking($inputStream,true);
|
123 | 145 | }
|
124 | 146 |
|
125 |
| -$ret =$this->readInput($inputStream,$question); |
| 147 | +$ret =$this->readInput($inputStream,$question,$output); |
126 | 148 |
|
127 | 149 | if (!$isBlocked) {
|
128 | 150 | stream_set_blocking($inputStream,false);
|
@@ -499,13 +521,12 @@ private function isInteractiveInput($inputStream): bool
|
499 | 521 | * @param resource $inputStream The handler resource
|
500 | 522 | * @param Question $question The question being asked
|
501 | 523 | */
|
502 |
| -privatefunctionreadInput($inputStream,Question$question):string|false |
| 524 | +privatefunctionreadInput($inputStream,Question$question,OutputInterface$output):string|false |
503 | 525 | {
|
504 | 526 | if (!$question->isMultiline()) {
|
505 | 527 | $cp =$this->setIOCodepage();
|
506 |
| -$ret =fgets($inputStream,4096); |
507 | 528 |
|
508 |
| -return$this->resetIOCodepage($cp,$ret); |
| 529 | +return$this->resetIOCodepage($cp,$this->handleCliInput($inputStream,$output)); |
509 | 530 | }
|
510 | 531 |
|
511 | 532 | $multiLineStreamReader =$this->cloneInputStream($inputStream);
|
@@ -586,4 +607,162 @@ private function cloneInputStream($inputStream)
|
586 | 607 |
|
587 | 608 | return$cloneStream;
|
588 | 609 | }
|
| 610 | + |
| 611 | +/** |
| 612 | + * @param resource $inputStream The handler resource |
| 613 | + */ |
| 614 | +privatefunctionhandleCliInput($inputStream,OutputInterface$output):string|false |
| 615 | + { |
| 616 | +if (!Terminal::hasSttyAvailable() ||'/' !== \DIRECTORY_SEPARATOR) { |
| 617 | +returnfgets($inputStream,4096); |
| 618 | + } |
| 619 | + |
| 620 | +// Memory not supported for stream_select |
| 621 | +$isStdin ='php://stdin' === (stream_get_meta_data($inputStream)['uri'] ??null); |
| 622 | +// Check for stdout and stderr because helpers are using stderr by default |
| 623 | +$isOutputSupported =$outputinstanceof StreamOutput ?\in_array(stream_get_meta_data($output->getStream())['uri'] ??null, ['php://stdout','php://stderr','php://output']) : |
| 624 | + ($outputinstanceof SymfonyStyle &&$output->getOutput()instanceof StreamOutput &&\in_array(stream_get_meta_data($output->getOutput()->getStream())['uri'] ??null, ['php://stdout','php://stderr','php://output'])); |
| 625 | +$sttyMode =shell_exec('stty -g'); |
| 626 | +// Disable icanon (so we can fread each keypress) |
| 627 | +shell_exec('stty -icanon -echo'); |
| 628 | + |
| 629 | +if ($isOutputSupported) { |
| 630 | +$originalOutput =$output; |
| 631 | +// This is needed for the input handling, when a question is in a section because then the inout is handled after the section |
| 632 | +// Verbosity level is set to normal to see the input because using quiet would not show in input |
| 633 | +$output =newConsoleOutput(); |
| 634 | + } |
| 635 | + |
| 636 | +$cursor =newCursor($output); |
| 637 | +$startXPos =$cursor->getCurrentPosition()[0]; |
| 638 | +$pressedKey =false; |
| 639 | +$ret = []; |
| 640 | +$currentInputXPos =0; |
| 641 | + |
| 642 | +while (!feof($inputStream) &&self::KEY_ENTER !==$pressedKey) { |
| 643 | +$read = [$inputStream]; |
| 644 | +$write =$except =null; |
| 645 | +while ($isStdin &&0 === @stream_select($read,$write,$except,0,100)) { |
| 646 | +// Give signal handlers a chance to run |
| 647 | +$read = [$inputStream]; |
| 648 | + } |
| 649 | +$pressedKey =fread($inputStream,1); |
| 650 | + |
| 651 | +if ((false ===$pressedKey ||0 ===\ord($pressedKey)) &&empty($ret)) { |
| 652 | +// Reset stty so it behaves normally again |
| 653 | +shell_exec('stty'.$sttyMode); |
| 654 | + |
| 655 | +returnfalse; |
| 656 | + } |
| 657 | + |
| 658 | +$unreadBytes =stream_get_meta_data($inputStream)['unread_bytes']; |
| 659 | +if ("\033" ===$pressedKey &&0 <$unreadBytes) { |
| 660 | +$pressedKey .=fread($inputStream,1); |
| 661 | +if (91 ===\ord($pressedKey[1]) &&1 <$unreadBytes) { |
| 662 | +// Ctrl keys / key combinations need at least 3 chars |
| 663 | +$pressedKey .=fread($inputStream,1); |
| 664 | +if (isset($pressedKey[2]) &&51 ===\ord($pressedKey[2]) &&2 <$unreadBytes) { |
| 665 | +// Del needs 4 chars |
| 666 | +$pressedKey .=fread($inputStream,1); |
| 667 | + } |
| 668 | +if (isset($pressedKey[2]) &&49 ===\ord($pressedKey[2]) &&2 <$unreadBytes) { |
| 669 | +// Ctrl + arrow left/right needs 6 chars |
| 670 | +$pressedKey .=fread($inputStream,3); |
| 671 | + } |
| 672 | + } |
| 673 | + }elseif ("\303" ===$pressedKey &&0 <$unreadBytes) { |
| 674 | +// Special chars need 2 chars |
| 675 | +$pressedKey .=fread($inputStream,1); |
| 676 | + } |
| 677 | + |
| 678 | +switch (true) { |
| 679 | +caseself::KEY_ARROW_LEFT ===$pressedKey &&$currentInputXPos >0: |
| 680 | +caseself::KEY_CTRL_B ===$pressedKey &&$currentInputXPos >0: |
| 681 | +$cursor->moveLeft(); |
| 682 | + --$currentInputXPos; |
| 683 | +break; |
| 684 | +caseself::KEY_ARROW_RIGHT ===$pressedKey &&$currentInputXPos <\count($ret): |
| 685 | +caseself::KEY_CTRL_F ===$pressedKey &&$currentInputXPos <\count($ret): |
| 686 | +$cursor->moveRight(); |
| 687 | + ++$currentInputXPos; |
| 688 | +break; |
| 689 | +caseself::KEY_CTRL_ARROW_LEFT ===$pressedKey &&$currentInputXPos >0: |
| 690 | +caseself::KEY_ALT_B ===$pressedKey &&$currentInputXPos >0: |
| 691 | +caseself::KEY_CTRL_SHIFT_ARROW_LEFT ===$pressedKey &&$currentInputXPos >0: |
| 692 | +do { |
| 693 | +$cursor->moveLeft(); |
| 694 | + --$currentInputXPos; |
| 695 | + }while ($currentInputXPos >0 && (1 <\strlen($ret[$currentInputXPos -1]) ||preg_match('/\w/',$ret[$currentInputXPos -1]))); |
| 696 | +break; |
| 697 | +caseself::KEY_CTRL_ARROW_RIGHT ===$pressedKey &&$currentInputXPos <\count($ret): |
| 698 | +caseself::KEY_ALT_F ===$pressedKey &&$currentInputXPos <\count($ret): |
| 699 | +caseself::KEY_CTRL_SHIFT_ARROW_RIGHT ===$pressedKey &&$currentInputXPos <\count($ret): |
| 700 | +do { |
| 701 | +$cursor->moveRight(); |
| 702 | + ++$currentInputXPos; |
| 703 | + }while ($currentInputXPos <\count($ret) && (1 <\strlen($ret[$currentInputXPos]) ||preg_match('/\w/',$ret[$currentInputXPos]))); |
| 704 | +break; |
| 705 | +caseself::KEY_CTRL_H ===$pressedKey &&$currentInputXPos >0: |
| 706 | +caseself::KEY_BACKSPACE ===$pressedKey &&$currentInputXPos >0: |
| 707 | +array_splice($ret,$currentInputXPos -1,1); |
| 708 | +$cursor->moveToColumn($startXPos); |
| 709 | +if ($isOutputSupported) { |
| 710 | +$output->write(implode('',$ret)); |
| 711 | + } |
| 712 | +$cursor->clearLineAfter() |
| 713 | + ->moveToColumn(($currentInputXPos +$startXPos) -1); |
| 714 | + --$currentInputXPos; |
| 715 | +break; |
| 716 | +caseself::KEY_DELETE ===$pressedKey &&$currentInputXPos <\count($ret): |
| 717 | +array_splice($ret,$currentInputXPos,1); |
| 718 | +$cursor->moveToColumn($startXPos); |
| 719 | +if ($isOutputSupported) { |
| 720 | +$output->write(implode('',$ret)); |
| 721 | + } |
| 722 | +$cursor->clearLineAfter() |
| 723 | + ->moveToColumn($currentInputXPos +$startXPos); |
| 724 | +break; |
| 725 | +caseself::KEY_HOME ===$pressedKey: |
| 726 | +caseself::KEY_CTRL_A ===$pressedKey: |
| 727 | +$cursor->moveToColumn($startXPos); |
| 728 | +$currentInputXPos =0; |
| 729 | +break; |
| 730 | +caseself::KEY_END ===$pressedKey: |
| 731 | +caseself::KEY_CTRL_E ===$pressedKey: |
| 732 | +$cursor->moveToColumn($startXPos +\count($ret)); |
| 733 | +$currentInputXPos =\count($ret); |
| 734 | +break; |
| 735 | +case !preg_match('@[[:cntrl:]]@',$pressedKey): |
| 736 | +if ($currentInputXPos >=0 &&$currentInputXPos <\count($ret)) { |
| 737 | +array_splice($ret,$currentInputXPos,0,$pressedKey); |
| 738 | +$cursor->moveToColumn($startXPos); |
| 739 | +if ($isOutputSupported) { |
| 740 | +$output->write(implode('',$ret)); |
| 741 | + } |
| 742 | +$cursor->clearLineAfter() |
| 743 | + ->moveToColumn($currentInputXPos +$startXPos +1); |
| 744 | + }else { |
| 745 | +$ret[] =$pressedKey; |
| 746 | +if ($isOutputSupported) { |
| 747 | +$output->write($pressedKey); |
| 748 | + } |
| 749 | + } |
| 750 | + ++$currentInputXPos; |
| 751 | +break; |
| 752 | +default: |
| 753 | +break; |
| 754 | + } |
| 755 | + } |
| 756 | + |
| 757 | +if ($isOutputSupported) { |
| 758 | +// Clear the output to write it to the original output |
| 759 | +$cursor->moveToColumn($startXPos)->clearLineAfter(); |
| 760 | +$originalOutput->writeln(implode('',$ret)); |
| 761 | + } |
| 762 | + |
| 763 | +// Reset stty so it behaves normally again |
| 764 | +shell_exec('stty'.$sttyMode); |
| 765 | + |
| 766 | +returnimplode('',$ret); |
| 767 | + } |
589 | 768 | }
|