Compare commits
5 Commits
Author | SHA1 | Date | |
---|---|---|---|
266c538f01 | |||
fdbcc7adae | |||
24b7a79827 | |||
14f1d7f0cf | |||
6eddf0e847 |
|
@ -9,6 +9,7 @@ $ composer require noccylabs/react-shell:@dev
|
|||
## Features
|
||||
|
||||
* Interactive line-editing including arrow keys, home, end, delete and all the commodities you are used to.
|
||||
* Automatic scrollback history, to avoid having to retype commands.
|
||||
* Input is edited on a single scrolling line.
|
||||
* Mostly Unicode-aware, meaning it will not have a nervous breakdown if you try to enter (single-width LTR) unicode characters.
|
||||
|
||||
|
|
44
examples/aborting.php
Normal file
44
examples/aborting.php
Normal file
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
use NoccyLabs\React\Shell\CommandHandler;
|
||||
use React\EventLoop\Loop;
|
||||
|
||||
require_once __DIR__."/../vendor/autoload.php";
|
||||
|
||||
$operation = new class {
|
||||
public bool $active = false;
|
||||
};
|
||||
|
||||
$shell = new NoccyLabs\React\Shell\Shell();
|
||||
|
||||
// The prompt event is invoked before every full redraw. Call redrawPrompt()
|
||||
// to trigger it manually. Set the prompt or the style here.
|
||||
$shell->on('prompt', function ($shell) {
|
||||
$shell->setPrompt(">> ");
|
||||
});
|
||||
|
||||
// Input handler, parse the commands.
|
||||
$shell->on('input', function ($args, $shell) use ($operation){
|
||||
switch ($args[0]) {
|
||||
case 'start':
|
||||
$operation->active = true;
|
||||
$shell->write("Operation started... Press Ctrl-C to abort\n");
|
||||
break;
|
||||
default:
|
||||
$shell->write("Type start to start the imaginary operation, then ctrl-c to abort it. Press ctrl-c again to exit.\n");
|
||||
}
|
||||
});
|
||||
|
||||
// The abort event lets you keep going if a specific operation is the main
|
||||
// context. For example, if you are copying a file ^C could stop the copy
|
||||
// operation.
|
||||
$shell->on('abort', function ($shell) use ($operation) {
|
||||
if ($operation->active == true) {
|
||||
$shell->write("Aborting operation\n");
|
||||
$operation->active = false;
|
||||
} else {
|
||||
$shell->write("Exiting\n");
|
||||
$shell->close();
|
||||
}
|
||||
});
|
||||
|
|
@ -8,16 +8,39 @@ require_once __DIR__."/../vendor/autoload.php";
|
|||
$shell = new NoccyLabs\React\Shell\Shell();
|
||||
|
||||
$commands = new CommandHandler();
|
||||
// Make it possible to use as few characters as possible as long as they match
|
||||
// a single command. For example with the commands foo, bar and baz, foo could
|
||||
// be executed with 'f', 'fo', or 'foo', while both bar and baz would require
|
||||
// the full name.
|
||||
$commands->setAllowAbbreviatedCommands(true);
|
||||
// Add commands
|
||||
$commands->add('help', function ($args, $shell) {
|
||||
$shell->write("This could be usage help :)\n");
|
||||
$shell->write("Exit by pressing ^C\n");
|
||||
$shell->write("Exit by typing quit or pressing ^C\n");
|
||||
});
|
||||
// This is how you terminate the shell. Obviously, you close it :) Using end()
|
||||
// on the shell differs somewhat between ReactPHP; close() will exit with
|
||||
// code 0, while end will exit with code 1, and print a friendly message.
|
||||
$commands->add('quit', function ($args, $shell) {
|
||||
$shell->close();
|
||||
});
|
||||
// The command handler gets everything that was not matched. Resolve your own
|
||||
// commands here, or print an error.
|
||||
$commands->on('command', function ($command, $args, $shell) {
|
||||
$shell->write("Bad command '{$command}', try help.\n");
|
||||
$shell->write("Arguments passed: ".json_encode($args)."\n");
|
||||
});
|
||||
|
||||
$shell->on('prompt', function ($shell) {
|
||||
$shell->setPrompt(date(">> "));
|
||||
|
||||
// The prompt event is invoked before every full redraw. Call redrawPrompt()
|
||||
// to trigger it manually. Set the prompt or the style here.
|
||||
$shell->on('prompt', function ($shell) {
|
||||
$shell->setPrompt(">> ");
|
||||
});
|
||||
// This is where we hook the CommandHandler to the shell.
|
||||
$shell->on('input', $commands);
|
||||
// And finally, the end event gives you a chance to shut down cleanly. After
|
||||
// returning from here, the shell will exit.
|
||||
$shell->on('end', function ($shell) {
|
||||
$shell->write("Shutting down...\n");
|
||||
});
|
||||
|
|
|
@ -6,16 +6,22 @@ require_once __DIR__."/../vendor/autoload.php";
|
|||
|
||||
$shell = new NoccyLabs\React\Shell\Shell();
|
||||
|
||||
// The prompt event is invoked before every full redraw. Call redrawPrompt()
|
||||
// to trigger it manually. Set the prompt or the style here.
|
||||
$shell->on('prompt', function ($shell) {
|
||||
$shell->setPrompt(date("H:i:s> "));
|
||||
});
|
||||
|
||||
// Input handler, parse the commands.
|
||||
$shell->on('input', function (array $input) use ($shell) {
|
||||
$shell->write("You entered: ".join(" ", $input)."\n");
|
||||
});
|
||||
|
||||
// The shell is an OutputStream
|
||||
// The shell is an OutputStream, and writing to it will handle hiding the prompt
|
||||
// and redrawing it afterwards!
|
||||
$shell->write("Hello World!\n\n");
|
||||
|
||||
// So output can be writen while you are typing!
|
||||
Loop::addPeriodicTimer(5, function () use ($shell) {
|
||||
$shell->write(date(DATE_ATOM)."\n");
|
||||
});
|
|
@ -13,7 +13,18 @@ class CommandHandler implements EventEmitterInterface
|
|||
|
||||
private array $commands = [];
|
||||
|
||||
private bool $allowAbbreviatedCommands = true;
|
||||
private bool $allowAbbreviatedCommands = false;
|
||||
|
||||
public function setAllowAbbreviatedCommands(bool $allow): self
|
||||
{
|
||||
$this->allowAbbreviatedCommands = $allow;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAllowAbbreviatedCommands(): bool
|
||||
{
|
||||
return $this->allowAbbreviatedCommands;
|
||||
}
|
||||
|
||||
public function add(string $command, callable $handler, array $signature=[]): self
|
||||
{
|
||||
|
@ -35,7 +46,7 @@ class CommandHandler implements EventEmitterInterface
|
|||
array_keys($this->commands),
|
||||
fn($c)=>str_starts_with($c,$command)
|
||||
);
|
||||
if (count($candidates)>2) {
|
||||
if (count($candidates)>1) {
|
||||
$this->emit("candidates", [ $candidates, $shell ]);
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -35,9 +35,17 @@ class Shell implements WritableStreamInterface, EventEmitterInterface
|
|||
|
||||
private int $promptWidth = 0;
|
||||
|
||||
private string $promptStyle = "36;1";
|
||||
private ?string $promptStyle = "2";
|
||||
|
||||
private string $inputStyle = "36";
|
||||
private ?string $inputStyle = "1";
|
||||
|
||||
private array $history = [];
|
||||
|
||||
private ?int $historyIndex = null;
|
||||
|
||||
private ?string $historyCache = null;
|
||||
|
||||
private int $maxHistory = 100;
|
||||
|
||||
public function __construct(?ReadableStreamInterface $input=null, ?WritableStreamInterface $output=null)
|
||||
{
|
||||
|
@ -48,6 +56,18 @@ class Shell implements WritableStreamInterface, EventEmitterInterface
|
|||
|
||||
Loop::futureTick($this->redrawPrompt(...));
|
||||
|
||||
Loop::addSignal(SIGINT, function () {
|
||||
Loop::futureTick(function () {
|
||||
if (count($this->listeners("abort")) == 0)
|
||||
$this->end("Received ^C\n");
|
||||
else
|
||||
$this->emit("abort", [ $this ]);
|
||||
});
|
||||
});
|
||||
// Loop::addSignal(SIGWINCH, function () {
|
||||
// Loop::futureTick($this->redrawPrompt(...));
|
||||
// });
|
||||
|
||||
// Save terminal settings and disable inupt buffering
|
||||
$this->oldStty = exec("stty -g");
|
||||
exec("stty -icanon min 1 time 0 -echo");
|
||||
|
@ -62,17 +82,25 @@ class Shell implements WritableStreamInterface, EventEmitterInterface
|
|||
public function setPrompt(string $prompt): void
|
||||
{
|
||||
$this->prompt = $prompt;
|
||||
$this->promptWidth = mb_strlen($this->prompt);
|
||||
$this->promptWidth = mb_strlen(preg_replace('<(\e\[[0-9;]+?m)>m', '', $this->prompt));
|
||||
}
|
||||
|
||||
private function handleInput($v) {
|
||||
|
||||
switch ($v) {
|
||||
|
||||
case "\x03":
|
||||
exit(0);
|
||||
case "\x03":
|
||||
if (count($this->listeners("abort")) == 0)
|
||||
$this->end("Received ^C\n");
|
||||
else
|
||||
$this->emit("abort", [ $this ]);
|
||||
|
||||
case "\n":
|
||||
// Update the history
|
||||
array_unshift($this->history, $this->buffer);
|
||||
while (count($this->history) > $this->maxHistory) array_pop($this->history);
|
||||
$this->historyIndex = 0;
|
||||
// Parse and empty the buffer
|
||||
$buffer = str_getcsv(trim($this->buffer), " ");
|
||||
$this->buffer = '';
|
||||
$this->cursorPos = 0;
|
||||
|
@ -98,7 +126,28 @@ class Shell implements WritableStreamInterface, EventEmitterInterface
|
|||
break;
|
||||
|
||||
case "\e[A": // up
|
||||
if ($this->historyIndex === null && count($this->history) > 0) {
|
||||
$this->historyCache = $this->buffer;
|
||||
$this->historyIndex = 0;
|
||||
$this->buffer = $this->history[$this->historyIndex]??'';
|
||||
} elseif ($this->historyIndex < count($this->history) - 1) {
|
||||
$this->historyIndex++;
|
||||
$this->buffer = $this->history[$this->historyIndex]??'';
|
||||
}
|
||||
$this->cursorPos = mb_strlen($this->buffer);
|
||||
break;
|
||||
case "\e[B": // down
|
||||
if ($this->historyIndex === null) {
|
||||
break;
|
||||
} elseif ($this->historyIndex === 0) {
|
||||
$this->historyIndex = null;
|
||||
$this->buffer = $this->historyCache;
|
||||
$this->historyCache = null;
|
||||
} else {
|
||||
$this->historyIndex--;
|
||||
$this->buffer = $this->history[$this->historyIndex]??'';
|
||||
}
|
||||
$this->cursorPos = mb_strlen($this->buffer);
|
||||
break;
|
||||
|
||||
case "\e[H": // home
|
||||
|
@ -119,7 +168,7 @@ class Shell implements WritableStreamInterface, EventEmitterInterface
|
|||
break;
|
||||
|
||||
default:
|
||||
if (mb_strlen($v) == 1 && ord($v) >= 32) {
|
||||
if (mb_strlen($v) == 1 && mb_ord($v) >= 32) {
|
||||
$this->buffer = mb_substr($this->buffer, 0, $this->cursorPos) .
|
||||
$v .
|
||||
mb_substr($this->buffer, $this->cursorPos);
|
||||
|
@ -155,12 +204,17 @@ class Shell implements WritableStreamInterface, EventEmitterInterface
|
|||
|
||||
public function end($data = null)
|
||||
{
|
||||
// NOP
|
||||
$this->hidePrompt();
|
||||
$this->ostream->write($data);
|
||||
$this->emit('end', [ $this ]);
|
||||
Loop::futureTick(fn() => exit(1));
|
||||
}
|
||||
|
||||
public function close()
|
||||
{
|
||||
// NOP
|
||||
$this->hidePrompt();
|
||||
$this->emit('end', [ $this ]);
|
||||
Loop::futureTick(fn() => exit(0));
|
||||
}
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user