Files
DomFramework/console.php
2018-06-11 19:53:12 +00:00

510 lines
16 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,
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 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
$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);
}
// }}}
}