ESPTerm - ESP8266 terminal emulator. Branches: [master] patches, [work] next release
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.
 
 
 
 
 
 
espterm-firmware/user/screen.c

1281 lines
26 KiB

#include <esp8266.h>
#include <httpd.h>
#include "screen.h"
#include "persist.h"
#include "sgr.h"
#include "ascii.h"
TerminalConfigBundle * const termconf = &persist.current.termconf;
TerminalConfigBundle termconf_scratch;
// forward declare
static void utf8_remap(char* out, char g, char table);
#define W termconf_scratch.width
#define H termconf_scratch.height
/**
* Highest permissible value of the color attribute
*/
#define COLOR_MAX 15
/**
* Screen cell data type (16 bits)
*/
typedef struct __attribute__((packed)){
char c[4]; // space for a full unicode character
Color fg : 4;
Color bg : 4;
u8 attrs;
} Cell;
/**
* The screen data array
*/
static Cell screen[MAX_SCREEN_SIZE];
#define TABSTOP_WORDS 5
/**
* Tab stops bitmap
*/
static u32 tab_stops[TABSTOP_WORDS];
/**
* Screen state structure
*/
static struct {
bool numpad_alt_mode; //!< Application Mode - affects how user input of control keys is sent
bool cursors_alt_mode; //!< Application mode for cursor keys
bool newline_mode;
bool reverse;
char charset0;
char charset1;
} scr;
typedef struct {
int x; //!< X coordinate
int y; //!< Y coordinate
bool hanging; //!< xenl state - cursor half-wrapped
// SGR
bool inverse; //!< not in attrs bc it's applied server-side (not sent to browser)
u8 attrs;
Color fg; //!< Foreground color for writing
Color bg; //!< Background color for writing
// Other attribs
int charsetN;
bool wraparound; //!< Wrapping when EOL
// Not saved/restored
bool insert_mode; //!< Insert mode (move rest of the line to the right)
bool visible; //!< Visible (not attribute, DEC special)
} CursorTypeDef;
/**
* Cursor position and attributes
*/
static CursorTypeDef cursor;
/**
* Saved cursor position, used with the SCP RCP commands
*/
static CursorTypeDef cursor_sav;
/**
* This is used to prevent premature change notifications
* (from nested calls)
*/
static volatile int notifyLock = 0;
#define NOTIFY_LOCK() do { \
notifyLock++; \
} while(0)
#define NOTIFY_DONE() do { \
if (notifyLock > 0) notifyLock--; \
if (notifyLock == 0) screen_notifyChange(CHANGE_CONTENT); \
} while(0)
/** Clear the hanging attribute if the cursor is no longer >= W */
#define clear_invalid_hanging() do { \
if (cursor.hanging && cursor.x != W-1) cursor.hanging = false; \
} while(false)
//region --- Settings ---
/**
* Restore hard defaults
*/
void terminal_restore_defaults(void)
{
termconf->default_bg = 0;
termconf->default_fg = 7;
termconf->width = SCR_DEF_WIDTH;
termconf->height = SCR_DEF_HEIGHT;
termconf->parser_tout_ms = SCR_DEF_PARSER_TOUT_MS;
termconf->display_tout_ms = SCR_DEF_DISPLAY_TOUT_MS;
termconf->fn_alt_mode = SCR_DEF_FN_ALT_MODE;
sprintf(termconf->title, SCR_DEF_TITLE);
for(int i=1; i <= 5; i++) {
sprintf(termconf->btn[i-1], "%d", i);
}
}
/**
* Apply settings after eg. restore from defaults
*/
void terminal_apply_settings(void)
{
terminal_apply_settings_noclear();
screen_init();
}
/** this is used when changing terminal settings that do not affect the screen size */
void terminal_apply_settings_noclear(void)
{
if (termconf->display_tout_ms == 0) termconf->display_tout_ms = SCR_DEF_DISPLAY_TOUT_MS;
memcpy(&termconf_scratch, termconf, sizeof(TerminalConfigBundle));
if (W*H > MAX_SCREEN_SIZE) {
error("BAD SCREEN SIZE: %d rows x %d cols", H, W);
error("reverting terminal settings to default");
terminal_restore_defaults();
persist_store();
memcpy(&termconf_scratch, termconf, sizeof(TerminalConfigBundle));
screen_init();
}
}
//endregion
//region --- Reset / Init ---
/**
* Init the screen (entire mappable area - for consistency)
*/
void ICACHE_FLASH_ATTR
screen_init(void)
{
NOTIFY_LOCK();
screen_reset();
NOTIFY_DONE();
}
/**
* Reset the cursor position & colors
*/
static void ICACHE_FLASH_ATTR
cursor_reset(void)
{
cursor.x = 0;
cursor.y = 0;
cursor.hanging = false;
cursor.visible = true;
cursor.insert_mode = false;
cursor.charsetN = 0;
cursor.wraparound = true;
screen_reset_sgr();
}
/**
* Reset the cursor (\e[0m)
*/
void ICACHE_FLASH_ATTR
screen_reset_sgr(void)
{
cursor.fg = termconf_scratch.default_fg;
cursor.bg = termconf_scratch.default_bg;
cursor.attrs = 0;
cursor.inverse = false;
}
/**
* Reset the screen - called by ESC c
*/
void ICACHE_FLASH_ATTR
screen_reset(void)
{
NOTIFY_LOCK();
cursor_reset();
scr.numpad_alt_mode = false;
scr.cursors_alt_mode = false;
scr.newline_mode = false;
scr.reverse = false;
scr.charset0 = 'B';
scr.charset1 = '0';
// size is left unchanged
screen_clear(CLEAR_ALL);
screen_clear_all_tabs();
NOTIFY_DONE();
}
/**
* Swap screen buffer / state
* this is CSI ? 47/1047/1049 h/l
* @param alternate - true if we're swapping, false if unswapping
*/
void ICACHE_FLASH_ATTR
screen_swap_state(bool alternate)
{
// TODO impl - backup/restore title, size, global attributes (not SGR)
}
//endregion
//region --- Tab stops ---
void ICACHE_FLASH_ATTR
screen_clear_all_tabs(void)
{
memset(tab_stops, 0, sizeof(tab_stops));
}
void ICACHE_FLASH_ATTR
screen_set_tab(void)
{
tab_stops[cursor.x/32] |= (1<<(cursor.x%32));
}
void ICACHE_FLASH_ATTR
screen_clear_tab(void)
{
tab_stops[cursor.x/32] &= ~(1<<(cursor.x%32));
}
/**
* Find tab stop closest to cursor to the right
* @return X pos or -1
*/
static int ICACHE_FLASH_ATTR
next_tab_stop(void)
{
// cursor must never go past EOL
if (cursor.x >= W-1) return -1;
// find first word to inspect
int idx = (cursor.x+1)/32;
int offs = (cursor.x+1)%32;
int cp = cursor.x;
while (idx < TABSTOP_WORDS) {
u32 w = tab_stops[idx];
w >>= offs;
for(;offs<32;offs++) {
cp++;
if (cp >= W) return -1;
if (w & 1) return cp;
w >>= 1;
}
offs = 0;
idx++;
}
return -1;
}
/**
* Find tab stop closest to cursor to the left
* @return X pos or -1
*/
static int ICACHE_FLASH_ATTR
prev_tab_stop(void)
{
// nowhere to go
if (cursor.x == 0) return -1;
// find first word to inspect
int idx = (cursor.x-1)/32;
int offs = (cursor.x-1)%32;
int cp = cursor.x;
while (idx >= 0) {
u32 w = tab_stops[idx];
w <<= 31-offs;
if (w == 0) {
cp -= cp%32;
if (cp < 0) return -1;
goto next;
}
for(;offs>=0;offs--) {
cp--;
if (cp < 0) return -1;
if (w & (1<<31)) return cp;
w <<= 1;
}
next:
offs = 31;
idx--;
}
return -1;
}
void ICACHE_FLASH_ATTR
screen_tab_forward(int count)
{
NOTIFY_LOCK();
for (; count > 0; count--) {
int tab = next_tab_stop();
if (tab != -1) {
cursor.x = tab;
}
else {
cursor.x = W - 1;
}
}
NOTIFY_DONE();
}
void ICACHE_FLASH_ATTR
screen_tab_reverse(int count)
{
NOTIFY_LOCK();
for (; count > 0; count--) {
int tab = prev_tab_stop();
if (tab != -1) {
cursor.x = tab;
} else {
cursor.x = 0;
}
}
NOTIFY_DONE();
}
//endregion
//region --- Clearing & inserting ---
/**
* Clear range, inclusive
*/
static inline void
clear_range(unsigned int from, unsigned int to)
{
if (to >= W*H) to = W*H-1;
Color fg = (cursor.inverse) ? cursor.bg : cursor.fg;
Color bg = (cursor.inverse) ? cursor.fg : cursor.bg;
Cell sample;
sample.c[0] = ' ';
sample.c[1] = 0;
sample.c[2] = 0;
sample.c[3] = 0;
sample.fg = fg;
sample.bg = bg;
sample.attrs = 0;
for (unsigned int i = from; i <= to; i++) {
memcpy(&screen[i], &sample, sizeof(Cell));
}
}
/**
* Clear screen area
*/
void ICACHE_FLASH_ATTR
screen_clear(ClearMode mode)
{
NOTIFY_LOCK();
switch (mode) {
case CLEAR_ALL:
clear_range(0, W * H - 1);
break;
case CLEAR_FROM_CURSOR:
clear_range((cursor.y * W) + cursor.x, W * H - 1);
break;
case CLEAR_TO_CURSOR:
clear_range(0, (cursor.y * W) + cursor.x);
break;
}
NOTIFY_DONE();
}
/**
* Line reset to gray-on-white, empty
*/
void ICACHE_FLASH_ATTR
screen_clear_line(ClearMode mode)
{
NOTIFY_LOCK();
switch (mode) {
case CLEAR_ALL:
clear_range(cursor.y * W, (cursor.y + 1) * W - 1);
break;
case CLEAR_FROM_CURSOR:
clear_range(cursor.y * W + cursor.x, (cursor.y + 1) * W - 1);
break;
case CLEAR_TO_CURSOR:
clear_range(cursor.y * W, cursor.y * W + cursor.x);
break;
}
NOTIFY_DONE();
}
void ICACHE_FLASH_ATTR
screen_clear_in_line(unsigned int count)
{
if (cursor.x + count >= W) {
screen_clear_line(CLEAR_FROM_CURSOR);
}
else {
NOTIFY_LOCK();
clear_range(cursor.y * W + cursor.x, cursor.y * W + cursor.x + count - 1);
NOTIFY_DONE();
}
}
void screen_insert_lines(unsigned int lines)
{
NOTIFY_LOCK();
// shift the following lines
int targetStart = cursor.y + lines;
if (targetStart >= H) {
targetStart = H;
} else {
// do the moving
for (int i = H-1; i >= targetStart; i--) {
memcpy(screen+i*W, screen+(i-lines)*W, sizeof(Cell)*W);
}
}
// clear the first line
screen_clear_line(CLEAR_ALL);
// copy it to the rest of the cleared region
for (int i = cursor.y+1; i < targetStart; i++) {
memcpy(screen+i*W, screen+cursor.y*W, sizeof(Cell)*W);
}
NOTIFY_DONE();
}
void screen_delete_lines(unsigned int lines)
{
NOTIFY_LOCK();
// shift lines up
int targetEnd = H - 1 - lines;
if (targetEnd <= cursor.y) {
targetEnd = cursor.y;
} else {
// do the moving
for (int i = cursor.y; i <= targetEnd; i++) {
memcpy(screen+i*W, screen+(i+lines)*W, sizeof(Cell)*W);
}
}
// clear the rest
clear_range(targetEnd*W, W*H-1);
NOTIFY_DONE();
}
void screen_insert_characters(unsigned int count)
{
NOTIFY_LOCK();
int targetStart = cursor.x + count;
if (targetStart >= W) {
targetStart = W-1;
} else {
// do the moving
for (int i = W-1; i >= targetStart; i--) {
memcpy(screen+cursor.y*W+i, screen+cursor.y*W+(i-count), sizeof(Cell));
}
}
clear_range(cursor.y*W+cursor.x, cursor.y*W+targetStart-1);
NOTIFY_DONE();
}
void screen_delete_characters(unsigned int count)
{
NOTIFY_LOCK();
int targetEnd = W - count;
if (targetEnd > cursor.x) {
// do the moving
for (int i = cursor.x; i <= targetEnd; i++) {
memcpy(screen+cursor.y*W+i, screen+cursor.y*W+(i+count), sizeof(Cell));
}
}
if (targetEnd <= cursor.x) {
screen_clear_line(CLEAR_FROM_CURSOR);
}
else {
clear_range(cursor.y * W + (W - count), (cursor.y + 1) * W - 1);
}
NOTIFY_DONE();
}
//endregion
//region --- Entire screen manipulation ---
void ICACHE_FLASH_ATTR
screen_fill_with_E(void)
{
NOTIFY_LOCK();
Cell sample;
sample.c[0] = 'E';
sample.c[1] = 0;
sample.c[2] = 0;
sample.c[3] = 0;
sample.fg = termconf_scratch.default_fg;
sample.bg = termconf_scratch.default_bg;
sample.attrs = 0;
for (unsigned int i = 0; i <= W*H-1; i++) {
memcpy(&screen[i], &sample, sizeof(Cell));
}
NOTIFY_DONE();
}
/**
* Change the screen size
*
* @param cols - new width
* @param rows - new height
*/
void ICACHE_FLASH_ATTR
screen_resize(int rows, int cols)
{
NOTIFY_LOCK();
// sanitize
if (cols < 1 || rows < 1) {
error("Screen size must be positive");
goto done;
}
if (cols * rows > MAX_SCREEN_SIZE) {
error("Max screen size exceeded");
goto done;
}
W = cols;
H = rows;
screen_reset();
done:
NOTIFY_DONE();
}
void ICACHE_FLASH_ATTR
screen_set_title(const char *title)
{
strncpy(termconf_scratch.title, title, TERM_TITLE_LEN);
screen_notifyChange(CHANGE_LABELS);
}
/**
* Shift screen upwards
*/
void ICACHE_FLASH_ATTR
screen_scroll_up(unsigned int lines)
{
NOTIFY_LOCK();
if (lines >= H - 1) {
screen_clear(CLEAR_ALL);
goto done;
}
// bad cmd
if (lines == 0) {
goto done;
}
int y;
for (y = 0; y < H - lines; y++) {
memcpy(screen + y * W, screen + (y + lines) * W, W * sizeof(Cell));
}
clear_range(y * W, W * H - 1);
done:
NOTIFY_DONE();
}
/**
* Shift screen downwards
*/
void ICACHE_FLASH_ATTR
screen_scroll_down(unsigned int lines)
{
NOTIFY_LOCK();
if (lines >= H - 1) {
screen_clear(CLEAR_ALL);
goto done;
}
// bad cmd
if (lines == 0) {
goto done;
}
int y;
for (y = H-1; y >= lines; y--) {
memcpy(screen + y * W, screen + (y - lines) * W, W * sizeof(Cell));
}
clear_range(0, lines * W-1);
done:
NOTIFY_DONE();
}
/** Set scrolling region */
void ICACHE_FLASH_ATTR
screen_set_scrolling_region(int from, int to)
{
// TODO implement (also add to scr)
if (from == 0 && to == 0) {
// whole screen
} else if (from > 1 && from < H-1 && to > from) {
// set range
} else {
// Bad range, do nothing
}
}
//endregion
//region --- Cursor manipulation ---
/**
* Set cursor position
*/
void ICACHE_FLASH_ATTR
screen_cursor_set(int y, int x)
{
NOTIFY_LOCK();
if (x >= W) x = W - 1;
if (y >= H) y = H - 1;
cursor.x = x;
cursor.y = y;
clear_invalid_hanging();
NOTIFY_DONE();
}
/**
* Set cursor position
*/
void ICACHE_FLASH_ATTR
screen_cursor_get(int *y, int *x)
{
*x = cursor.x;
*y = cursor.y;
}
/**
* Set cursor X position
*/
void ICACHE_FLASH_ATTR
screen_cursor_set_x(int x)
{
NOTIFY_LOCK();
if (x >= W) x = W - 1;
cursor.x = x;
clear_invalid_hanging();
NOTIFY_DONE();
}
/**
* Set cursor Y position
*/
void ICACHE_FLASH_ATTR
screen_cursor_set_y(int y)
{
NOTIFY_LOCK();
if (y >= H) y = H - 1;
cursor.y = y;
NOTIFY_DONE();
}
/**
* Relative cursor move
*/
void ICACHE_FLASH_ATTR
screen_cursor_move(int dy, int dx, bool scroll)
{
NOTIFY_LOCK();
int move;
clear_invalid_hanging();
if (cursor.hanging && dx < 0) {
dx += 1; // consume one step on the removal of "xenl"
cursor.hanging = false;
}
cursor.x += dx;
cursor.y += dy;
if (cursor.x >= (int)W) cursor.x = W - 1;
if (cursor.x < (int)0) cursor.x = 0;
if (cursor.y < 0) {
move = -cursor.y;
cursor.y = 0;
if (scroll) screen_scroll_down((unsigned int)move);
}
if (cursor.y >= H) {
move = cursor.y - (H - 1);
cursor.y = H - 1;
if (scroll) screen_scroll_up((unsigned int)move);
}
NOTIFY_DONE();
}
/**
* Save the cursor pos
*/
void ICACHE_FLASH_ATTR
screen_cursor_save(bool withAttrs)
{
// always save with attribs
memcpy(&cursor_sav, &cursor, sizeof(CursorTypeDef));
}
/**
* Restore the cursor pos
*/
void ICACHE_FLASH_ATTR
screen_cursor_restore(bool withAttrs)
{
NOTIFY_LOCK();
if (withAttrs) {
memcpy(&cursor_sav, &cursor, sizeof(CursorTypeDef));
} else {
cursor.x = cursor_sav.x;
cursor.y = cursor_sav.y;
cursor.hanging = cursor_sav.hanging;
}
NOTIFY_DONE();
}
//endregion
//region --- Attribute setting ---
/**
* Enable cursor display
*/
void ICACHE_FLASH_ATTR
screen_set_cursor_visible(bool visible)
{
NOTIFY_LOCK();
cursor.visible = visible;
NOTIFY_DONE();
}
/**
* Enable autowrap
*/
void ICACHE_FLASH_ATTR
screen_wrap_enable(bool enable)
{
cursor.wraparound = enable;
}
/**
* Set cursor foreground color
*/
void ICACHE_FLASH_ATTR
screen_set_fg(Color color)
{
if (color > COLOR_MAX) color = COLOR_MAX;
cursor.fg = color;
}
/**
* Set cursor background coloor
*/
void ICACHE_FLASH_ATTR
screen_set_bg(Color color)
{
if (color > COLOR_MAX) color = COLOR_MAX;
cursor.bg = color;
}
void ICACHE_FLASH_ATTR
screen_set_sgr(u8 attrs, bool ena)
{
if (ena) {
cursor.attrs |= attrs;
}
else {
cursor.attrs &= ~attrs;
}
}
void ICACHE_FLASH_ATTR
screen_set_sgr_inverse(bool ena)
{
cursor.inverse = ena;
}
/**
* Check if coords are in range
*
* @param y
* @param x
* @return OK
*/
bool ICACHE_FLASH_ATTR
screen_isCoordValid(int y, int x)
{
return x >= 0 && y >= 0 && x < W && y < H;
}
void ICACHE_FLASH_ATTR
screen_set_charset_n(int Gx)
{
if (Gx < 0 || Gx > 1) return; // bad n
cursor.charsetN = Gx;
}
void ICACHE_FLASH_ATTR
screen_set_charset(int Gx, char charset)
{
if (Gx == 0) scr.charset0 = charset;
if (Gx == 1) scr.charset1 = charset;
}
void ICACHE_FLASH_ATTR
screen_set_insert_mode(bool insert)
{
cursor.insert_mode = insert;
}
void ICACHE_FLASH_ATTR
screen_set_numpad_alt_mode(bool alt_mode)
{
NOTIFY_LOCK();
scr.numpad_alt_mode = alt_mode;
NOTIFY_DONE();
}
void ICACHE_FLASH_ATTR
screen_set_cursors_alt_mode(bool alt_mode)
{
NOTIFY_LOCK();
scr.cursors_alt_mode = alt_mode;
NOTIFY_DONE();
}
void ICACHE_FLASH_ATTR
screen_set_reverse_video(bool reverse)
{
NOTIFY_LOCK();
scr.reverse = reverse;
NOTIFY_DONE();
}
void ICACHE_FLASH_ATTR
screen_set_newline_mode(bool nlm)
{
scr.newline_mode = nlm;
}
void ICACHE_FLASH_ATTR
screen_set_origin_mode(bool region_origin)
{
// TODO implement (also add to scr)
}
void ICACHE_FLASH_ATTR
screen_report_sgr(char *buffer)
{
buffer += sprintf(buffer, "0");
if (cursor.attrs & ATTR_BOLD) buffer += sprintf(buffer, ";%d", SGR_BOLD);
if (cursor.attrs & ATTR_FAINT) buffer += sprintf(buffer, ";%d", SGR_FAINT);
if (cursor.attrs & ATTR_ITALIC) buffer += sprintf(buffer, ";%d", SGR_ITALIC);
if (cursor.attrs & ATTR_UNDERLINE) buffer += sprintf(buffer, ";%d", SGR_UNDERLINE);
if (cursor.attrs & ATTR_BLINK) buffer += sprintf(buffer, ";%d", SGR_BLINK);
if (cursor.attrs & ATTR_FRAKTUR) buffer += sprintf(buffer, ";%d", SGR_FRAKTUR);
if (cursor.attrs & ATTR_STRIKE) buffer += sprintf(buffer, ";%d", SGR_STRIKE);
if (cursor.inverse) buffer += sprintf(buffer, ";%d", SGR_INVERSE);
if (cursor.fg != termconf->default_fg) buffer += sprintf(buffer, ";%d", ((cursor.fg > 7) ? SGR_FG_BRT_START : SGR_FG_START) + (cursor.fg&7));
if (cursor.bg != termconf->default_bg) buffer += sprintf(buffer, ";%d", ((cursor.bg > 7) ? SGR_BG_BRT_START : SGR_BG_START) + (cursor.bg&7));
}
//endregion
//region --- Printing ---
/**
* Set a character in the cursor color, move to right with wrap.
*/
void ICACHE_FLASH_ATTR
screen_putchar(const char *ch)
{
NOTIFY_LOCK();
// clear "hanging" flag if not possible
clear_invalid_hanging();
// Special treatment for CRLF
switch (ch[0]) {
case CR:
screen_cursor_set_x(0);
goto done;
case LF:
screen_cursor_move(1, 0, true); // can scroll
if (scr.newline_mode) {
// like CR
screen_cursor_set_x(0);
}
goto done;
case BS:
if (cursor.x > 0) {
// backspace should go to col 79 if "hanging" after 80 (as if it never actually left the 80th col)
if (cursor.hanging) {
cursor.hanging = false;
}
cursor.x--;
}
// we should not wrap around, and backspace should not even clear the cell (verified in xterm)
goto done;
default:
if (ch[0] < SP) {
// Discard
warn("Ignoring control char %d", (int)ch[0]);
goto done;
}
}
if (cursor.hanging) {
// perform the scheduled wrap if hanging
// if auto-wrap = off, it overwrites the last char
if (cursor.wraparound) {
cursor.x = 0;
cursor.y++;
// Y wrap
if (cursor.y > H - 1) {
// Scroll up, so we have space for writing
screen_scroll_up(1);
cursor.y = H - 1;
}
cursor.hanging = false;
}
}
Cell *c = &screen[cursor.x + cursor.y * W];
// move the rest of the line if we're in Insert Mode
if (cursor.x < W-1 && cursor.insert_mode) screen_insert_characters(1);
if (ch[1] == 0 && ch[0] <= 0x7f) {
// we have len=1 and ASCII
utf8_remap(c->c, ch[0], (cursor.charsetN == 0) ? scr.charset0 : scr.charset1);
}
else {
// copy unicode char
strncpy(c->c, ch, 4);
}
if (cursor.inverse) {
c->fg = cursor.bg;
c->bg = cursor.fg;
} else {
c->fg = cursor.fg;
c->bg = cursor.bg;
}
c->attrs = cursor.attrs;
cursor.x++;
// X wrap
if (cursor.x >= W) {
cursor.hanging = true; // hanging - next typed char wraps around, but backspace and arrows still stay on the same line.
cursor.x = W - 1;
}
done:
NOTIFY_DONE();
}
/**
* translates VT100 ACS escape codes to Unicode values.
* Based on rxvt-unicode screen.C table.
*/
static const u16 vt100_to_unicode[62] =
{
// ? ? ? ? ? ? ?
// A=UPARR B=DNARR C=RTARR D=LFARR E=FLBLK F=3/4BL G=SNOMN
0x2191, 0x2193, 0x2192, 0x2190, 0x2588, 0x259a, 0x2603,
// H= I= J= K= L= M= N=
0, 0, 0, 0, 0, 0, 0,
// O= P= Q= R= S= T= U=
0, 0, 0, 0, 0, 0, 0,
// V= W= X= Y= Z= [= \=
0, 0, 0, 0, 0, 0, 0,
// ? ? v->0 v->1 v->2 v->3 v->4
// ]= ^= _=SPC `=DIAMN a=HSMED b=HT c=FF
0, 0, 0x0020, 0x25c6, 0x2592, 0x2409, 0x240c,
// v->5 v->6 v->7 v->8 v->9 v->a v->b
// d=CR e=LF f=DEGRE g=PLSMN h=NL i=VT j=SL-BR
0x240d, 0x240a, 0x00b0, 0x00b1, 0x2424, 0x240b, 0x2518,
// v->c v->d v->e v->f v->10 v->11 v->12
// k=SL-TR l=SL-TL m=SL-BL n=SL-+ o=SL-T1 p=SL-T2 q=SL-HZ
0x2510, 0x250c, 0x2514, 0x253c, 0x23ba, 0x23bb, 0x2500,
// v->13 v->14 v->15 v->16 v->17 v->18 v->19
// r=SL-T4 s=SL-T5 t=SL-VR u=SL-VL v=SL-HU w=Sl-HD x=SL-VT
0x23bc, 0x23bd, 0x251c, 0x2524, 0x2534, 0x252c, 0x2502,
// v->1a v->1b b->1c v->1d v->1e/a3 v->1f
// y=LT-EQ z=GT-EQ {=PI |=NOTEQ }=POUND ~=DOT
0x2264, 0x2265, 0x03c0, 0x2260, 0x20a4, 0x00b7
};
/**
* UTF remap
* @param out - output char[4]
* @param g - ASCII char
* @param table - table name (0, A, B)
*/
static void ICACHE_FLASH_ATTR
utf8_remap(char *out, char g, char table)
{
u16 utf = 0;
switch (table)
{
case '0': /* DEC Special Character & Line Drawing Set */
if ((g >= 0x41) && (g <= 0x7e) && (vt100_to_unicode[g - 0x41])) {
utf = vt100_to_unicode[g - 0x41];
}
break;
case 'A': /* UK, replaces # with GBP */
if (g == '#') utf = 0x20a4;
break;
default:
// no remap
utf = (unsigned char)g;
break;
}
// Encode to UTF-8
if (utf > 0x7F) {
// formulas taken from: https://gist.github.com/yamamushi/5823402
if ((utf >= 0x80) && (utf <= 0x07FF)) {
// 2-byte unicode
out[0] = (char) ((utf >> 0x06) ^ 0xC0);
out[1] = (char) (((utf ^ 0xFFC0) | 0x80) & ~0x40);
out[2]=0;
}
else {
// 3-byte unicode
out[0] = (char) (((utf ^ 0xFC0FFF) >> 0x0C) | 0xE0);
out[1] = (char) ((((utf ^ 0xFFF03F) >> 0x06) | 0x80) & ~0x40);
out[2] = (char) (((utf ^ 0xFFFC0) | 0x80) & ~0x40);
out[3]=0;
}
} else {
// low ASCII
out[0] = (char) utf;
out[1] = 0;
}
}
//endregion
//region --- Serialization ---
struct ScreenSerializeState {
Color lastFg;
Color lastBg;
bool lastAttrs;
char lastChar[4];
int index;
};
void ICACHE_FLASH_ATTR
encode2B(u16 number, WordB2 *stru)
{
stru->lsb = (u8) (number % 127);
number = (u16) ((number - stru->lsb) / 127);
stru->lsb += 1;
stru->msb = (u8) (number + 1);
}
void ICACHE_FLASH_ATTR
encode3B(u32 number, WordB3 *stru)
{
stru->lsb = (u8) (number % 127);
number = (number - stru->lsb) / 127;
stru->lsb += 1;
stru->msb = (u8) (number % 127);
number = (number - stru->msb) / 127;
stru->msb += 1;
stru->xsb = (u8) (number + 1);
}
/**
* buffer should be at least 64+5*10+6 long (title + buttons + 6), ie. 120
* @param buffer
* @param buf_len
*/
void ICACHE_FLASH_ATTR
screenSerializeLabelsToBuffer(char *buffer, size_t buf_len)
{
// let's just assume it's long enough - called with the huge websocket buffer
sprintf(buffer, "T%s\x01%s\x01%s\x01%s\x01%s\x01%s", // use 0x01 as separator
termconf_scratch.title,
termconf_scratch.btn[0],
termconf_scratch.btn[1],
termconf_scratch.btn[2],
termconf_scratch.btn[3],
termconf_scratch.btn[4]
);
}
/**
* Serialize the screen to a data buffer. May need multiple calls if the buffer is insufficient in size.
*
* @warning MAKE SURE *DATA IS NULL BEFORE FIRST CALL!
* Call with NULL 'buffer' at the end to free the data struct.
*
* @param buffer - buffer array of limited size. If NULL, indicates this is the last call.
* @param buf_len - buffer array size
* @param data - opaque pointer to internal data structure for storing state between repeated calls
* if NULL, indicates this is the first call.
* @return HTTPD_CGI_DONE or HTTPD_CGI_MORE. If more, repeat with the same DATA.
*/
httpd_cgi_state ICACHE_FLASH_ATTR
screenSerializeToBuffer(char *buffer, size_t buf_len, void **data)
{
struct ScreenSerializeState *ss = *data;
if (buffer == NULL) {
if (ss != NULL) free(ss);
return HTTPD_CGI_DONE;
}
Cell *cell, *cell0;
WordB2 w1, w2, w3, w4, w5;
WordB3 lw1;
size_t remain = buf_len; int used = 0;
char *bb = buffer;
// Ideally we'd use snprintf here!
#define bufprint(fmt, ...) do { \
used = sprintf(bb, fmt, ##__VA_ARGS__); \
if(used>0) { bb += used; remain -= used; } \
} while(0)
if (ss == NULL) {
*data = ss = malloc(sizeof(struct ScreenSerializeState));
ss->index = 0;
ss->lastBg = 0;
ss->lastFg = 0;
ss->lastAttrs = 0;
memset(ss->lastChar, 0, 4); // this ensures the first char is never "repeat"
encode2B((u16) H, &w1);
encode2B((u16) W, &w2);
encode2B((u16) cursor.y, &w3);
encode2B((u16) cursor.x, &w4);
encode2B((u16) (
(cursor.visible ? 0x01 : 0) |
(cursor.hanging ? 0x02 : 0) |
(scr.cursors_alt_mode ? 0x04 : 0) |
(scr.numpad_alt_mode ? 0x08 : 0) |
(termconf->fn_alt_mode ? 0x10 : 0)
)
, &w5);
// H W X Y Attribs
bufprint("S%c%c%c%c%c%c%c%c%c%c", w1.lsb, w1.msb, w2.lsb, w2.msb, w3.lsb, w3.msb, w4.lsb, w4.msb, w5.lsb, w5.msb);
}
int i = ss->index;
while(i < W*H && remain > 12) {
cell = cell0 = &screen[i];
// Count how many times same as previous
int repCnt = 0;
while (i < W*H
&& cell->fg == ss->lastFg
&& cell->bg == ss->lastBg
&& cell->attrs == ss->lastAttrs
&& strneq(cell->c, ss->lastChar, 4)) {
// Repeat
repCnt++;
cell = &screen[++i];
}
if (repCnt == 0) {
// No repeat
bool changeAttrs = cell0->attrs != ss->lastAttrs;
bool changeColors = (cell0->fg != ss->lastFg || cell0->bg != ss->lastBg);
Color fg, bg;
// Reverse fg and bg if we're in global reverse mode
if (! scr.reverse) {
fg = cell0->fg;
bg = cell0->bg;
}
else {
fg = cell0->bg;
bg = cell0->fg;
}
if (!changeAttrs && changeColors) {
encode2B(fg | (bg<<4), &w1);
bufprint("\x03%c%c", w1.lsb, w1.msb);
}
else if (changeAttrs && !changeColors) {
// attrs only
encode2B(cell0->attrs, &w1);
bufprint("\x04%c%c", w1.lsb, w1.msb);
}
else if (changeAttrs && changeColors) {
// colors and attrs
encode3B((u32) (fg | (bg<<4) | (cell0->attrs<<8)), &lw1);
bufprint("\x01%c%c%c", lw1.lsb, lw1.msb, lw1.xsb);
}
// copy the symbol, until first 0 or reached 4 bytes
char c;
int j = 0;
while ((c = cell->c[j++]) != 0 && j < 4) {
bufprint("%c", c);
}
ss->lastFg = cell0->fg;
ss->lastBg = cell0->bg;
ss->lastAttrs = cell0->attrs;
memcpy(ss->lastChar, cell0->c, 4);
i++;
} else {
// Repeat count
encode2B((u16) repCnt, &w1);
bufprint("\x02%c%c", w1.lsb, w1.msb);
}
}
ss->index = i;
if (i < W*H-1) {
return HTTPD_CGI_MORE;
}
return HTTPD_CGI_DONE;
}
//endregion