diff --git a/examples/aborting.php b/examples/aborting.php new file mode 100644 index 0000000..719108b --- /dev/null +++ b/examples/aborting.php @@ -0,0 +1,44 @@ +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(); + } +}); + diff --git a/examples/commands.php b/examples/commands.php index 7402d2a..d1cb062 100644 --- a/examples/commands.php +++ b/examples/commands.php @@ -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"); +}); diff --git a/src/CommandHandler.php b/src/CommandHandler.php index 73da3b1..9336ef4 100644 --- a/src/CommandHandler.php +++ b/src/CommandHandler.php @@ -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 { diff --git a/src/Shell.php b/src/Shell.php index 197184f..5101d7c 100644 --- a/src/Shell.php +++ b/src/Shell.php @@ -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)); }