Hoa central
Readline.php
Go to the documentation of this file.
1 <?php
2 
37 namespace Hoa\Console\Readline;
38 
39 use Hoa\Console;
40 use Hoa\Core;
41 
50 class Readline
51 {
57  const STATE_CONTINUE = 1;
58 
64  const STATE_BREAK = 2;
65 
71  const STATE_NO_ECHO = 4;
72 
78  protected $_line = null;
79 
85  protected $_lineCurrent = 0;
86 
92  protected $_lineLength = 0;
93 
99  protected $_buffer = null;
100 
106  protected $_mapping = [];
107 
113  protected $_history = [];
114 
120  protected $_historyCurrent = 0;
121 
127  protected $_historySize = 0;
128 
134  protected $_prefix = null;
135 
141  protected $_autocompleter = null;
142 
143 
144 
150  public function __construct()
151  {
152  if (OS_WIN) {
153  return;
154  }
155 
156  $this->_mapping["\033[A"] = xcallable($this, '_bindArrowUp');
157  $this->_mapping["\033[B"] = xcallable($this, '_bindArrowDown');
158  $this->_mapping["\033[C"] = xcallable($this, '_bindArrowRight');
159  $this->_mapping["\033[D"] = xcallable($this, '_bindArrowLeft');
160  $this->_mapping["\001"] = xcallable($this, '_bindControlA');
161  $this->_mapping["\002"] = xcallable($this, '_bindControlB');
162  $this->_mapping["\005"] = xcallable($this, '_bindControlE');
163  $this->_mapping["\006"] = xcallable($this, '_bindControlF');
164  $this->_mapping["\010"] =
165  $this->_mapping["\177"] = xcallable($this, '_bindBackspace');
166  $this->_mapping["\027"] = xcallable($this, '_bindControlW');
167  $this->_mapping["\n"] = xcallable($this, '_bindNewline');
168  $this->_mapping["\t"] = xcallable($this, '_bindTab');
169 
170  return;
171  }
172 
179  public function readLine($prefix = null)
180  {
181  if (feof(STDIN)) {
182  return false;
183  }
184 
185  $direct = Console::isDirect(STDIN);
186 
187  if (false === $direct || OS_WIN) {
188  $out = fgets(STDIN);
189 
190  if (false === $out) {
191  return false;
192  }
193 
194  $out = substr($out, 0, -1);
195 
196  if (true === $direct) {
197  echo $prefix;
198  } else {
199  echo $prefix, $out, "\n";
200  }
201 
202  return $out;
203  }
204 
205  $this->resetLine();
206  $this->setPrefix($prefix);
207  $read = [STDIN];
208  echo $prefix;
209 
210  while (true) {
211  @stream_select($read, $write, $except, 30, 0);
212 
213  if (empty($read)) {
214  $read = [STDIN];
215 
216  continue;
217  }
218 
219  $char = $this->_read();
220  $this->_buffer = $char;
221  $return = $this->_readLine($char);
222 
223  if (0 === ($return & self::STATE_NO_ECHO)) {
224  echo $this->_buffer;
225  }
226 
227  if (0 !== ($return & self::STATE_BREAK)) {
228  break;
229  }
230  }
231 
232  return $this->getLine();
233  }
234 
241  public function _readLine($char)
242  {
243  if (isset($this->_mapping[$char]) &&
244  is_callable($this->_mapping[$char])) {
245  $mapping = $this->_mapping[$char];
246  $return = $mapping($this);
247  } else {
248  if (isset($this->_mapping[$char])) {
249  $this->_buffer = $this->_mapping[$char];
250  }
251 
252  if ($this->getLineLength() == $this->getLineCurrent()) {
253  $this->appendLine($this->_buffer);
254  $return = static::STATE_CONTINUE;
255  } else {
256  $this->insertLine($this->_buffer);
257  $tail = mb_substr(
258  $this->getLine(),
259  $this->getLineCurrent() - 1
260  );
261  $this->_buffer = "\033[K" . $tail . str_repeat(
262  "\033[D",
263  mb_strlen($tail) - 1
264  );
265 
266  $return = static::STATE_CONTINUE;
267  }
268  }
269 
270  return $return;
271  }
272 
279  public function addMappings(Array $mappings)
280  {
281  foreach ($mappings as $key => $mapping) {
282  $this->addMapping($key, $mapping);
283  }
284 
285  return;
286  }
287 
301  public function addMapping($key, $mapping)
302  {
303  if ('\e[' === substr($key, 0, 3)) {
304  $this->_mapping["\033[" . substr($key, 3)] = $mapping;
305  } elseif ('\C-' === substr($key, 0, 3)) {
306  $_key = ord(strtolower(substr($key, 3))) - 96;
307  $this->_mapping[chr($_key)] = $mapping;
308  } else {
309  $this->_mapping[$key] = $mapping;
310  }
311 
312  return;
313  }
314 
321  public function addHistory($line = null)
322  {
323  if (empty($line)) {
324  return;
325  }
326 
327  $this->_history[] = $line;
328  $this->_historyCurrent = $this->_historySize++;
329 
330  return;
331  }
332 
338  public function clearHistory()
339  {
340  unset($this->_history);
341  $this->_history = [];
342  $this->_historyCurrent = 0;
343  $this->_historySize = 1;
344 
345  return;
346  }
347 
354  public function getHistory($i = null)
355  {
356  if (null === $i) {
358  }
359 
360  if (!isset($this->_history[$i])) {
361  return null;
362  }
363 
364  return $this->_history[$i];
365  }
366 
372  public function previousHistory()
373  {
374  if (0 >= $this->_historyCurrent) {
375  return $this->getHistory(0);
376  }
377 
378  return $this->getHistory($this->_historyCurrent--);
379  }
380 
386  public function nextHistory()
387  {
388  if ($this->_historyCurrent + 1 >= $this->_historySize) {
389  return $this->getLine();
390  }
391 
392  return $this->getHistory(++$this->_historyCurrent);
393  }
394 
400  public function getLine()
401  {
402  return $this->_line;
403  }
404 
411  public function appendLine($append)
412  {
413  $this->_line .= $append;
414  $this->_lineLength = mb_strlen($this->_line);
415  $this->_lineCurrent = $this->_lineLength;
416 
417  return;
418  }
419 
426  public function insertLine($insert)
427  {
428  if ($this->_lineLength == $this->_lineCurrent) {
429  return $this->appendLine($insert);
430  }
431 
432  $this->_line = mb_substr($this->_line, 0, $this->_lineCurrent) .
433  $insert .
434  mb_substr($this->_line, $this->_lineCurrent);
435  $this->_lineLength = mb_strlen($this->_line);
436  $this->_lineCurrent += mb_strlen($insert);
437 
438  return;
439  }
440 
446  protected function resetLine()
447  {
448  $this->_line = null;
449  $this->_lineCurrent = 0;
450  $this->_lineLength = 0;
451 
452  return;
453  }
454 
460  public function getLineCurrent()
461  {
462  return $this->_lineCurrent;
463  }
464 
470  public function getLineLength()
471  {
472  return $this->_lineLength;
473  }
474 
480  public function setPrefix($prefix)
481  {
482  $this->_prefix = $prefix;
483 
484  return;
485  }
486 
492  public function getPrefix()
493  {
494  return $this->_prefix;
495  }
496 
502  public function getBuffer()
503  {
504  return $this->_buffer;
505  }
506 
513  public function setAutocompleter(Autocompleter $autocompleter)
514  {
515  $old = $this->_autocompleter;
516  $this->_autocompleter = $autocompleter;
517 
518  return $old;
519  }
520 
527  public function getAutocompleter()
528  {
529  return $this->_autocompleter;
530  }
531 
538  public function _read($length = 512)
539  {
540  return fread(STDIN, $length);
541  }
542 
549  public function setLine($line)
550  {
551  $this->_line = $line;
552  $this->_lineLength = mb_strlen($this->_line);
553  $this->_lineCurrent = $this->_lineLength;
554 
555  return;
556  }
557 
564  public function setLineCurrent($current)
565  {
566  $this->_lineCurrent = $current;
567 
568  return;
569  }
570 
577  public function setLineLength($length)
578  {
579  $this->_lineLength = $length;
580 
581  return;
582  }
583 
590  public function setBuffer($buffer)
591  {
592  $this->_buffer = $buffer;
593 
594  return;
595  }
596 
604  public function _bindArrowUp(Readline $self)
605  {
606  if (0 === (static::STATE_CONTINUE & static::STATE_NO_ECHO)) {
607  Console\Cursor::clear('↔');
608  echo $self->getPrefix();
609  }
610  $self->setBuffer($buffer = $self->previousHistory());
611  $self->setLine($buffer);
612 
613  return static::STATE_CONTINUE;
614  }
615 
623  public function _bindArrowDown(Readline $self)
624  {
625  if (0 === (static::STATE_CONTINUE & static::STATE_NO_ECHO)) {
626  Console\Cursor::clear('↔');
627  echo $self->getPrefix();
628  }
629 
630  $self->setBuffer($buffer = $self->nextHistory());
631  $self->setLine($buffer);
632 
633  return static::STATE_CONTINUE;
634  }
635 
643  public function _bindArrowRight(Readline $self)
644  {
645  if ($self->getLineLength() > $self->getLineCurrent()) {
646  if (0 === (static::STATE_CONTINUE & static::STATE_NO_ECHO)) {
647  Console\Cursor::move('→');
648  }
649 
650  $self->setLineCurrent($self->getLineCurrent() + 1);
651  }
652 
653  $self->setBuffer(null);
654 
655  return static::STATE_CONTINUE;
656  }
657 
665  public function _bindArrowLeft(Readline $self)
666  {
667  if (0 < $self->getLineCurrent()) {
668  if (0 === (static::STATE_CONTINUE & static::STATE_NO_ECHO)) {
669  Console\Cursor::move('←');
670  }
671 
672  $self->setLineCurrent($self->getLineCurrent() - 1);
673  }
674 
675  $self->setBuffer(null);
676 
677  return static::STATE_CONTINUE;
678  }
679 
687  public function _bindBackspace(Readline $self)
688  {
689  $buffer = null;
690 
691  if (0 < $self->getLineCurrent()) {
692  if (0 === (static::STATE_CONTINUE & static::STATE_NO_ECHO)) {
693  Console\Cursor::move('←');
694  Console\Cursor::clear('→');
695  }
696 
697  if ($self->getLineLength() == $current = $self->getLineCurrent()) {
698  $self->setLine(mb_substr($self->getLine(), 0, -1));
699  } else {
700  $line = $self->getLine();
701  $current = $self->getLineCurrent();
702  $tail = mb_substr($line, $current);
703  $buffer = $tail . str_repeat("\033[D", mb_strlen($tail));
704  $self->setLine(mb_substr($line, 0, $current - 1) . $tail);
705  $self->setLineCurrent($current - 1);
706  }
707  }
708 
709  $self->setBuffer($buffer);
710 
711  return static::STATE_CONTINUE;
712  }
713 
721  public function _bindControlA(Readline $self)
722  {
723  for ($i = $self->getLineCurrent() - 1; 0 <= $i; --$i) {
724  $self->_bindArrowLeft($self);
725  }
726 
727  return static::STATE_CONTINUE;
728  }
729 
737  public function _bindControlB(Readline $self)
738  {
739  $current = $self->getLineCurrent();
740 
741  if (0 === $current) {
742  return static::STATE_CONTINUE;
743  }
744 
745  $words = preg_split(
746  '#\b#u',
747  $self->getLine(),
748  -1,
749  PREG_SPLIT_OFFSET_CAPTURE | PREG_SPLIT_NO_EMPTY
750  );
751 
752  for (
753  $i = 0, $max = count($words) - 1;
754  $i < $max && $words[$i + 1][1] < $current;
755  ++$i
756  );
757 
758  for ($j = $words[$i][1] + 1; $current >= $j; ++$j) {
759  $self->_bindArrowLeft($self);
760  }
761 
762  return static::STATE_CONTINUE;
763  }
764 
772  public function _bindControlE(Readline $self)
773  {
774  for (
775  $i = $self->getLineCurrent(), $max = $self->getLineLength();
776  $i < $max;
777  ++$i
778  ) {
779  $self->_bindArrowRight($self);
780  }
781 
782  return static::STATE_CONTINUE;
783  }
784 
792  public function _bindControlF(Readline $self)
793  {
794  $current = $self->getLineCurrent();
795 
796  if ($self->getLineLength() === $current) {
797  return static::STATE_CONTINUE;
798  }
799 
800  $words = preg_split(
801  '#\b#u',
802  $self->getLine(),
803  -1,
804  PREG_SPLIT_OFFSET_CAPTURE | PREG_SPLIT_NO_EMPTY
805  );
806 
807  for (
808  $i = 0, $max = count($words) - 1;
809  $i < $max && $words[$i][1] < $current;
810  ++$i
811  );
812 
813  if (!isset($words[$i + 1])) {
814  $words[$i + 1] = [1 => $self->getLineLength()];
815  }
816 
817  for ($j = $words[$i + 1][1]; $j > $current; --$j) {
818  $self->_bindArrowRight($self);
819  }
820 
821  return static::STATE_CONTINUE;
822  }
823 
831  public function _bindControlW(Readline $self)
832  {
833  $current = $self->getLineCurrent();
834 
835  if (0 === $current) {
836  return static::STATE_CONTINUE;
837  }
838 
839  $words = preg_split(
840  '#\b#u',
841  $self->getLine(),
842  -1,
843  PREG_SPLIT_OFFSET_CAPTURE | PREG_SPLIT_NO_EMPTY
844  );
845 
846  for (
847  $i = 0, $max = count($words) - 1;
848  $i < $max && $words[$i + 1][1] < $current;
849  ++$i
850  );
851 
852  for ($j = $words[$i][1] + 1; $current >= $j; ++$j) {
853  $self->_bindBackspace($self);
854  }
855 
856  return static::STATE_CONTINUE;
857  }
858 
865  public function _bindNewline(Readline $self)
866  {
867  $self->addHistory($self->getLine());
868 
869  return static::STATE_BREAK;
870  }
871 
878  public function _bindTab(Readline $self)
879  {
880  $autocompleter = $self->getAutocompleter();
881  $state = static::STATE_CONTINUE | static::STATE_NO_ECHO;
882 
883  if (null === $autocompleter) {
884  return $state;
885  }
886 
887  $current = $self->getLineCurrent();
888  $line = $self->getLine();
889 
890  if (0 === $current) {
891  return $state;
892  }
893 
894  $matches = preg_match_all(
895  '#' . $autocompleter->getWordDefinition() . '#u',
896  $line,
897  $words,
898  PREG_OFFSET_CAPTURE
899  );
900 
901  if (0 === $matches) {
902  return $state;
903  }
904 
905  for (
906  $i = 0, $max = count($words[0]);
907  $i < $max && $current > $words[0][$i][1];
908  ++$i
909  );
910 
911  $word = $words[0][$i - 1];
912 
913  if ('' === trim($word[0])) {
914  return $state;
915  }
916 
917  $prefix = mb_substr($word[0], 0, $current - $word[1]);
918  $solution = $autocompleter->complete($prefix);
919  $length = mb_strlen($prefix);
920 
921  if (null === $solution) {
922  return $state;
923  }
924 
925  if (is_array($solution)) {
926  $_solution = $solution;
927  $count = count($_solution) - 1;
928  $cWidth = 0;
929  $window = Console\Window::getSize();
930  $wWidth = $window['x'];
931  $cursor = Console\Cursor::getPosition();
932 
933  array_walk($_solution, function (&$value) use (&$cWidth) {
934  $handle = mb_strlen($value);
935 
936  if ($handle > $cWidth) {
937  $cWidth = $handle;
938  }
939 
940  return;
941  });
942  array_walk($_solution, function (&$value) use (&$cWidth) {
943  $handle = mb_strlen($value);
944 
945  if ($handle >= $cWidth) {
946  return;
947  }
948 
949  $value .= str_repeat(' ', $cWidth - $handle);
950 
951  return;
952  });
953 
954  $mColumns = (int) floor($wWidth / ($cWidth + 2));
955  $mLines = (int) ceil(($count + 1) / $mColumns);
956  --$mColumns;
957  $i = 0;
958 
959  if (0 > $window['y'] - $cursor['y'] - $mLines) {
960  Console\Window::scroll('↑', $mLines);
961  Console\Cursor::move('↑', $mLines);
962  }
963 
966  Console\Cursor::move('↓ LEFT');
967  Console\Cursor::clear('↓');
968 
969  foreach ($_solution as $j => $s) {
970  echo "\033[0m", $s, "\033[0m";
971 
972  if ($i++ < $mColumns) {
973  echo ' ';
974  } else {
975  $i = 0;
976 
977  if (isset($_solution[$j + 1])) {
978  echo "\n";
979  }
980  }
981  }
982 
985 
986  ++$mColumns;
987  $read = [STDIN];
988  $mColumn = -1;
989  $mLine = -1;
990  $coord = -1;
991  $unselect = function () use (
992  &$mColumn,
993  &$mLine,
994  &$coord,
995  &$_solution,
996  &$cWidth
997  ) {
1000  Console\Cursor::move('↓ LEFT');
1001  Console\Cursor::move('→', $mColumn * ($cWidth + 2));
1002  Console\Cursor::move('↓', $mLine);
1003  echo "\033[0m" . $_solution[$coord] . "\033[0m";
1006 
1007  return;
1008  };
1009  $select = function () use (
1010  &$mColumn,
1011  &$mLine,
1012  &$coord,
1013  &$_solution,
1014  &$cWidth
1015  ) {
1018  Console\Cursor::move('↓ LEFT');
1019  Console\Cursor::move('→', $mColumn * ($cWidth + 2));
1020  Console\Cursor::move('↓', $mLine);
1021  echo "\033[7m" . $_solution[$coord] . "\033[0m";
1024 
1025  return;
1026  };
1027  $init = function () use (
1028  &$mColumn,
1029  &$mLine,
1030  &$coord,
1031  &$select
1032  ) {
1033  $mColumn = 0;
1034  $mLine = 0;
1035  $coord = 0;
1036  $select();
1037 
1038  return;
1039  };
1040 
1041  while (true) {
1042  @stream_select($read, $write, $except, 30, 0);
1043 
1044  if (empty($read)) {
1045  $read = [STDIN];
1046 
1047  continue;
1048  }
1049 
1050  switch ($char = $self->_read()) {
1051  case "\033[A":
1052  if (-1 === $mColumn && -1 === $mLine) {
1053  $init();
1054 
1055  break;
1056  }
1057 
1058  $unselect();
1059  $coord = max(0, $coord - $mColumns);
1060  $mLine = (int) floor($coord / $mColumns);
1061  $mColumn = $coord % $mColumns;
1062  $select();
1063 
1064  break;
1065 
1066  case "\033[B":
1067  if (-1 === $mColumn && -1 === $mLine) {
1068  $init();
1069 
1070  break;
1071  }
1072 
1073  $unselect();
1074  $coord = min($count, $coord + $mColumns);
1075  $mLine = (int) floor($coord / $mColumns);
1076  $mColumn = $coord % $mColumns;
1077  $select();
1078 
1079  break;
1080 
1081  case "\t":
1082  case "\033[C":
1083  if (-1 === $mColumn && -1 === $mLine) {
1084  $init();
1085 
1086  break;
1087  }
1088 
1089  $unselect();
1090  $coord = min($count, $coord + 1);
1091  $mLine = (int) floor($coord / $mColumns);
1092  $mColumn = $coord % $mColumns;
1093  $select();
1094 
1095  break;
1096 
1097  case "\033[D":
1098  if (-1 === $mColumn && -1 === $mLine) {
1099  $init();
1100 
1101  break;
1102  }
1103 
1104  $unselect();
1105  $coord = max(0, $coord - 1);
1106  $mLine = (int) floor($coord / $mColumns);
1107  $mColumn = $coord % $mColumns;
1108  $select();
1109 
1110  break;
1111 
1112  case "\n":
1113  if (-1 !== $mColumn && -1 !== $mLine) {
1114  $tail = mb_substr($line, $current);
1115  $current -= $length;
1116  $self->setLine(
1117  mb_substr($line, 0, $current) .
1118  $solution[$coord] .
1119  $tail
1120  );
1121  $self->setLineCurrent(
1122  $current + mb_strlen($solution[$coord])
1123  );
1124 
1125  Console\Cursor::move('←', $length);
1126  echo $solution[$coord];
1127  Console\Cursor::clear('→');
1128  echo $tail;
1129  Console\Cursor::move('←', mb_strlen($tail));
1130  }
1131 
1132  default:
1133  $mColumn = -1;
1134  $mLine = -1;
1135  $coord = -1;
1137  Console\Cursor::move('↓ LEFT');
1138  Console\Cursor::clear('↓');
1140 
1141  if ("\033" !== $char && "\n" !== $char) {
1142  $self->setBuffer($char);
1143 
1144  return $self->_readLine($char);
1145  }
1146 
1147  break 2;
1148  }
1149  }
1150 
1151  return $state;
1152  }
1153 
1154  $tail = mb_substr($line, $current);
1155  $current -= $length;
1156  $self->setLine(
1157  mb_substr($line, 0, $current) .
1158  $solution .
1159  $tail
1160  );
1161  $self->setLineCurrent(
1162  $current + mb_strlen($solution)
1163  );
1164 
1165  Console\Cursor::move('←', $length);
1166  echo $solution;
1167  Console\Cursor::clear('→');
1168  echo $tail;
1169  Console\Cursor::move('←', mb_strlen($tail));
1170 
1171  return $state;
1172  }
1173 }
1174 
1179 
1183 Core\Consistency::flexEntity('Hoa\Console\Readline\Readline');
static scroll($directions, $repeat=1)
Definition: Window.php:273
static getPosition()
Definition: Cursor.php:189
_bindNewline(Readline $self)
Definition: Readline.php:865
addMappings(Array $mappings)
Definition: Readline.php:279
_bindControlW(Readline $self)
Definition: Readline.php:831
static getSize()
Definition: Window.php:118
setAutocompleter(Autocompleter $autocompleter)
Definition: Readline.php:513
static hide()
Definition: Cursor.php:329
_bindControlF(Readline $self)
Definition: Readline.php:792
static move($steps, $repeat=1)
Definition: Cursor.php:66
_bindArrowRight(Readline $self)
Definition: Readline.php:643
static advancedInteraction()
Definition: Console.php:142
_bindControlE(Readline $self)
Definition: Readline.php:772
addMapping($key, $mapping)
Definition: Readline.php:301
static restore()
Definition: Cursor.php:250
static clear($parts= 'all')
Definition: Cursor.php:271
_bindTab(Readline $self)
Definition: Readline.php:878
_bindArrowUp(Readline $self)
Definition: Readline.php:604
_bindArrowLeft(Readline $self)
Definition: Readline.php:665
_bindArrowDown(Readline $self)
Definition: Readline.php:623
static save()
Definition: Cursor.php:238
static show()
Definition: Cursor.php:341
_bindControlB(Readline $self)
Definition: Readline.php:737
_bindBackspace(Readline $self)
Definition: Readline.php:687
static isDirect($pipe)
Definition: Console.php:261
_bindControlA(Readline $self)
Definition: Readline.php:721