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:
parent
b6727bed80
commit
5a45ca9c46
@ -13,5 +13,8 @@
|
|||||||
"psr-4": {
|
"psr-4": {
|
||||||
"NoccyLabs\\Shell\\": "lib/"
|
"NoccyLabs\\Shell\\": "lib/"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"noccylabs/tinyevent": "~0.1.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
144
lib/Shell.php
144
lib/Shell.php
@ -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;
|
||||||
|
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user