7 Commits

Author SHA1 Message Date
809c04abfa Code cleanup, better examples, tasks added 2017-01-24 14:42:43 +01:00
4cd5cc2620 Comments and improvements 2017-01-24 00:49:13 +01:00
5a45ca9c46 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
2017-01-23 23:28:12 +01:00
b6727bed80 Fixed timer implementation 2017-01-22 23:22:13 +01:00
0e5a25567c Added exception handling to run() 2016-11-21 01:26:06 +01:00
bdec60717f Improved the cursor in LineRead when using history 2016-11-19 15:46:13 +01:00
43a6475192 Added an execute() method to context to catch unhandled commands 2016-11-19 14:18:53 +01:00
14 changed files with 542 additions and 116 deletions

29
CHANGELOG.md Normal file
View File

@ -0,0 +1,29 @@
Changelog and Upgrade Instructions
==================================
## 0.2.x to 0.3.x
Major changes:
* 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*)

View File

@ -0,0 +1,12 @@
NoccyLabs Shell Core
====================
This library helps make elegant command line applications that spawn an isolated shell.
It uses a standalone implementation for buffered input with support for arrow keys to
navigate the history and more.
Note that this library requirements a fully ANSI compatible terminal with UTF-8 support
in order to use colors, control the cursor position etc. As it uses `stty` to configure
input buffering, it will likely not work on Windows.

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";
}, [ 'descr'=>'Say hello' ]);
// Run the shell
$myShell->run(); $myShell->run();

20
examples/catchall.php Normal file
View File

@ -0,0 +1,20 @@
<?php
require_once __DIR__."/../vendor/autoload.php";
use NoccyLabs\Shell\Shell;
use NoccyLabs\Shell\Context;
class CatchAllContext extends Context
{
public function execute($cmd, ...$arg)
{
printf("Executing: %s %s\n", $cmd, join(" ",$arg));
return true;
}
}
$myShell = new Shell();
$myShell->setPrompt("test>");
$myShell->pushContext(new CatchAllContext());
$myShell->run();

View File

@ -0,0 +1,44 @@
<?php
/*
* This example demonstrates how to use events to catch commands that
* have not been handled by any context or builtin.
*
*/
require_once __DIR__."/../vendor/autoload.php";
use NoccyLabs\Shell\Shell;
use NoccyLabs\Shell\Style;
use NoccyLabs\Shell\Context;
$myShell = new Shell();
// Add a listeners for various command aspects
$myShell->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();

19
examples/errors.php Normal file
View File

@ -0,0 +1,19 @@
<?php
require_once __DIR__."/../vendor/autoload.php";
use NoccyLabs\Shell\Shell;
use NoccyLabs\Shell\Context;
class CatchAllContext extends Context
{
public function execute($cmd, ...$arg)
{
throw new \Exception("Uh-oh! Error!");
}
}
$myShell = new Shell();
$myShell->setPrompt("test>");
$myShell->pushContext(new CatchAllContext());
$myShell->run();

60
examples/tasks.php Normal file
View File

@ -0,0 +1,60 @@
<?php
/*
* This example demonstrates how to use events to catch commands that
* have not been handled by any context or builtin.
*
*/
require_once __DIR__."/../vendor/autoload.php";
use NoccyLabs\Shell\Shell;
use NoccyLabs\Shell\Style;
use NoccyLabs\Shell\Context;
use NoccyLabs\Shell\TaskInterface;
class MyTask implements TaskInterface
{
protected $start;
protected $last;
public function __construct()
{
$this->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();

View File

@ -24,15 +24,21 @@ class MyShell extends Shell
protected $seq = 0; protected $seq = 0;
protected function configure() public function __construct()
{ {
$context = new MyContext(); $context = new MyContext();
$this->pushContext($context); $this->pushContext($context);
$this->updatePrompt(); $this->updatePrompt();
$this->addTimer(5000, function () { $t1 = $this->addTimer(5000, function () {
echo "5 seconds\n"; echo "5 seconds\n";
}); });
$app = $this;
$t2 = $this->addTimer(15000, function () use ($t1, $app) {
echo "Removing timers...\n";
$app->removeTimer($t1);
});
} }
protected function updatePrompt() protected function updatePrompt()

View File

@ -132,6 +132,7 @@ class Context
$info = $this->commandInfo[$command]; $info = $this->commandInfo[$command];
$args = array_key_exists("args",$info)?$info['args']:""; $args = array_key_exists("args",$info)?$info['args']:"";
$help = array_key_exists("help",$info)?$info['help']:""; $help = array_key_exists("help",$info)?$info['help']:"";
$help = $help?:(array_key_exists("descr",$info)?$info['descr']:"");
$ret[trim("{$command} {$args}")] = $help; $ret[trim("{$command} {$args}")] = $help;
} }
return $ret; return $ret;
@ -146,6 +147,23 @@ class Context
return array_key_exists('global', $info); return array_key_exists('global', $info);
} }
/**
* Catch-all handler for commands not defined in context, globally or builtin.
* Override this function and return true if the command is handled ok.
*
* @param string $command The command to execute
* @param string[] $args The arguments to the command
* @return bool True if the command was handled
*/
public function execute($command, ...$args)
{
return false;
}
/**
* Get the name of the context
*
*/
public function getName() public function getName()
{ {
return $this->name; return $this->name;

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;
} }
@ -161,22 +166,33 @@ class LineRead
if ($this->posHistory == 0) { if ($this->posHistory == 0) {
$this->stashedBuffer = $this->buffer; $this->stashedBuffer = $this->buffer;
} }
if ($this->posCursor == strlen($this->buffer)) {
$this->posCursor = -1;
}
if ($this->posHistory < count($this->history)) { if ($this->posHistory < count($this->history)) {
$this->posHistory++; $this->posHistory++;
$this->buffer = $this->history[$this->posHistory-1]; $this->buffer = $this->history[$this->posHistory-1];
$this->redraw();
} }
if ($this->posCursor == -1) {
$this->posCursor = strlen($this->buffer);
}
$this->redraw();
break; break;
case "\e[B": // down case "\e[B": // down
if ($this->posCursor == strlen($this->buffer)) {
$this->posCursor = -1;
}
if ($this->posHistory > 1) { if ($this->posHistory > 1) {
$this->posHistory--; $this->posHistory--;
$this->buffer = $this->history[$this->posHistory-1]; $this->buffer = $this->history[$this->posHistory-1];
$this->redraw();
} elseif ($this->posHistory > 0) { } elseif ($this->posHistory > 0) {
$this->posHistory--; $this->posHistory--;
$this->buffer = $this->stashedBuffer; $this->buffer = $this->stashedBuffer;
$this->redraw();
} }
if ($this->posCursor == -1) {
$this->posCursor = strlen($this->buffer);
}
$this->redraw();
break; break;
default: default:
fprintf(STDERR, "\n%s\n", substr($code,1)); fprintf(STDERR, "\n%s\n", substr($code,1));

View File

@ -3,32 +3,79 @@
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_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
*/
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 = []; * @var object[] Created timers
*/
protected $timers = []; protected $timers = [];
/**
* @var TaskInterface[] Created tasks
*/
protected $tasks = [];
/**
* @var string The prompt string
*/
protected $prompt = ">"; protected $prompt = ">";
/**
* @var Style The style applied to the prompt
*/
protected $prompt_style = null;
/**
* @var Style The style applied to the input text
*/
protected $input_style = null;
/**
* Constructor
*
*/
public function __construct() public function __construct()
{ {
$this->configure(); $t = $this;
register_shutdown_function(function () use (&$t) {
if ($t) unset($t);
});
} }
protected function configure() /**
* Destructor
*
*/
public function __destruct()
{ {
if ($this->lineReader) {
$this->lineReader = null;
}
} }
/** /**
@ -44,7 +91,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 +107,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 +128,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
@ -137,6 +160,7 @@ class Shell
private $userdata; private $userdata;
public function __construct($interval, callable $handler, array $userdata) { public function __construct($interval, callable $handler, array $userdata) {
$this->interval = $interval / 1000; $this->interval = $interval / 1000;
$this->next = microtime(true) + $this->interval;
$this->handler = $handler; $this->handler = $handler;
$this->userdata = $userdata; $this->userdata = $userdata;
} }
@ -157,11 +181,49 @@ class Shell
* *
* @param Timer $timer * @param Timer $timer
*/ */
public function removeTimer(Timer $timer) public function removeTimer($timer)
{ {
$this->timers = array_filter($this->timers, function ($v) use ($timer) {
return ($v !== $timer);
});
} }
/**
* 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
*
* @param string $text The text
*/
public function setPrompt($text) public function setPrompt($text)
{ {
$this->prompt = $text; $this->prompt = $text;
@ -171,6 +233,34 @@ class Shell
} }
} }
/**
* Set the prompt style
*
* @param Style $style The style to apply to the prompt
*/
public function setPromptStyle(Style $style)
{
$this->prompt_style = $style;
if ($this->lineReader) {
$this->lineReader->setPromptStyle($style);
}
}
/**
* Set the input style
*
* @param Style $style The style to apply to the text
*/
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.
* *
@ -205,13 +295,25 @@ class Shell
* Execute a command with arguments. * Execute a command with arguments.
* *
* @param string $command The command name to execute * @param string $command The command name to execute
* @param string $args Arguments * @param string ...$args Arguments
* @return mixed * @return mixed
* @throws Exception\BadCommandExcception * @throws Exception\BadCommandExcception
*/ */
public function executeCommand($command, ...$args) public function executeCommand($command, ...$args)
{ {
if ($this->dispatchEvent(self::EVT_BEFORE_COMMAND, [
'command' => $command,
'args' => $args
])->isPropagationStopped()) {
return true;
}
if ($this->executeBuiltin($command, ...$args)) { if ($this->executeBuiltin($command, ...$args)) {
$this->dispatchEvent(self::EVT_AFTER_COMMAND, [
'command' => $command,
'args' => $args,
'type' => 'builtin'
]);
return; return;
} }
@ -221,12 +323,45 @@ class Shell
if ($ret instanceof Context) { if ($ret instanceof Context) {
$this->pushContext($ret); $this->pushContext($ret);
} }
$this->dispatchEvent(self::EVT_AFTER_COMMAND, [
'command' => $command,
'args' => $args,
'type' => 'command'
]);
return; 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;
}
// Throw error if the command could not be found // Throw error if the command could not be found
throw new Exception\BadCommandException("Command {$command} not found"); throw new Exception\BadCommandException("Command {$command} not found");
} }
/**
* Execute a built-in command
*
* @param string $command Command name
* @param mixed ...$args Arguments
* @return bool True if the command was handled OK
*/
public function executeBuiltin($command, ...$args) public function executeBuiltin($command, ...$args)
{ {
switch ($command) { switch ($command) {
@ -299,31 +434,25 @@ 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);
*/
} }
/**
* Start the shell
*
*/
public function run() public function run()
{ {
try {
$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.
@ -332,12 +461,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;
} }
@ -347,6 +476,15 @@ class Shell
foreach ($this->timers as $timer) { foreach ($this->timers as $timer) {
$timer->update(); $timer->update();
} }
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) { if ($buffer) {
$this->executeBuffer($buffer); $this->executeBuffer($buffer);
} }
@ -356,22 +494,48 @@ class Shell
if (trim($output)) { if (trim($output)) {
$this->lineReader->erase(); $this->lineReader->erase();
echo rtrim($output)."\n"; echo rtrim($output)."\n";
if ($this->running)
$this->lineReader->redraw(); $this->lineReader->redraw();
} }
if (!$this->context) { if (!$this->context) {
$this->stop(); $this->stop();
break;
} }
if ($buffer) { if ($buffer) {
$this->dispatchEvent("prompt", [ "context"=>$this->context ]); $this->dispatchEvent(self::EVT_UPDATE_PROMPT);
} }
} }
} catch (\Exception $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;
} }
/**
* Helper function to emit an event with shell instance and context included.
*
* @param string $type The event type
* @param array[] $data The userdata of the event
*/
private function dispatchEvent($type, array $data=[])
{
$data['shell'] = $this;
$data['context'] = $this->context;
$event = new Event($type, $data);
$this->emitEvent($type, $event);
return $event;
}
/**
* Stop the shell; calling this method will cause the main run() to return.
*
*/
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;

10
lib/TaskInterface.php Normal file
View File

@ -0,0 +1,10 @@
<?php
namespace NoccyLabs\Shell;
interface TaskInterface
{
public function update();
public function isValid();
}