configure(); } protected function configure() { } /** * 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("context.update", [ "context"=>$this->context ]); } /** * 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("context.update", [ "context"=>$this->context ]); 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); } public function createContext() { $context = new Context(); $this->pushContext($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. * 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=[]) { } /** * Remove a created timer. * * @param Timer $timer */ public function removeTimer(Timer $timer) { } public function setPrompt($text) { if (!$this->lineReader) { return; } $this->lineReader->setPromptText($text); } /** * 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 foreach(array_merge([ $this->context ] , $this->contextStack) as $context) { if ($context->hasCommand($command)) { $handler = $context->getCommand($command); break; } } 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->executeBuiltin($command, ...$args)) { return; } // Call the handler if the command was found if (($target = $this->findCommand($command))) { $ret = $target(...$args); if ($ret instanceof Context) { $this->pushContext($ret); } return; } // Throw error if the command could not be found throw new Exception\BadCommandException("Command {$command} not found"); } public function executeBuiltin($command, ...$args) { switch ($command) { case '.': $type = basename(strtr(get_class($this->context), "\\", "/")); printf("%s<%s>: %s\n", $type, $this->context->getName(), json_encode($this->context->getData())); $level = 0; foreach ($this->contextStack as $context) { $type = basename(strtr(get_class($context), "\\", "/")); printf(" %s- %s<%s>\n", str_repeat(" ",$level++), $type, $context->getName()); } break; case '..': if (count($this->contextStack)>0) $this->popContext(); break; case 'help': $help = $this->context->getCommandHelp(); printf("Commands in current context:\n\n"); foreach ($help as $command=>$info) { printf(" %-20s %s\n", $command, $info); } printf("\nGlobal commands:\n\n"); printf(" %-20s %s\n", "exit", "Leave the shell"); printf(" %-20s %s\n", "..", "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"; } /* 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() { $this->lineReader = new LineRead(); $this->lineReader->setPromptText("shell>"); $this->lineReader->setPromptStyle(new Style(Style::BR_GREEN)); $this->lineReader->setCommandStyle(new Style(Style::GREEN)); $this->running = true; $this->dispatchEvent("prompt", [ "context"=>$this->context ]); while ($this->running) { // Update the input stuff, sleep if nothing to do. if (!($buffer = $this->lineReader->update())) { usleep(10000); } // we get a ^C on ^C, so deal with the ^C. if ($buffer == "\x03") { $this->stop(); continue; } // Execute the buffer ob_start(); $this->dispatchEvent("update"); if ($buffer) { $this->executeBuffer($buffer); } $output = ob_get_contents(); ob_end_clean(); if (trim($output)) { $this->lineReader->erase(); echo rtrim($output)."\n"; $this->lineReader->redraw(); } if (!$this->context) { $this->stop(); } if ($buffer) { $this->dispatchEvent("prompt", [ "context"=>$this->context ]); } } $this->lineReader = null; } public function stop() { $this->running = false; } }