diff --git a/composer.json b/composer.json index 2a8e397..1cea0fb 100644 --- a/composer.json +++ b/composer.json @@ -13,5 +13,8 @@ "psr-4": { "NoccyLabs\\Shell\\": "lib/" } + }, + "require": { + "noccylabs/tinyevent": "~0.1.1" } -} \ No newline at end of file +} diff --git a/examples/basic.php b/examples/basic.php index e951ddc..0413c68 100644 --- a/examples/basic.php +++ b/examples/basic.php @@ -1,12 +1,36 @@ setPromptStyle(new Style(Style::BR_GREEN, Style::GREEN)); +$myShell->setInputStyle(new Style(Style::BR_CYAN)); + +// Add a listener to update the prompt +$myShell->addListener(Shell::EVT_UPDATE_PROMPT, function ($e) { + $e->shell->setPrompt(date("H:i:s").">"); +}); + +// Set the initial prompt, not really needed. $myShell->setPrompt("test>"); -$myShell->pushContext(new Context()); + +// Create an anonymous context and add a command +$ctx = $myShell->createContext("root"); +$ctx->addCommand("hello", function () { + echo "Hello World!\n"; +}); + +// Run the shell $myShell->run(); diff --git a/lib/LineRead.php b/lib/LineRead.php index cf80e5f..eea9165 100644 --- a/lib/LineRead.php +++ b/lib/LineRead.php @@ -2,6 +2,10 @@ namespace NoccyLabs\Shell; +/** + * This is a readline-like implementation in pure PHP. + * + */ class LineRead { @@ -127,6 +131,7 @@ class LineRead array_unshift($this->history, $this->buffer); $this->buffer = null; $this->posCursor = 0; + $this->posHistory = 0; printf("\n\r"); $this->state = self::STATE_IDLE; } diff --git a/lib/Shell.php b/lib/Shell.php index bdb001e..72cf3f6 100644 --- a/lib/Shell.php +++ b/lib/Shell.php @@ -3,32 +3,50 @@ namespace NoccyLabs\Shell; use NoccyLabs\Shell\LineRead; +use NoccyLabs\TinyEvent\EventEmitterTrait; +use NoccyLabs\TinyEvent\Event; class Shell { - const EV_PROMPT = "prompt"; - const EV_COMMAND = "command"; + use EventEmitterTrait; + const EVT_UPDATE_PROMPT = "shell.prompt"; // called to update the prompt + const EVT_BEFORE_COMMAND = "shell.command.before"; // before a command is executed + const EVT_AFTER_COMMAND = "shell.command.after"; // after a command is executed + const EVT_NO_COMMAND = "shell.command.missing"; // no such command found + const EVT_CONTEXT_CHANGED = "shell.context"; // a new context is activated + const EVT_SHELL_START = "shell.start"; // the shell is about to start + const EVT_SHELL_STOP = "shell.stop"; // the shell is about to exit + const EVT_SHELL_ABORT = "shell.abort"; // the shell was aborted (ctrl-c) + const EVT_SHELL_ESCAPE = "shell.escape"; // escape key pressed + + /** + * @var LineRead The lineread instance + */ protected $lineReader = null; - + /** + * @var Context The current context + */ protected $context = null; - + /** + * @var Context[] The stack of parent contexts + */ protected $contextStack = []; - + protected $listeners = []; protected $timers = []; + protected $tasks = []; + protected $prompt = ">"; + protected $prompt_style = null; + + protected $input_style = null; + public function __construct() { - $this->configure(); - } - - protected function configure() - { - } /** @@ -44,7 +62,7 @@ class Shell } $context->setShell($this); $this->context = $context; - $this->dispatchEvent("context.update", [ "context"=>$this->context ]); + $this->dispatchEvent(self::EVT_CONTEXT_CHANGED); } /** @@ -60,7 +78,7 @@ class Shell } else { $this->context = null; } - $this->dispatchEvent("context.update", [ "context"=>$this->context ]); + $this->dispatchEvent(self::EVT_CONTEXT_CHANGED); return $previous; } @@ -81,43 +99,19 @@ class Shell return join($separator,$stack); } - public function createContext() + /** + * Create a new empty context and push it on the stack. + * + * @param string $name The name of the context to create + * @return Context The created context + */ + public function createContext($name) { - $context = new Context(); + $context = new Context($name); $this->pushContext($context); return $context; } - /** - * Add a handler to be called when an event fires. - * - * @param string $event - * @param callable $handler - */ - public function addListener($event, callable $handler) - { - if (!array_key_exists($event,$this->listeners)) { - $this->listeners[$event] = []; - } - $this->listeners[$event][] = $handler; - } - - /** - * Invoke event handlers for event. - * - * @param string $event - * @param callable $handler - */ - protected function dispatchEvent($event, array $data=[]) - { - if (!array_key_exists($event,$this->listeners)) { - return; - } - foreach ($this->listeners[$event] as $handler) { - call_user_func($handler, (object)$data, $this); - } - } - /** * Add a callback to be called at a regular interval during the update phase. * Adding timers with a low interval (less than 200 i.e. 0.2s) may have an @@ -174,6 +168,24 @@ class Shell } } + public function setPromptStyle(Style $style) + { + $this->prompt_style = $style; + + if ($this->lineReader) { + $this->lineReader->setPromptStyle($style); + } + } + + public function setInputStyle(Style $style) + { + $this->input_style = $style; + + if ($this->lineReader) { + $this->lineReader->setCommandStyle($style); + } + } + /** * Find a command and return a closure. * @@ -269,12 +281,12 @@ class Shell } } ksort($ghelp); - printf("Commands in current context:\n"); + //printf("Commands in current context:\n"); foreach ($help as $command=>$info) { printf(" %-20s %s\n", $command, $info); } if (count($ghelp)) { - printf("\nImported from parent contexts:\n"); + //printf("\nImported from parent contexts:\n"); foreach ($ghelp as $command=>$info) { printf(" %-20s %s\n", $command, $info); } @@ -308,18 +320,6 @@ class Shell } catch (Exception\ShellException $e) { echo "\e[31;91;1m{$e->getMessage()}\e[0m\n"; } - /* - if (array_key_exists($commandName, $this->commands)) { - $command = $this->commands[$commandName]; - if ($command instanceof Command) { - $command->run($buffer); - } else { - call_user_func_array($command,$buffer); - } - return; - } - $this->writeln("Bad command: ".$commandName); - */ } public function run() @@ -328,12 +328,13 @@ class Shell $this->lineReader = new LineRead(); $this->lineReader->setPromptText($this->prompt); - $this->lineReader->setPromptStyle(new Style(Style::BR_GREEN)); - $this->lineReader->setCommandStyle(new Style(Style::GREEN)); + $this->lineReader->setPromptStyle($this->prompt_style?:new Style(Style::BR_GREEN)); + $this->lineReader->setCommandStyle($this->input_style?:new Style(Style::GREEN)); $this->running = true; - $this->dispatchEvent("prompt", [ "context"=>$this->context ]); + $this->dispatchEvent(self::EVT_UPDATE_PROMPT); + $this->dispatchEvent(self::EVT_SHELL_START); while ($this->running) { // Update the input stuff, sleep if nothing to do. @@ -342,12 +343,12 @@ class Shell } // Escape is handy too... if ($buffer == "\e") { - $this->dispatchEvent("shell.abort"); + $this->dispatchEvent(self::EVT_SHELL_ESCAPE); continue; } // we get a ^C on ^C, so deal with the ^C. if ($buffer == "\x03") { - $this->dispatchEvent("shell.stop"); + $this->dispatchEvent(self::EVT_SHELL_ABORT); $this->stop(); continue; } @@ -357,6 +358,9 @@ class Shell foreach ($this->timers as $timer) { $timer->update(); } + foreach ($this->tasks as $task) { + $task->update(); + } if ($buffer) { $this->executeBuffer($buffer); } @@ -374,7 +378,7 @@ class Shell } if ($buffer) { - $this->dispatchEvent("prompt", [ "context"=>$this->context ]); + $this->dispatchEvent(self::EVT_UPDATE_PROMPT); } } @@ -382,10 +386,20 @@ class Shell fprintf(STDERR, "\e[31;1mFatal: Unhandled exception\e[0m\n\n%s\n", $e); } + $this->dispatchEvent(self::EVT_SHELL_STOP); + $this->lineReader = null; } + private function dispatchEvent($type, array $data=[]) + { + $data['shell'] = $this; + $data['context'] = $this->context; + $event = new Event($type, $data); + $this->emitEvent($type, $event); + } + public function stop() { $this->running = false; diff --git a/lib/Style.php b/lib/Style.php index 963e1d4..6239068 100644 --- a/lib/Style.php +++ b/lib/Style.php @@ -11,16 +11,17 @@ class Style const GREEN = 2; const YELLOW = 3; const BLUE = 4; - const CYAN = 5; - const MAGENTA = 6; + const MAGENTA = 5; + const CYAN = 6; const WHITE = 7; + const GRAY = 8; const BR_RED = 9; const BR_GREEN = 10; const BR_YELLOW = 11; const BR_BLUE = 12; - const BR_CYAN = 13; - const BR_MAGENTA = 14; + const BR_MAGENTA = 13; + const BR_CYAN = 14; const BR_WHITE = 15; const A_INTENSE = 1;