|
34 | 34 | */
|
35 | 35 | class QuestionHelperextends Helper
|
36 | 36 | {
|
| 37 | +publicconstKEY_ARROW_RIGHT ="\033[C"; |
| 38 | +publicconstKEY_ARROW_LEFT ="\033[D"; |
| 39 | +publicconstKEY_ENTER ="\n"; |
| 40 | +publicconstKEY_DELETE ="\e[3~"; |
| 41 | +publicconstKEY_CTRL_A ="\001"; |
| 42 | +publicconstKEY_CTRL_B ="\002"; |
| 43 | +publicconstKEY_CTRL_E ="\005"; |
| 44 | +publicconstKEY_CTRL_F ="\006"; |
| 45 | +publicconstKEY_CTRL_H ="\010"; |
| 46 | +publicconstKEY_BACKSPACE ="\177"; |
| 47 | + |
37 | 48 | /**
|
38 | 49 | * @var resource|null
|
39 | 50 | */
|
@@ -123,7 +134,7 @@ private function doAsk(OutputInterface $output, Question $question): mixed
|
123 | 134 | }
|
124 | 135 |
|
125 | 136 | if (false ===$ret) {
|
126 |
| -$ret =$this->readInput($inputStream,$question); |
| 137 | +$ret =$this->readInput($inputStream,$question,$output); |
127 | 138 | if (false ===$ret) {
|
128 | 139 | thrownewMissingInputException('Aborted.');
|
129 | 140 | }
|
@@ -511,13 +522,12 @@ private function isInteractiveInput($inputStream): bool
|
511 | 522 | * @param resource $inputStream The handler resource
|
512 | 523 | * @param Question $question The question being asked
|
513 | 524 | */
|
514 |
| -privatefunctionreadInput($inputStream,Question$question):string|false |
| 525 | +privatefunctionreadInput($inputStream,Question$question,OutputInterface$output):string|false |
515 | 526 | {
|
516 | 527 | if (!$question->isMultiline()) {
|
517 | 528 | $cp =$this->setIOCodepage();
|
518 |
| -$ret =fgets($inputStream,4096); |
519 | 529 |
|
520 |
| -return$this->resetIOCodepage($cp,$ret); |
| 530 | +return$this->resetIOCodepage($cp,$this->handleCliInput($inputStream,$output)); |
521 | 531 | }
|
522 | 532 |
|
523 | 533 | $multiLineStreamReader =$this->cloneInputStream($inputStream);
|
@@ -598,4 +608,125 @@ private function cloneInputStream($inputStream)
|
598 | 608 |
|
599 | 609 | return$cloneStream;
|
600 | 610 | }
|
| 611 | + |
| 612 | +/** |
| 613 | + * @param resource $inputStream The handler resource |
| 614 | + */ |
| 615 | +privatefunctionhandleCliInput($inputStream,OutputInterface$output):string|false |
| 616 | + { |
| 617 | +if (!Terminal::hasSttyAvailable()) { |
| 618 | +returnfgets($inputStream,4096); |
| 619 | + } |
| 620 | + |
| 621 | +// memory not supported for stream_select |
| 622 | +$isStdin ='php://stdin' === (stream_get_meta_data($inputStream)['uri'] ??null); |
| 623 | +$sttyMode =shell_exec('stty -g'); |
| 624 | +// Disable icanon (so we can fread each keypress) and echo (we'll do echoing here instead) |
| 625 | +shell_exec('stty -icanon -echo'); |
| 626 | + |
| 627 | +$cursor =newCursor($output); |
| 628 | +$startXPos =$cursor->getCurrentPosition()[0]; |
| 629 | +$pressedKey =false; |
| 630 | +$ret = []; |
| 631 | +$currentInputXPos =0; |
| 632 | + |
| 633 | +while (!feof($inputStream) &&self::KEY_ENTER !==$pressedKey) { |
| 634 | +$read = [$inputStream]; |
| 635 | +$write =$except =null; |
| 636 | +while ($isStdin &&0 === @stream_select($read,$write,$except,0,100)) { |
| 637 | +// Give signal handlers a chance to run |
| 638 | +$read = [$inputStream]; |
| 639 | + } |
| 640 | +$pressedKey =fread($inputStream,1); |
| 641 | + |
| 642 | +if ((false ===$pressedKey ||0 ===\ord($pressedKey)) &&empty($ret)) { |
| 643 | +// Reset stty so it behaves normally again |
| 644 | +shell_exec('stty'.$sttyMode); |
| 645 | + |
| 646 | +returnfalse; |
| 647 | + } |
| 648 | + |
| 649 | +if ("\033" ===$pressedKey) { |
| 650 | +// ctrl keys need at least 3 chars |
| 651 | +$pressedKey .=fread($inputStream,2); |
| 652 | +if (isset($pressedKey[2]) &&51 ===\ord($pressedKey[2])) { |
| 653 | +// del needs 4 |
| 654 | +$pressedKey .=fread($inputStream,1); |
| 655 | + } |
| 656 | + }elseif ("\303" ===$pressedKey) { |
| 657 | +// special chars need 2 chars |
| 658 | +$pressedKey .=fread($inputStream,1); |
| 659 | + } |
| 660 | + |
| 661 | +switch (true) { |
| 662 | +caseself::KEY_ARROW_LEFT ===$pressedKey &&$currentInputXPos >0: |
| 663 | +caseself::KEY_CTRL_B ===$pressedKey &&$currentInputXPos >0: |
| 664 | +$cursor->moveLeft(); |
| 665 | + --$currentInputXPos; |
| 666 | +break; |
| 667 | +caseself::KEY_ARROW_RIGHT ===$pressedKey &&$currentInputXPos <\count($ret): |
| 668 | +caseself::KEY_CTRL_F ===$pressedKey &&$currentInputXPos <\count($ret): |
| 669 | +$cursor->moveRight(); |
| 670 | + ++$currentInputXPos; |
| 671 | +break; |
| 672 | +caseself::KEY_CTRL_H ===$pressedKey &&$currentInputXPos >0: |
| 673 | +caseself::KEY_BACKSPACE ===$pressedKey &&$currentInputXPos >0: |
| 674 | +array_splice($ret,$currentInputXPos -1,1); |
| 675 | +$cursor->moveToColumn($startXPos); |
| 676 | +if ($isStdin) { |
| 677 | +$output->write(implode('',$ret)); |
| 678 | + } |
| 679 | +$cursor->clearLineAfter() |
| 680 | + ->moveToColumn(($currentInputXPos +$startXPos) -1); |
| 681 | + --$currentInputXPos; |
| 682 | +break; |
| 683 | +caseself::KEY_DELETE ===$pressedKey &&$currentInputXPos <\count($ret): |
| 684 | +array_splice($ret,$currentInputXPos,1); |
| 685 | +$cursor->moveToColumn($startXPos); |
| 686 | +if ($isStdin) { |
| 687 | +$output->write(implode('',$ret)); |
| 688 | + } |
| 689 | +$cursor->clearLineAfter() |
| 690 | + ->moveToColumn($currentInputXPos +$startXPos); |
| 691 | +break; |
| 692 | +caseself::KEY_CTRL_A ===$pressedKey: |
| 693 | +$cursor->moveToColumn($startXPos); |
| 694 | +$currentInputXPos =0; |
| 695 | +break; |
| 696 | +caseself::KEY_CTRL_E ===$pressedKey: |
| 697 | +$cursor->moveToColumn($startXPos +\count($ret)); |
| 698 | +$currentInputXPos =\count($ret); |
| 699 | +break; |
| 700 | +case !preg_match('@[[:cntrl:]]@',$pressedKey): |
| 701 | +if ($currentInputXPos >=0 &&$currentInputXPos <\count($ret)) { |
| 702 | +array_splice($ret,$currentInputXPos,0,$pressedKey); |
| 703 | +$cursor->moveToColumn($startXPos); |
| 704 | +if ($isStdin) { |
| 705 | +$output->write(implode('',$ret)); |
| 706 | + } |
| 707 | +$cursor->clearLineAfter() |
| 708 | + ->moveToColumn($currentInputXPos +$startXPos +1); |
| 709 | + }else { |
| 710 | +$ret[] =$pressedKey; |
| 711 | +if ($isStdin) { |
| 712 | +$output->write($pressedKey); |
| 713 | + } |
| 714 | + } |
| 715 | + ++$currentInputXPos; |
| 716 | +break; |
| 717 | +caseself::KEY_ENTER ===$pressedKey: |
| 718 | +if ($isStdin) { |
| 719 | +$output->writeln(''); |
| 720 | + } |
| 721 | +break; |
| 722 | +default: |
| 723 | +break; |
| 724 | + } |
| 725 | + } |
| 726 | + |
| 727 | +// Reset stty so it behaves normally again |
| 728 | +shell_exec('stty'.$sttyMode); |
| 729 | + |
| 730 | +returnimplode('',$ret); |
| 731 | + } |
601 | 732 | }
|