805 righe
25 KiB
C++
805 righe
25 KiB
C++
#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);
|
|
}
|