Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit51b434d

Browse files
committed
[Console] Support left and right arrow for QuestionHelper
1 parent521d210 commit51b434d

File tree

2 files changed

+165
-4
lines changed

2 files changed

+165
-4
lines changed

‎src/Symfony/Component/Console/Helper/QuestionHelper.php

Lines changed: 135 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,17 @@
3434
*/
3535
class QuestionHelperextends Helper
3636
{
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+
3748
/**
3849
* @var resource|null
3950
*/
@@ -123,7 +134,7 @@ private function doAsk(OutputInterface $output, Question $question): mixed
123134
}
124135

125136
if (false ===$ret) {
126-
$ret =$this->readInput($inputStream,$question);
137+
$ret =$this->readInput($inputStream,$question,$output);
127138
if (false ===$ret) {
128139
thrownewMissingInputException('Aborted.');
129140
}
@@ -511,13 +522,12 @@ private function isInteractiveInput($inputStream): bool
511522
* @param resource $inputStream The handler resource
512523
* @param Question $question The question being asked
513524
*/
514-
privatefunctionreadInput($inputStream,Question$question):string|false
525+
privatefunctionreadInput($inputStream,Question$question,OutputInterface$output):string|false
515526
{
516527
if (!$question->isMultiline()) {
517528
$cp =$this->setIOCodepage();
518-
$ret =fgets($inputStream,4096);
519529

520-
return$this->resetIOCodepage($cp,$ret);
530+
return$this->resetIOCodepage($cp,$this->handleCliInput($inputStream,$output));
521531
}
522532

523533
$multiLineStreamReader =$this->cloneInputStream($inputStream);
@@ -598,4 +608,125 @@ private function cloneInputStream($inputStream)
598608

599609
return$cloneStream;
600610
}
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+
}
601732
}

‎src/Symfony/Component/Console/Tests/Helper/QuestionHelperTest.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,36 @@ public function testAsk()
172172
$this->assertEquals('What time is it?',stream_get_contents($output->getStream()));
173173
}
174174

175+
/**
176+
* @dataProvider getAskInputWithControlsData
177+
*/
178+
publicfunctiontestAskInputWithControls(string$input,string$expected)
179+
{
180+
if (!Terminal::hasSttyAvailable()) {
181+
$this->markTestSkipped('`stty` is required to test autocomplete functionality');
182+
}
183+
$dialog =newQuestionHelper();
184+
185+
$inputStream =$this->getInputStream($input);
186+
187+
$question =newQuestion('Question?');
188+
$this->assertEquals($expected,$dialog->ask($this->createStreamableInputInterfaceMock($inputStream),$this->createOutputInterface(),$question));
189+
}
190+
191+
publicfunctiongetAskInputWithControlsData()
192+
{
193+
return [
194+
['test1234,;.:_-+*#\\()/@!äßñ','test1234,;.:_-+*#\\()/@!äßñ'],
195+
['tet'.QuestionHelper::KEY_ARROW_LEFT.'s','test'],
196+
['tesñt'.QuestionHelper::KEY_ARROW_LEFT.QuestionHelper::KEY_BACKSPACE,'test'],
197+
['tesst'.QuestionHelper::KEY_CTRL_B.QuestionHelper::KEY_CTRL_H,'test'],
198+
['tes@t'.QuestionHelper::KEY_ARROW_LEFT.QuestionHelper::KEY_ARROW_LEFT.QuestionHelper::KEY_DELETE,'test'],
199+
['test'.QuestionHelper::KEY_ARROW_LEFT.QuestionHelper::KEY_ARROW_LEFT.'1'.QuestionHelper::KEY_ARROW_RIGHT.'2','te1s2t'],
200+
['test'.QuestionHelper::KEY_ARROW_LEFT.QuestionHelper::KEY_ARROW_LEFT.'1'.QuestionHelper::KEY_CTRL_F.'2','te1s2t'],
201+
['es'.QuestionHelper::KEY_CTRL_A.'t'.QuestionHelper::KEY_CTRL_E.'t','test'],
202+
];
203+
}
204+
175205
publicfunctiontestAskNonTrimmed()
176206
{
177207
$dialog =newQuestionHelper();

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp