600 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			600 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
<?php
 | 
						|
 | 
						|
namespace NoccyLabs\Shell;
 | 
						|
 | 
						|
use NoccyLabs\Shell\LineRead;
 | 
						|
use NoccyLabs\TinyEvent\EventEmitterTrait;
 | 
						|
use NoccyLabs\TinyEvent\Event;
 | 
						|
 | 
						|
class Shell
 | 
						|
{
 | 
						|
    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_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;
 | 
						|
    /**
 | 
						|
     * @var Context The current context
 | 
						|
     */
 | 
						|
    protected $context = null;
 | 
						|
    /**
 | 
						|
     * @var Context[] The stack of parent contexts
 | 
						|
     */
 | 
						|
    protected $contextStack = [];
 | 
						|
    /**
 | 
						|
     * @var object[] Created timers
 | 
						|
     */ 
 | 
						|
    protected $timers = [];
 | 
						|
    /**
 | 
						|
     * @var TaskInterface[] Created tasks
 | 
						|
     */
 | 
						|
    protected $tasks = [];
 | 
						|
    /**
 | 
						|
     * @var string The prompt string
 | 
						|
     */
 | 
						|
    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;
 | 
						|
    /**
 | 
						|
     * @var callable The callback to pass the input on to
 | 
						|
     */
 | 
						|
    protected $input_callback = null;
 | 
						|
    /**
 | 
						|
     * @var string Question prompt for getInput()
 | 
						|
     */
 | 
						|
    protected $input_prompt = null;
 | 
						|
    /**
 | 
						|
     * @var string The prompt before changing to $input_prompt
 | 
						|
     */
 | 
						|
    protected $input_last_prompt = null;
 | 
						|
 | 
						|
    /**
 | 
						|
     * Constructor
 | 
						|
     *
 | 
						|
     */
 | 
						|
    public function __construct()
 | 
						|
    {
 | 
						|
        $t = $this;
 | 
						|
        register_shutdown_function(function () use (&$t) {
 | 
						|
            if ($t) unset($t);
 | 
						|
        });
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Destructor
 | 
						|
     *
 | 
						|
     */
 | 
						|
    public function __destruct()
 | 
						|
    {
 | 
						|
        if ($this->lineReader) {
 | 
						|
            $this->lineReader = null;
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Return the current context
 | 
						|
     *
 | 
						|
     * @return Context The current context
 | 
						|
     */
 | 
						|
    public function getContext()
 | 
						|
    {
 | 
						|
        return $this->context;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Push a new primary context, saving the previous contexts on a stack.
 | 
						|
     *
 | 
						|
     * @param Context $context
 | 
						|
     */
 | 
						|
    public function pushContext(Context $context)
 | 
						|
    {
 | 
						|
        if ($this->context) {
 | 
						|
            $context->setParent($this->context);
 | 
						|
            array_unshift($this->contextStack, $this->context);
 | 
						|
        }
 | 
						|
        $context->setShell($this);
 | 
						|
        $this->context = $context;
 | 
						|
        $this->dispatchEvent(self::EVT_CONTEXT_CHANGED);
 | 
						|
        $context->onEnter();
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Pop the current context.
 | 
						|
     *
 | 
						|
     * @return Context
 | 
						|
     */
 | 
						|
    public function popContext()
 | 
						|
    {
 | 
						|
        $previous = $this->context;
 | 
						|
        if (count($this->contextStack)>0) {
 | 
						|
            $this->context = array_shift($this->contextStack);
 | 
						|
        } else {
 | 
						|
            $this->context = null;
 | 
						|
        }
 | 
						|
        $this->dispatchEvent(self::EVT_CONTEXT_CHANGED);
 | 
						|
        return $previous;
 | 
						|
    }
 | 
						|
 | 
						|
    public function getContextPath($separator=":")
 | 
						|
    {
 | 
						|
        // Return null if we don't have a current context
 | 
						|
        if (!$this->context) 
 | 
						|
            return null;
 | 
						|
        
 | 
						|
        // Assemble the contexts to walk
 | 
						|
        $stack = [ $this->context->getName() ];
 | 
						|
        foreach ($this->contextStack as $context) {
 | 
						|
            $stack[] = $context->getName();
 | 
						|
        }
 | 
						|
 | 
						|
        // Reverse the order to make it more logical
 | 
						|
        $stack = array_reverse($stack);
 | 
						|
        return join($separator,$stack);
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * 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($name);
 | 
						|
        $this->pushContext($context);
 | 
						|
        return $context;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * 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
 | 
						|
     * impact on performance.
 | 
						|
     *
 | 
						|
     * @param int $interval Interval in ms
 | 
						|
     * @param callable $handler The handler
 | 
						|
     * @param array $userdata Data to be passed to the handler
 | 
						|
     * @return Timer 
 | 
						|
     */
 | 
						|
    public function addTimer($interval, callable $handler, array $userdata=[])
 | 
						|
    {
 | 
						|
        $timer = new class($interval, $handler, $userdata) {
 | 
						|
            private $next;
 | 
						|
            private $interval;
 | 
						|
            private $handler;
 | 
						|
            private $userdata;
 | 
						|
            public function __construct($interval, callable $handler, array $userdata) {
 | 
						|
                $this->interval = $interval / 1000;
 | 
						|
                $this->next = microtime(true) + $this->interval;
 | 
						|
                $this->handler = $handler;
 | 
						|
                $this->userdata = $userdata;
 | 
						|
            }
 | 
						|
            public function update() {
 | 
						|
                $now = microtime(true);
 | 
						|
                if ($now > $this->next) {
 | 
						|
                    $this->next = $now + $this->interval;
 | 
						|
                    call_user_func($this->handler, $this->userdata);
 | 
						|
                }
 | 
						|
            }
 | 
						|
        };
 | 
						|
        $this->timers[] = $timer;
 | 
						|
        return $timer;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Remove a created timer.
 | 
						|
     *
 | 
						|
     * @param 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)
 | 
						|
    {
 | 
						|
        $this->prompt = $text;
 | 
						|
 | 
						|
        if ($this->lineReader) {
 | 
						|
            $this->lineReader->setPromptText($text);
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * 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.
 | 
						|
     *
 | 
						|
     * @return callable The command
 | 
						|
     */ 
 | 
						|
    private function findCommand($command)
 | 
						|
    {
 | 
						|
        // Go over current context and walk through stack until finding command
 | 
						|
        if ($this->context->hasCommand($command)) {
 | 
						|
            $handler = $this->context->getCommand($command);
 | 
						|
        } else {
 | 
						|
            foreach($this->contextStack as $context) {
 | 
						|
                if ($context->hasCommand($command) && $context->isCommandGlobal($command)) {
 | 
						|
                    $handler = $context->getCommand($command);
 | 
						|
                    break;
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        // No handler...
 | 
						|
        if (empty($handler)) {
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
 | 
						|
        // Return closure
 | 
						|
        return function (...$args) use ($handler) {
 | 
						|
            return call_user_func($handler, ...$args);
 | 
						|
        };
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Execute a command with arguments.
 | 
						|
     *
 | 
						|
     * @param string $command The command name to execute
 | 
						|
     * @param string ...$args Arguments
 | 
						|
     * @return mixed 
 | 
						|
     * @throws Exception\BadCommandExcception
 | 
						|
     */
 | 
						|
    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;
 | 
						|
        }
 | 
						|
 | 
						|
        // Call the handler if the command was found
 | 
						|
        if (($target = $this->findCommand($command))) {
 | 
						|
            $ret = $target(...$args);
 | 
						|
            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;
 | 
						|
        }
 | 
						|
 | 
						|
        // Throw error if the command could not be 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)
 | 
						|
    {
 | 
						|
        switch ($command) {
 | 
						|
            case '.':
 | 
						|
                $type = basename(strtr(get_class($this->context), "\\", "/"));
 | 
						|
                printf("%s<%s>: %s\n", $type, $this->context->getName(), $this->context->getContextInfo());
 | 
						|
                $level = 0;
 | 
						|
                foreach ($this->contextStack as $context) {
 | 
						|
                    $type = basename(strtr(get_class($context), "\\", "/"));
 | 
						|
                    printf(" %s└─%s<%s>: %s\n", str_repeat("  ",$level++), $type, $context->getName(), $context->getContextInfo());
 | 
						|
                }
 | 
						|
                break;
 | 
						|
            case '..':
 | 
						|
                if (count($this->contextStack)>0)
 | 
						|
                    $this->popContext();
 | 
						|
                break;
 | 
						|
            case 'help':
 | 
						|
                $help = $this->context->getCommandHelp();
 | 
						|
                $ghelp = [];
 | 
						|
                foreach ($this->contextStack as $context) {
 | 
						|
                    $commands = $context->getCommandHelp();
 | 
						|
                    foreach ($commands as $command=>$info) {
 | 
						|
                        if (strpos(" ",$command)!==false) {
 | 
						|
                            list ($cmd,$arg)=explode(" ",$command,2);
 | 
						|
                        } else {
 | 
						|
                            $cmd = $command;
 | 
						|
                        }
 | 
						|
                        if ($context->isCommandGlobal($cmd)) {
 | 
						|
                            $ghelp[$command] = $info;
 | 
						|
                        }
 | 
						|
                    }
 | 
						|
                }
 | 
						|
                ksort($ghelp);
 | 
						|
                $_ = function($command,$args,$info) {
 | 
						|
                    printf("  \e[96m%s\e[0m \e[0;3m%s\e[0m  \e[30G\e[36m%s\e[0m\n", $command, $args, $info);
 | 
						|
                };
 | 
						|
                printf("\e[1mCommands:\e[0m\n");
 | 
						|
                foreach ($help as $command=>$info) {
 | 
						|
                    if (strpos($command," ")!==false) {
 | 
						|
                        list($command,$args) = explode(" ",$command,2);
 | 
						|
                    } else $args=null;
 | 
						|
                    $_($command, $args,$info);
 | 
						|
                }
 | 
						|
                if (count($ghelp)) {
 | 
						|
                    printf("\e[1mCommands from parent contexts:\e[0m\n");
 | 
						|
                    if (strpos($command," ")!==false) {
 | 
						|
                        list($command,$args) = explode(" ",$command,2);
 | 
						|
                    } else $args=null;
 | 
						|
                    $_($command, $args,$info);
 | 
						|
                }
 | 
						|
                printf("\e[1mGlobal commands:\e[0m\n");
 | 
						|
                    $_("exit", null, "Leave the shell");
 | 
						|
                    $_(".", null, "Show the context tree");
 | 
						|
                    $_("..", null, "Discard the current context and go to parent");
 | 
						|
                break;
 | 
						|
            case 'exit':
 | 
						|
                $this->stop();
 | 
						|
                break;
 | 
						|
            default:
 | 
						|
                return false;
 | 
						|
        }        
 | 
						|
        return true;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Parse a string and execute the resulting command.
 | 
						|
     *
 | 
						|
     * @param string $command The string to parse
 | 
						|
     * @return mixed
 | 
						|
     */ 
 | 
						|
    public function executeBuffer(string $command)
 | 
						|
    {
 | 
						|
        $args = str_getcsv($command, " ", "\"", "\\");
 | 
						|
        $command = array_shift($args);
 | 
						|
 | 
						|
        try {
 | 
						|
            $this->executeCommand($command, ...$args);
 | 
						|
        } catch (Exception\ShellException $e) {
 | 
						|
            echo "\e[31;91;1m{$e->getMessage()}\e[0m\n";
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Start the shell
 | 
						|
     *
 | 
						|
     */
 | 
						|
    public function run()
 | 
						|
    {
 | 
						|
        try {
 | 
						|
            $this->lineReader = new LineRead();
 | 
						|
 | 
						|
            $this->lineReader->setPromptText($this->prompt);
 | 
						|
            $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(self::EVT_UPDATE_PROMPT);
 | 
						|
            $this->dispatchEvent(self::EVT_SHELL_START);
 | 
						|
 | 
						|
            while ($this->running) {
 | 
						|
                // Update the input stuff, sleep if nothing to do.
 | 
						|
                if (!($buffer = $this->lineReader->update())) {
 | 
						|
                    usleep(10000);
 | 
						|
                }
 | 
						|
                // Escape is handy too...
 | 
						|
                if ($buffer == "\e") {
 | 
						|
                    $this->dispatchEvent(self::EVT_SHELL_ESCAPE);
 | 
						|
                    continue;
 | 
						|
                }
 | 
						|
                // we get a ^C on ^C, so deal with the ^C.
 | 
						|
                if ($buffer == "\x03") {
 | 
						|
                    $this->dispatchEvent(self::EVT_SHELL_ABORT);
 | 
						|
                    $this->stop();
 | 
						|
                    continue;
 | 
						|
                }
 | 
						|
                // Execute the buffer
 | 
						|
                ob_start();
 | 
						|
                $this->dispatchEvent("update");
 | 
						|
                foreach ($this->timers as $timer) {
 | 
						|
                    $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) {
 | 
						|
                    $this->executeBuffer($buffer);
 | 
						|
                }
 | 
						|
                $output = ob_get_contents();
 | 
						|
                ob_end_clean();
 | 
						|
 | 
						|
                if (trim($output)) {
 | 
						|
                    $this->lineReader->erase();
 | 
						|
                    echo rtrim($output)."\n";
 | 
						|
                    if ($this->running)
 | 
						|
                        $this->lineReader->redraw();
 | 
						|
                }
 | 
						|
 | 
						|
                if (!$this->context) {
 | 
						|
                    $this->stop();
 | 
						|
                    break;
 | 
						|
                }
 | 
						|
 | 
						|
                if ($buffer) {
 | 
						|
                    $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;
 | 
						|
 | 
						|
    }
 | 
						|
    /**
 | 
						|
     * 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;
 | 
						|
    }
 | 
						|
 | 
						|
    public function getInput($prompt, callable $callback)
 | 
						|
    {
 | 
						|
        $this->addListener(Shell::EVT_UPDATE_PROMPT, [ $this, "onInputPrompt" ], $prompt);
 | 
						|
        $this->addListener(Shell::EVT_BEFORE_COMMAND, [ $this, "onInputHandler" ], $this->prompt, $callback);
 | 
						|
    }
 | 
						|
    
 | 
						|
    public function onInputPrompt(Event $e, $prompt)
 | 
						|
    {
 | 
						|
        $this->setPrompt($prompt);
 | 
						|
        $e->stopPropagation();
 | 
						|
        $this->removeListener(Shell::EVT_UPDATE_PROMPT, [ $this, "onInputPrompt" ], $prompt);
 | 
						|
    }
 | 
						|
    
 | 
						|
    public function onInputHandler(Event $e, $last_prompt, $callback)
 | 
						|
    {
 | 
						|
        // Restore the prompt
 | 
						|
        $this->setPrompt($last_prompt);
 | 
						|
        // Remove the listeners and compose the result string
 | 
						|
        $this->removeListener(Shell::EVT_BEFORE_COMMAND, [ $this, "onInputHandler" ], $last_prompt, $callback);
 | 
						|
        $input = trim($e->command." ".join(" ",$e->args));
 | 
						|
        $e->stopPropagation();
 | 
						|
        // Call the callback
 | 
						|
        call_user_func($callback, $input);
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Stop the shell; calling this method will cause the main run() to return.
 | 
						|
     *
 | 
						|
     */
 | 
						|
    public function stop()
 | 
						|
    {
 | 
						|
        $this->running = false;
 | 
						|
    }
 | 
						|
}
 |