You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

759 lines
23 KiB

  1. #define TFT_BLACK 0x0000
  2. #define TFT_BLUE 0x0014
  3. #define TFT_RED 0xA000
  4. #define TFT_GREEN 0x0500
  5. #define TFT_CYAN 0x0514
  6. #define TFT_MAGENTA 0xA014
  7. #define TFT_YELLOW 0xA500
  8. #define TFT_WHITE 0xA514
  9. #define TFT_BOLD_BLACK 0x8410
  10. #define TFT_BOLD_BLUE 0x001F
  11. #define TFT_BOLD_RED 0xF800
  12. #define TFT_BOLD_GREEN 0x07E0
  13. #define TFT_BOLD_CYAN 0x07FF
  14. #define TFT_BOLD_MAGENTA 0xF81F
  15. #define TFT_BOLD_YELLOW 0xFFE0
  16. #define TFT_BOLD_WHITE 0xFFFF
  17. #include "tintty.h"
  18. #include "font454.h"
  19. // exported variable for input logic
  20. // @todo refactor
  21. bool tintty_cursor_key_mode_application;
  22. const uint16_t ANSI_COLORS[] = {
  23. TFT_BLACK,
  24. TFT_RED,
  25. TFT_GREEN,
  26. TFT_YELLOW,
  27. TFT_BLUE,
  28. TFT_MAGENTA,
  29. TFT_CYAN,
  30. TFT_WHITE
  31. };
  32. const uint16_t ANSI_BOLD_COLORS[] = {
  33. TFT_BOLD_BLACK,
  34. TFT_BOLD_RED,
  35. TFT_BOLD_GREEN,
  36. TFT_BOLD_YELLOW,
  37. TFT_BOLD_BLUE,
  38. TFT_BOLD_MAGENTA,
  39. TFT_BOLD_CYAN,
  40. TFT_BOLD_WHITE
  41. };
  42. // cursor animation
  43. const int16_t IDLE_CYCLE_MAX = 500;
  44. const int16_t IDLE_CYCLE_ON = (IDLE_CYCLE_MAX/2);
  45. const int16_t TAB_SIZE = 4;
  46. // cursor and character position is in global buffer coordinate space (may exceed screen height)
  47. struct tintty_state {
  48. // @todo consider storing cursor position as single int offset
  49. int16_t cursor_col, cursor_row;
  50. uint16_t bg_ansi_color, fg_ansi_color;
  51. bool bold;
  52. // cursor mode
  53. bool cursor_key_mode_application;
  54. // saved DEC cursor info (in screen coords)
  55. int16_t dec_saved_col, dec_saved_row, dec_saved_bg, dec_saved_fg;
  56. uint8_t dec_saved_g4bank;
  57. bool dec_saved_bold, dec_saved_no_wrap;
  58. // @todo deal with integer overflow
  59. int16_t top_row; // first displayed row in a logical scrollback buffer
  60. bool no_wrap;
  61. bool cursor_hidden;
  62. char out_char;
  63. int16_t out_char_col, out_char_row;
  64. uint8_t out_char_g4bank; // current set shift state, G0 to G3
  65. int16_t out_clear_before, out_clear_after;
  66. uint8_t g4bank_char_set[4];
  67. int16_t idle_cycle_count; // @todo track during blocking reads mid-command
  68. } state;
  69. struct tintty_rendered {
  70. int16_t cursor_col, cursor_row;
  71. int16_t top_row;
  72. } rendered;
  73. // @todo support negative cursor_row
  74. void _render(tintty_display *display) {
  75. // expose the cursor key mode state
  76. tintty_cursor_key_mode_application = state.cursor_key_mode_application;
  77. // if scrolling, prepare the "recycled" screen area
  78. if (state.top_row != rendered.top_row) {
  79. // clear the new piece of screen to be recycled as blank space
  80. // @todo handle scroll-up
  81. if (state.top_row > rendered.top_row) {
  82. // pre-clear the lines at the bottom
  83. // @todo always use black instead of current background colour?
  84. // @todo deal with overflow from multiplication by CHAR_HEIGHT
  85. /*int16_t old_bottom_y = rendered.top_row * FONT_HEIGHT + display->screen_row_count * FONT_HEIGHT; // bottom of text may not align with screen height
  86. int16_t new_bottom_y = state.top_row * FONT_HEIGHT + display->screen_height; // extend to bottom edge of new displayed area
  87. int16_t clear_sbuf_bottom = new_bottom_y % display->screen_height;
  88. int16_t clear_height = min((int)display->screen_height, new_bottom_y - old_bottom_y);
  89. int16_t clear_sbuf_top = clear_sbuf_bottom - clear_height;*/
  90. // if rectangle straddles the screen buffer top edge, render that slice at bottom edge
  91. /*if (clear_sbuf_top < 0) {
  92. display->fill_rect(
  93. 0,
  94. clear_sbuf_top + display->screen_height,
  95. display->screen_width,
  96. -clear_sbuf_top,
  97. ANSI_COLORS[state.bg_ansi_color]
  98. );
  99. }*/
  100. // if rectangle is not entirely above top edge, render the normal slice
  101. /*if (clear_sbuf_bottom > 0) {
  102. display->fill_rect(
  103. 0,
  104. max(0, (int)clear_sbuf_top),
  105. display->screen_width,
  106. clear_sbuf_bottom - max(0, (int)clear_sbuf_top),
  107. ANSI_COLORS[state.bg_ansi_color]
  108. );
  109. }*/
  110. }
  111. // update displayed scroll
  112. display->set_vscroll((state.top_row) % display->screen_row_count); // @todo deal with overflow from multiplication
  113. // save rendered state
  114. rendered.top_row = state.top_row;
  115. }
  116. // render character if needed
  117. if (state.out_char != 0) {
  118. const uint16_t fg_tft_color = state.bold ? ANSI_BOLD_COLORS[state.fg_ansi_color] : ANSI_COLORS[state.fg_ansi_color];
  119. const uint16_t bg_tft_color = ANSI_COLORS[state.bg_ansi_color];
  120. const uint8_t char_set = state.g4bank_char_set[state.out_char_g4bank & 0x03]; // ensure 0-3 value
  121. display->print_character(state.out_char_col, state.out_char_row, fg_tft_color, bg_tft_color, state.out_char);
  122. // clear for next render
  123. state.out_char = 0;
  124. state.out_clear_before = 0;
  125. state.out_clear_after = 0;
  126. // the char draw may overpaint the cursor, in which case
  127. // mark it for repaint
  128. if (
  129. rendered.cursor_col == state.out_char_col &&
  130. rendered.cursor_row == state.out_char_row
  131. ) {
  132. display->print_cursor(rendered.cursor_col, rendered.cursor_row, ANSI_COLORS[state.bg_ansi_color]);
  133. rendered.cursor_col = -1;
  134. }
  135. }
  136. // reflect new cursor bar render state
  137. const bool cursor_bar_shown = (
  138. !state.cursor_hidden &&
  139. state.idle_cycle_count < IDLE_CYCLE_ON
  140. );
  141. // clear existing rendered cursor bar if needed
  142. // @todo detect if it is already cleared during scroll
  143. if (rendered.cursor_col >= 0) {
  144. if (
  145. !cursor_bar_shown ||
  146. rendered.cursor_col != state.cursor_col ||
  147. rendered.cursor_row != state.cursor_row
  148. ) {
  149. display->print_cursor(rendered.cursor_col, rendered.cursor_row, ANSI_COLORS[state.bg_ansi_color]);
  150. // record the fact that cursor bar is not on screen
  151. rendered.cursor_col = -1;
  152. }
  153. }
  154. // render new cursor bar if not already shown
  155. // (sometimes right after clearing existing bar)
  156. if (rendered.cursor_col < 0) {
  157. if (cursor_bar_shown) {
  158. display->print_cursor(rendered.cursor_col, rendered.cursor_row, ANSI_COLORS[state.bg_ansi_color]);
  159. display->print_cursor(state.cursor_col, state.cursor_row, state.bold ? ANSI_BOLD_COLORS[state.fg_ansi_color] : ANSI_COLORS[state.fg_ansi_color]);
  160. // save new rendered state
  161. rendered.cursor_col = state.cursor_col;
  162. rendered.cursor_row = state.cursor_row;
  163. }
  164. }
  165. }
  166. void bell() {
  167. // TODO
  168. // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/ledc.html
  169. // https://github.com/lbernstone/Tone32
  170. }
  171. void _ensure_cursor_vscroll(tintty_display *display) {
  172. // move displayed window down to cover cursor
  173. // @todo support scrolling up as well
  174. if (state.cursor_row - state.top_row >= display->screen_row_count) {
  175. state.top_row = state.cursor_row - display->screen_row_count + 1;
  176. }
  177. }
  178. void _send_sequence(
  179. void (*send_char)(char ch),
  180. char* str
  181. ) {
  182. // send zero-terminated sequence character by character
  183. while (*str) {
  184. send_char(*str);
  185. str += 1;
  186. }
  187. }
  188. char _read_decimal(
  189. char (*peek_char)(),
  190. char (*read_char)()
  191. ) {
  192. uint16_t accumulator = 0;
  193. while (isdigit(peek_char())) {
  194. const char digit_character = read_char();
  195. const uint16_t digit = digit_character - '0';
  196. accumulator = accumulator * 10 + digit;
  197. }
  198. return accumulator;
  199. }
  200. void _apply_graphic_rendition(
  201. uint16_t* arg_list,
  202. uint16_t arg_count
  203. ) {
  204. if (arg_count == 0) {
  205. // special case for resetting to default style
  206. state.bg_ansi_color = 0;
  207. state.fg_ansi_color = 7;
  208. state.bold = false;
  209. return;
  210. }
  211. // process commands
  212. // @todo support bold/etc for better colour support
  213. // @todo 39/49?
  214. for (uint16_t arg_index = 0; arg_index < arg_count; arg_index += 1) {
  215. const uint16_t arg_value = arg_list[arg_index];
  216. if (arg_value == 0) {
  217. // reset to default style
  218. state.bg_ansi_color = 0;
  219. state.fg_ansi_color = 7;
  220. state.bold = false;
  221. } else if (arg_value == 1) {
  222. // bold
  223. state.bold = true;
  224. } else if (arg_value >= 30 && arg_value <= 37) {
  225. // foreground ANSI colour
  226. state.fg_ansi_color = arg_value - 30;
  227. } else if (arg_value >= 40 && arg_value <= 47) {
  228. // background ANSI colour
  229. state.bg_ansi_color = arg_value - 40;
  230. }
  231. }
  232. }
  233. void _apply_mode_setting(
  234. bool mode_on,
  235. uint16_t* arg_list,
  236. uint16_t arg_count
  237. ) {
  238. // process modes
  239. for (uint16_t arg_index = 0; arg_index < arg_count; arg_index += 1) {
  240. const uint16_t mode_id = arg_list[arg_index];
  241. switch (mode_id) {
  242. case 4:
  243. // insert/replace mode
  244. // @todo this should be off for most practical purposes anyway?
  245. // ... otherwise visually shifting line text is expensive
  246. break;
  247. case 20:
  248. // auto-LF
  249. // ignoring per http://vt100.net/docs/vt220-rm/chapter4.html section 4.6.6
  250. break;
  251. case 34:
  252. // cursor visibility
  253. state.cursor_hidden = !mode_on;
  254. break;
  255. }
  256. }
  257. }
  258. void _exec_escape_question_command(
  259. char (*peek_char)(),
  260. char (*read_char)(),
  261. void (*send_char)(char ch)
  262. ) {
  263. // @todo support multiple mode commands
  264. // per http://vt100.net/docs/vt220-rm/chapter4.html section 4.6.1,
  265. // ANSI and DEC modes cannot mix; that is, '[?25;20;?7l' is not a valid Esc-command
  266. // (noting this because https://www.gnu.org/software/screen/manual/html_node/Control-Sequences.html
  267. // makes it look like the question mark is a prefix)
  268. const uint16_t mode = _read_decimal(peek_char, read_char);
  269. const bool mode_on = (read_char() != 'l');
  270. switch (mode) {
  271. case 1:
  272. // cursor key mode (normal/application)
  273. state.cursor_key_mode_application = mode_on;
  274. break;
  275. case 7:
  276. // auto wrap mode
  277. state.no_wrap = !mode_on;
  278. break;
  279. case 25:
  280. // cursor visibility
  281. state.cursor_hidden = !mode_on;
  282. break;
  283. }
  284. }
  285. // @todo cursor position report
  286. void _exec_escape_bracket_command_with_args(
  287. char (*peek_char)(),
  288. char (*read_char)(),
  289. void (*send_char)(char ch),
  290. tintty_display *display,
  291. uint16_t* arg_list,
  292. uint16_t arg_count
  293. ) {
  294. // convenient arg getter
  295. #define ARG(index, default_value) (arg_count > index ? arg_list[index] : default_value)
  296. // process next character after Escape-code, bracket and any numeric arguments
  297. const char command_character = read_char();
  298. switch (command_character) {
  299. case '?':
  300. // question-mark commands
  301. _exec_escape_question_command(peek_char, read_char, send_char);
  302. break;
  303. case 'A':
  304. // cursor up (no scroll)
  305. state.cursor_row = max((int)state.top_row, state.cursor_row - ARG(0, 1));
  306. break;
  307. case 'B':
  308. // cursor down (no scroll)
  309. state.cursor_row = min(state.top_row + display->screen_row_count - 1, state.cursor_row + ARG(0, 1));
  310. break;
  311. case 'C':
  312. // cursor right (no scroll)
  313. state.cursor_col = min(display->screen_col_count - 1, state.cursor_col + ARG(0, 1));
  314. break;
  315. case 'D':
  316. // cursor left (no scroll)
  317. state.cursor_col = max(0, state.cursor_col - ARG(0, 1));
  318. break;
  319. case 'H':
  320. case 'f':
  321. // Direct Cursor Addressing (row;col)
  322. state.cursor_col = max(0, min(display->screen_col_count - 1, ARG(1, 1) - 1));
  323. state.cursor_row = state.top_row + max(0, min(display->screen_row_count - 1, ARG(0, 1) - 1));
  324. break;
  325. case 'J':
  326. // clear screen
  327. state.out_char = ' ';
  328. state.out_char_col = state.cursor_col;
  329. state.out_char_row = state.cursor_row;
  330. {
  331. const int16_t rel_row = state.cursor_row - state.top_row;
  332. state.out_clear_before = ARG(0, 0) != 0
  333. ? rel_row * display->screen_col_count + state.cursor_col
  334. : 0;
  335. state.out_clear_after = ARG(0, 0) != 1
  336. ? (display->screen_row_count - 1 - rel_row) * display->screen_col_count + (display->screen_col_count - 1 - state.cursor_col)
  337. : 0;
  338. }
  339. break;
  340. case 'K':
  341. // clear line
  342. state.out_char = ' ';
  343. state.out_char_col = state.cursor_col;
  344. state.out_char_row = state.cursor_row;
  345. state.out_clear_before = ARG(0, 0) != 0
  346. ? state.cursor_col
  347. : 0;
  348. state.out_clear_after = ARG(0, 0) != 1
  349. ? display->screen_col_count - 1 - state.cursor_col
  350. : 0;
  351. break;
  352. case 'm':
  353. // graphic rendition mode
  354. _apply_graphic_rendition(arg_list, arg_count);
  355. break;
  356. case 'h':
  357. // set mode
  358. _apply_mode_setting(true, arg_list, arg_count);
  359. break;
  360. case 'l':
  361. // unset mode
  362. _apply_mode_setting(false, arg_list, arg_count);
  363. break;
  364. }
  365. }
  366. void _exec_escape_bracket_command(
  367. char (*peek_char)(),
  368. char (*read_char)(),
  369. void (*send_char)(char ch),
  370. tintty_display *display
  371. ) {
  372. const uint16_t MAX_COMMAND_ARG_COUNT = 10;
  373. uint16_t arg_list[MAX_COMMAND_ARG_COUNT];
  374. uint16_t arg_count = 0;
  375. // start parsing arguments if any
  376. // (this means that '' is treated as no arguments, but '0;' is treated as two arguments, each being zero)
  377. // @todo ignore trailing semi-colon instead of treating it as marking an extra zero arg?
  378. if (isdigit(peek_char())) {
  379. // keep consuming arguments while we have space
  380. while (arg_count < MAX_COMMAND_ARG_COUNT) {
  381. // consume decimal number
  382. arg_list[arg_count] = _read_decimal(peek_char, read_char);
  383. arg_count += 1;
  384. // stop processing if next char is not separator
  385. if (peek_char() != ';') {
  386. break;
  387. }
  388. // consume separator before starting next argument
  389. read_char();
  390. }
  391. }
  392. _exec_escape_bracket_command_with_args(
  393. peek_char,
  394. read_char,
  395. send_char,
  396. display,
  397. arg_list,
  398. arg_count
  399. );
  400. }
  401. // set the characters displayed for given G0-G3 bank
  402. void _exec_character_set(
  403. uint8_t g4bank_index,
  404. char (*read_char)()
  405. ) {
  406. switch (read_char()) {
  407. case 'A':
  408. case 'B':
  409. // normal character set (UK/US)
  410. state.g4bank_char_set[g4bank_index] = 0;
  411. break;
  412. case '0':
  413. // line-drawing
  414. state.g4bank_char_set[g4bank_index] = 1;
  415. break;
  416. default:
  417. // alternate sets are unsupported
  418. state.g4bank_char_set[g4bank_index] = 0;
  419. break;
  420. }
  421. }
  422. // @todo terminal reset
  423. // @todo parse modes with arguments even if they are no-op
  424. void _exec_escape_code(
  425. char (*peek_char)(),
  426. char (*read_char)(),
  427. void (*send_char)(char ch),
  428. tintty_display *display
  429. ) {
  430. // read next character after Escape-code
  431. // @todo time out?
  432. char esc_character = read_char();
  433. // @todo support for (, ), #, c, cursor save/restore
  434. switch (esc_character) {
  435. case '[':
  436. _exec_escape_bracket_command(peek_char, read_char, send_char, display);
  437. break;
  438. case 'D':
  439. // index (move down and possibly scroll)
  440. state.cursor_row += 1;
  441. _ensure_cursor_vscroll(display);
  442. break;
  443. case 'M':
  444. // reverse index (move up and possibly scroll)
  445. state.cursor_row -= 1;
  446. _ensure_cursor_vscroll(display);
  447. break;
  448. case 'E':
  449. // next line
  450. state.cursor_row += 1;
  451. state.cursor_col = 0;
  452. _ensure_cursor_vscroll(display);
  453. break;
  454. case 'Z':
  455. // Identify Terminal (DEC Private)
  456. _send_sequence(send_char, "\e[?1;0c"); // DA response: no options
  457. break;
  458. case '7':
  459. // save cursor
  460. // @todo verify that the screen-relative coordinate approach is valid
  461. state.dec_saved_col = state.cursor_col;
  462. state.dec_saved_row = state.cursor_row - state.top_row; // relative to top
  463. state.dec_saved_bg = state.bg_ansi_color;
  464. state.dec_saved_fg = state.fg_ansi_color;
  465. state.dec_saved_g4bank = state.out_char_g4bank;
  466. state.dec_saved_bold = state.bold;
  467. state.dec_saved_no_wrap = state.no_wrap;
  468. break;
  469. case '8':
  470. // restore cursor
  471. state.cursor_col = state.dec_saved_col;
  472. state.cursor_row = state.dec_saved_row + state.top_row; // relative to top
  473. state.bg_ansi_color = state.dec_saved_bg;
  474. state.fg_ansi_color = state.dec_saved_fg;
  475. state.out_char_g4bank = state.dec_saved_g4bank;
  476. state.bold = state.dec_saved_bold;
  477. state.no_wrap = state.dec_saved_no_wrap;
  478. break;
  479. case '=':
  480. case '>':
  481. // keypad mode setting - ignoring
  482. break;
  483. case '(':
  484. // set G0
  485. _exec_character_set(0, read_char);
  486. break;
  487. case ')':
  488. // set G1
  489. _exec_character_set(1, read_char);
  490. break;
  491. case '*':
  492. // set G2
  493. _exec_character_set(2, read_char);
  494. break;
  495. case '+':
  496. // set G3
  497. _exec_character_set(3, read_char);
  498. break;
  499. default:
  500. // unrecognized character, silently ignore
  501. break;
  502. }
  503. }
  504. void _main(
  505. char (*peek_char)(),
  506. char (*read_char)(),
  507. void (*send_char)(char str),
  508. tintty_display *display
  509. ) {
  510. // start in default idle state
  511. char initial_character = read_char();
  512. if (initial_character >= 0x20 && initial_character <= 0x7e) {
  513. // output displayable character
  514. state.out_char = initial_character;
  515. state.out_char_col = state.cursor_col;
  516. state.out_char_row = state.cursor_row;
  517. // update caret
  518. state.cursor_col += 1;
  519. if (state.cursor_col >= display->screen_col_count) {
  520. if (state.no_wrap) {
  521. state.cursor_col = display->screen_col_count - 1;
  522. } else {
  523. state.cursor_col = 0;
  524. state.cursor_row += 1;
  525. _ensure_cursor_vscroll(display);
  526. }
  527. }
  528. // reset idle state
  529. state.idle_cycle_count = 0;
  530. } else {
  531. // @todo bell, answer-back (0x05), delete
  532. switch (initial_character) {
  533. case '\a':
  534. // bell
  535. bell();
  536. break;
  537. case '\n':
  538. // line-feed
  539. state.cursor_row += 1;
  540. _ensure_cursor_vscroll(display);
  541. break;
  542. case '\r':
  543. // carriage-return
  544. state.cursor_col = 0;
  545. break;
  546. case '\b':
  547. // backspace
  548. state.cursor_col -= 1;
  549. if (state.cursor_col < 0) {
  550. if (state.no_wrap) {
  551. state.cursor_col = 0;
  552. } else {
  553. state.cursor_col = display->screen_col_count - 1;
  554. state.cursor_row -= 1;
  555. _ensure_cursor_vscroll(display);
  556. }
  557. }
  558. break;
  559. case '\t':
  560. // tab
  561. {
  562. // @todo blank out the existing characters? not sure if that is expected
  563. const int16_t tab_num = state.cursor_col / TAB_SIZE;
  564. state.cursor_col = min(display->screen_col_count - 1, (tab_num + 1) * TAB_SIZE);
  565. }
  566. break;
  567. case '\e':
  568. // Escape-command
  569. _exec_escape_code(peek_char, read_char, send_char, display);
  570. break;
  571. case '\x0f':
  572. // Shift-In (use G0)
  573. // see also the fun reason why these are called this way:
  574. // https://en.wikipedia.org/wiki/Shift_Out_and_Shift_In_characters
  575. state.out_char_g4bank = 0;
  576. break;
  577. case '\x0e':
  578. // Shift-Out (use G1)
  579. state.out_char_g4bank = 1;
  580. break;
  581. default:
  582. // nothing, just animate cursor
  583. delay(1);
  584. state.idle_cycle_count = (state.idle_cycle_count + 1) % IDLE_CYCLE_MAX;
  585. }
  586. }
  587. _render(display);
  588. }
  589. void tintty_run(
  590. char (*peek_char)(),
  591. char (*read_char)(),
  592. void (*send_char)(char str),
  593. tintty_display *display
  594. ) {
  595. // set up initial state
  596. state.cursor_col = 0;
  597. state.cursor_row = 0;
  598. state.top_row = 0;
  599. state.no_wrap = 0;
  600. state.cursor_hidden = 0;
  601. state.bg_ansi_color = 0;
  602. state.fg_ansi_color = 7;
  603. state.bold = false;
  604. state.cursor_key_mode_application = false;
  605. state.dec_saved_col = 0;
  606. state.dec_saved_row = 0;
  607. state.dec_saved_bg = state.bg_ansi_color;
  608. state.dec_saved_fg = state.fg_ansi_color;
  609. state.dec_saved_g4bank = 0;
  610. state.dec_saved_bold = state.bold;
  611. state.dec_saved_no_wrap = false;
  612. state.out_char = 0;
  613. state.out_char_g4bank = 0;
  614. state.g4bank_char_set[0] = 0;
  615. state.g4bank_char_set[1] = 0;
  616. state.g4bank_char_set[2] = 0;
  617. state.g4bank_char_set[3] = 0;
  618. rendered.cursor_col = -1;
  619. rendered.cursor_row = -1;
  620. // clear screen
  621. display->fill_rect(0, 0, display->screen_width, display->screen_height, TFT_BLACK);
  622. // reset TFT scroll to default
  623. display->set_vscroll(0);
  624. // initial render
  625. _render(display);
  626. // send CR to indicate that the screen is ready
  627. // (this works with the agetty --wait-cr option to help wait until Arduino boots)
  628. send_char('\r');
  629. // main read cycle
  630. while (1) {
  631. _main(peek_char, read_char, send_char, display);
  632. }
  633. }
  634. void tintty_idle(
  635. tintty_display *display
  636. ) {
  637. delay(1);
  638. // animate cursor
  639. state.idle_cycle_count = (state.idle_cycle_count + 1) % IDLE_CYCLE_MAX;
  640. // re-render
  641. _render(display);
  642. }