git-svn-id: https://svn.fournier38.fr/svn/ProgSVN/trunk@4233 bf3deb0d-5f1a-0410-827f-c0cc1f45334c
510 lines
16 KiB
PHP
510 lines
16 KiB
PHP
<?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,
|
||
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);
|
||
}
|
||
// }}}
|
||
|
||
}
|