Cursor.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748
  1. <?php
  2. /**
  3. * Hoa
  4. *
  5. *
  6. * @license
  7. *
  8. * New BSD License
  9. *
  10. * Copyright © 2007-2017, Hoa community. All rights reserved.
  11. *
  12. * Redistribution and use in source and binary forms, with or without
  13. * modification, are permitted provided that the following conditions are met:
  14. * * Redistributions of source code must retain the above copyright
  15. * notice, this list of conditions and the following disclaimer.
  16. * * Redistributions in binary form must reproduce the above copyright
  17. * notice, this list of conditions and the following disclaimer in the
  18. * documentation and/or other materials provided with the distribution.
  19. * * Neither the name of the Hoa nor the names of its contributors may be
  20. * used to endorse or promote products derived from this software without
  21. * specific prior written permission.
  22. *
  23. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  24. * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  25. * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  26. * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE
  27. * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  28. * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  29. * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  30. * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  31. * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  32. * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  33. * POSSIBILITY OF SUCH DAMAGE.
  34. */
  35. namespace Hoa\Console;
  36. /**
  37. * Class \Hoa\Console\Cursor.
  38. *
  39. * Allow to manipulate the cursor.
  40. *
  41. * @copyright Copyright © 2007-2017 Hoa community
  42. * @license New BSD License
  43. */
  44. class Cursor
  45. {
  46. /**
  47. * Move the cursor.
  48. * Steps can be:
  49. * • u, up, ↑ : move to the previous line;
  50. * • U, UP : move to the first line;
  51. * • r, right, → : move to the next column;
  52. * • R, RIGHT : move to the last column;
  53. * • d, down, ↓ : move to the next line;
  54. * • D, DOWN : move to the last line;
  55. * • l, left, ← : move to the previous column;
  56. * • L, LEFT : move to the first column.
  57. * Steps can be concatened by a single space if $repeat is equal to 1.
  58. *
  59. * @param string $steps Steps.
  60. * @param int $repeat How many times do we move?
  61. * @return void
  62. */
  63. public static function move($steps, $repeat = 1)
  64. {
  65. if (1 > $repeat) {
  66. return;
  67. } elseif (1 === $repeat) {
  68. $handle = explode(' ', $steps);
  69. } else {
  70. $handle = explode(' ', $steps, 1);
  71. }
  72. $tput = Console::getTput();
  73. $output = Console::getOutput();
  74. foreach ($handle as $step) {
  75. switch ($step) {
  76. case 'u':
  77. case 'up':
  78. case '↑':
  79. $output->writeAll(
  80. str_replace(
  81. '%p1%d',
  82. $repeat,
  83. $tput->get('parm_up_cursor')
  84. )
  85. );
  86. break;
  87. case 'U':
  88. case 'UP':
  89. static::moveTo(null, 1);
  90. break;
  91. case 'r':
  92. case 'right':
  93. case '→':
  94. $output->writeAll(
  95. str_replace(
  96. '%p1%d',
  97. $repeat,
  98. $tput->get('parm_right_cursor')
  99. )
  100. );
  101. break;
  102. case 'R':
  103. case 'RIGHT':
  104. static::moveTo(9999);
  105. break;
  106. case 'd':
  107. case 'down':
  108. case '↓':
  109. $output->writeAll(
  110. str_replace(
  111. '%p1%d',
  112. $repeat,
  113. $tput->get('parm_down_cursor')
  114. )
  115. );
  116. break;
  117. case 'D':
  118. case 'DOWN':
  119. static::moveTo(null, 9999);
  120. break;
  121. case 'l':
  122. case 'left':
  123. case '←':
  124. $output->writeAll(
  125. str_replace(
  126. '%p1%d',
  127. $repeat,
  128. $tput->get('parm_left_cursor')
  129. )
  130. );
  131. break;
  132. case 'L':
  133. case 'LEFT':
  134. static::moveTo(1);
  135. break;
  136. }
  137. }
  138. return;
  139. }
  140. /**
  141. * Move to the line X and the column Y.
  142. * If null, use the current coordinate.
  143. *
  144. * @param int $x X coordinate.
  145. * @param int $y Y coordinate.
  146. * @return void
  147. */
  148. public static function moveTo($x = null, $y = null)
  149. {
  150. if (null === $x || null === $y) {
  151. $position = static::getPosition();
  152. if (null === $x) {
  153. $x = $position['x'];
  154. }
  155. if (null === $y) {
  156. $y = $position['y'];
  157. }
  158. }
  159. Console::getOutput()->writeAll(
  160. str_replace(
  161. ['%i%p1%d', '%p2%d'],
  162. [$y, $x],
  163. Console::getTput()->get('cursor_address')
  164. )
  165. );
  166. return;
  167. }
  168. /**
  169. * Get current position (x and y) of the cursor.
  170. *
  171. * @return array
  172. */
  173. public static function getPosition()
  174. {
  175. $tput = Console::getTput();
  176. $user7 = $tput->get('user7');
  177. if (null === $user7) {
  178. return [
  179. 'x' => 0,
  180. 'y' => 0
  181. ];
  182. }
  183. Console::getOutput()->writeAll($user7);
  184. $input = Console::getInput();
  185. // Read $tput->get('user6').
  186. $input->read(2); // skip \033 and [.
  187. $x = null;
  188. $y = null;
  189. $handle = &$y;
  190. do {
  191. $char = $input->readCharacter();
  192. switch ($char) {
  193. case ';':
  194. $handle = &$x;
  195. break;
  196. case 'R':
  197. break 2;
  198. default:
  199. $handle .= $char;
  200. }
  201. } while (true);
  202. return [
  203. 'x' => (int) $x,
  204. 'y' => (int) $y
  205. ];
  206. }
  207. /**
  208. * Save current position.
  209. *
  210. * @return void
  211. */
  212. public static function save()
  213. {
  214. Console::getOutput()->writeAll(
  215. Console::getTput()->get('save_cursor')
  216. );
  217. return;
  218. }
  219. /**
  220. * Restore cursor to the last saved position.
  221. *
  222. * @return void
  223. */
  224. public static function restore()
  225. {
  226. Console::getOutput()->writeAll(
  227. Console::getTput()->get('restore_cursor')
  228. );
  229. return;
  230. }
  231. /**
  232. * Clear the screen.
  233. * Part can be:
  234. * • a, all, ↕ : clear entire screen and static::move(1, 1);
  235. * • u, up, ↑ : clear from cursor to beginning of the screen;
  236. * • r, right, → : clear from cursor to the end of the line;
  237. * • d, down, ↓ : clear from cursor to end of the screen;
  238. * • l, left, ← : clear from cursor to beginning of the screen;
  239. * • line, ↔ : clear all the line and static::move(1).
  240. * Parts can be concatenated by a single space.
  241. *
  242. * @param string $parts Parts to clean.
  243. * @return void
  244. */
  245. public static function clear($parts = 'all')
  246. {
  247. $tput = Console::getTput();
  248. $output = Console::getOutput();
  249. foreach (explode(' ', $parts) as $part) {
  250. switch ($part) {
  251. case 'a':
  252. case 'all':
  253. case '↕':
  254. $output->writeAll($tput->get('clear_screen'));
  255. static::moveTo(1, 1);
  256. break;
  257. case 'u':
  258. case 'up':
  259. case '↑':
  260. $output->writeAll("\033[1J");
  261. break;
  262. case 'r':
  263. case 'right':
  264. case '→':
  265. $output->writeAll($tput->get('clr_eol'));
  266. break;
  267. case 'd':
  268. case 'down':
  269. case '↓':
  270. $output->writeAll($tput->get('clr_eos'));
  271. break;
  272. case 'l':
  273. case 'left':
  274. case '←':
  275. $output->writeAll($tput->get('clr_bol'));
  276. break;
  277. case 'line':
  278. case '↔':
  279. $output->writeAll("\r" . $tput->get('clr_eol'));
  280. break;
  281. }
  282. }
  283. return;
  284. }
  285. /**
  286. * Hide the cursor.
  287. *
  288. * @return void
  289. */
  290. public static function hide()
  291. {
  292. Console::getOutput()->writeAll(
  293. Console::getTput()->get('cursor_invisible')
  294. );
  295. return;
  296. }
  297. /**
  298. * Show the cursor.
  299. *
  300. * @return void
  301. */
  302. public static function show()
  303. {
  304. Console::getOutput()->writeAll(
  305. Console::getTput()->get('cursor_visible')
  306. );
  307. return;
  308. }
  309. /**
  310. * Colorize cursor.
  311. * Attributes can be:
  312. * • n, normal : normal;
  313. * • b, bold : bold;
  314. * • u, underlined : underlined;
  315. * • bl, blink : blink;
  316. * • i, inverse : inverse;
  317. * • !b, !bold : normal weight;
  318. * • !u, !underlined : not underlined;
  319. * • !bl, !blink : steady;
  320. * • !i, !inverse : positive;
  321. * • fg(color), foreground(color) : set foreground to “color”;
  322. * • bg(color), background(color) : set background to “color”.
  323. * “color” can be:
  324. * • default;
  325. * • black;
  326. * • red;
  327. * • green;
  328. * • yellow;
  329. * • blue;
  330. * • magenta;
  331. * • cyan;
  332. * • white;
  333. * • 0-256 (classic palette);
  334. * • #hexa.
  335. * Attributes can be concatenated by a single space.
  336. *
  337. * @param string $attributes Attributes.
  338. * @return void
  339. */
  340. public static function colorize($attributes)
  341. {
  342. static $_rgbTo256 = null;
  343. if (null === $_rgbTo256) {
  344. $_rgbTo256 = [
  345. '000000', '800000', '008000', '808000', '000080', '800080',
  346. '008080', 'c0c0c0', '808080', 'ff0000', '00ff00', 'ffff00',
  347. '0000ff', 'ff00ff', '00ffff', 'ffffff', '000000', '00005f',
  348. '000087', '0000af', '0000d7', '0000ff', '005f00', '005f5f',
  349. '005f87', '005faf', '005fd7', '005fff', '008700', '00875f',
  350. '008787', '0087af', '0087d7', '0087ff', '00af00', '00af5f',
  351. '00af87', '00afaf', '00afd7', '00afff', '00d700', '00d75f',
  352. '00d787', '00d7af', '00d7d7', '00d7ff', '00ff00', '00ff5f',
  353. '00ff87', '00ffaf', '00ffd7', '00ffff', '5f0000', '5f005f',
  354. '5f0087', '5f00af', '5f00d7', '5f00ff', '5f5f00', '5f5f5f',
  355. '5f5f87', '5f5faf', '5f5fd7', '5f5fff', '5f8700', '5f875f',
  356. '5f8787', '5f87af', '5f87d7', '5f87ff', '5faf00', '5faf5f',
  357. '5faf87', '5fafaf', '5fafd7', '5fafff', '5fd700', '5fd75f',
  358. '5fd787', '5fd7af', '5fd7d7', '5fd7ff', '5fff00', '5fff5f',
  359. '5fff87', '5fffaf', '5fffd7', '5fffff', '870000', '87005f',
  360. '870087', '8700af', '8700d7', '8700ff', '875f00', '875f5f',
  361. '875f87', '875faf', '875fd7', '875fff', '878700', '87875f',
  362. '878787', '8787af', '8787d7', '8787ff', '87af00', '87af5f',
  363. '87af87', '87afaf', '87afd7', '87afff', '87d700', '87d75f',
  364. '87d787', '87d7af', '87d7d7', '87d7ff', '87ff00', '87ff5f',
  365. '87ff87', '87ffaf', '87ffd7', '87ffff', 'af0000', 'af005f',
  366. 'af0087', 'af00af', 'af00d7', 'af00ff', 'af5f00', 'af5f5f',
  367. 'af5f87', 'af5faf', 'af5fd7', 'af5fff', 'af8700', 'af875f',
  368. 'af8787', 'af87af', 'af87d7', 'af87ff', 'afaf00', 'afaf5f',
  369. 'afaf87', 'afafaf', 'afafd7', 'afafff', 'afd700', 'afd75f',
  370. 'afd787', 'afd7af', 'afd7d7', 'afd7ff', 'afff00', 'afff5f',
  371. 'afff87', 'afffaf', 'afffd7', 'afffff', 'd70000', 'd7005f',
  372. 'd70087', 'd700af', 'd700d7', 'd700ff', 'd75f00', 'd75f5f',
  373. 'd75f87', 'd75faf', 'd75fd7', 'd75fff', 'd78700', 'd7875f',
  374. 'd78787', 'd787af', 'd787d7', 'd787ff', 'd7af00', 'd7af5f',
  375. 'd7af87', 'd7afaf', 'd7afd7', 'd7afff', 'd7d700', 'd7d75f',
  376. 'd7d787', 'd7d7af', 'd7d7d7', 'd7d7ff', 'd7ff00', 'd7ff5f',
  377. 'd7ff87', 'd7ffaf', 'd7ffd7', 'd7ffff', 'ff0000', 'ff005f',
  378. 'ff0087', 'ff00af', 'ff00d7', 'ff00ff', 'ff5f00', 'ff5f5f',
  379. 'ff5f87', 'ff5faf', 'ff5fd7', 'ff5fff', 'ff8700', 'ff875f',
  380. 'ff8787', 'ff87af', 'ff87d7', 'ff87ff', 'ffaf00', 'ffaf5f',
  381. 'ffaf87', 'ffafaf', 'ffafd7', 'ffafff', 'ffd700', 'ffd75f',
  382. 'ffd787', 'ffd7af', 'ffd7d7', 'ffd7ff', 'ffff00', 'ffff5f',
  383. 'ffff87', 'ffffaf', 'ffffd7', 'ffffff', '080808', '121212',
  384. '1c1c1c', '262626', '303030', '3a3a3a', '444444', '4e4e4e',
  385. '585858', '606060', '666666', '767676', '808080', '8a8a8a',
  386. '949494', '9e9e9e', 'a8a8a8', 'b2b2b2', 'bcbcbc', 'c6c6c6',
  387. 'd0d0d0', 'dadada', 'e4e4e4', 'eeeeee'
  388. ];
  389. }
  390. $tput = Console::getTput();
  391. if (1 >= $tput->count('max_colors')) {
  392. return;
  393. }
  394. $handle = [];
  395. foreach (explode(' ', $attributes) as $attribute) {
  396. switch ($attribute) {
  397. case 'n':
  398. case 'normal':
  399. $handle[] = 0;
  400. break;
  401. case 'b':
  402. case 'bold':
  403. $handle[] = 1;
  404. break;
  405. case 'u':
  406. case 'underlined':
  407. $handle[] = 4;
  408. break;
  409. case 'bl':
  410. case 'blink':
  411. $handle[] = 5;
  412. break;
  413. case 'i':
  414. case 'inverse':
  415. $handle[] = 7;
  416. break;
  417. case '!b':
  418. case '!bold':
  419. $handle[] = 22;
  420. break;
  421. case '!u':
  422. case '!underlined':
  423. $handle[] = 24;
  424. break;
  425. case '!bl':
  426. case '!blink':
  427. $handle[] = 25;
  428. break;
  429. case '!i':
  430. case '!inverse':
  431. $handle[] = 27;
  432. break;
  433. default:
  434. if (0 === preg_match('#^([^\(]+)\(([^\)]+)\)$#', $attribute, $m)) {
  435. break;
  436. }
  437. $shift = 0;
  438. switch ($m[1]) {
  439. case 'fg':
  440. case 'foreground':
  441. $shift = 0;
  442. break;
  443. case 'bg':
  444. case 'background':
  445. $shift = 10;
  446. break;
  447. default:
  448. break 2;
  449. }
  450. $_handle = 0;
  451. $_keyword = true;
  452. switch ($m[2]) {
  453. case 'black':
  454. $_handle = 30;
  455. break;
  456. case 'red':
  457. $_handle = 31;
  458. break;
  459. case 'green':
  460. $_handle = 32;
  461. break;
  462. case 'yellow':
  463. $_handle = 33;
  464. break;
  465. case 'blue':
  466. $_handle = 34;
  467. break;
  468. case 'magenta':
  469. $_handle = 35;
  470. break;
  471. case 'cyan':
  472. $_handle = 36;
  473. break;
  474. case 'white':
  475. $_handle = 37;
  476. break;
  477. case 'default':
  478. $_handle = 39;
  479. break;
  480. default:
  481. $_keyword = false;
  482. if (256 <= $tput->count('max_colors') &&
  483. '#' === $m[2][0]) {
  484. $rgb = hexdec(substr($m[2], 1));
  485. $r = ($rgb >> 16) & 255;
  486. $g = ($rgb >> 8) & 255;
  487. $b = $rgb & 255;
  488. $distance = null;
  489. foreach ($_rgbTo256 as $i => $_rgb) {
  490. $_rgb = hexdec($_rgb);
  491. $_r = ($_rgb >> 16) & 255;
  492. $_g = ($_rgb >> 8) & 255;
  493. $_b = $_rgb & 255;
  494. $d = sqrt(
  495. pow($_r - $r, 2)
  496. + pow($_g - $g, 2)
  497. + pow($_b - $b, 2)
  498. );
  499. if (null === $distance ||
  500. $d <= $distance) {
  501. $distance = $d;
  502. $_handle = $i;
  503. }
  504. }
  505. } else {
  506. $_handle = intval($m[2]);
  507. }
  508. }
  509. if (true === $_keyword) {
  510. $handle[] = $_handle + $shift;
  511. } else {
  512. $handle[] = (38 + $shift) . ';5;' . $_handle;
  513. }
  514. }
  515. }
  516. Console::getOutput()->writeAll("\033[" . implode(';', $handle) . "m");
  517. return;
  518. }
  519. /**
  520. * Change color number to a specific RGB color.
  521. *
  522. * @param int $fromCode Color number.
  523. * @param int $toColor RGB color.
  524. * @return void
  525. */
  526. public static function changeColor($fromCode, $toColor)
  527. {
  528. $tput = Console::getTput();
  529. if (true !== $tput->has('can_change')) {
  530. return;
  531. }
  532. $r = ($toColor >> 16) & 255;
  533. $g = ($toColor >> 8) & 255;
  534. $b = $toColor & 255;
  535. Console::getOutput()->writeAll(
  536. str_replace(
  537. [
  538. '%p1%d',
  539. 'rgb:',
  540. '%p2%{255}%*%{1000}%/%2.2X/',
  541. '%p3%{255}%*%{1000}%/%2.2X/',
  542. '%p4%{255}%*%{1000}%/%2.2X'
  543. ],
  544. [
  545. $fromCode,
  546. '',
  547. sprintf('%02x', $r),
  548. sprintf('%02x', $g),
  549. sprintf('%02x', $b)
  550. ],
  551. $tput->get('initialize_color')
  552. )
  553. );
  554. return;
  555. }
  556. /**
  557. * Set cursor style.
  558. * Style can be:
  559. * • b, block, ▋: block;
  560. * • u, underline, _: underline;
  561. * • v, vertical, |: vertical.
  562. *
  563. * @param int $style Style.
  564. * @param bool $blink Whether the cursor is blink or steady.
  565. * @return void
  566. */
  567. public static function setStyle($style, $blink = true)
  568. {
  569. if (OS_WIN) {
  570. return;
  571. }
  572. switch ($style) {
  573. case 'b':
  574. case 'block':
  575. case '▋':
  576. $_style = 1;
  577. break;
  578. case 'u':
  579. case 'underline':
  580. case '_':
  581. $_style = 2;
  582. break;
  583. case 'v':
  584. case 'vertical':
  585. case '|':
  586. $_style = 5;
  587. break;
  588. }
  589. if (false === $blink) {
  590. ++$_style;
  591. }
  592. // Not sure what tput entry we can use here…
  593. Console::getOutput()->writeAll("\033[" . $_style . " q");
  594. return;
  595. }
  596. /**
  597. * Make a stupid “bip”.
  598. *
  599. * @return void
  600. */
  601. public static function bip()
  602. {
  603. Console::getOutput()->writeAll(
  604. Console::getTput()->get('bell')
  605. );
  606. return;
  607. }
  608. }
  609. /**
  610. * Advanced interaction.
  611. */
  612. Console::advancedInteraction();