| // Copyright 2016 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "textcon.h" |
| |
| #include <assert.h> |
| #include <string.h> |
| #include <zircon/compiler.h> |
| |
| #include <fbl/algorithm.h> |
| |
| static inline void invalidate(textcon_t* tc, int x, int y, int w, int h) { |
| tc->invalidate(tc->cookie, x, y, w, h); |
| } |
| static inline void movecursor(textcon_t* tc, int x, int y) { tc->movecursor(tc->cookie, x, y); } |
| static inline void push_scrollback_line(textcon_t* tc, int y) { |
| tc->push_scrollback_line(tc->cookie, y); |
| } |
| static inline void setparam(textcon_t* tc, int param, uint8_t* arg, size_t arglen) { |
| tc->setparam(tc->cookie, param, arg, arglen); |
| } |
| |
| // Construct a vc_char_t from the given character using the current colors. |
| static inline vc_char_t make_vc_char(textcon_t* tc, uint8_t ch) { |
| return (vc_char_t)(ch | (((vc_char_t)tc->fg & 15) << 8) | (((vc_char_t)tc->bg & 15) << 12)); |
| } |
| |
| // Note that x coordinates are allowed to be one-past-the-end, as |
| // described in textcon.h. |
| static vc_char_t* dataxy(textcon_t* tc, int x, int y) { |
| assert(x >= 0); |
| // In particular, this assertion is <= to allow the one-past-the-end. |
| assert(x <= tc->w); |
| assert(y >= 0); |
| assert(y < tc->h); |
| return tc->data + y * tc->w + x; |
| } |
| |
| static vc_char_t* get_start_of_line(textcon_t* tc, int y) { |
| assert(y >= 0); |
| assert(y <= tc->h); |
| return tc->data + y * tc->w; |
| } |
| |
| static int clampx(textcon_t* tc, int x) { return x < 0 ? 0 : x >= tc->w ? tc->w - 1 : x; } |
| |
| static int clampxatedge(textcon_t* tc, int x) { return x < 0 ? 0 : x > tc->w ? tc->w : x; } |
| |
| static int clampy(textcon_t* tc, int y) { return y < 0 ? 0 : y >= tc->h ? tc->h - 1 : y; } |
| |
| static void moveto(textcon_t* tc, int x, int y) { |
| tc->x = clampx(tc, x); |
| tc->y = clampy(tc, y); |
| } |
| |
| static inline void moverel(textcon_t* tc, int dx, int dy) { moveto(tc, tc->x + dx, tc->y + dy); } |
| |
| static void fill(vc_char_t* ptr, vc_char_t val, size_t count) { |
| while (count-- > 0) { |
| *ptr++ = val; |
| } |
| } |
| |
| static void erase_region(textcon_t* tc, int x0, int y0, int x1, int y1) { |
| if (x0 >= tc->w) { |
| return; |
| } |
| x1 = clampx(tc, x1); |
| vc_char_t* ptr = dataxy(tc, x0, y0); |
| vc_char_t* end = dataxy(tc, x1, y1) + 1; |
| fill(ptr, make_vc_char(tc, ' '), end - ptr); |
| invalidate(tc, x0, y0, x1 - x0 + 1, y1 - y0 + 1); |
| } |
| |
| static void erase_screen(textcon_t* tc, int arg) { |
| switch (arg) { |
| case 0: // erase downward |
| erase_region(tc, tc->x, tc->y, tc->w - 1, tc->h - 1); |
| break; |
| case 1: // erase upward |
| erase_region(tc, 0, 0, tc->x, tc->y); |
| break; |
| case 2: // erase all |
| erase_region(tc, 0, 0, tc->w - 1, tc->h - 1); |
| break; |
| } |
| } |
| |
| static void erase_line(textcon_t* tc, int arg) { |
| switch (arg) { |
| case 0: // erase to EOL |
| erase_region(tc, tc->x, tc->y, tc->w - 1, tc->y); |
| break; |
| case 1: // erase from BOL |
| erase_region(tc, 0, tc->y, tc->x, tc->y); |
| break; |
| case 2: // erase line |
| erase_region(tc, 0, tc->y, tc->w - 1, tc->y); |
| break; |
| } |
| } |
| |
| static void erase_chars(textcon_t* tc, int arg) { |
| if (tc->x >= tc->w) { |
| return; |
| } |
| if (arg < 0) { |
| arg = 0; |
| } |
| int x_erase_end = fbl::min(tc->x + arg, tc->w); |
| |
| vc_char_t* dst = dataxy(tc, tc->x, tc->y); |
| vc_char_t* src = dataxy(tc, x_erase_end, tc->y); |
| vc_char_t* end = dataxy(tc, tc->w, tc->y); |
| |
| while (src < end) { |
| *dst++ = *src++; |
| } |
| while (dst < end) { |
| *dst++ = make_vc_char(tc, ' '); |
| } |
| |
| invalidate(tc, tc->x, tc->y, tc->w - tc->x, 1); |
| } |
| |
| void tc_copy_lines(textcon_t* tc, int y_dest, int y_src, int line_count) { |
| vc_char_t* dest = get_start_of_line(tc, y_dest); |
| vc_char_t* src = get_start_of_line(tc, y_src); |
| memmove(dest, src, line_count * tc->w * sizeof(vc_char_t)); |
| } |
| |
| static void clear_lines(textcon_t* tc, int y, int line_count) { |
| fill(get_start_of_line(tc, y), make_vc_char(tc, ' '), line_count * tc->w); |
| invalidate(tc, 0, y, tc->w, line_count); |
| } |
| |
| // Scroll the region between line |y0| (inclusive) and |y1| (exclusive). |
| // Scroll by |diff| lines, which may be positive (for moving lines up) or |
| // negative (for moving lines down). |
| static void scroll_lines(textcon_t* tc, int y0, int y1, int diff) { |
| int delta = diff > 0 ? diff : -diff; |
| if (delta > y1 - y0) |
| delta = y1 - y0; |
| int copy_count = y1 - y0 - delta; |
| if (diff > 0) { |
| // Scroll up. |
| for (int i = 0; i < delta; ++i) { |
| push_scrollback_line(tc, y0 + i); |
| } |
| tc->copy_lines(tc->cookie, y0, y0 + delta, copy_count); |
| clear_lines(tc, y0 + copy_count, delta); |
| } else { |
| // Scroll down. |
| tc->copy_lines(tc->cookie, y0 + delta, y0, copy_count); |
| clear_lines(tc, y0, delta); |
| } |
| } |
| |
| static void scroll_up(textcon_t* tc) { scroll_lines(tc, tc->scroll_y0, tc->scroll_y1, 1); } |
| |
| // positive = up, negative = down |
| static void scroll_at_pos(textcon_t* tc, int dir) { |
| if (tc->y < tc->scroll_y0) |
| return; |
| if (tc->y >= tc->scroll_y1) |
| return; |
| |
| scroll_lines(tc, tc->y, tc->scroll_y1, dir); |
| } |
| |
| void set_scroll(textcon_t* tc, int y0, int y1) { |
| if (y0 > y1) { |
| return; |
| } |
| tc->scroll_y0 = (y0 < 0) ? 0 : y0; |
| tc->scroll_y1 = (y1 > tc->h) ? tc->h : y1; |
| } |
| |
| static void savecursorpos(textcon_t* tc) { |
| tc->save_x = tc->x; |
| tc->save_y = tc->y; |
| } |
| |
| static void restorecursorpos(textcon_t* tc) { |
| tc->x = clampxatedge(tc, tc->save_x); |
| tc->y = clampy(tc, tc->save_y); |
| } |
| |
| static void putc_plain(textcon_t* tc, uint8_t c); |
| static void putc_escape2(textcon_t* tc, uint8_t c); |
| |
| static void putc_ignore(textcon_t* tc, uint8_t c) { tc->putc = putc_plain; } |
| |
| static void putc_param(textcon_t* tc, uint8_t c) { |
| switch (c) { |
| case '0': |
| case '1': |
| case '2': |
| case '3': |
| case '4': |
| case '5': |
| case '6': |
| case '7': |
| case '8': |
| case '9': |
| tc->num = tc->num * 10 + (c - '0'); |
| return; |
| case ';': |
| if (tc->argn_count < TC_MAX_ARG) { |
| tc->argn[tc->argn_count++] = tc->num; |
| } |
| tc->putc = putc_escape2; |
| break; |
| default: |
| if (tc->argn_count < TC_MAX_ARG) { |
| tc->argn[tc->argn_count++] = tc->num; |
| } |
| tc->putc = putc_escape2; |
| putc_escape2(tc, c); |
| break; |
| } |
| } |
| |
| #define ARG0(def) ((tc->argn_count > 0) ? tc->argn[0] : (def)) |
| #define ARG1(def) ((tc->argn_count > 1) ? tc->argn[1] : (def)) |
| |
| static void putc_dec(textcon_t* tc, uint8_t c) { |
| switch (c) { |
| case '0': |
| case '1': |
| case '2': |
| case '3': |
| case '4': |
| case '5': |
| case '6': |
| case '7': |
| case '8': |
| case '9': |
| tc->num = tc->num * 10 + (c - '0'); |
| return; |
| case 'h': |
| if (tc->num == 25) |
| setparam(tc, TC_SHOW_CURSOR, NULL, 0); |
| break; |
| case 'l': |
| if (tc->num == 25) |
| setparam(tc, TC_HIDE_CURSOR, NULL, 0); |
| break; |
| default: |
| putc_plain(tc, c); |
| break; |
| } |
| tc->putc = putc_plain; |
| } |
| |
| static textcon_param_t osc_to_param(int osc) { |
| switch (osc) { |
| case 2: |
| return TC_SET_TITLE; |
| default: |
| return TC_INVALID; |
| } |
| } |
| |
| static void putc_osc2(textcon_t* tc, uint8_t c) { |
| switch (c) { |
| case 7: { // end command |
| textcon_param_t param = osc_to_param(ARG0(-1)); |
| if (param != TC_INVALID && tc->argstr_size) |
| setparam(tc, param, tc->argstr, tc->argstr_size); |
| tc->putc = putc_plain; |
| break; |
| } |
| default: |
| if (tc->argstr_size < TC_MAX_ARG_LENGTH) |
| tc->argstr[tc->argstr_size++] = c; |
| break; |
| } |
| } |
| |
| static void putc_osc(textcon_t* tc, uint8_t c) { |
| switch (c) { |
| case '0': |
| case '1': |
| case '2': |
| case '3': |
| case '4': |
| case '5': |
| case '6': |
| case '7': |
| case '8': |
| case '9': |
| tc->num = tc->num * 10 + (c - '0'); |
| return; |
| case ';': |
| if (tc->argn_count < TC_MAX_ARG) { |
| tc->argn[tc->argn_count++] = tc->num; |
| } |
| memset(tc->argstr, 0, TC_MAX_ARG_LENGTH); |
| tc->argstr_size = 0; |
| tc->putc = putc_osc2; |
| break; |
| default: |
| if (tc->argn_count < TC_MAX_ARG) { |
| tc->argn[tc->argn_count++] = tc->num; |
| } |
| tc->putc = putc_osc2; |
| putc_osc2(tc, c); |
| break; |
| } |
| } |
| |
| static void putc_escape2(textcon_t* tc, uint8_t c) { |
| int x, y; |
| switch (c) { |
| case '0': |
| case '1': |
| case '2': |
| case '3': |
| case '4': |
| case '5': |
| case '6': |
| case '7': |
| case '8': |
| case '9': |
| tc->num = c - '0'; |
| tc->putc = putc_param; |
| return; |
| case ';': // end parameter |
| if (tc->argn_count < TC_MAX_ARG) { |
| tc->argn[tc->argn_count++] = 0; |
| } |
| return; |
| case '?': |
| tc->num = 0; |
| tc->argn_count = 0; |
| tc->putc = putc_dec; |
| return; |
| case 'A': // (CUU) Cursor Up |
| moverel(tc, 0, -ARG0(1)); |
| break; |
| case 'B': // (CUD) Cursor Down |
| moverel(tc, 0, ARG0(1)); |
| break; |
| case 'C': // (CUF) Cursor Forward |
| moverel(tc, ARG0(1), 0); |
| break; |
| case 'D': // (CUB) Cursor Backward |
| moverel(tc, -ARG0(1), 0); |
| break; |
| case 'E': |
| moveto(tc, 0, tc->y + ARG0(1)); |
| break; |
| case 'F': |
| moveto(tc, 0, tc->y - ARG0(1)); |
| break; |
| case 'G': // move xpos absolute |
| x = ARG0(1); |
| moveto(tc, x ? (x - 1) : 0, tc->y); |
| break; |
| case 'H': // (CUP) Cursor Position |
| case 'f': // (HVP) Horizontal and Vertical Position |
| x = ARG1(1); |
| y = ARG0(1); |
| moveto(tc, x ? (x - 1) : 0, y ? (y - 1) : 0); |
| break; |
| case 'J': // (ED) erase in display |
| erase_screen(tc, ARG0(0)); |
| break; |
| case 'K': // (EL) erase in line |
| erase_line(tc, ARG0(0)); |
| break; |
| case 'L': // (IL) insert line(s) at cursor |
| scroll_at_pos(tc, -ARG0(1)); |
| break; |
| case 'M': // (DL) delete line(s) at cursor |
| scroll_at_pos(tc, ARG0(1)); |
| break; |
| case 'P': // (DCH) delete character(s) |
| erase_chars(tc, ARG0(1)); |
| break; |
| case 'd': // move ypos absolute |
| y = ARG0(1); |
| moveto(tc, tc->x, y ? (y - 1) : 0); |
| break; |
| case 'm': // (SGR) Character Attributes |
| if (tc->argn_count == 0) { // no params == default param |
| tc->argn[0] = 0; |
| tc->argn_count = 1; |
| } |
| for (int i = 0; i < tc->argn_count; i++) { |
| int n = tc->argn[i]; |
| if ((n >= 30) && (n <= 37)) { // set fg |
| tc->fg = (uint8_t)(n - 30); |
| } else if ((n >= 40) && (n <= 47)) { // set bg |
| tc->bg = (uint8_t)(n - 40); |
| } else if ((n == 1) && (tc->fg <= 7)) { // bold |
| tc->fg = (uint8_t)(tc->fg + 8); |
| } else if (n == 0) { // reset |
| tc->fg = tc->init_fg; |
| tc->bg = tc->init_bg; |
| } else if (n == 7) { // reverse |
| uint8_t temp = tc->fg; |
| tc->fg = tc->bg; |
| tc->bg = temp; |
| } else if (n == 39) { // default fg |
| tc->fg = tc->init_fg; |
| } else if (n == 49) { // default bg |
| tc->bg = tc->init_bg; |
| } |
| } |
| break; |
| case 'r': // set scroll region |
| set_scroll(tc, ARG0(1) - 1, ARG1(tc->h)); |
| break; |
| case 's': // save cursor position ?? |
| savecursorpos(tc); |
| break; |
| case 'u': // restore cursor position ?? |
| restorecursorpos(tc); |
| break; |
| case '@': // (ICH) Insert Blank Character(s) |
| case 'T': // Initiate Highlight Mouse Tracking (xterm) |
| case 'c': // (DA) Send Device Attributes |
| case 'g': // (TBC) Tab Clear |
| case 'h': // (SM) Set Mode (4=Insert,20=AutoNewline) |
| case 'l': // (RM) Reset Mode (4=Replace,20=NormalLinefeed) |
| case 'n': // (DSR) Device Status Report |
| case 'x': // Request Terminal Parameters |
| default: |
| break; |
| } |
| movecursor(tc, tc->x, tc->y); |
| tc->putc = putc_plain; |
| } |
| |
| static void putc_escape(textcon_t* tc, uint8_t c) { |
| switch (c) { |
| case 27: // escape |
| return; |
| case '(': |
| case ')': |
| case '*': |
| case '+': |
| // select various character sets |
| tc->putc = putc_ignore; |
| return; |
| case '[': |
| tc->num = 0; |
| tc->argn_count = 0; |
| tc->putc = putc_escape2; |
| return; |
| case ']': |
| tc->num = 0; |
| tc->argn_count = 0; |
| tc->putc = putc_osc; |
| return; |
| case '7': // (DECSC) Save Cursor |
| savecursorpos(tc); |
| // save attribute |
| break; |
| case '8': // (DECRC) Restore Cursor |
| restorecursorpos(tc); |
| movecursor(tc, tc->x, tc->y); |
| break; |
| case 'E': // (NEL) Next Line |
| tc->x = 0; |
| __FALLTHROUGH; |
| case 'D': // (IND) Index |
| tc->y++; |
| if (tc->y >= tc->scroll_y1) { |
| tc->y--; |
| scroll_up(tc); |
| } |
| movecursor(tc, tc->x, tc->y); |
| break; |
| case 'M': // (RI) Reverse Index) |
| tc->y--; |
| if (tc->y < tc->scroll_y0) { |
| tc->y++; |
| scroll_at_pos(tc, -1); |
| } |
| movecursor(tc, tc->x, tc->y); |
| break; |
| } |
| tc->putc = putc_plain; |
| } |
| |
| static void putc_cr(textcon_t* tc) { tc->x = 0; } |
| |
| static void putc_lf(textcon_t* tc) { |
| tc->y++; |
| if (tc->y >= tc->scroll_y1) { |
| tc->y--; |
| scroll_up(tc); |
| } |
| } |
| |
| static void putc_plain(textcon_t* tc, uint8_t c) { |
| switch (c) { |
| case 7: // bell |
| break; |
| case 8: // backspace / ^H |
| if (tc->x > 0) |
| tc->x--; |
| break; |
| case 9: // tab / ^I |
| moveto(tc, (tc->x + 8) & (~7), tc->y); |
| break; |
| case 10: // newline |
| putc_cr(tc); // should we imply this? |
| putc_lf(tc); |
| break; |
| case 12: |
| erase_screen(tc, 2); |
| break; |
| case 13: // carriage return |
| putc_cr(tc); |
| break; |
| case 27: // escape |
| tc->putc = putc_escape; |
| return; |
| default: |
| if ((c < ' ') || (c > 127)) { |
| return; |
| } |
| if (tc->x >= tc->w) { |
| // Apply deferred line wrap upon printing first character beyond |
| // end of current line. |
| putc_cr(tc); |
| putc_lf(tc); |
| } |
| dataxy(tc, tc->x, tc->y)[0] = make_vc_char(tc, c); |
| invalidate(tc, tc->x, tc->y, 1, 1); |
| tc->x++; |
| break; |
| } |
| movecursor(tc, tc->x, tc->y); |
| } |
| |
| void tc_init(textcon_t* tc, int w, int h, vc_char_t* data, uint8_t fg, uint8_t bg, int cursor_x, |
| int cursor_y) { |
| tc->w = w; |
| tc->h = h; |
| tc->x = cursor_x; |
| tc->y = cursor_y; |
| tc->data = data; |
| tc->scroll_y0 = 0; |
| tc->scroll_y1 = h; |
| tc->save_x = 0; |
| tc->save_y = 0; |
| tc->fg = fg; |
| tc->bg = bg; |
| tc->init_fg = fg; |
| tc->init_bg = bg; |
| tc->putc = putc_plain; |
| } |
| |
| void tc_seth(textcon_t* tc, int h) { |
| // tc->data must be big enough for the new height |
| int old_h = h; |
| tc->h = h; |
| |
| // Move contents. |
| int y = 0; |
| if (old_h > h) { |
| vc_char_t* dst = dataxy(tc, 0, tc->scroll_y0); |
| vc_char_t* src = dataxy(tc, 0, tc->scroll_y0 + old_h - h); |
| vc_char_t* end = dataxy(tc, 0, tc->scroll_y1); |
| do { |
| push_scrollback_line(tc, y); |
| } while (++y < old_h - h); |
| memmove(dst, src, (end - dst) * sizeof(vc_char_t)); |
| tc->y -= old_h - h; |
| } else if (old_h < h) { |
| do { |
| fill(dataxy(tc, 0, tc->scroll_y1 + y), make_vc_char(tc, ' '), tc->w); |
| } while (++y < h - old_h); |
| } |
| tc->y = clampy(tc, tc->y); |
| |
| // Try to fix up the scroll region. |
| if (tc->scroll_y0 >= h) { |
| tc->scroll_y0 = 0; |
| } |
| if (tc->scroll_y1 == old_h) { |
| tc->scroll_y1 = h; |
| } else { |
| tc->scroll_y1 = tc->scroll_y1 >= h ? h : tc->scroll_y1; |
| } |
| |
| invalidate(tc, 0, 0, tc->w, tc->h); |
| movecursor(tc, tc->x, tc->y); |
| } |