Files
DomFramework/console.php

674 lines
20 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/** DomFramework
* @package domframework
* @author Dominique Fournier <dominique@fournier38.fr>
*/
/** 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, 27,
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
* The UTF8 chars are return as multiple length chars
* @return the pressed char
*/
public function getKey ()
// {{{
{
$char = fgetc (STDIN);
if ($char === chr (27))
{
$sequence = $char;
$char2 = fgetc (STDIN);
if (ord ($char2) === 27)
{
// Sequence of ESC ESC
}
elseif (ord ($char2) === 79)
{
// Start an ESC SS3 sequence
// Like F2 Key
$sequence .= $char2;
$char = fgetc (STDIN);
$sequence .= $char;
}
elseif (ord ($char2) === 91)
{
// Start an ESC CSI sequence. Do not display it, just return it.
// The ESC squences are used to communicate the cursor keys, associated
// with the Ctrl key, by example
// ESC [ is followed by any number (including none) of "parameter bytes"
// in the range 0x300x3F (ASCII 09:;<=>?), then by any number of
// "intermediate bytes" in the range 0x200x2F (ASCII space and
// !"#$%&'()*+,-./), then finally by a single "final byte" in the range
// 0x400x7E (ASCII @AZ[\]^_`az{|}~).[14]:5.4
$sequence .= $char2;
$char = fgetc (STDIN);
while (ord ($char) < 64 || ord ($char) > 126)
{
$sequence .= $char;
$char = fgetc (STDIN);
}
$sequence .= $char;
}
else
{
$this->consoleException ("Invalid ESC seq : ".ord($char2));
}
return $sequence;
}
// UTF Sequence :
// - char1 < 128 : One char (like ascii)
// - char1 >= 194 && char1 <= 223 : Two chars
// - char1 >= 224 && char1 <= 239 : Three chars
// - char1 >= 240 && char1 <= 244 : Four chars
if (ord ($char) < 128)
{
// One Char like ascii
}
elseif (ord ($char) >= 194 && ord ($char) <= 223)
{
// Two chars
$char .= fgetc (STDIN);
}
elseif (ord ($char) >= 224 && ord ($char) <= 239)
{
// Three chars
$char .= fgetc (STDIN).fgetc (STDIN);
}
elseif (ord ($char) >= 240 && ord ($char) <= 244)
{
// Four chars
$char .= fgetc (STDIN).fgetc (STDIN).fgetc (STDIN);
}
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 = mb_strlen ($this->lineContent);
// The cursor position from first char of line.
$cursorPos = $minLength;
// Manage the history and a temporary buffer if the user has already type
// something before calling the history
$historyTmp = "";
$historyPos = count ($this->history);
$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, $this->mb_str_split ($stopperChar)))
{
// End of process with stopperChars
$this->lineContent = "";
break;
}
if ($this->completionKeys !== false &&
in_array ($char, $this->mb_str_split ($this->completionKeys)))
// Manage autocompletion
// {{{
{
$completeArr = call_user_func ($this->completionFunction, $string);
$isAssoc = is_array ($completeArr) &&
array_diff_key ($completeArr, array_keys (array_keys ($completeArr)));
if (count ($completeArr) === 1)
{
if (substr ($string, -1) !== " ")
{
// Continuous word is unique : replace the last word by the proposal
$pos = mb_strrpos ($string, " ");
if ($pos === false)
$string = "";
else
{
$string = mb_substr ($string, 0, $pos +1);
}
}
else
{
// Next word is unique : add this to the string
}
if ($isAssoc)
$string .= key ($completeArr);
else
$string .= reset ($completeArr);
}
else
{
// Multiple answers : display them
if ($isAssoc)
{
// In associative array, the key is the possible answer to
// autocompletion, and the value is the helper message
// Get the largest key length to make a beautiful alignment
$maxlen = 0;
foreach ($completeArr as $key => $val)
$maxlen = max ($maxlen, mb_strlen ($key));
$maxlen = $maxlen + 5;
echo "\n";
foreach ($completeArr as $key => $val)
printf ("%-${maxlen}s %s\n", $key, $val);
}
else
{
echo "\n".implode ("\n", $completeArr)."\n";
}
}
$cursorPos = mb_strlen ($prompt.$string);
$this->rewriteLine ($prompt.$string);
$this->moveCursor ($cursorPos);
}
// }}}
elseif (ord($char) === 21)
// Empty line from prompt to cursor (Ctrl+U)
// {{{
{
$string = mb_substr ($string, $cursorPos - $minLength);
$cursorPos = $minLength;
$this->rewriteLine ($prompt.$string);
$this->moveCursor ($cursorPos);
}
// }}}
elseif (ord($char) === 23)
// Remove the last word (Ctrl+W)
// {{{
{
$tmp = mb_substr ($string, 0, $cursorPos - $minLength);
$end = mb_substr ($string, $cursorPos - $minLength);
$tmp = rtrim ($tmp);
$pos = mb_strrpos ($tmp, " ");
if ($pos !== false)
$pos++;
$string = mb_substr ($string, 0, $pos).$end;
$cursorPos = $minLength + $pos;
$this->rewriteLine ($prompt.$string);
$this->moveCursor ($cursorPos);
}
// }}}
elseif (ord($char) === 127)
// Remove the previous char (Backspace)
// {{{
{
if ($cursorPos <= $minLength)
continue;
$cursorPos--;
$strArr = $this->mb_str_split ($string);
unset ($strArr[$cursorPos - $minLength]);
$string = implode ($strArr);
$this->rewriteLine ($prompt.$string);
$this->moveCursor ($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 (49).chr (59).chr (53).chr (67))
// Cursor right + Ctrl : cursor jump by word
// {{{
{
$tmp = mb_substr ($string, $cursorPos - $minLength);
$tmp = ltrim ($tmp);
$pos = strpos ($tmp, " ");
if ($pos !== false)
$cursorPos += $pos +1 ;
else
$cursorPos = mb_strlen ($prompt.$string);
$this->moveCursor ($cursorPos);
}
// }}}
elseif ($char === chr (27).chr (91).chr (49).chr (59).chr (53).chr (68))
// Cursor left + Ctrl : cursor jump by word
// {{{
{
$tmp = mb_substr ($string, 0, $cursorPos - $minLength);
$tmp = rtrim ($tmp);
$pos = strrpos ($tmp, " ");
if ($pos !== false) $pos++;
$cursorPos = $minLength + $pos;
$this->moveCursor ($cursorPos);
}
// }}}
elseif ($char === chr (27).chr (91).chr (65))
// Cursor up : display the previous history if defined
// {{{
{
if ($string !== "" && $historyTmp == "")
{
$historyTmp = $string;
}
if ($historyPos > 0)
{
$historyPos--;
$string = $this->history[$historyPos];
$cursorPos = mb_strlen ($prompt.$string);
$this->rewriteLine ($prompt.$string);
$this->moveCursor ($cursorPos);
}
}
// }}}
elseif ($char === chr (27).chr (91).chr (66))
// Cursor down : display the next history if defined
// {{{
{
if ($historyPos < count ($this->history) - 1)
{
$historyPos++;
$string = $this->history[$historyPos];
}
elseif ($historyTmp !== "")
{
$string = $historyTmp;
}
$cursorPos = mb_strlen ($prompt.$string);
$this->rewriteLine ($prompt.$string);
$this->moveCursor ($cursorPos);
}
// }}}
elseif ($char === chr (27).chr (91).chr (67))
// Cursor right
// {{{
{
if ($cursorPos < mb_strlen ($this->lineContent))
{
$cursorPos++;
$this->moveCursor ($cursorPos);
}
}
// }}}
elseif ($char === chr (27).chr (91).chr (68))
// Cursor left
// {{{
{
if ($cursorPos > $minLength)
{
$cursorPos--;
$this->moveCursor ($cursorPos);
}
}
// }}}
elseif ($char === chr (27).chr (91).chr (70))
// End key
// {{{
{
$cursorPos = $minLength + mb_strlen ($string);
$this->moveCursor ($cursorPos);
}
// }}}
elseif ($char === chr (27).chr (91).chr (72))
// Home key
// {{{
{
$cursorPos = $minLength;
$this->moveCursor ($cursorPos);
}
// }}}
elseif ($char === chr (27).chr (91).chr (51).chr (126))
// Remove the char under the cursor (Delete)
// {{{
{
if ($cursorPos >= mb_strlen ($prompt.$string))
continue;
$strArr = $this->mb_str_split ($string);
unset ($strArr[$cursorPos - $minLength]);
$string = implode ($strArr);
$this->rewriteLine ($prompt.$string);
$this->moveCursor ($cursorPos);
}
// }}}
}
else
// Normal char : Add it to the string
// {{{
{
$strArr = $this->mb_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++;
$this->rewriteLine ($prompt.$string);
$this->moveCursor ($cursorPos);
}
// }}}
}
return $string;
}
// }}}
/** Rewrite the line with the provided $text.
* Delete all the old data
* @param string $text The new text to use on line
*/
private function rewriteLine ($text)
// {{{
{
if ($this->echoMode)
{
echo "\r".str_repeat (" ", mb_strlen ($this->lineContent))."\r";
$this->lineContent = $text;
echo $this->lineContent;
}
}
// }}}
/** Move the cursor on position $position
* @param integer $cursorPos The new position on line
*/
private function moveCursor ($cursorPos)
// {{{
{
if ($this->echoMode)
{
echo "\r".str_repeat (chr (27).chr (91).chr (67), $cursorPos);
}
}
// }}}
/** Clear the existing line.
*/
public function clearLine ()
// {{{
{
echo "\r".str_repeat (" ", mb_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;
}
// }}}
/** Get the actual history in memory
*/
public function getHistory ()
// {{{
{
return $this->history;
}
// }}}
/** Clear the history
* Do NOT write the empty history on disk
*/
public function clearHistory ()
// {{{
{
$this->history = array ();
return $this;
}
// }}}
/** 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");
file_put_contents ($historyFile, implode ("\n", $this->history),
FILE_APPEND);
return $this;
}
// }}}
/** 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.
* The new line can not be empty : it is not stored, but without error
* 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");
if (trim ($line) === "")
return $this;
$this->history[] = $line;
$this->history = array_slice ($this->history, -$this->historyMaxSize);
return $this;
}
// }}}
/** 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);
}
// }}}
/** This function return an array with each char, but supports UTF-8
* @param string $string The string to explode
* @return array
*/
private function mb_str_split ($string)
// {{{
{
$res = array();
for ($i = 0; $i < mb_strlen ($string); $i++)
$res[] = mb_substr ($string, $i, 1);
return $res;
}
// }}}
}