diff --git a/console.php b/console.php index d93be59..c895e7f 100644 --- a/console.php +++ b/console.php @@ -29,7 +29,7 @@ class console * displayed but are correctely captured */ private $nonWriteableChar = array (1, 2, 4, 6, 8, 9, 16, 18, - 20, 21, 22, 23, 24, 25, + 20, 21, 22, 23, 24, 25, 27, 127); /* The history list in an array */ @@ -121,6 +121,7 @@ class console /** 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 () @@ -130,25 +131,69 @@ class console 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) + $char2 = fgetc (STDIN); + if (ord ($char2) === 27) { - $sequence .= $char; - $char = fgetc (STDIN); + // 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 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 + $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)); } - $sequence .= $char; 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; @@ -165,9 +210,13 @@ class console { // Gets can not delete chars before the call. Keep the prompt (if exists) $prompt = $this->lineContent; - $minLength = strlen ($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) { @@ -178,16 +227,18 @@ class console $this->lineContent = ""; break; } - if ($stopperChar !== false && in_array ($char, str_split ($stopperChar))) + 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, str_split ($this->completionKeys))) + in_array ($char, $this->mb_str_split ($this->completionKeys))) + // Manage autocompletion + // {{{ { - // Manage autocompletion $completeArr = call_user_func ($this->completionFunction, $string); if (count ($completeArr) === 1) { @@ -199,12 +250,12 @@ class console else { // Continuous word is unique : replace the last word by the proposal - $pos = strrpos ($string, " "); + $pos = mb_strrpos ($string, " "); if ($pos === false) $string = reset ($completeArr); else { - $string = substr ($string, 0, $pos +1); + $string = mb_substr ($string, 0, $pos +1); $string .= reset ($completeArr); } } @@ -214,65 +265,51 @@ class console // 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); + $cursorPos = mb_strlen ($prompt.$string); + $this->rewriteLine ($prompt.$string); + $this->moveCursor ($cursorPos); } + // }}} elseif (ord($char) === 21) + // Empty line from prompt to cursor (Ctrl+U) + // {{{ { - // 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; + $string = mb_substr ($string, $cursorPos - $minLength); $cursorPos = $minLength; - // Move the cursor on the correct position - echo "\r".str_repeat (chr (27).chr (91).chr (67), $cursorPos); - // }}} + $this->rewriteLine ($prompt.$string); + $this->moveCursor ($cursorPos); } + // }}} elseif (ord($char) === 23) + // Remove the last word (Ctrl+W) + // {{{ { - // Remove the last word (Ctrl+W) - // TODO : Cursor management !! - $tmp = substr ($string, 0, $cursorPos - $minLength); - $end = substr ($string, $cursorPos - $minLength); + $tmp = mb_substr ($string, 0, $cursorPos - $minLength); + $end = mb_substr ($string, $cursorPos - $minLength); $tmp = rtrim ($tmp); - $pos = strrpos ($tmp, " "); - $string = substr ($string, 0, $pos).$end; + $pos = mb_strrpos ($tmp, " "); + if ($pos !== false) + $pos++; + $string = mb_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); - } + $this->rewriteLine ($prompt.$string); + $this->moveCursor ($cursorPos); } + // }}} elseif (ord($char) === 127) + // Remove the previous char (Backspace) + // {{{ { - // Remove the previous char (Backspace) - // {{{ - if (strlen ($this->lineContent) < $minLength + 1) - continue; - if ($cursorPos < $minLength +1) + if ($cursorPos <= $minLength) continue; $cursorPos--; - $strArr = str_split ($string); + $strArr = $this->mb_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); - } - // }}} + $this->rewriteLine ($prompt.$string); + $this->moveCursor ($cursorPos); } + // }}} elseif (ord ($char{0}) === 27) { // ESC SEQUENCE @@ -280,96 +317,175 @@ class console 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)) + if ($char === chr (27).chr (91).chr (49).chr (59).chr (53).chr (67)) + // Cursor right + Ctrl : cursor jump by word + // {{{ { - // Cursor right - // {{{ - if ($cursorPos < strlen ($this->lineContent)) - { - echo $char; - $cursorPos++; - } - // }}} + $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 (68)) + // }}} + 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 + // {{{ { - // Cursor left - // {{{ if ($cursorPos > $minLength) { - echo $char; $cursorPos--; + $this->moveCursor ($cursorPos); } - // }}} } + // }}} elseif ($char === chr (27).chr (91).chr (70)) + // End key + // {{{ { - // End key - // {{{ - $cursorPos = $minLength + strlen ($string); - // Move the cursor on the correct position - echo "\r".str_repeat (chr (27).chr (91).chr (67), $cursorPos); - // }}} + $cursorPos = $minLength + mb_strlen ($string); + $this->moveCursor ($cursorPos); } + // }}} elseif ($char === chr (27).chr (91).chr (72)) + // Home key + // {{{ { - // Home key - // {{{ $cursorPos = $minLength; - // Move the cursor on the correct position - echo "\r".str_repeat (chr (27).chr (91).chr (67), $cursorPos); - // }}} + $this->moveCursor ($cursorPos); } + // }}} elseif ($char === chr (27).chr (91).chr (51).chr (126)) + // Remove the char under the cursor (Delete) + // {{{ { - // Remove the char under the cursor (Delete) - // {{{ - if ($cursorPos >= strlen ($this->lineContent)) + if ($cursorPos >= mb_strlen ($prompt.$string)) continue; - $cursorPos; - $strArr = str_split ($string); + $strArr = $this->mb_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); - } - // }}} + $this->rewriteLine ($prompt.$string); + $this->moveCursor ($cursorPos); } + // }}} } else + // Normal char : Add it to the string + // {{{ { - // Normal char : Add it to the string - // {{{ - $strArr = str_split ($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++; - 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); - // }}} + $this->rewriteLine ($prompt.$string); + $this->moveCursor ($cursorPos); } + // }}} } return $string; } // }}} - /** Clear the existing line + /** 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 (" ", strlen ($this->lineContent))."\r"; + echo "\r".str_repeat (" ", mb_strlen ($this->lineContent))."\r"; $this->lineContent = ""; } // }}} @@ -397,6 +513,15 @@ class console } // }}} + /** Get the actual history in memory + */ + public function getHistory () + // {{{ + { + return $this->history; + } + // }}} + /** Clear the history * Do NOT write the empty history on disk */ @@ -404,6 +529,7 @@ class console // {{{ { $this->history = array (); + return $this; } // }}} @@ -433,8 +559,9 @@ class console $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_put_contents ($historyFile, implode ("\n", $this->history), FILE_APPEND); + return $this; } // }}} @@ -466,6 +593,7 @@ class console // }}} /** 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 */ @@ -475,8 +603,11 @@ class console 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; } // }}} @@ -506,4 +637,17 @@ class console } // }}} + /** 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; + } + // }}} }