kitty/kitty/vt-parser.c
2024-02-25 09:57:23 +05:30

1728 lines
60 KiB
C

/*
* vt-parser.c
* Copyright (C) 2023 Kovid Goyal <kovid at kovidgoyal.net>
*
* Distributed under terms of the GPL3 license.
*/
// TODO: Test clipboard kitten with 52 and 5522
// TODO: Test screen_request_capabilities
#include "vt-parser.h"
#include "charsets.h"
#include "screen.h"
#include "base64.h"
#include "control-codes.h"
#define EXTENDED_OSC_SENTINEL ESC
#define PARSER_BUF_SZ (8u * 1024u)
#define PENDING_BUF_INCREMENT (16u * 1024u)
#define PENDING_BUF_LIMIT (32u * READ_BUF_SZ)
// Macros {{{
#define SET_STATE(x) self->vte_state = VTE_##x; self->parser_buf_pos = 0; self->utf8.state = UTF8_ACCEPT; self->utf8.prev = UTF8_ACCEPT;
#define IS_DIGIT \
case '0': \
case '1': \
case '2': \
case '3': \
case '4': \
case '5': \
case '6': \
case '7': \
case '8': \
case '9':
#ifdef DUMP_COMMANDS
static void
_report_error(PyObject *dump_callback, id_type window_id, const char *fmt, ...) {
va_list argptr;
va_start(argptr, fmt);
RAII_PyObject(temp, PyUnicode_FromFormatV(fmt, argptr));
va_end(argptr);
if (temp != NULL) {
RAII_PyObject(wid, PyLong_FromUnsignedLongLong(window_id));
Py_XDECREF(PyObject_CallFunctionObjArgs(dump_callback, wid, temp, NULL)); PyErr_Clear();
}
}
static void
_report_params(PyObject *dump_callback, id_type window_id, const char *name, int *params, unsigned int count, Region *r) {
static char buf[MAX_PARAMS*3] = {0};
unsigned int i, p=0;
if (r) p += snprintf(buf + p, sizeof(buf) - 2, "%u %u %u %u ", r->top, r->left, r->bottom, r->right);
for(i = 0; i < count && p < MAX_PARAMS*3-20; i++) {
int n = snprintf(buf + p, MAX_PARAMS*3 - p, "%i ", params[i]);
if (n < 0) break;
p += n;
}
buf[p] = 0;
Py_XDECREF(PyObject_CallFunction(dump_callback, "Kss", window_id, name, buf)); PyErr_Clear();
}
#define DUMP_UNUSED
#define REPORT_ERROR(...) _report_error(self->dump_callback, self->window_id, __VA_ARGS__);
#define REPORT_COMMAND1(name) \
Py_XDECREF(PyObject_CallFunction(self->dump_callback, "Ks", self->window_id, #name)); PyErr_Clear();
#define REPORT_COMMAND2(name, x) \
Py_XDECREF(PyObject_CallFunction(self->dump_callback, "Ksi", self->window_id, #name, (int)x)); PyErr_Clear();
#define REPORT_COMMAND3(name, x, y) \
Py_XDECREF(PyObject_CallFunction(self->dump_callback, "Ksii", self->window_id, #name, (int)x, (int)y)); PyErr_Clear();
#define GET_MACRO(_1,_2,_3,NAME,...) NAME
#define REPORT_COMMAND(...) GET_MACRO(__VA_ARGS__, REPORT_COMMAND3, REPORT_COMMAND2, REPORT_COMMAND1, SENTINEL)(__VA_ARGS__)
#define REPORT_VA_COMMAND(...) Py_XDECREF(PyObject_CallFunction(self->dump_callback, __VA_ARGS__)); PyErr_Clear();
#define REPORT_DRAW(ch) \
Py_XDECREF(PyObject_CallFunction(self->dump_callback, "KsC", self->window_id, "draw", ch)); PyErr_Clear();
#define REPORT_PARAMS(name, params, num, region) _report_params(self->dump_callback, self->window_id, name, params, num_params, region)
#define FLUSH_DRAW \
Py_XDECREF(PyObject_CallFunction(self->dump_callback, "KsO", self->window_id, "draw", Py_None)); PyErr_Clear();
#define REPORT_OSC(name, string) \
Py_XDECREF(PyObject_CallFunction(self->dump_callback, "KsO", self->window_id, #name, string)); PyErr_Clear();
#define REPORT_OSC2(name, code, string) \
Py_XDECREF(PyObject_CallFunction(self->dump_callback, "KsiO", self->window_id, #name, code, string)); PyErr_Clear();
#define REPORT_HYPERLINK(id, url) \
Py_XDECREF(PyObject_CallFunction(self->dump_callback, "Kszz", self->window_id, "set_active_hyperlink", id, url)); PyErr_Clear();
#else
#define REPORT_ERROR(...) log_error(ERROR_PREFIX " " __VA_ARGS__);
#define REPORT_COMMAND(...)
#define REPORT_VA_COMMAND(...)
#define REPORT_DRAW(ch)
#define REPORT_PARAMS(...)
#define FLUSH_DRAW
#define REPORT_OSC(name, string)
#define REPORT_OSC2(name, code, string)
#define REPORT_HYPERLINK(id, url)
#endif
// }}}
// Utils {{{
static const uint64_t pow_10_array[] = {
1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000, 10000000000
};
static int64_t
utoi(const uint8_t *buf, unsigned int sz) {
int64_t ans = 0;
const uint8_t *p = buf;
int mult = 1;
if (sz && *p == '-') {
mult = -1; p++; sz--;
}
// Ignore leading zeros
while(sz > 0) {
if (*p == '0') { p++; sz--; }
else break;
}
if (sz < sizeof(pow_10_array)/sizeof(pow_10_array[0])) {
for (int i = sz-1, j=0; i >= 0; i--, j++) {
ans += (p[i] - '0') * pow_10_array[j];
}
}
return ans * mult;
}
// }}}
typedef enum VTEState {
VTE_NORMAL, VTE_ESC = ESC, VTE_CSI = ESC_CSI, VTE_OSC = ESC_OSC, VTE_DCS = ESC_DCS, VTE_APC = ESC_APC, VTE_PM = ESC_PM
} VTEState;
typedef struct PS {
id_type window_id;
unsigned parser_buf_pos;
struct { UTF8State prev, state; } utf8;
VTEState vte_state;
// this is used only during dispatch of a single byte, its present here just to avoid adding an extra parameter to accumulate_osc()
bool extended_osc_code;
struct {
monotonic_t activated_at, wait_time;
unsigned stop_escape_code_type;
size_t capacity, used;
struct { size_t start, length; } current;
uint8_t *buf;
} pending_mode;
// these are temporary variables set only for duration of a parse call
PyObject *dump_callback;
Screen *screen;
const uint8_t *input_data;
size_t input_sz, input_pos;
monotonic_t now;
uint8_t parser_buf[PARSER_BUF_SZ + 8]; // +8 to ensure we can always zero terminate
bool draining_pending;
} PS;
// Normal mode {{{
static void
draw_byte(PS *self, uint8_t b) {
uint32_t ch;
switch (decode_utf8(&self->utf8.state, &ch, b)) {
case UTF8_ACCEPT:
REPORT_DRAW(ch);
screen_draw(self->screen, ch, true);
break;
case UTF8_REJECT: {
bool prev_was_accept = self->utf8.prev == UTF8_ACCEPT;
self->utf8.state = UTF8_ACCEPT; self->utf8.prev = UTF8_ACCEPT;
ch = 0xfffd; // unicode replacement char
REPORT_DRAW(ch);
screen_draw(self->screen, ch, true);
if (!prev_was_accept) draw_byte(self, b);
} break;
}
self->utf8.prev = self->utf8.state;
}
static void
dispatch_normal_mode_byte(PS *self) {
#define CALL_SCREEN_HANDLER(name) REPORT_COMMAND(name); name(self->screen); break;
uint8_t ch = self->input_data[self->input_pos++];
switch(ch) {
case BEL:
CALL_SCREEN_HANDLER(screen_bell);
case BS:
CALL_SCREEN_HANDLER(screen_backspace);
case HT:
CALL_SCREEN_HANDLER(screen_tab);
case LF:
case VT:
case FF:
CALL_SCREEN_HANDLER(screen_linefeed);
case CR:
CALL_SCREEN_HANDLER(screen_carriage_return);
case SI:
REPORT_ERROR("Ignoring request to change charset as we only support UTF-8"); break;
case SO:
REPORT_ERROR("Ignoring request to change charset as we only support UTF-8"); break;
case ESC:
SET_STATE(ESC); break;
case NUL:
case DEL:
break; // no-op
default:
draw_byte(self, ch);
break;
}
#undef CALL_SCREEN_HANDLER
}
// }}}
// Esc mode {{{
#define IS_ESCAPED_CHAR \
case '%': \
case '(': \
case ')': \
case '*': \
case '+': \
case '-': \
case '.': \
case '/': \
case ' ': \
case '#'
static void
screen_nel(Screen *screen) { screen_carriage_return(screen); screen_linefeed(screen); }
static void
dispatch_esc_mode_byte(PS *self) {
#define CALL_ED(name) REPORT_COMMAND(name); name(self->screen); SET_STATE(NORMAL);
#define CALL_ED1(name, ch) REPORT_COMMAND(name, ch); name(self->screen, ch); SET_STATE(NORMAL);
#define CALL_ED2(name, a, b) REPORT_COMMAND(name, a, b); name(self->screen, a, b); SET_STATE(NORMAL);
uint8_t ch = self->input_data[self->input_pos++];
switch(self->parser_buf_pos) {
case 0:
switch (ch) {
case ESC_DCS:
SET_STATE(DCS); break;
case ESC_OSC:
SET_STATE(OSC); break;
case ESC_CSI:
SET_STATE(CSI); break;
case ESC_APC:
SET_STATE(APC); break;
case ESC_PM:
SET_STATE(PM); break;
case ESC_RIS:
CALL_ED(screen_reset); break;
case ESC_IND:
CALL_ED(screen_index); break;
case ESC_NEL:
CALL_ED(screen_nel); break;
case ESC_RI:
CALL_ED(screen_reverse_index); break;
case ESC_HTS:
CALL_ED(screen_set_tab_stop); break;
case ESC_DECSC:
CALL_ED(screen_save_cursor); break;
case ESC_DECRC:
CALL_ED(screen_restore_cursor); break;
case ESC_DECKPNM:
CALL_ED(screen_normal_keypad_mode); break;
case ESC_DECKPAM:
CALL_ED(screen_alternate_keypad_mode); break;
IS_ESCAPED_CHAR:
self->parser_buf[self->parser_buf_pos++] = ch;
break;
default:
REPORT_ERROR("%s0x%x", "Unknown char after ESC: ", ch);
SET_STATE(NORMAL); break;
}
break;
default:
switch(self->parser_buf[0]) {
case '%':
switch(ch) {
case '@':
REPORT_ERROR("Ignoring attempt to switch to non-utf8 encoding");
break;
case 'G':
REPORT_ERROR("Ignoring attempt to switch to utf8 encoding as we are always utf-8");
break;
default:
REPORT_ERROR("Unhandled Esc %% code: 0x%x", ch); break;
}
break;
case '#':
if (ch == '8') { CALL_ED(screen_align); }
else { REPORT_ERROR("Unhandled Esc # code: 0x%x", ch); }
break;
case '(':
case ')':
switch(ch) {
case 'A':
case 'B':
case '0':
case 'U':
case 'V':
// dont report this error as fish shell designates charsets for some unholy reason, creating lot of noise in the tests
/* REPORT_ERROR("Ignoring attempt to designate charset as we support only UTF-8"); */
break;
default:
REPORT_ERROR("Unknown charset: 0x%x", ch); break;
}
break;
case ' ':
switch(ch) {
case 'F':
case 'G':
REPORT_ERROR("Ignoring attempt to turn on/off C1 controls as we only support C0 controls"); break;
default:
REPORT_ERROR("Unhandled ESC SP escape code: 0x%x", ch); break;
}
break;
default:
REPORT_ERROR("Unhandled charset related escape code: 0x%x 0x%x", self->parser_buf[0], ch); break;
}
SET_STATE(NORMAL);
break;
}
#undef CALL_ED
#undef CALL_ED1
} // }}}
// OSC {{{
static bool
is_extended_osc(const PS *self) {
return self->parser_buf_pos > 2 && self->parser_buf[0] == EXTENDED_OSC_SENTINEL && self->parser_buf[1] == 1 && self->parser_buf[2] == ';';
}
static void
continue_osc_52(PS *self) {
self->parser_buf[0] = '5'; self->parser_buf[1] = '2'; self->parser_buf[2] = ';';
self->parser_buf[3] = ';'; self->parser_buf_pos = 4;
}
static bool
handle_extended_osc_code(PS *self) {
// Handle extra long OSC 52 codes
if (self->parser_buf[0] != '5' || self->parser_buf[1] != '2' || self->parser_buf[2] != ';') return false;
self->parser_buf[0] = EXTENDED_OSC_SENTINEL; self->parser_buf[1] = 1;
return true;
}
static bool
accumulate_osc(PS *self) {
uint8_t ch = self->input_data[self->input_pos++];
self->extended_osc_code = false;
switch(ch) {
case BEL:
return true;
case NUL:
case DEL:
break;
case ESC_ST:
if (self->parser_buf_pos > 0 && self->parser_buf[self->parser_buf_pos - 1] == ESC) {
self->parser_buf_pos--;
return true;
}
/* fallthrough */
default:
if (!self->draining_pending && self->parser_buf_pos >= PARSER_BUF_SZ - 1) {
if (handle_extended_osc_code(self)) self->extended_osc_code = true;
else REPORT_ERROR("OSC sequence too long (> %d bytes) truncating.", PARSER_BUF_SZ);
return true;
}
self->parser_buf[self->parser_buf_pos++] = ch;
break;
}
return false;
}
static bool
parse_osc_8(char *buf, char **id, char **url) {
// the spec says only ASCII printable chars are allowed in OSC 8
char *boundary = strstr(buf, ";");
if (boundary == NULL) return false;
*boundary = 0;
if (*(boundary + 1)) *url = boundary + 1;
char *save = NULL, *token = strtok_r(buf, ":", &save);
while (token != NULL) {
size_t len = strlen(token);
if (len > 3 && token[0] == 'i' && token[1] == 'd' && token[2] == '=' && token[3]) {
*id = token + 3;
break;
}
token = strtok_r(NULL, ":", &save);
}
return true;
}
static void
dispatch_hyperlink(PS *self, size_t pos, size_t size) {
if (!size) return; // ignore empty OSC 8 since it must have two semi-colons to be valid, which means one semi-colon here
char *buf = (char*)self->parser_buf + pos;
buf[size] = 0; // this is safe because we have an extra 8 bytes after PARSER_BUF_SZ
char *id = NULL, *url = NULL;
if (parse_osc_8(buf, &id, &url)) {
REPORT_HYPERLINK(id, url);
set_active_hyperlink(self->screen, id, url);
} else {
REPORT_ERROR("Ignoring malformed OSC 8 code");
}
}
static void
dispatch_osc(PS *self) {
#define DISPATCH_OSC_WITH_CODE(name) REPORT_OSC2(name, code, mv); name(self->screen, code, mv);
#define DISPATCH_OSC(name) REPORT_OSC(name, mv); name(self->screen, mv);
#define START_DISPATCH {\
RAII_PyObject(mv, PyMemoryView_FromMemory((char*)self->parser_buf + i, limit - i, PyBUF_READ)); \
if (mv) {
#define END_DISPATCH_WITHOUT_BREAK }; PyErr_Clear(); }
#define END_DISPATCH }; PyErr_Clear(); break; }
const unsigned int limit = self->parser_buf_pos;
int code=0;
unsigned int i;
for (i = 0; i < MIN(limit, 5u); i++) {
if (self->parser_buf[i] < '0' || self->parser_buf[i] > '9') break;
}
if (i > 0) {
code = utoi(self->parser_buf, i);
if (i < limit && self->parser_buf[i] == ';') i++;
} else {
if (is_extended_osc(self)) {
// partial OSC 52
i = 3;
code = -52;
}
}
switch(code) {
case 0:
START_DISPATCH
DISPATCH_OSC(set_title);
DISPATCH_OSC(set_icon);
END_DISPATCH
case 1:
START_DISPATCH
DISPATCH_OSC(set_icon);
END_DISPATCH
case 2:
START_DISPATCH
DISPATCH_OSC(set_title);
END_DISPATCH
case 4:
case 104:
START_DISPATCH
DISPATCH_OSC_WITH_CODE(set_color_table_color);
END_DISPATCH
case 6:
case 7:
#ifdef DUMP_COMMANDS
START_DISPATCH
REPORT_OSC2(process_cwd_notification, code, mv);
END_DISPATCH_WITHOUT_BREAK
#endif
process_cwd_notification(self->screen, code, (char*)self->parser_buf + i, limit-i);
break;
case 8:
dispatch_hyperlink(self, i, limit-i);
break;
case 9:
case 99:
case 777:
case 1337:
START_DISPATCH
DISPATCH_OSC_WITH_CODE(desktop_notify)
END_DISPATCH
case 10:
case 11:
case 12:
case 17:
case 19:
case 22:
case 110:
case 111:
case 112:
case 117:
case 119:
START_DISPATCH
DISPATCH_OSC_WITH_CODE(set_dynamic_color);
END_DISPATCH
case 52: case -52: case 5522:
START_DISPATCH
DISPATCH_OSC_WITH_CODE(clipboard_control);
if (code == -52) continue_osc_52(self);
END_DISPATCH
case 133:
#ifdef DUMP_COMMANDS
START_DISPATCH
REPORT_OSC2(shell_prompt_marking, code, mv);
END_DISPATCH_WITHOUT_BREAK
#endif
if (limit > i) {
self->parser_buf[limit] = 0; // safe to do as we have 8 extra bytes after PARSER_BUF_SZ
shell_prompt_marking(self->screen, (char*)self->parser_buf + i);
}
break;
case FILE_TRANSFER_CODE:
START_DISPATCH
DISPATCH_OSC(file_transmission);
END_DISPATCH
case 30001:
REPORT_COMMAND(screen_push_dynamic_colors);
screen_push_colors(self->screen, 0);
break;
case 30101:
REPORT_COMMAND(screen_pop_dynamic_colors);
screen_pop_colors(self->screen, 0);
break;
case 697:
REPORT_ERROR("Ignoring OSC 697, typically used by Fig for shell integration");
break;
default:
REPORT_ERROR("Unknown OSC code: %u", code);
break;
}
#undef DISPATCH_OSC
#undef DISPATCH_OSC_WITH_CODE
#undef START_DISPATCH
#undef END_DISPATCH
}
// }}}
// DCS {{{
static bool
accumulate_dcs(PS *self) {
uint8_t ch = self->input_data[self->input_pos++];
switch(ch) {
case NUL:
case DEL:
break;
case ESC:
START_ALLOW_CASE_RANGE
case 32 ... 126:
END_ALLOW_CASE_RANGE
if (self->parser_buf_pos > 0 && self->parser_buf[self->parser_buf_pos-1] == ESC) {
if (ch == '\\') { self->parser_buf_pos--; return true; }
REPORT_ERROR("DCS sequence contained ESC without trailing \\ at pos: %u ignoring the sequence", self->parser_buf_pos);
SET_STATE(ESC); return false;
}
if (self->parser_buf_pos >= PARSER_BUF_SZ - 1) {
REPORT_ERROR("DCS sequence too long, truncating.");
return true;
}
self->parser_buf[self->parser_buf_pos++] = ch;
break;
default:
REPORT_ERROR("DCS sequence contained non-printable character: 0x%x ignoring the sequence", ch);
}
return false;
}
static bool
startswith(const uint8_t *string, ssize_t sz, const char *prefix, ssize_t l) {
if (sz < l) return false;
for (ssize_t i = 0; i < l; i++) {
if (string[i] != (unsigned char)prefix[i]) return false;
}
return true;
}
#define PENDING_MODE_CHAR '='
static void
dispatch_dcs(PS *self) {
if (self->parser_buf_pos < 2) return;
switch(self->parser_buf[0]) {
case '+':
case '$':
if (self->parser_buf[1] == 'q') {
self->parser_buf[self->parser_buf_pos] = 0; // safe to do since we have 8 extra bytes after PARSER_BUF_SZ
PyObject *mv = PyMemoryView_FromMemory((char*)self->parser_buf + 2, self->parser_buf_pos-2, PyBUF_READ);
if (mv) {
REPORT_OSC2(screen_request_capabilities, (char)self->parser_buf[0], mv);
Py_DECREF(mv);
} else PyErr_Clear();
screen_request_capabilities(self->screen, (char)self->parser_buf[0], (char*)self->parser_buf + 2);
} else {
REPORT_ERROR("Unrecognized DCS %c code: 0x%x", (char)self->parser_buf[0], self->parser_buf[1]);
}
break;
case PENDING_MODE_CHAR:
if (self->parser_buf_pos > 2 && (self->parser_buf[1] == '1' || self->parser_buf[1] == '2') && self->parser_buf[2] == 's') {
if (self->parser_buf[1] == '1') {
self->pending_mode.activated_at = monotonic();
REPORT_COMMAND(screen_start_pending_mode);
} else {
// ignore stop without matching start, see queue_pending_bytes()
// for how stop is detected while in pending mode.
REPORT_ERROR("Pending mode stop command issued while not in pending mode, this can"
" be either a bug in the terminal application or caused by a timeout with no data"
" received for too long or by too much data in pending mode");
REPORT_COMMAND(screen_stop_pending_mode);
}
} else {
REPORT_ERROR("Unrecognized DCS %c code: 0x%x", (char)self->parser_buf[0], self->parser_buf[1]);
}
break;
case '@':
if (startswith(self->parser_buf + 1, self->parser_buf_pos - 2, "kitty-", sizeof("kitty-") - 1)) {
if (startswith(self->parser_buf + 7, self->parser_buf_pos - 2, "cmd{", sizeof("cmd{") - 1)) {
PyObject *cmd = PyMemoryView_FromMemory((char*)self->parser_buf + 10, self->parser_buf_pos - 10, PyBUF_READ);
if (cmd != NULL) {
REPORT_OSC2(screen_handle_cmd, (char)self->parser_buf[0], cmd);
screen_handle_cmd(self->screen, cmd);
Py_DECREF(cmd);
} else PyErr_Clear();
#define IF_SIMPLE_PREFIX(prefix, func) \
if (startswith(self->parser_buf + 7, self->parser_buf_pos - 1, prefix, sizeof(prefix) - 1)) { \
const size_t pp_size = sizeof("kitty") + sizeof(prefix); \
PyObject *msg = PyMemoryView_FromMemory((char*)self->parser_buf + pp_size, self->parser_buf_pos - pp_size, PyBUF_READ); \
if (msg != NULL) { \
REPORT_OSC2(func, (char)self->parser_buf[0], msg); \
screen_handle_kitty_dcs(self->screen, #func, msg); \
Py_DECREF(msg); \
} else PyErr_Clear();
} else IF_SIMPLE_PREFIX("overlay-ready|", handle_overlay_ready)
} else IF_SIMPLE_PREFIX("kitten-result|", handle_kitten_result)
} else IF_SIMPLE_PREFIX("print|", handle_remote_print)
} else IF_SIMPLE_PREFIX("echo|", handle_remote_echo)
} else IF_SIMPLE_PREFIX("ssh|", handle_remote_ssh)
} else IF_SIMPLE_PREFIX("ask|", handle_remote_askpass)
} else IF_SIMPLE_PREFIX("clone|", handle_remote_clone)
} else IF_SIMPLE_PREFIX("edit|", handle_remote_edit)
#undef IF_SIMPLE_PREFIX
} else {
self->parser_buf[self->parser_buf_pos] = 0; // safe to do as we have 8 extra bytes after PARSER_BUF_SZ
REPORT_ERROR("Unrecognized DCS @ code: %s", self->parser_buf);
}
}
break;
default:
REPORT_ERROR("Unrecognized DCS code: 0x%x", self->parser_buf[0]);
break;
}
}
// }}}
// CSI {{{
#define CSI_SECONDARY \
case ' ': \
case '!': \
case '"': \
case '#': \
case '$': \
case '%': \
case '&': \
case '\'': \
case '(': \
case ')': \
case '*': \
case '+': \
case ',': \
case '-': \
case '.': \
case '/':
static bool
accumulate_csi(PS *self) {
#define ENSURE_SPACE \
if (self->parser_buf_pos > PARSER_BUF_SZ - 1) { \
REPORT_ERROR("CSI sequence too long, ignoring"); \
SET_STATE(NORMAL); \
return false; \
}
uint8_t ch = self->input_data[self->input_pos++];
switch(ch) {
IS_DIGIT
CSI_SECONDARY
case ':':
case ';':
ENSURE_SPACE;
self->parser_buf[self->parser_buf_pos++] = ch;
break;
case '?':
case '>':
case '<':
case '=':
if (self->parser_buf_pos != 0) {
REPORT_ERROR("Invalid character in CSI: 0x%x, ignoring the sequence", ch);
SET_STATE(NORMAL);
return false;
}
ENSURE_SPACE;
self->parser_buf[self->parser_buf_pos++] = ch;
break;
START_ALLOW_CASE_RANGE
case 'a' ... 'z':
case 'A' ... 'Z':
END_ALLOW_CASE_RANGE
case '@':
case '`':
case '{':
case '|':
case '}':
case '~':
self->parser_buf[self->parser_buf_pos] = ch;
return true;
case BEL:
case BS:
case HT:
case LF:
case VT:
case FF:
case CR:
case SO:
case SI:
self->input_pos--;
dispatch_normal_mode_byte(self);
break;
case NUL:
case DEL:
SET_STATE(NORMAL);
break; // no-op
default:
REPORT_ERROR("Invalid character in CSI: 0x%x, ignoring the sequence", ch);
SET_STATE(NORMAL);
return false;
}
return false;
#undef ENSURE_SPACE
}
#ifdef DUMP_COMMANDS
static void
parse_sgr_dump(PS *self, uint8_t *buf, unsigned int num, int *params, const char *report_name UNUSED, Region *region) {
Screen *screen = self->screen;
#else
void
parse_sgr(Screen *screen, const uint8_t *buf, unsigned int num, int *params, const char *report_name UNUSED, Region *region) {
#endif
enum State { START, NORMAL, MULTIPLE, COLOR, COLOR1, COLOR3 };
enum State state = START;
unsigned int num_params, num_start, i;
#define READ_PARAM { params[num_params++] = utoi(buf + num_start, i - num_start); }
#define SEND_SGR { REPORT_PARAMS(report_name, params, num_params, region); select_graphic_rendition(screen, params, num_params, region); state = START; num_params = 0; }
for (i=0, num_start=0, num_params=0; i < num && num_params < MAX_PARAMS; i++) {
switch(buf[i]) {
IS_DIGIT
switch(state) {
case START:
num_start = i;
state = NORMAL;
num_params = 0;
break;
default:
break;
}
break;
case ';':
switch(state) {
case START:
params[num_params++] = 0;
SEND_SGR;
break;
case NORMAL:
READ_PARAM;
switch(params[0]) {
case 38:
case 48:
case 58:
state = COLOR;
num_start = i + 1;
break;
default:
SEND_SGR;
break;
}
break;
case MULTIPLE:
READ_PARAM;
SEND_SGR;
break;
case COLOR:
READ_PARAM;
switch(params[1]) {
case 2:
state = COLOR3;
break;
case 5:
state = COLOR1;
break;
default:
REPORT_ERROR("Invalid SGR color code with unknown color type: %u", params[1]);
return;
}
num_start = i + 1;
break;
case COLOR1:
READ_PARAM;
SEND_SGR;
break;
case COLOR3:
READ_PARAM;
if (num_params == 5) { SEND_SGR; }
else num_start = i + 1;
break;
}
break;
case ':':
switch(state) {
case START:
REPORT_ERROR("Invalid SGR code containing ':' at an invalid location: %u", i);
return;
case NORMAL:
READ_PARAM;
state = MULTIPLE;
num_start = i + 1;
break;
case MULTIPLE:
READ_PARAM;
num_start = i + 1;
break;
case COLOR:
case COLOR1:
case COLOR3:
REPORT_ERROR("Invalid SGR code containing disallowed character: %c (U+%x)", buf[i], buf[i]);
return;
}
break;
default:
REPORT_ERROR("Invalid SGR code containing disallowed character: %c (U+%x)", buf[i], buf[i]);
return;
}
}
switch(state) {
case START:
if (num_params < MAX_PARAMS) params[num_params++] = 0;
SEND_SGR;
break;
case COLOR1:
case NORMAL:
case MULTIPLE:
if (i > num_start && num_params < MAX_PARAMS) { READ_PARAM; }
if (num_params) { SEND_SGR; }
else { REPORT_ERROR("Incomplete SGR code"); }
break;
case COLOR:
REPORT_ERROR("Invalid SGR code containing incomplete semi-colon separated color sequence");
break;
case COLOR3:
if (i > num_start && num_params < MAX_PARAMS) READ_PARAM;
if (num_params != 5) {
REPORT_ERROR("Invalid SGR code containing incomplete semi-colon separated color sequence");
break;
}
if (num_params) { SEND_SGR; }
else { REPORT_ERROR("Incomplete SGR code"); }
break;
}
#undef READ_PARAM
#undef SEND_SGR
}
static unsigned int
parse_region(Region *r, uint8_t *buf, unsigned int num) {
unsigned int i, start, num_params = 0;
int params[8] = {0};
for (i=0, start=0; i < num && num_params < 4; i++) {
switch(buf[i]) {
IS_DIGIT
break;
default:
if (i > start) params[num_params++] = utoi(buf + start, i - start);
else if (i == start && buf[i] == ';') params[num_params++] = 0;
start = i + 1;
break;
}
}
switch(num_params) {
case 0:
break;
case 1:
r->top = params[0];
break;
case 2:
r->top = params[0]; r->left = params[1];
break;
case 3:
r->top = params[0]; r->left = params[1]; r->bottom = params[2];
break;
default:
r->top = params[0]; r->left = params[1]; r->bottom = params[2]; r->right = params[3];
break;
}
return i;
}
static void
screen_cursor_up2(Screen *s, unsigned int count) { screen_cursor_up(s, count, false, -1); }
static void
screen_cursor_back1(Screen *s, unsigned int count) { screen_cursor_back(s, count, -1); }
static void
screen_tabn(Screen *s, unsigned int count) { for (index_type i=0; i < MAX(1u, count); i++) screen_tab(s); }
static const char*
csi_letter(unsigned code) {
static char buf[8];
if (33 <= code && code <= 126) snprintf(buf, sizeof(buf), "%c", code);
else snprintf(buf, sizeof(buf), "0x%x", code);
return buf;
}
static const char*
repr_csi_params(int *params, unsigned int num_params) {
if (!num_params) return "";
static char buf[256];
unsigned int pos = 0, i = 0;
while (pos < 200 && i++ < num_params && sizeof(buf) > pos + 1) {
const char *fmt = i < num_params ? "%i, " : "%i";
int ret = snprintf(buf + pos, sizeof(buf) - pos - 1, fmt, params[i-1]);
if (ret < 0) return "An error occurred formatting the params array";
pos += ret;
}
buf[pos] = 0;
return buf;
}
static void
dispatch_csi(PS *self) {
#define AT_MOST_ONE_PARAMETER { \
if (num_params > 1) { \
REPORT_ERROR("CSI code %s has %u > 1 parameters", csi_letter(code), num_params); \
break; \
} \
}
#define NON_NEGATIVE_PARAM(x) { \
if (x < 0) { \
REPORT_ERROR("CSI code %s is not allowed to have negative parameter (%d)", csi_letter(code), x); \
break; \
} \
}
#define CALL_CSI_HANDLER1(name, defval) \
AT_MOST_ONE_PARAMETER; \
p1 = num_params > 0 ? params[0] : defval; \
NON_NEGATIVE_PARAM(p1); \
REPORT_COMMAND(name, p1); \
name(self->screen, p1); \
break;
#define CALL_CSI_HANDLER1P(name, defval, qch) \
AT_MOST_ONE_PARAMETER; \
p1 = num_params > 0 ? params[0] : defval; \
NON_NEGATIVE_PARAM(p1); \
private = start_modifier == qch; \
REPORT_COMMAND(name, p1, private); \
name(self->screen, p1, private); \
break;
#define CALL_CSI_HANDLER1S(name, defval) \
AT_MOST_ONE_PARAMETER; \
p1 = num_params > 0 ? params[0] : defval; \
NON_NEGATIVE_PARAM(p1); \
REPORT_COMMAND(name, p1, start_modifier); \
name(self->screen, p1, start_modifier); \
break;
#define CALL_CSI_HANDLER1M(name, defval) \
AT_MOST_ONE_PARAMETER; \
p1 = num_params > 0 ? params[0] : defval; \
NON_NEGATIVE_PARAM(p1); \
REPORT_COMMAND(name, p1, end_modifier); \
name(self->screen, p1, end_modifier); \
break;
#define CALL_CSI_HANDLER2(name, defval1, defval2) \
if (num_params > 2) { \
REPORT_ERROR("CSI code %s has %u > 2 parameters", csi_letter(code), num_params); \
break; \
} \
p1 = num_params > 0 ? params[0] : defval1; \
p2 = num_params > 1 ? params[1] : defval2; \
NON_NEGATIVE_PARAM(p1); \
NON_NEGATIVE_PARAM(p2); \
REPORT_COMMAND(name, p1, p2); \
name(self->screen, p1, p2); \
break;
#define SET_MODE(func) \
p1 = start_modifier == '?' ? 5 : 0; \
for (i = 0; i < num_params; i++) { \
NON_NEGATIVE_PARAM(params[i]); \
REPORT_COMMAND(func, params[i], start_modifier == '?'); \
func(self->screen, params[i] << p1); \
} \
break;
#define NO_MODIFIERS(modifier, special, special_msg) { \
if (start_modifier || end_modifier) { \
if (special && modifier == special) { REPORT_ERROR(special_msg); } \
else { REPORT_ERROR("CSI code %s has unsupported start modifier: %s or end modifier: %s", csi_letter(code), csi_letter(start_modifier), csi_letter(end_modifier));} \
break; \
} \
}
char start_modifier = 0, end_modifier = 0;
uint8_t *buf = self->parser_buf, code = self->parser_buf[self->parser_buf_pos];
unsigned int num = self->parser_buf_pos, start, i, num_params=0;
static int params[MAX_PARAMS] = {0}, p1, p2;
bool private;
if (buf[0] == '>' || buf[0] == '<' || buf[0] == '?' || buf[0] == '!' || buf[0] == '=') {
start_modifier = (char)self->parser_buf[0];
buf++; num--;
}
if (num > 0) {
switch(buf[num-1]) {
CSI_SECONDARY
end_modifier = (char)buf[--num];
break;
}
}
if (code == SGR && !start_modifier && !end_modifier) {
#ifdef DUMP_COMMANDS
parse_sgr_dump(self, buf, num, params, "select_graphic_rendition", NULL);
#else
parse_sgr(self->screen, buf, num, params, "select_graphic_rendition", NULL);
#endif
return;
}
if (code == 'r' && !start_modifier && end_modifier == '$') {
// DECCARA
Region r = {0};
unsigned int consumed = parse_region(&r, buf, num);
num -= consumed; buf += consumed;
#ifdef DUMP_COMMANDS
parse_sgr_dump(self, buf, num, params, "deccara", &r);
#else
parse_sgr(self->screen, buf, num, params, "deccara", &r);
#endif
return;
}
for (i=0, start=0; i < num; i++) {
switch(buf[i]) {
IS_DIGIT
break;
case '-':
if (i > start) {
REPORT_ERROR("CSI code can contain hyphens only at the start of numbers");
return;
}
break;
default:
if (i > start) params[num_params++] = utoi(buf + start, i - start);
else if (i == start && buf[i] == ';') params[num_params++] = 0;
if (num_params >= MAX_PARAMS) { i = num; start = num + 1; }
else { start = i + 1; break; }
}
}
if (i > start) params[num_params++] = utoi(buf + start, i - start);
switch(code) {
case ICH:
NO_MODIFIERS(end_modifier, ' ', "Shift left escape code not implemented");
CALL_CSI_HANDLER1(screen_insert_characters, 1);
case REP:
CALL_CSI_HANDLER1(screen_repeat_character, 1);
case CUU:
NO_MODIFIERS(end_modifier, ' ', "Shift right escape code not implemented");
CALL_CSI_HANDLER1(screen_cursor_up2, 1);
case CUD:
case VPR:
CALL_CSI_HANDLER1(screen_cursor_down, 1);
case CUF:
case HPR:
CALL_CSI_HANDLER1(screen_cursor_forward, 1);
case CUB:
CALL_CSI_HANDLER1(screen_cursor_back1, 1);
case CNL:
CALL_CSI_HANDLER1(screen_cursor_down1, 1);
case CPL:
CALL_CSI_HANDLER1(screen_cursor_up1, 1);
case CHA:
case HPA:
CALL_CSI_HANDLER1(screen_cursor_to_column, 1);
case VPA:
CALL_CSI_HANDLER1(screen_cursor_to_line, 1);
case CBT:
CALL_CSI_HANDLER1(screen_backtab, 1);
case CHT:
CALL_CSI_HANDLER1(screen_tabn, 1);
case CUP:
case HVP:
CALL_CSI_HANDLER2(screen_cursor_position, 1, 1);
case ED:
CALL_CSI_HANDLER1P(screen_erase_in_display, 0, '?');
case EL:
CALL_CSI_HANDLER1P(screen_erase_in_line, 0, '?');
case IL:
CALL_CSI_HANDLER1(screen_insert_lines, 1);
case DL:
CALL_CSI_HANDLER1(screen_delete_lines, 1);
case DCH:
if (end_modifier == '#' && !start_modifier) {
CALL_CSI_HANDLER1(screen_push_colors, 0);
} else {
CALL_CSI_HANDLER1(screen_delete_characters, 1);
}
case 'Q':
if (end_modifier == '#' && !start_modifier) { CALL_CSI_HANDLER1(screen_pop_colors, 0); }
REPORT_ERROR("Unknown CSI Q sequence with start and end modifiers: '%c' '%c' and %u parameters", start_modifier, end_modifier, num_params);
break;
case 'R':
if (end_modifier == '#' && !start_modifier) {
REPORT_COMMAND(screen_report_color_stack);
screen_report_color_stack(self->screen);
break;
}
REPORT_ERROR("Unknown CSI R sequence with start and end modifiers: '%c' '%c' and %u parameters", start_modifier, end_modifier, num_params);
break;
case ECH:
CALL_CSI_HANDLER1(screen_erase_characters, 1);
case DA:
CALL_CSI_HANDLER1S(report_device_attributes, 0);
case TBC:
CALL_CSI_HANDLER1(screen_clear_tab_stop, 0);
case SM:
SET_MODE(screen_set_mode);
case RM:
SET_MODE(screen_reset_mode);
case DSR:
CALL_CSI_HANDLER1P(report_device_status, 0, '?');
case 's':
if (!start_modifier && !end_modifier && !num_params) {
REPORT_COMMAND(screen_save_cursor);
screen_save_cursor(self->screen);
break;
} else if (start_modifier == '?' && !end_modifier) {
if (!num_params) {
REPORT_COMMAND(screen_save_modes);
screen_save_modes(self->screen);
} else { SET_MODE(screen_save_mode); }
break;
}
REPORT_ERROR("Unknown CSI s sequence with start and end modifiers: '%c' '%c' and %u parameters", start_modifier, end_modifier, num_params);
break;
case 't':
if (!num_params) {
REPORT_ERROR("Unknown CSI t sequence with start and end modifiers: '%c' '%c' and no parameters", start_modifier, end_modifier);
break;
}
if (start_modifier || end_modifier) {
REPORT_ERROR("Unknown CSI t sequence with start and end modifiers: '%c' '%c', %u parameters and first parameter: %d", start_modifier, end_modifier, num_params, params[0]);
break;
}
switch(params[0]) {
case 4:
case 8:
log_error("Escape codes to resize text area are not supported");
break;
case 14:
case 16:
case 18:
CALL_CSI_HANDLER1(screen_report_size, 0);
break;
case 22:
case 23:
if (num_params == 3 && !params[2]) num_params = 2; // ignore extra 0, generated by weechat or ncurses
CALL_CSI_HANDLER2(screen_manipulate_title_stack, 22, 0);
break;
default:
REPORT_ERROR("Unknown CSI t window manipulation sequence with %u parameters and first parameter: %d", num_params, params[0]);
break;
}
break;
case 'u':
if (!start_modifier && !end_modifier && !num_params) {
REPORT_COMMAND(screen_restore_cursor);
screen_restore_cursor(self->screen);
break;
}
if (!end_modifier && start_modifier == '?') {
REPORT_COMMAND(screen_report_key_encoding_flags);
screen_report_key_encoding_flags(self->screen);
break;
}
if (!end_modifier && start_modifier == '=') {
CALL_CSI_HANDLER2(screen_set_key_encoding_flags, 0, 1);
break;
}
if (!end_modifier && start_modifier == '>') {
CALL_CSI_HANDLER1(screen_push_key_encoding_flags, 0);
break;
}
if (!end_modifier && start_modifier == '<') {
CALL_CSI_HANDLER1(screen_pop_key_encoding_flags, 1);
break;
}
REPORT_ERROR("Unknown CSI u sequence with start and end modifiers: '%c' '%c' and %u parameters", start_modifier, end_modifier, num_params);
break;
case 'r':
if (!start_modifier && !end_modifier) {
// DECSTBM
CALL_CSI_HANDLER2(screen_set_margins, 0, 0);
} else if (start_modifier == '?' && !end_modifier) {
if (!num_params) {
REPORT_COMMAND(screen_restore_modes);
screen_restore_modes(self->screen);
} else { SET_MODE(screen_restore_mode); }
break;
}
REPORT_ERROR("Unknown CSI r sequence with start and end modifiers: '%c' '%c' and %u parameters", start_modifier, end_modifier, num_params);
break;
case 'x':
if (!start_modifier && end_modifier == '*') {
CALL_CSI_HANDLER1(screen_decsace, 0);
}
REPORT_ERROR("Unknown CSI x sequence with start and end modifiers: '%c' '%c'", start_modifier, end_modifier);
break;
case DECSCUSR:
if (!start_modifier && end_modifier == ' ') {
CALL_CSI_HANDLER1M(screen_set_cursor, 1);
}
if (start_modifier == '>' && !end_modifier) {
CALL_CSI_HANDLER1(screen_xtversion, 0);
}
REPORT_ERROR("Unknown CSI q sequence with start and end modifiers: '%c' '%c'", start_modifier, end_modifier);
break;
case SU:
NO_MODIFIERS(end_modifier, ' ', "Select presentation directions escape code not implemented");
CALL_CSI_HANDLER1(screen_scroll, 1);
case SD:
if (!start_modifier && end_modifier == '+') {
CALL_CSI_HANDLER1(screen_reverse_scroll_and_fill_from_scrollback, 1);
} else {
NO_MODIFIERS(start_modifier, 0, "");
CALL_CSI_HANDLER1(screen_reverse_scroll, 1);
}
break;
case DECSTR:
if (end_modifier == '$') {
// DECRQM
CALL_CSI_HANDLER1P(report_mode_status, 0, '?');
} else {
REPORT_ERROR("Unknown DECSTR CSI sequence with start and end modifiers: '%c' '%c'", start_modifier, end_modifier);
}
break;
case 'm':
if (start_modifier == '>' && !end_modifier) {
REPORT_ERROR(
"The application is trying to use xterm's modifyOtherKeys."
" This is superseded by the kitty keyboard protocol: https://sw.kovidgoyal.net/kitty/keyboard-protocol/"
" the application should be updated to use that"
);
break;
}
/* fallthrough */
default:
REPORT_ERROR("Unknown CSI code: '%c' with start_modifier: '%c' and end_modifier: '%c' and parameters: '%s'", code, start_modifier, end_modifier, repr_csi_params(params, num_params));
}
}
// }}}
// APC mode {{{
#include "parse-graphics-command.h"
static void
dispatch_apc(PS *self) {
if (self->parser_buf_pos < 2) return;
switch(self->parser_buf[0]) {
case 'G':
parse_graphics_code(self, self->parser_buf, self->parser_buf_pos);
break;
default:
REPORT_ERROR("Unrecognized APC code: 0x%x", self->parser_buf[0]);
break;
}
}
// }}}
// PM mode {{{
static void
dispatch_pm(PS *self) {
if (self->parser_buf_pos < 2) return;
switch(self->parser_buf[0]) {
default:
REPORT_ERROR("Unrecognized PM code: 0x%x", self->parser_buf[0]);
break;
}
}
// }}}
static bool
accumulate_oth(PS *self) {
uint8_t ch = self->input_data[self->input_pos++];
switch(ch) {
case BEL:
return true;
case DEL:
case NUL:
break;
case ESC_ST:
if (self->parser_buf_pos > 0 && self->parser_buf[self->parser_buf_pos - 1] == ESC) {
self->parser_buf_pos--;
return true;
}
/* fallthrough */
default:
if (self->parser_buf_pos >= PARSER_BUF_SZ - 1) {
REPORT_ERROR("OTH sequence too long, truncating.");
return true;
}
self->parser_buf[self->parser_buf_pos++] = ch;
break;
}
return false;
}
#define dispatch_single_byte(dispatch, watch_for_pending) { \
switch(self->vte_state) { \
case VTE_ESC: \
dispatch##_esc_mode_byte(self); \
break; \
case VTE_CSI: \
if (accumulate_csi(self)) { dispatch##_csi(self); SET_STATE(NORMAL); watch_for_pending; } \
break; \
case VTE_OSC: \
if (accumulate_osc(self)) { \
dispatch##_osc(self); \
if (self->extended_osc_code) { \
self->input_pos--; \
if (accumulate_osc(self)) { dispatch##_osc(self); SET_STATE(NORMAL); } \
} else { SET_STATE(NORMAL); } \
} \
break; \
case VTE_APC: \
if (accumulate_oth(self)) { dispatch##_apc(self); SET_STATE(NORMAL); } \
break; \
case VTE_PM: \
if (accumulate_oth(self)) { dispatch##_pm(self); SET_STATE(NORMAL); } \
break; \
case VTE_DCS: \
if (accumulate_dcs(self)) { dispatch##_dcs(self); SET_STATE(NORMAL); watch_for_pending; } \
if (self->vte_state == ESC) { self->input_pos--; dispatch##_esc_mode_byte(self); } \
break; \
case VTE_NORMAL: \
dispatch##_normal_mode_byte(self); \
break; \
} \
} \
// Pending mode {{{
static void
ensure_pending_space(PS *self, size_t amt) {
if (self->pending_mode.capacity < self->pending_mode.used + amt) {
if (self->pending_mode.capacity) {
self->pending_mode.capacity += self->pending_mode.capacity >= READ_BUF_SZ ? PENDING_BUF_INCREMENT : self->pending_mode.capacity;
} else self->pending_mode.capacity = PENDING_BUF_INCREMENT;
self->pending_mode.buf = realloc(self->pending_mode.buf, self->pending_mode.capacity);
if (!self->pending_mode.buf) fatal("Out of memory");
}
}
static void
pending_normal_mode_byte(PS *self) {
uint8_t ch = self->input_data[self->input_pos++];
switch(ch) {
case ESC:
SET_STATE(ESC); break;
default:
ensure_pending_space(self, 1);
self->pending_mode.buf[self->pending_mode.used++] = ch;
break;
}
}
static void
pending_esc_mode_byte(PS *self) {
uint8_t ch = self->input_data[self->input_pos++];
if (self->parser_buf_pos > 0) {
ensure_pending_space(self, 3);
self->pending_mode.buf[self->pending_mode.used++] = ESC;
self->pending_mode.buf[self->pending_mode.used++] = self->parser_buf[self->parser_buf_pos - 1];
self->pending_mode.buf[self->pending_mode.used++] = ch;
SET_STATE(NORMAL);
return;
}
switch (ch) {
case ESC_DCS:
SET_STATE(DCS); break;
case ESC_OSC:
SET_STATE(OSC); break;
case ESC_CSI:
SET_STATE(CSI); break;
case ESC_APC:
SET_STATE(APC); break;
case ESC_PM:
SET_STATE(PM); break;
IS_ESCAPED_CHAR:
self->parser_buf[self->parser_buf_pos++] = ch;
break;
default:
ensure_pending_space(self, 2);
self->pending_mode.buf[self->pending_mode.used++] = ESC;
self->pending_mode.buf[self->pending_mode.used++] = ch;
SET_STATE(NORMAL); break;
}
}
#define pb(i) self->parser_buf[i]
static void
pending_escape_code(PS *self, char_type start_ch, char_type end_ch) {
ensure_pending_space(self, 4 + self->parser_buf_pos);
self->pending_mode.buf[self->pending_mode.used++] = ESC;
self->pending_mode.buf[self->pending_mode.used++] = start_ch;
memcpy(self->pending_mode.buf + self->pending_mode.used, self->parser_buf, self->parser_buf_pos);
self->pending_mode.used += self->parser_buf_pos;
if (start_ch != ESC_CSI) self->pending_mode.buf[self->pending_mode.used++] = ESC;
self->pending_mode.buf[self->pending_mode.used++] = end_ch;
}
static void pending_pm(PS *self) { pending_escape_code(self, ESC_PM, ESC_ST); }
static void pending_apc(PS *self) { pending_escape_code(self, ESC_APC, ESC_ST); }
static void
pending_osc(PS *self) {
const bool extended = is_extended_osc(self);
pending_escape_code(self, ESC_OSC, ESC_ST);
if (extended) continue_osc_52(self);
}
static void
pending_dcs(PS *self) {
if (self->parser_buf_pos >= 3 && pb(0) == '=' && (pb(1) == '1' || pb(1) == '2') && pb(2) == 's') {
self->pending_mode.activated_at = pb(1) == '1' ? monotonic() : 0;
if (pb(1) == '1') {
REPORT_COMMAND(screen_start_pending_mode);
self->pending_mode.activated_at = monotonic();
} else {
self->pending_mode.stop_escape_code_type = ESC_DCS;
self->pending_mode.activated_at = 0;
}
} else pending_escape_code(self, ESC_DCS, ESC_ST);
}
static void
pending_csi(PS *self) {
if (self->parser_buf_pos == 5 && pb(0) == '?' && pb(1) == '2' && pb(2) == '0' && pb(3) == '2' && pb(4) == '6' && (pb(5) == 'h' || pb(5) == 'l')) {
if (pb(5) == 'h') {
REPORT_COMMAND(screen_set_mode, 2026, 1);
self->pending_mode.activated_at = monotonic();
} else {
self->pending_mode.activated_at = 0;
self->pending_mode.stop_escape_code_type = ESC_CSI;
}
} else pending_escape_code(self, ESC_CSI, pb(self->parser_buf_pos));
}
#undef pb
static void
queue_pending_bytes(PS *self) {
while (self->input_pos < self->input_sz) {
dispatch_single_byte(pending, if (!self->pending_mode.activated_at) goto end);
}
end:
FLUSH_DRAW;
}
static void
parse_pending_bytes(PS *self) {
const uint8_t *orig_input_data = self->input_data; size_t orig_input_sz = self->input_sz, orig_input_pos = self->input_pos;
self->input_data = self->pending_mode.buf; self->input_sz = self->pending_mode.used; self->input_pos = 0;
self->draining_pending = true;
while (self->input_pos < self->input_sz) {
dispatch_single_byte(dispatch, ;);
}
self->draining_pending = false;
self->input_data = orig_input_data; self->input_sz = orig_input_sz; self->input_pos = orig_input_pos;
}
static void
dump_partial_escape_code_to_pending(PS *self) {
ensure_pending_space(self, self->parser_buf_pos + 2);
if (self->parser_buf_pos) {
switch(self->vte_state) {
case VTE_NORMAL: case VTE_ESC: break;
case VTE_CSI: case VTE_OSC: case VTE_DCS: case VTE_APC: case VTE_PM:
self->pending_mode.buf[self->pending_mode.used++] = ESC;
self->pending_mode.buf[self->pending_mode.used++] = self->vte_state;
break;
}
memcpy(self->pending_mode.buf + self->pending_mode.used, self->parser_buf, self->parser_buf_pos);
self->pending_mode.used += self->parser_buf_pos;
} else if (self->vte_state == VTE_ESC) {
self->pending_mode.buf[self->pending_mode.used++] = ESC;
}
}
// }}}
static void
parse_bytes_watching_for_pending(PS *self) {
while (self->input_pos < self->input_sz) {
dispatch_single_byte(dispatch, if (self->pending_mode.activated_at) goto end);
}
end:
FLUSH_DRAW;
}
static void
do_parse_vt(PS *self) {
enum STATE {START, PARSE_PENDING, PARSE_READ_BUF, QUEUE_PENDING};
enum STATE state = START;
VTEState vte_state_at_start_of_pending = VTE_NORMAL;
do {
switch(state) {
case START:
if (self->pending_mode.activated_at) {
if (self->pending_mode.activated_at + self->pending_mode.wait_time < self->now || self->pending_mode.capacity >= PENDING_BUF_LIMIT) {
dump_partial_escape_code_to_pending(self);
self->pending_mode.activated_at = 0;
state = START;
} else state = QUEUE_PENDING;
} else {
state = self->pending_mode.used ? PARSE_PENDING : PARSE_READ_BUF;
}
break;
case PARSE_PENDING:
self->vte_state = vte_state_at_start_of_pending;
vte_state_at_start_of_pending = VTE_NORMAL;
parse_pending_bytes(self);
self->pending_mode.used = 0;
self->pending_mode.activated_at = 0; // ignore any pending starts in the pending bytes
if (self->pending_mode.capacity > READ_BUF_SZ + PENDING_BUF_INCREMENT) {
self->pending_mode.capacity = READ_BUF_SZ;
self->pending_mode.buf = realloc(self->pending_mode.buf, self->pending_mode.capacity);
if (!self->pending_mode.buf) fatal("Out of memory");
}
if (self->pending_mode.stop_escape_code_type) {
if (self->pending_mode.stop_escape_code_type == ESC_DCS) { REPORT_COMMAND(screen_stop_pending_mode); }
else if (self->pending_mode.stop_escape_code_type == ESC_CSI) { REPORT_COMMAND(screen_reset_mode, 2026, 1); }
self->pending_mode.stop_escape_code_type = 0;
}
state = START;
break;
case PARSE_READ_BUF:
self->pending_mode.activated_at = 0;
parse_bytes_watching_for_pending(self);
state = START;
break;
case QUEUE_PENDING: {
self->pending_mode.stop_escape_code_type = 0;
if (self->pending_mode.used >= READ_BUF_SZ) {
dump_partial_escape_code_to_pending(self);
self->pending_mode.activated_at = 0;
state = START;
break;
}
if (!self->pending_mode.used) vte_state_at_start_of_pending = self->vte_state;
queue_pending_bytes(self);
state = START;
} break;
}
} while(self->input_pos < self->input_sz || (!self->pending_mode.activated_at && self->pending_mode.used));
}
// Boilerplate {{{
#define setup_worker \
static void
run_worker(Screen *screen, PyObject *dump_callback, monotonic_t now) {
PS *self = (PS*)screen->vt_parser->state;
#ifdef DUMP_COMMANDS
self->window_id = screen->window_id;
if (screen->read_buf_sz && dump_callback) {
RAII_PyObject(mv, PyMemoryView_FromMemory((char*)screen->read_buf, screen->read_buf_sz, PyBUF_READ));
PyObject *ret = PyObject_CallFunction(dump_callback, "KsO", screen->window_id, "bytes", mv);
if (ret) { Py_DECREF(ret); } else { PyErr_Clear(); }
}
#endif
self->input_data = screen->read_buf; self->input_sz = screen->read_buf_sz; self->input_pos = 0;
self->dump_callback = dump_callback; self->now = now; self->screen = screen;
do_parse_vt(self);
screen->read_buf_sz = 0;
}
#ifdef DUMP_COMMANDS
void
parse_vt_dump(Parser *p) {
do_parse_vt((PS*)p->state);
}
void
parse_worker_dump(Screen *screen, PyObject *dump_callback, monotonic_t now) { run_worker(screen, dump_callback, now); }
#else
static void
parse_vt(Parser *p) {
do_parse_vt((PS*)p->state);
}
extern void parse_vt_dump(Parser *p);
void
parse_worker(Screen *screen, PyObject *dump_callback, monotonic_t now) { run_worker(screen, dump_callback, now); }
#endif
#ifndef DUMP_COMMANDS
static PyObject*
new(PyTypeObject *type UNUSED, PyObject *args, PyObject UNUSED *kwds) {
id_type window_id=0;
if (!PyArg_ParseTuple(args, "|K", &window_id)) return NULL;
return (PyObject*) alloc_vt_parser(window_id);
}
void
free_vt_parser(Parser* self) {
if (self->state) {
PS *s = (PS*)self->state;
free(s->pending_mode.buf); s->pending_mode.buf = NULL;
free(self->state); self->state = NULL;
}
Py_TYPE(self)->tp_free((PyObject*)self);
}
static void
reset(PS *self) {
self->vte_state = VTE_NORMAL;
self->utf8.state = UTF8_ACCEPT;
self->utf8.prev = UTF8_ACCEPT;
self->parser_buf_pos = 0;
self->pending_mode.activated_at = 0;
self->pending_mode.stop_escape_code_type = 0;
}
void
reset_vt_parser(Parser *self) {
reset((PS*)self->state);
}
extern PyTypeObject Screen_Type;
static PyObject*
py_parse(Parser *p, PyObject *args) {
PS *self = (PS*)p->state;
const uint8_t *data; Py_ssize_t sz;
PyObject *dump_callback = NULL;
if (!PyArg_ParseTuple(args, "O!y#|O", &Screen_Type, &self->screen, &data, &sz, &dump_callback)) return NULL;
self->input_data = data; self->input_sz = sz; self->dump_callback = dump_callback;
self->input_pos = 0; self->now = monotonic();
if (dump_callback) parse_vt_dump(p); else parse_vt(p);
self->input_data = NULL; self->input_sz = 0; self->dump_callback = NULL; self->screen = NULL;
Py_RETURN_NONE;
}
static PyMethodDef methods[] = {
{"parse_bytes", (PyCFunction)py_parse, METH_VARARGS, ""},
{NULL},
};
PyTypeObject Parser_Type = {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "fast_data_types.Parser",
.tp_basicsize = sizeof(Parser),
.tp_dealloc = (destructor)free_vt_parser,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = "VT Escape code parser",
.tp_methods = methods,
.tp_new = new,
};
Parser*
alloc_vt_parser(id_type window_id) {
Parser *self = (Parser*)Parser_Type.tp_alloc(&Parser_Type, 1);
if (self != NULL) {
self->state = calloc(1, sizeof(PS));
if (!self->state) { Py_CLEAR(self); PyErr_NoMemory(); return NULL; }
PS *state = (PS*)self->state;
state->window_id = window_id;
state->pending_mode.wait_time = s_double_to_monotonic_t(2.0);
}
return self;
}
bool vt_parser_has_pending_data(Parser* p) { return ((PS*)p->state)->pending_mode.used != 0; }
monotonic_t vt_parser_pending_activated_at(Parser*p) { return ((PS*)p->state)->pending_mode.activated_at; }
void vt_parser_set_pending_activated_at(Parser*p, monotonic_t n) { ((PS*)p->state)->pending_mode.activated_at = n; }
monotonic_t vt_parser_pending_wait_time(Parser*p) { return ((PS*)p->state)->pending_mode.wait_time; }
void vt_parser_set_pending_wait_time(Parser*p, monotonic_t n) { ((PS*)p->state)->pending_mode.wait_time = n; }
INIT_TYPE(Parser)
#endif
// }}}