Rewrite, cleanup and bugfixes

* More events added, constants cleaned up
* Events now handled using noccylabs/tinyevent
* Fixed magenta/cyan mixup in style
* Fixed LineRead not resetting history pointer on command
This commit is contained in:
Chris 2017-01-23 23:28:12 +01:00
parent b6727bed80
commit 5a45ca9c46
5 changed files with 119 additions and 72 deletions

View File

@ -13,5 +13,8 @@
"psr-4": { "psr-4": {
"NoccyLabs\\Shell\\": "lib/" "NoccyLabs\\Shell\\": "lib/"
} }
},
"require": {
"noccylabs/tinyevent": "~0.1.1"
} }
} }

View File

@ -1,12 +1,36 @@
<?php <?php
/*
* Basic shell example, demonstrates adding a listener to update the prompt with
* the current time as well as creating an "anonymous" context and adding a
* command to it. It also shows how to change the style of the prompt.
*
*/
require_once __DIR__."/../vendor/autoload.php"; require_once __DIR__."/../vendor/autoload.php";
use NoccyLabs\Shell\Shell; use NoccyLabs\Shell\Shell;
use NoccyLabs\Shell\Style;
use NoccyLabs\Shell\Context; use NoccyLabs\Shell\Context;
$myShell = new Shell(); $myShell = new Shell();
// Set the style of prompt and input
$myShell->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->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(); $myShell->run();

View File

@ -2,6 +2,10 @@
namespace NoccyLabs\Shell; namespace NoccyLabs\Shell;
/**
* This is a readline-like implementation in pure PHP.
*
*/
class LineRead class LineRead
{ {
@ -127,6 +131,7 @@ class LineRead
array_unshift($this->history, $this->buffer); array_unshift($this->history, $this->buffer);
$this->buffer = null; $this->buffer = null;
$this->posCursor = 0; $this->posCursor = 0;
$this->posHistory = 0;
printf("\n\r"); printf("\n\r");
$this->state = self::STATE_IDLE; $this->state = self::STATE_IDLE;
} }

View File

