*/ /** Allow to manage a linux Console to have a minimal but working text interface * When using this class, you must use the $console::echo method and not * display directely on screen * Like readline, but all in PHP */ class console { // PROPERTIES // {{{ /** Save the initial stty value */ private $initSttyState; /** Line Content */ private $lineContent = ""; /** If true, display each char the user has pressed (echo mode) */ private $echoMode = true; /** List of non printable chars in decimal. The non printable chars are not * displayed but are correctely captured */ private $nonWriteableChar = array (1, 2, 4, 6, 8, 9, 16, 18, 20, 21, 22, 23, 24, 25, 127); /* The history list in an array */ private $history = array (); /** The history max size in entries */ private $historyMaxSize = 1000; /** Set the completion keys. Not set by default */ private $completionKeys = false; /** Set the function called when the completion char is called */ private $completionFunction = array (); // }}} /** The constructor init the console. * Check if we have the rights to execute, if wa have in cli... */ public function __construct () // {{{ { if (! function_exists ("exec")) throw $this->ConsoleException ("No exec support in PHP"); $this->initSttyState = exec ("stty -g"); // Set the terminal to return the value each time a key is pressed. // Do not display anything, so we don't see the characters when the user is // deleting. exec ("stty -echo -icanon min 1 time 0"); } // }}} /** The destructor return the terminal to initial state */ public function __destruct () // {{{ { exec ("stty $this->initSttyState"); } // }}} /** Each time a key is pressed by the user, display the value on screen (echo) */ public function setEcho () // {{{ { $this->echoMode = true; } // }}} /** Each time a key is pressed by the user, DO NOT display the value on screen * (echo disabled) */ public function unsetEcho () // {{{ { $this->echoMode = false; } // }}} /** Display a text on screen * @param string $message The message to display */ public function echo ($message) // {{{ { echo $message; $this->lineContent .= $message; } // }}} /** Wait one valid character from the user. * The non printable chars are not displayed, nor returned * The ESC Sequences are skipped * @return the pressed char */ public function getc () // {{{ { $char = $this->getKey (); while (in_array (ord ($char), $this->nonWriteableChar)) $char = $this->getKey (); return $char; } // }}} /** Wait one key pressed by the user. If the key pressed is an ESC sequence, * return this sequence * The non printable chars are not displayed, but are correctely returned * @return the pressed char */ public function getKey () // {{{ { $char = fgetc (STDIN); if ($char === chr (27)) { $sequence = $char; // Start an ESC CSI sequence. Do not display it, just return it, complete. // ESC [ is followed by any number (including none) of "parameter bytes" // in the range 0x30–0x3F (ASCII 0–9:;<=>?), then by any number of // "intermediate bytes" in the range 0x20–0x2F (ASCII space and // !"#$%&'()*+,-./), then finally by a single "final byte" in the range // 0x40–0x7E (ASCII @A–Z[\]^_`a–z{|}~).[14]:5.4 $char = fgetc (STDIN); if ($char !== chr (91)) $this->consoleException ("Invalid ESC CSI sequence provided"); $sequence .= $char; $char = fgetc (STDIN); while (ord ($char) < 64 || ord ($char) > 126) { $sequence .= $char; $char = fgetc (STDIN); } $sequence .= $char; return $sequence; } if ($this->echoMode && ! in_array (ord ($char), $this->nonWriteableChar)) echo $char; return $char; } // }}} /** Get the line of characters pressed by the user and return the result. * Stop when the user valid by \n. * Manage correctely the backspace, the Ctrl+W to remove word... * @return The typed string */ public function readline ($stopperChar = false) // {{{ { // Gets can not delete chars before the call. Keep the prompt (if exists) $prompt = $this->lineContent; $minLength = strlen ($this->lineContent); // The cursor position from first char of line. $cursorPos = $minLength; $string = ""; while (1) { $char = $this->getKey (); if ($stopperChar === false && $char === "\n") { // End of process without stopperChars $this->lineContent = ""; break; } if ($stopperChar !== false && in_array ($char, str_split ($stopperChar))) { // End of process with stopperChars $this->lineContent = ""; break; } if ($this->completionKeys !== false && in_array ($char, str_split ($this->completionKeys))) { // Manage autocompletion $completeArr = call_user_func ($this->completionFunction, $string); if (count ($completeArr) === 1) { if (substr ($string, -1) === " ") { // Next word is unique : add this to the string $string .= reset ($completeArr); } else { // Continuous word is unique : replace the last word by the proposal $pos = strrpos ($string, " "); if ($pos === false) $string = reset ($completeArr); else { $string = substr ($string, 0, $pos +1); $string .= reset ($completeArr); } } } else { // Multiple answers : display them echo "\n".implode ("\n", $completeArr)."\n"; } echo "\r".str_repeat (" ", strlen ($this->lineContent))."\r"; $this->lineContent = $prompt.$string; echo $this->lineContent; $cursorPos = mb_strlen ($this->lineContent); } elseif (ord($char) === 21) { // Empty line next to prompt (Ctrl+U) // {{{ $string = substr ($string, $cursorPos - $minLength); echo "\r".str_repeat (" ", strlen ($this->lineContent))."\r"; $this->lineContent = $prompt.$string; echo $this->lineContent; $cursorPos = $minLength; // Move the cursor on the correct position echo "\r".str_repeat (chr (27).chr (91).chr (67), $cursorPos); // }}} } elseif (ord($char) === 23) { // Remove the last word (Ctrl+W) // TODO : Cursor management !! $tmp = substr ($string, 0, $cursorPos - $minLength); $end = substr ($string, $cursorPos - $minLength); $tmp = rtrim ($tmp); $pos = strrpos ($tmp, " "); $string = substr ($string, 0, $pos).$end; $cursorPos = $minLength + $pos; if ($this->echoMode) { echo "\r".str_repeat (" ", strlen ($this->lineContent))."\r"; $this->lineContent = $prompt.$string; echo $this->lineContent; // Move the cursor on the correct position echo "\r".str_repeat (chr (27).chr (91).chr (67), $cursorPos); } } elseif (ord($char) === 127) { // Remove the previous char (Backspace) // {{{ if (strlen ($this->lineContent) < $minLength + 1) continue; if ($cursorPos < $minLength +1) continue; $cursorPos--; $strArr = str_split ($string); unset ($strArr[$cursorPos - $minLength]); $string = implode ($strArr); if ($this->echoMode) { echo "\r".str_repeat (" ", strlen ($this->lineContent))."\r"; $this->lineContent = $prompt.$string; echo $this->lineContent; // Move the cursor on the correct position echo "\r".str_repeat (chr (27).chr (91).chr (67), $cursorPos); } // }}} } elseif (ord ($char{0}) === 27) { // ESC SEQUENCE /*file_put_contents ("/tmp/debug", "SEQ CHAR=", FILE_APPEND); foreach (str_split ($char) as $key) file_put_contents ("/tmp/debug", ord ($key)." ", FILE_APPEND); file_put_contents ("/tmp/debug", "\n", FILE_APPEND);*/ if ($char === chr (27).chr (91).chr (67)) { // Cursor right // {{{ if ($cursorPos < strlen ($this->lineContent)) { echo $char; $cursorPos++; } // }}} } elseif ($char === chr (27).chr (91).chr (68)) { // Cursor left // {{{ if ($cursorPos > $minLength) { echo $char; $cursorPos--; } // }}} } elseif ($char === chr (27).chr (91).chr (70)) { // End key // {{{ $cursorPos = $minLength + strlen ($string); // Move the cursor on the correct position echo "\r".str_repeat (chr (27).chr (91).chr (67), $cursorPos); // }}} } elseif ($char === chr (27).chr (91).chr (72)) { // Home key // {{{ $cursorPos = $minLength; // Move the cursor on the correct position echo "\r".str_repeat (chr (27).chr (91).chr (67), $cursorPos); // }}} } elseif ($char === chr (27).chr (91).chr (51).chr (126)) { // Remove the char under the cursor (Delete) // {{{ if ($cursorPos >= strlen ($this->lineContent)) continue; $cursorPos; $strArr = str_split ($string); unset ($strArr[$cursorPos - $minLength]); $string = implode ($strArr); if ($this->echoMode) { echo "\r".str_repeat (" ", strlen ($this->lineContent))."\r"; $this->lineContent = $prompt.$string; echo $this->lineContent; // Move the cursor on the correct position echo "\r".str_repeat (chr (27).chr (91).chr (67), $cursorPos); } // }}} } } else { // Normal char : Add it to the string // {{{ $strArr = str_split ($string); $firstArr = array_slice ($strArr, 0, $cursorPos - $minLength); $lastArr = array_slice ($strArr, $cursorPos - $minLength); $insertArr = array ($char); $strArr = array_merge ($firstArr, $insertArr, $lastArr); $string = implode ($strArr); $cursorPos++; echo "\r"; $this->lineContent = $prompt.$string; echo $this->lineContent; // Move the cursor on the correct position echo "\r".str_repeat (chr (27).chr (91).chr (67), $cursorPos); // }}} } } return $string; } // }}} /** Clear the existing line */ public function clearLine () // {{{ { echo "\r".str_repeat (" ", strlen ($this->lineContent))."\r"; $this->lineContent = ""; } // }}} /** Call a specific function when a completion key is pressed * @param string|bool $completionKeys The list of the completion keys. False * unset the method * @param callable $completionFunction The function called when one of the * completion keys is pressed. * The function must get the partial text as first parameter, and must return * an array with the possibilities */ public function completeFunction ($completionKeys, $completionFunction) // {{{ { if (! is_string ($completionKeys) && ! is_boolean ($completionKeys)) $this->consoleException ("Can not set the completionKeys : not a string"); if ($completionKeys === true) $this->consoleException ("Can not set the completionKeys : not false"); if (! is_callable ($completionFunction)) $this->consoleException ("Can not set the completionFunction : ". "not a callable function"); $this->completionKeys = $completionKeys; $this->completionFunction = $completionFunction; } // }}} /** Clear the history * Do NOT write the empty history on disk */ public function clearHistory () // {{{ { $this->history = array (); } // }}} /** Write the history to disk. * @param string $historyFile The history file where the history is stored */ public function writeHistory ($historyFile) // {{{ { if (file_exists ($historyFile)) { if (! is_writeable ($historyFile)) $this->consoleException ("History file '$historyFile' ". "is not writeable"); $history = file_get_contents ($historyFile); if ($history === false) $this->consoleException ("History file '$historyFile' can not be read"); $historyArr = explode ("\n", $history); if (! isset ($historyArr[0]) || $historyArr[0] !== "__HISTORY__") $this->consoleException ("History file '$historyFile' ". "is not an history file : do not touch\n"); } elseif (! file_exists (dirname ($historyFile))) $this->consoleException ("History file '$historyFile' ". "can not be created: parent directory doesn't exists"); elseif (! is_dir (dirname ($historyFile))) $this->consoleException ("History file '$historyFile' ". "can not be created: parent directory is not a directory"); file_put_contents ($historyFile, "__HISTORY__\n"); return !! file_put_contents ($historyFile, implode ("\n", $this->history), FILE_APPEND); } // }}} /** Read the history from the disk * If the file doesn't exists, return an empty array * @param string $historyFile The history file where the history is stored */ public function readHistory ($historyFile) // {{{ { if (! file_exists ($historyFile)) { $this->history = array (); return array (); } if (! is_readable ($historyFile)) $this->consoleException ("History file '$historyFile' can not be read"); $history = file_get_contents ($historyFile); if ($history === false) $this->consoleException ("History file '$historyFile' can not be read"); $historyArr = explode ("\n", $history); if (! isset ($historyArr[0]) || $historyArr[0] !== "__HISTORY__") $this->consoleException ("History file '$historyFile' ". "is not an history file : do not touch\n"); array_shift ($historyArr); $this->history = $historyArr; return $this->history; } // }}} /** Add a new entry in history. * Do NOT write the history on disk. * @param string The new entry to add in history */ public function addHistory ($line) // {{{ { if (! is_string ($line)) $this->consoleException ("Can not add line to history : ". "it is not a string"); $this->history[] = $line; $this->history = array_slice ($this->history, -$this->historyMaxSize); } // }}} /** Get/Set the maximum number of entries in the history * If null, get the defined maximum number * @param integer|null $historyMaxSize The maximum number of entries */ public function historyMaxSize ($historyMaxSize = null) // {{{ { if ($historyMaxSize === null) return $this->historyMaxSize; if (intval ($historyMaxSize) < 1) $this->consoleException ("Can not set the historyMaxSize : ". "negative value provided"); $this->historyMaxSize = intval ($historyMaxSize); } // }}} /** Error management * @param string $message The message to throw in the exception */ public function consoleException ($message) // {{{ { throw new \Exception ($message, 500); } // }}} }