Bugfixes, updated examples

* Added example of using the abort event
* Added setter/getter for allowAbbreviatedCommands
* Fixed SIGINT handling
This commit is contained in:
Chris 2024-02-28 18:40:26 +01:00
parent 24b7a79827
commit fdbcc7adae
4 changed files with 107 additions and 9 deletions

44
examples/aborting.php Normal file
View 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();
}
});

View File

@ -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");
});
// 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");
});

View File

@ -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
{

View File

@ -35,9 +35,9 @@ 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 = [];
@ -56,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");
@ -77,8 +89,11 @@ class Shell implements WritableStreamInterface, EventEmitterInterface
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
@ -152,7 +167,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);
@ -188,12 +203,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));
}