@ -3,32 +3,50 @@
namespace NoccyLabs\Shell; namespace NoccyLabs\Shell;
use NoccyLabs\Shell\LineRead; use NoccyLabs\Shell\LineRead;
use NoccyLabs\TinyEvent\EventEmitterTrait;
use NoccyLabs\TinyEvent\Event;
class Shell class Shell
{ {
const EV_PROMPT = "prompt"; use EventEmitterTrait;
const EV_COMMAND = "command";
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; protected $lineReader = null;
/**
* @var Context The current context
*/
protected $context = null; protected $context = null;
/**
* @var Context[] The stack of parent contexts
*/
protected $contextStack = []; protected $contextStack = [];
protected $listeners = []; protected $listeners = [];
protected $timers = []; protected $timers = [];
protected $tasks = [];
protected $prompt = ">"; protected $prompt = ">";
protected $prompt_style = null;
protected $input_style = null;
public function __construct() public function __construct()
{ {
$this->configure();
}
protected function configure()
{
} }
/** /**
@ -44,7 +62,7 @@ class Shell
} }
$context->setShell($this); $context->setShell($this);
$this->context = $context; $this->context = $context;
$this->dispatchEvent("context.update", [ "context"=>$this->context ]); $this->dispatchEvent(self::EVT_CONTEXT_CHANGED);
} }
/** /**
@ -60,7 +78,7 @@ class Shell
} else { } else {
$this->context = null; $this->context = null;
} }
$this->dispatchEvent("context.update", [ "context"=>$this->context ]); $this->dispatchEvent(self::EVT_CONTEXT_CHANGED);
return $previous; return $previous;
} }
@ -81,43 +99,19 @@ class Shell
return join($separator,$stack); 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); $this->pushContext($context);
return $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. * 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 * 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. * Find a command and return a closure.
* *
@ -269,12 +281,12 @@ class Shell
} }
} }
ksort($ghelp); ksort($ghelp);
printf("Commands in current context:\n"); //printf("Commands in current context:\n");
foreach ($help as $command=>$info) { foreach ($help as $command=>$info) {
printf(" %-20s %s\n", $command, $info); printf(" %-20s %s\n", $command, $info);
} }
if (count($ghelp)) { if (count($ghelp)) {
printf("\nImported from parent contexts:\n"); //printf("\nImported from parent contexts:\n");
foreach ($ghelp as $command=>$info) { foreach ($ghelp as $command=>$info) {
printf(" %-20s %s\n", $command, $info); printf(" %-20s %s\n", $command, $info);
} }
@ -308,18 +320,6 @@ class Shell
} catch (Exception\ShellException $e) { } catch (Exception\ShellException $e) {
echo "\e[31;91;1m{$e->getMessage()}\e[0m\n"; 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() public function run()
@ -328,12 +328,13 @@ class Shell
$this->lineReader = new LineRead(); $this->lineReader = new LineRead();
$this->lineReader->setPromptText($this->prompt); $this->lineReader->setPromptText($this->prompt);
$this->lineReader->setPromptStyle(new Style(Style::BR_GREEN)); $this->lineReader->setPromptStyle($this->prompt_style?:new Style(Style::BR_GREEN));
$this->lineReader->setCommandStyle(new Style(Style::GREEN)); $this->lineReader->setCommandStyle($this->input_style?:new Style(Style::GREEN));
$this->running = true; $this->running = true;
$this->dispatchEvent("prompt", [ "context"=>$this->context ]); $this->dispatchEvent(self::EVT_UPDATE_PROMPT);
$this->dispatchEvent(self::EVT_SHELL_START);
while ($this->running) { while ($this->running) {
// Update the input stuff, sleep if nothing to do. // Update the input stuff, sleep if nothing to do.
@ -342,12 +343,12 @@ class Shell
} }
// Escape is handy too... // Escape is handy too...
if ($buffer == "\e") { if ($buffer == "\e") {
$this->dispatchEvent("shell.abort"); $this->dispatchEvent(self::EVT_SHELL_ESCAPE);
continue; continue;
} }
// we get a ^C on ^C, so deal with the ^C. // we get a ^C on ^C, so deal with the ^C.
if ($buffer == "\x03") { if ($buffer == "\x03") {
$this->dispatchEvent("shell.stop"); $this->dispatchEvent(self::EVT_SHELL_ABORT);
$this->stop(); $this->stop();
continue; continue;
} }
@ -357,6 +358,9 @@ class Shell
foreach ($this->timers as $timer) { foreach ($this->timers as $timer) {
$timer->update(); $timer->update();
} }
foreach ($this->tasks as $task) {
$task->update();
}
if ($buffer) { if ($buffer) {
$this->executeBuffer($buffer); $this->executeBuffer($buffer);
} }
@ -374,7 +378,7 @@ class Shell
} }
if ($buffer) { 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); fprintf(STDERR, "\e[31;1mFatal: Unhandled exception\e[0m\n\n%s\n", $e);
} }
$this->dispatchEvent(self::EVT_SHELL_STOP);
$this->lineReader = null; $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() public function stop()
{ {
$this->running = false; $this->running = false;

View File

@ -11,16 +11,17 @@ class Style
const GREEN = 2; const GREEN = 2;
const YELLOW = 3; const YELLOW = 3;
const BLUE = 4; const BLUE = 4;
const CYAN = 5; const MAGENTA = 5;
const MAGENTA = 6; const CYAN = 6;
const WHITE = 7; const WHITE = 7;
const GRAY = 8; const GRAY = 8;
const BR_RED = 9; const BR_RED = 9;
const BR_GREEN = 10; const BR_GREEN = 10;
const BR_YELLOW = 11; const BR_YELLOW = 11;
const BR_BLUE = 12; const BR_BLUE = 12;
const BR_CYAN = 13; const BR_MAGENTA = 13;
const BR_MAGENTA = 14; const BR_CYAN = 14;
const BR_WHITE = 15; const BR_WHITE = 15;
const A_INTENSE = 1; const A_INTENSE = 1;