Skip to content

Commit df1175f

Browse files
committed
feat: add arrow-key select() and multiselect() to Prompt
1 parent 10ea628 commit df1175f

6 files changed

Lines changed: 855 additions & 5 deletions

File tree

‎system/CLI/KeyReader.php‎

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of CodeIgniter 4 framework.
7+
*
8+
* (c) CodeIgniter Foundation <admin@codeigniter.com>
9+
*
10+
* For the full copyright and license information, please view
11+
* the LICENSE file that was distributed with this source code.
12+
*/
13+
14+
namespace CodeIgniter\CLI;
15+
16+
/**
17+
* Reader of single keypresses from a stream.
18+
*
19+
* @internal
20+
*/
21+
final class KeyReader
22+
{
23+
private const KEY_PATTERN = '/\e\[[0-9;]*[A-Za-z~]|\eO[A-Z]|./s';
24+
25+
private string $savedMode = '';
26+
27+
/**
28+
* @param resource $stream
29+
* @param bool $raw Whether to switch the terminal to raw mode while open
30+
*/
31+
public function __construct(
32+
private readonly mixed $stream,
33+
private readonly bool $raw,
34+
) {
35+
}
36+
37+
/**
38+
* Returns `false` when raw mode was wanted but the terminal settings could not be read.
39+
*/
40+
public function open(): bool
41+
{
42+
if (! $this->raw) {
43+
return true;
44+
}
45+
46+
// @codeCoverageIgnoreStart
47+
$this->savedMode = trim((string) shell_exec('stty -g < /dev/tty 2>/dev/null'));
48+
49+
if ($this->savedMode === '') {
50+
return false;
51+
}
52+
53+
// Signals stay off so Ctrl+C arrives as a key, letting the terminal be restored before the process ends.
54+
shell_exec('stty -icanon -isig -echo < /dev/tty 2>/dev/null');
55+
56+
return true;
57+
// @codeCoverageIgnoreEnd
58+
}
59+
60+
public function close(): void
61+
{
62+
if ($this->savedMode === '') {
63+
return;
64+
}
65+
66+
// @codeCoverageIgnoreStart
67+
shell_exec(sprintf('stty %s < /dev/tty 2>/dev/null', escapeshellarg($this->savedMode)));
68+
69+
$this->savedMode = '';
70+
// @codeCoverageIgnoreEnd
71+
}
72+
73+
/**
74+
* Returns the keys of the next read, or an empty list at the end of the stream.
75+
*
76+
* @return list<string>
77+
*/
78+
public function read(): array
79+
{
80+
$bytes = fread($this->stream, 16);
81+
82+
if ($bytes === false || $bytes === '') {
83+
return [];
84+
}
85+
86+
preg_match_all(self::KEY_PATTERN, $bytes, $matches);
87+
88+
return $matches[0];
89+
}
90+
}

0 commit comments

Comments
 (0)