#define TFT_BLACK 0x0000 #define TFT_BLUE 0x0014 #define TFT_RED 0xA000 #define TFT_GREEN 0x0500 #define TFT_CYAN 0x0514 #define TFT_MAGENTA 0xA014 #define TFT_YELLOW 0xA500 #define TFT_WHITE 0xA514 #define TFT_BOLD_BLACK 0x8410 #define TFT_BOLD_BLUE 0x001F #define TFT_BOLD_RED 0xF800 #define TFT_BOLD_GREEN 0x07E0 #define TFT_BOLD_CYAN 0x07FF #define TFT_BOLD_MAGENTA 0xF81F #define TFT_BOLD_YELLOW 0xFFE0 #define TFT_BOLD_WHITE 0xFFFF #include "tintty.h" #include "font454.h" // exported variable for input logic // @todo refactor bool tintty_cursor_key_mode_application; const uint16_t ANSI_COLORS[] = { TFT_BLACK, TFT_RED, TFT_GREEN, TFT_YELLOW, TFT_BLUE, TFT_MAGENTA, TFT_CYAN, TFT_WHITE }; const uint16_t ANSI_BOLD_COLORS[] = { TFT_BOLD_BLACK, TFT_BOLD_RED, TFT_BOLD_GREEN, TFT_BOLD_YELLOW, TFT_BOLD_BLUE, TFT_BOLD_MAGENTA, TFT_BOLD_CYAN, TFT_BOLD_WHITE }; // cursor animation const int16_t IDLE_CYCLE_MAX = 500; const int16_t IDLE_CYCLE_ON = (IDLE_CYCLE_MAX/2); const int16_t TAB_SIZE = 4; // cursor and character position is in global buffer coordinate space (may exceed screen height) struct tintty_state { // @todo consider storing cursor position as single int offset int16_t cursor_col, cursor_row; uint16_t bg_ansi_color, fg_ansi_color; bool bold; // cursor mode bool cursor_key_mode_application; // saved DEC cursor info (in screen coords) int16_t dec_saved_col, dec_saved_row, dec_saved_bg, dec_saved_fg; uint8_t dec_saved_g4bank; bool dec_saved_bold, dec_saved_no_wrap; // @todo deal with integer overflow int16_t top_row; // first displayed row in a logical scrollback buffer bool no_wrap; bool cursor_hidden; char out_char; int16_t out_char_col, out_char_row; uint8_t out_char_g4bank; // current set shift state, G0 to G3 int16_t out_clear_before, out_clear_after; uint8_t g4bank_char_set[4]; int16_t idle_cycle_count; // @todo track during blocking reads mid-command } state; struct tintty_rendered { int16_t cursor_col, cursor_row; int16_t top_row; } rendered; // @todo support negative cursor_row void _render(tintty_display *display) { // expose the cursor key mode state tintty_cursor_key_mode_application = state.cursor_key_mode_application; // if scrolling, prepare the "recycled" screen area if (state.top_row != rendered.top_row) { // clear the new piece of screen to be recycled as blank space // @todo handle scroll-up if (state.top_row > rendered.top_row) { // pre-clear the lines at the bottom // @todo always use black instead of current background colour? // @todo deal with overflow from multiplication by CHAR_HEIGHT /*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 int16_t new_bottom_y = state.top_row * FONT_HEIGHT + display->screen_height; // extend to bottom edge of new displayed area int16_t clear_sbuf_bottom = new_bottom_y % display->screen_height; int16_t clear_height = min((int)display->screen_height, new_bottom_y - old_bottom_y); int16_t clear_sbuf_top = clear_sbuf_bottom - clear_height;*/ // if rectangle straddles the screen buffer top edge, render that slice at bottom edge /*if (clear_sbuf_top < 0) { display->fill_rect( 0, clear_sbuf_top + display->screen_height, display->screen_width, -clear_sbuf_top, ANSI_COLORS[state.bg_ansi_color] ); }*/ // if rectangle is not entirely above top edge, render the normal slice /*if (clear_sbuf_bottom > 0) { display->fill_rect( 0, max(0, (int)clear_sbuf_top), display->screen_width, clear_sbuf_bottom - max(0, (int)clear_sbuf_top), ANSI_COLORS[state.bg_ansi_color] ); }*/ } // update displayed scroll display->set_vscroll((state.top_row) % display->screen_row_count); // @todo deal with overflow from multiplication // save rendered state rendered.top_row = state.top_row; } // render character if needed if (state.out_char != 0) { const uint16_t fg_tft_color = state.bold ? ANSI_BOLD_COLORS[state.fg_ansi_color] : ANSI_COLORS[state.fg_ansi_color]; const uint16_t bg_tft_color = ANSI_COLORS[state.bg_ansi_color]; const uint8_t char_set = state.g4bank_char_set[state.out_char_g4bank & 0x03]; // ensure 0-3 value display->print_character(state.out_char_col, state.out_char_row, fg_tft_color, bg_tft_color, state.out_char); // line-before // @todo detect when straddling edge of buffer if (state.out_clear_before > 0) { const int16_t line_before_chars = min(state.out_char_col, state.out_clear_before); const int16_t lines_before = (state.out_clear_before - line_before_chars) / display->screen_col_count; display->fill_rect( (state.out_char_col - line_before_chars), (state.out_char_row) % display->screen_row_count, // @todo deal with overflow from multiplication line_before_chars, 1, ANSI_COLORS[state.bg_ansi_color] ); for (int16_t i = 0; i < lines_before; i += 1) { display->fill_rect( 0, (state.out_char_row - 1 - i) % display->screen_row_count, // @todo deal with overflow from multiplication display->screen_col_count, 1, ANSI_COLORS[state.bg_ansi_color] ); } } // line-after // @todo detect when straddling edge of buffer if (state.out_clear_after > 0) { const int16_t line_after_chars = min(display->screen_col_count - 1 - state.out_char_col, (int) state.out_clear_after); const int16_t lines_after = (state.out_clear_after - line_after_chars) / display->screen_col_count; display->fill_rect( state.out_char_col + 1, (state.out_char_row) % display->screen_row_count, // @todo deal with overflow from multiplication line_after_chars, 1, ANSI_COLORS[state.bg_ansi_color] ); for (int16_t i = 0; i < lines_after; i += 1) { display->fill_rect( 0, (state.out_char_row + 1 + i) % display->screen_row_count, // @todo deal with overflow from multiplication display->screen_col_count, 1, ANSI_COLORS[state.bg_ansi_color] ); } } // clear for next render state.out_char = 0; state.out_clear_before = 0; state.out_clear_after = 0; // the char draw may overpaint the cursor, in which case // mark it for repaint if ( rendered.cursor_col == state.out_char_col && rendered.cursor_row == state.out_char_row ) { display->print_cursor(rendered.cursor_col, rendered.cursor_row, ANSI_COLORS[state.bg_ansi_color]); rendered.cursor_col = -1; } } // reflect new cursor bar render state const bool cursor_bar_shown = ( !state.cursor_hidden && state.idle_cycle_count < IDLE_CYCLE_ON ); // clear existing rendered cursor bar if needed // @todo detect if it is already cleared during scroll if (rendered.cursor_col >= 0) { if ( !cursor_bar_shown || rendered.cursor_col != state.cursor_col || rendered.cursor_row != state.cursor_row ) { display->print_cursor(rendered.cursor_col, rendered.cursor_row, ANSI_COLORS[state.bg_ansi_color]); // record the fact that cursor bar is not on screen rendered.cursor_col = -1; } } // render new cursor bar if not already shown // (sometimes right after clearing existing bar) if (rendered.cursor_col < 0) { if (cursor_bar_shown) { display->print_cursor(rendered.cursor_col, rendered.cursor_row, ANSI_COLORS[state.bg_ansi_color]); display->print_cursor(state.cursor_col, state.cursor_row, state.bold ? ANSI_BOLD_COLORS[state.fg_ansi_color] : ANSI_COLORS[state.fg_ansi_color]); // save new rendered state rendered.cursor_col = state.cursor_col; rendered.cursor_row = state.cursor_row; } } } void _ensure_cursor_vscroll(tintty_display *display) { // move displayed window down to cover cursor // @todo support scrolling up as well if (state.cursor_row - state.top_row >= display->screen_row_count) { state.top_row = state.cursor_row - display->screen_row_count + 1; } } void _send_sequence( void (*send_char)(char ch), char* str ) { // send zero-terminated sequence character by character while (*str) { send_char(*str); str += 1; } } char _read_decimal( char (*peek_char)(), char (*read_char)() ) { uint16_t accumulator = 0; while (isdigit(peek_char())) { const char digit_character = read_char(); const uint16_t digit = digit_character - '0'; accumulator = accumulator * 10 + digit; } return accumulator; } void _apply_graphic_rendition( uint16_t* arg_list, uint16_t arg_count ) { if (arg_count == 0) { // special case for resetting to default style state.bg_ansi_color = 0; state.fg_ansi_color = 7; state.bold = false; return; } // process commands // @todo support bold/etc for better colour support // @todo 39/49? for (uint16_t arg_index = 0; arg_index < arg_count; arg_index += 1) { const uint16_t arg_value = arg_list[arg_index]; if (arg_value == 0) { // reset to default style state.bg_ansi_color = 0; state.fg_ansi_color = 7; state.bold = false; } else if (arg_value == 1) { // bold state.bold = true; } else if (arg_value >= 30 && arg_value <= 37) { // foreground ANSI colour state.fg_ansi_color = arg_value - 30; } else if (arg_value >= 40 && arg_value <= 47) { // background ANSI colour state.bg_ansi_color = arg_value - 40; } } } void _apply_mode_setting( bool mode_on, uint16_t* arg_list, uint16_t arg_count ) { // process modes for (uint16_t arg_index = 0; arg_index < arg_count; arg_index += 1) { const uint16_t mode_id = arg_list[arg_index]; switch (mode_id) { case 4: // insert/replace mode // @todo this should be off for most practical purposes anyway? // ... otherwise visually shifting line text is expensive break; case 20: // auto-LF // ignoring per http://vt100.net/docs/vt220-rm/chapter4.html section 4.6.6 break; case 34: // cursor visibility state.cursor_hidden = !mode_on; break; } } } void _exec_escape_question_command( char (*peek_char)(), char (*read_char)(), void (*send_char)(char ch) ) { // @todo support multiple mode commands // per http://vt100.net/docs/vt220-rm/chapter4.html section 4.6.1, // ANSI and DEC modes cannot mix; that is, '[?25;20;?7l' is not a valid Esc-command // (noting this because https://www.gnu.org/software/screen/manual/html_node/Control-Sequences.html // makes it look like the question mark is a prefix) const uint16_t mode = _read_decimal(peek_char, read_char); const bool mode_on = (read_char() != 'l'); switch (mode) { case 1: // cursor key mode (normal/application) state.cursor_key_mode_application = mode_on; break; case 7: // auto wrap mode state.no_wrap = !mode_on; break; case 25: // cursor visibility state.cursor_hidden = !mode_on; break; } } // @todo cursor position report void _exec_escape_bracket_command_with_args( char (*peek_char)(), char (*read_char)(), void (*send_char)(char ch), tintty_display *display, uint16_t* arg_list, uint16_t arg_count ) { // convenient arg getter #define ARG(index, default_value) (arg_count > index ? arg_list[index] : default_value) // process next character after Escape-code, bracket and any numeric arguments const char command_character = read_char(); switch (command_character) { case '?': // question-mark commands _exec_escape_question_command(peek_char, read_char, send_char); break; case 'A': // cursor up (no scroll) state.cursor_row = max((int)state.top_row, state.cursor_row - ARG(0, 1)); break; case 'B': // cursor down (no scroll) state.cursor_row = min(state.top_row + display->screen_row_count - 1, state.cursor_row + ARG(0, 1)); break; case 'C': // cursor right (no scroll) state.cursor_col = min(display->screen_col_count - 1, state.cursor_col + ARG(0, 1)); break; case 'D': // cursor left (no scroll) state.cursor_col = max(0, state.cursor_col - ARG(0, 1)); break; case 'H': case 'f': // Direct Cursor Addressing (row;col) state.cursor_col = max(0, min(display->screen_col_count - 1, ARG(1, 1) - 1)); state.cursor_row = state.top_row + max(0, min(display->screen_row_count - 1, ARG(0, 1) - 1)); break; case 'J': // clear screen state.out_char = ' '; state.out_char_col = state.cursor_col; state.out_char_row = state.cursor_row; { const int16_t rel_row = state.cursor_row - state.top_row; state.out_clear_before = ARG(0, 0) != 0 ? rel_row * display->screen_col_count + state.cursor_col : 0; state.out_clear_after = ARG(0, 0) != 1 ? (display->screen_row_count - 1 - rel_row) * display->screen_col_count + (display->screen_col_count - 1 - state.cursor_col) : 0; } break; case 'K': // clear line state.out_char = ' '; state.out_char_col = state.cursor_col; state.out_char_row = state.cursor_row; state.out_clear_before = ARG(0, 0) != 0 ? state.cursor_col : 0; state.out_clear_after = ARG(0, 0) != 1 ? display->screen_col_count - 1 - state.cursor_col : 0; break; case 'm': // graphic rendition mode _apply_graphic_rendition(arg_list, arg_count); break; case 'h': // set mode _apply_mode_setting(true, arg_list, arg_count); break; case 'l': // unset mode _apply_mode_setting(false, arg_list, arg_count); break; } } void _exec_escape_bracket_command( char (*peek_char)(), char (*read_char)(), void (*send_char)(char ch), tintty_display *display ) { const uint16_t MAX_COMMAND_ARG_COUNT = 10; uint16_t arg_list[MAX_COMMAND_ARG_COUNT]; uint16_t arg_count = 0; // start parsing arguments if any // (this means that '' is treated as no arguments, but '0;' is treated as two arguments, each being zero) // @todo ignore trailing semi-colon instead of treating it as marking an extra zero arg? if (isdigit(peek_char())) { // keep consuming arguments while we have space while (arg_count < MAX_COMMAND_ARG_COUNT) { // consume decimal number arg_list[arg_count] = _read_decimal(peek_char, read_char); arg_count += 1; // stop processing if next char is not separator if (peek_char() != ';') { break; } // consume separator before starting next argument read_char(); } } _exec_escape_bracket_command_with_args( peek_char, read_char, send_char, display, arg_list, arg_count ); } // set the characters displayed for given G0-G3 bank void _exec_character_set( uint8_t g4bank_index, char (*read_char)() ) { switch (read_char()) { case 'A': case 'B': // normal character set (UK/US) state.g4bank_char_set[g4bank_index] = 0; break; case '0': // line-drawing state.g4bank_char_set[g4bank_index] = 1; break; default: // alternate sets are unsupported state.g4bank_char_set[g4bank_index] = 0; break; } } // @todo terminal reset // @todo parse modes with arguments even if they are no-op void _exec_escape_code( char (*peek_char)(), char (*read_char)(), void (*send_char)(char ch), tintty_display *display ) { // read next character after Escape-code // @todo time out? char esc_character = read_char(); // @todo support for (, ), #, c, cursor save/restore switch (esc_character) { case '[': _exec_escape_bracket_command(peek_char, read_char, send_char, display); break; case 'D': // index (move down and possibly scroll) state.cursor_row += 1; _ensure_cursor_vscroll(display); break; case 'M': // reverse index (move up and possibly scroll) state.cursor_row -= 1; _ensure_cursor_vscroll(display); break; case 'E': // next line state.cursor_row += 1; state.cursor_col = 0; _ensure_cursor_vscroll(display); break; case 'Z': // Identify Terminal (DEC Private) _send_sequence(send_char, "\e[?1;0c"); // DA response: no options break; case '7': // save cursor // @todo verify that the screen-relative coordinate approach is valid state.dec_saved_col = state.cursor_col; state.dec_saved_row = state.cursor_row - state.top_row; // relative to top state.dec_saved_bg = state.bg_ansi_color; state.dec_saved_fg = state.fg_ansi_color; state.dec_saved_g4bank = state.out_char_g4bank; state.dec_saved_bold = state.bold; state.dec_saved_no_wrap = state.no_wrap; break; case '8': // restore cursor state.cursor_col = state.dec_saved_col; state.cursor_row = state.dec_saved_row + state.top_row; // relative to top state.bg_ansi_color = state.dec_saved_bg; state.fg_ansi_color = state.dec_saved_fg; state.out_char_g4bank = state.dec_saved_g4bank; state.bold = state.dec_saved_bold; state.no_wrap = state.dec_saved_no_wrap; break; case '=': case '>': // keypad mode setting - ignoring break; case '(': // set G0 _exec_character_set(0, read_char); break; case ')': // set G1 _exec_character_set(1, read_char); break; case '*': // set G2 _exec_character_set(2, read_char); break; case '+': // set G3 _exec_character_set(3, read_char); break; default: // unrecognized character, silently ignore break; } } void _main( char (*peek_char)(), char (*read_char)(), void (*send_char)(char str), tintty_display *display, tintty_keyboard *keyboard ) { // start in default idle state char initial_character = read_char(); if (initial_character >= 0x20 && initial_character <= 0x7e) { // output displayable character state.out_char = initial_character; state.out_char_col = state.cursor_col; state.out_char_row = state.cursor_row; // update caret state.cursor_col += 1; if (state.cursor_col >= display->screen_col_count) { if (state.no_wrap) { state.cursor_col = display->screen_col_count - 1; } else { state.cursor_col = 0; state.cursor_row += 1; _ensure_cursor_vscroll(display); } } // reset idle state state.idle_cycle_count = 0; } else { // @todo bell, answer-back (0x05), delete switch (initial_character) { case '\a': // bell keyboard->bell(); break; case '\n': // line-feed state.cursor_row += 1; _ensure_cursor_vscroll(display); break; case '\r': // carriage-return state.cursor_col = 0; break; case '\b': // backspace state.cursor_col -= 1; if (state.cursor_col < 0) { if (state.no_wrap) { state.cursor_col = 0; } else { state.cursor_col = display->screen_col_count - 1; state.cursor_row -= 1; _ensure_cursor_vscroll(display); } } break; case '\t': // tab { // @todo blank out the existing characters? not sure if that is expected const int16_t tab_num = state.cursor_col / TAB_SIZE; state.cursor_col = min(display->screen_col_count - 1, (tab_num + 1) * TAB_SIZE); } break; case '\e': // Escape-command _exec_escape_code(peek_char, read_char, send_char, display); break; case '\x0f': // Shift-In (use G0) // see also the fun reason why these are called this way: // https://en.wikipedia.org/wiki/Shift_Out_and_Shift_In_characters state.out_char_g4bank = 0; break; case '\x0e': // Shift-Out (use G1) state.out_char_g4bank = 1; break; default: // nothing, just animate cursor delay(1); state.idle_cycle_count = (state.idle_cycle_count + 1) % IDLE_CYCLE_MAX; } } _render(display); } void tintty_run( char (*peek_char)(), char (*read_char)(), void (*send_char)(char str), tintty_display *display, tintty_keyboard *keyboard ) { // set up initial state state.cursor_col = 0; state.cursor_row = 0; state.top_row = 0; state.no_wrap = 0; state.cursor_hidden = 0; state.bg_ansi_color = 0; state.fg_ansi_color = 7; state.bold = false; state.cursor_key_mode_application = false; state.dec_saved_col = 0; state.dec_saved_row = 0; state.dec_saved_bg = state.bg_ansi_color; state.dec_saved_fg = state.fg_ansi_color; state.dec_saved_g4bank = 0; state.dec_saved_bold = state.bold; state.dec_saved_no_wrap = false; state.out_char = 0; state.out_char_g4bank = 0; state.g4bank_char_set[0] = 0; state.g4bank_char_set[1] = 0; state.g4bank_char_set[2] = 0; state.g4bank_char_set[3] = 0; rendered.cursor_col = -1; rendered.cursor_row = -1; // clear screen display->fill_rect(0, 0, display->screen_width, display->screen_height, TFT_BLACK); // reset TFT scroll to default display->set_vscroll(0); // initial render _render(display); // send CR to indicate that the screen is ready // (this works with the agetty --wait-cr option to help wait until Arduino boots) send_char('\r'); // main read cycle while (1) { _main(peek_char, read_char, send_char, display, keyboard); } } void tintty_idle( tintty_display *display ) { delay(1); // animate cursor state.idle_cycle_count = (state.idle_cycle_count + 1) % IDLE_CYCLE_MAX; // re-render _render(display); }