diff --git a/CHANGELOG.md b/CHANGELOG.md index 75d7bf8..985cf36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,25 @@ Changelog and Upgrade Instructions Major changes: - * The `Shell::EV_*` constants have been renamed to `Shell::EVT_*`. - * The `configure` method has been removed. - * Events are now dispatched using the *NoccyLabs/TinyEvent* library. + * The `Shell::EV_*` constants have been renamed to `Shell::EVT_*`. + * The `configure` method has been removed. If you really need to + extend the `Shell` class, use the constructor to configure your + shell (see *examples/timers.php*) + * Events are now dispatched using the *NoccyLabs/TinyEvent* library. + The main difference here is in how the event object received. The + shell instance is now passed in the object, and you can provide any + userdata to be passed on to the handler when you add the listener. + * The event `Shell::EVT_BAD_COMMAND` will be fired if a command can + not be found, assuming `Context->execute()` does not accept the + command. (see *examples/commandevents.php*) + * When calling `Context->addCommand()` you can now use both `help` + and `descr` as the last argument to provide the description text + for the command. This change is intended to make `addCommand` and + the doccomment `@descr` more similar. + +New features: + + * Tasks can now be added and removed from the shell. Tasks will receive + a call to their `update()` method once per main loop, until they are + removed or return false from the `isValid()` method. Tasks need to + implement the `TaskInterface` interface. (see *examples/tasks.php*) \ No newline at end of file diff --git a/examples/basic.php b/examples/basic.php index 0413c68..596a89f 100644 --- a/examples/basic.php +++ b/examples/basic.php @@ -30,7 +30,7 @@ $myShell->setPrompt("test>"); $ctx = $myShell->createContext("root"); $ctx->addCommand("hello", function () { echo "Hello World!\n"; -}); +}, [ 'descr'=>'Say hello' ]); // Run the shell $myShell->run(); diff --git a/examples/commandevents.php b/examples/commandevents.php new file mode 100644 index 0000000..e45a7bf --- /dev/null +++ b/examples/commandevents.php @@ -0,0 +1,44 @@ +addListener(Shell::EVT_BAD_COMMAND, function ($e) { + echo "EVT_BAD_COMMAND:\n"; + printf("| command: %s\n", $e->command); + printf("|_args: %s\n", join(" ",$e->args)); +}); +$myShell->addListener(Shell::EVT_BEFORE_COMMAND, function ($e) { + echo "EVT_BEFORE_COMMAND:\n"; + printf("| command: %s\n", $e->command); + printf("|_args: %s\n", join(" ",$e->args)); +}); +$myShell->addListener(Shell::EVT_AFTER_COMMAND, function ($e) { + echo "EVT_AFTER_COMMAND:\n"; + printf("| command: %s\n", $e->command); + printf("| args: %s\n", join(" ",$e->args)); + printf("|_type: %s\n", $e->type); +}); + +// Set the initial prompt, not really needed. +$myShell->setPrompt("test>"); + +// 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/examples/tasks.php b/examples/tasks.php new file mode 100644 index 0000000..6d280df --- /dev/null +++ b/examples/tasks.php @@ -0,0 +1,60 @@ +start = time(); + } + public function update() + { + if ($this->last < time()) { + echo date("H:i:s")."\n"; + $this->last = time(); + } + } + public function isValid() + { + return (time() - $this->start < 5); + } +} + +$myShell = new Shell(); + +$myShell->addListener(Shell::EVT_TASK_CREATED, function ($e) { + echo "EVT_TASK_CREATED:\n"; + printf("|_task: %s\n", get_class($e->task)); +}); +$myShell->addListener(Shell::EVT_TASK_DESTROYED, function ($e) { + echo "EVT_TASK_DESTROYED:\n"; + printf("|_task: %s\n", get_class($e->task)); +}); + + +$myShell->addTask(new MyTask()); + +// Set the initial prompt, not really needed. +$myShell->setPrompt("test>"); + +// 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/Context.php b/lib/Context.php index c8f36e9..7f27e4f 100644 --- a/lib/Context.php +++ b/lib/Context.php @@ -132,6 +132,7 @@ class Context $info = $this->commandInfo[$command]; $args = array_key_exists("args",$info)?$info['args']:""; $help = array_key_exists("help",$info)?$info['help']:""; + $help = $help?:(array_key_exists("descr",$info)?$info['descr']:""); $ret[trim("{$command} {$args}")] = $help; } return $ret; diff --git a/lib/Shell.php b/lib/Shell.php index 926126a..db3e0e8 100644 --- a/lib/Shell.php +++ b/lib/Shell.php @@ -13,12 +13,14 @@ class Shell 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_BAD_COMMAND = "shell.command.bad"; // 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 + const EVT_TASK_CREATED = "task.created"; // a task was created + const EVT_TASK_DESTROYED = "task.destryed"; // a task was removed or invalidated /** * @var LineRead The lineread instance @@ -34,10 +36,10 @@ class Shell protected $contextStack = []; /** * @var object[] Created timers - */ + */ protected $timers = []; - /** - * @var object[] Running subtasks + /** + * @var TaskInterface[] Created tasks */ protected $tasks = []; /** @@ -186,6 +188,37 @@ class Shell }); } + /** + * Add a task to be update():d in the main loop. + * + * @param TaskInterface $task The task to add + */ + public function addTask(TaskInterface $task) + { + if ($this->dispatchEvent(self::EVT_TASK_CREATED, [ + 'task' => $task + ])->isPropagationStopped()) { + return; + } + $this->tasks[] = $task; + } + + /** + * Remove a previously added task. This can also be done by the task returning + * false from its isValid() method. + * + * @param TaskInterface $task The task to remove + */ + public function removeTask(TaskInterface $task) + { + $this->tasks = array_filter($this->tasks, function ($item) use ($task) { + return ($item != $task); + }); + $this->dispatchEvent(self::EVT_TASK_DESTROYED, [ + 'task' => $task + ]); + } + /** * Set the prompt text * @@ -268,7 +301,19 @@ class Shell */ public function executeCommand($command, ...$args) { + if ($this->dispatchEvent(self::EVT_BEFORE_COMMAND, [ + 'command' => $command, + 'args' => $args + ])->isPropagationStopped()) { + return true; + } + if ($this->executeBuiltin($command, ...$args)) { + $this->dispatchEvent(self::EVT_AFTER_COMMAND, [ + 'command' => $command, + 'args' => $args, + 'type' => 'builtin' + ]); return; } @@ -278,11 +323,31 @@ class Shell if ($ret instanceof Context) { $this->pushContext($ret); } + $this->dispatchEvent(self::EVT_AFTER_COMMAND, [ + 'command' => $command, + 'args' => $args, + 'type' => 'command' + ]); return; } // Call 'execute' on the current context if ($this->context->execute($command, ...$args)) { + $this->dispatchEvent(self::EVT_AFTER_COMMAND, [ + 'command' => $command, + 'args' => $args, + 'type' => 'execute' + ]); + return; + } + + // Fire the EVT_BAD_COMMAND event and return if the event propagation + // has been stopped. + $evt = $this->dispatchEvent(self::EVT_BAD_COMMAND, [ + 'command'=>$command, + 'args'=>$args + ]); + if ($evt->isPropagationStopped()) { return; } @@ -330,12 +395,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); } @@ -411,8 +476,14 @@ class Shell foreach ($this->timers as $timer) { $timer->update(); } - foreach ($this->tasks as $task) { + foreach ($this->tasks as $taskidx=>$task) { $task->update(); + if (!$task->isValid()) { + $this->dispatchEvent(self::EVT_TASK_DESTROYED, [ + 'task' => $task + ]); + unset($this->tasks[$taskidx]); + } } if ($buffer) { $this->executeBuffer($buffer); @@ -423,11 +494,13 @@ class Shell if (trim($output)) { $this->lineReader->erase(); echo rtrim($output)."\n"; - $this->lineReader->redraw(); + if ($this->running) + $this->lineReader->redraw(); } if (!$this->context) { $this->stop(); + break; } if ($buffer) { @@ -456,6 +529,7 @@ class Shell $data['context'] = $this->context; $event = new Event($type, $data); $this->emitEvent($type, $event); + return $event; } /** diff --git a/lib/TaskInterface.php b/lib/TaskInterface.php new file mode 100644 index 0000000..1a08f06 --- /dev/null +++ b/lib/TaskInterface.php @@ -0,0 +1,10 @@ +