From f6faecbaaab028b7da2e9dd75d4c12d3aba694e0 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 16 Nov 2016 11:44:41 +0530 Subject: [PATCH] Start work on CSI parser Also cleaup error reporting and command dumping macros --- kitty/control-codes.h | 115 +++++++++++++++++++++++++++++++++++ kitty/parser.c | 138 ++++++++++++++++++++++++++++++++---------- kitty_tests/parser.py | 10 +-- 3 files changed, 227 insertions(+), 36 deletions(-) diff --git a/kitty/control-codes.h b/kitty/control-codes.h index 0fcbc5fac..bce427849 100644 --- a/kitty/control-codes.h +++ b/kitty/control-codes.h @@ -65,11 +65,13 @@ #define OSC 0x9d // Sharp control codes +// ------------------- // Align display #define DECALN '8' // Esc control codes +// ------------------ // *Reset*. #define RIS 'c' @@ -104,4 +106,117 @@ // Set alternate keypad mode #define DECPAM '=' +// ECMA-48 CSI sequences. +// --------------------- +// *Insert character*: Insert the indicated # of blank characters. +#define ICH '@' + +// *Cursor up*: Move cursor up the indicated # of lines in same column. +// Cursor stops at top margin. +#define CUU 'A' + +// *Cursor down*: Move cursor down the indicated # of lines in same +// column. Cursor stops at bottom margin. +#define CUD 'B' + +// *Cursor forward*: Move cursor right the indicated # of columns. +// Cursor stops at right margin. +#define CUF 'C' + +// *Cursor back*: Move cursor left the indicated # of columns. Cursor +// stops at left margin. +#define CUB 'D' + +// *Cursor next line*: Move cursor down the indicated # of lines to +// column 1. +#define CNL 'E' + +// *Cursor previous line*: Move cursor up the indicated # of lines to +// column 1. +#define CPL 'F' + +// *Cursor horizontal align*: Move cursor to the indicated column in +// current line. +#define CHA 'G' + +// *Cursor position*: Move cursor to the indicated line, column (origin +// at ``1, 1``). +#define CUP 'H' + +// *Erase data* (default: from cursor to end of line). +#define ED 'J' + +// *Erase in line* (default: from cursor to end of line). +#define EL 'K' + +// *Insert line*: Insert the indicated # of blank lines, starting from +// the current line. Lines displayed below cursor move down. Lines moved +// past the bottom margin are lost. +#define IL 'L' + +// *Delete line*: Delete the indicated # of lines, starting from the +// current line. As lines are deleted, lines displayed below cursor +// move up. Lines added to bottom of screen have spaces with same +// character attributes as last line move up. +#define DL 'M' + +// *Delete character*: Delete the indicated # of characters on the +// current line. When character is deleted, all characters to the right +// of cursor move left. +#define DCH 'P' + +// *Erase character*: Erase the indicated # of characters on the +// current line. +#define ECH 'X' + +// *Horizontal position relative*: Same as :data:`CUF`. +#define HPR 'a' + +// *Device Attributes*. +#define DA 'c' + +// *Vertical position adjust*: Move cursor to the indicated line, +// current column. +#define VPA 'd' + +// *Vertical position relative*: Same as :data:`CUD`. +#define VPR 'e' + +// *Horizontal / Vertical position*: Same as :data:`CUP`. +#define HVP 'f' + +// *Tabulation clear*: Clears a horizontal tab stop at cursor position. +#define TBC 'g' + +// *Set mode*. +#define SM 'h' + +// *Reset mode*. +#define RM 'l' + +// *Select graphics rendition*: The terminal can display the following +// character attributes that change the character display without +// changing the character (see :mod:`pyte.graphics`). +#define SGR 'm' + +// *Device status report*. +#define DSR 'n' + +// Soft reset +#define DECSTR 'p' + +// *Select top and bottom margins*: Selects margins, defining the +// scrolling region; parameters are top and bottom line. If called +// without any arguments, whole screen is used. +#define DECSTBM 'r' + +// *Horizontal position adjust*: Same as :data:`CHA`. +#define HPA '\'' + + +// Misc sequences +// ---------------- + +// Change cursor shape/blink +#define DECSCUSR 'q' diff --git a/kitty/parser.c b/kitty/parser.c index dd15cb845..12e80002f 100644 --- a/kitty/parser.c +++ b/kitty/parser.c @@ -5,6 +5,7 @@ * Distributed under terms of the GPL3 license. */ +#include #include "data-types.h" #include "control-codes.h" @@ -14,17 +15,63 @@ #define OSC_STATE 3 #define DCS_STATE 4 +#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, const char *fmt, ...) { + va_list argptr; + va_start(argptr, fmt); + PyObject *temp = PyUnicode_FromFormatV(fmt, argptr); + va_end(argptr); + if (temp != NULL) { + Py_XDECREF(PyObject_CallFunctionObjArgs(dump_callback, temp, NULL)); PyErr_Clear(); + Py_CLEAR(temp); + } +} + +#define REPORT_ERROR(...) _report_error(dump_callback, __VA_ARGS__); + +#define REPORT_COMMAND0(name) \ + Py_XDECREF(PyObject_CallFunction(dump_callback, "s", #name)); PyErr_Clear(); + +#define REPORT_COMMAND1(name, x) \ + Py_XDECREF(PyObject_CallFunction(dump_callback, "si", #name, (int)x)); PyErr_Clear(); + +#define REPORT_COMMAND2(name, x, y) \ + Py_XDECREF(PyObject_CallFunction(dump_callback, "sii", #name, (int)x, (int)y)); PyErr_Clear(); + #define HANDLER(name) \ static inline void read_##name(Screen *screen, uint8_t UNUSED *buf, unsigned int UNUSED buflen, unsigned int UNUSED *pos, PyObject UNUSED *dump_callback) + #else + +#define REPORT_ERROR(...) fprintf(stderr, "[PARSE ERROR] "); fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\n"); + +#define REPORT_COMMAND0(name) +#define REPORT_COMMAND1(name, x) +#define REPORT_COMMAND2(name, x, y) + #define HANDLER(name) \ static inline void read_##name(Screen *screen, uint8_t UNUSED *buf, unsigned int UNUSED buflen, unsigned int UNUSED *pos) + #endif + #define SET_STATE(state) screen->parser_state = state; screen->parser_buf_pos = 0; // Parse text {{{ + + HANDLER(text) { uint8_t ch; @@ -37,16 +84,9 @@ HANDLER(text) { screen->parser_text_start = 0; \ } -#ifdef DUMP_COMMANDS #define CALL_SCREEN_HANDLER(name) \ - DRAW_TEXT; \ - Py_XDECREF(PyObject_CallFunction(dump_callback, "sC", #name, (int)ch)); PyErr_Clear(); \ + DRAW_TEXT; REPORT_COMMAND1(name, ch); \ name(screen, ch); break; -#else -#define CALL_SCREEN_HANDLER(name) \ - DRAW_TEXT; \ - name(screen, ch); break; -#endif switch(ch) { case BEL: @@ -83,6 +123,7 @@ HANDLER(text) { } DRAW_TEXT; } +#define moo 1 // }}} // Parse ESC {{{ @@ -90,13 +131,7 @@ HANDLER(text) { static inline void screen_linefeed2(Screen *screen) { screen_linefeed(screen, '\n'); } static inline void escape_dispatch(Screen *screen, uint8_t ch, PyObject UNUSED *dump_callback) { -#ifdef DUMP_COMMANDS -#define CALL_ED(name) \ - Py_XDECREF(PyObject_CallFunction(dump_callback, "s", #name)); PyErr_Clear(); \ - name(screen); break; -#else -#define CALL_ED(name) name(screen); break; -#endif +#define CALL_ED(name) REPORT_COMMAND0(name); name(screen); break; switch (ch) { case RIS: CALL_ED(screen_reset); @@ -116,36 +151,24 @@ static inline void escape_dispatch(Screen *screen, uint8_t ch, PyObject UNUSED * CALL_ED(screen_normal_keypad_mode); case DECPAM: CALL_ED(screen_alternate_keypad_mode); -#ifdef DUMP_COMMANDS default: - Py_XDECREF(PyObject_CallFunction(dump_callback, "sB", "Unknown char in escape_dispatch: ", ch)); PyErr_Clear(); -#endif + REPORT_ERROR("%s%d", "Unknown char in escape_dispatch: ", ch); } } static inline void sharp_dispatch(Screen *screen, uint8_t ch, PyObject UNUSED *dump_callback) { switch(ch) { case DECALN: -#ifdef DUMP_COMMANDS - Py_XDECREF(PyObject_CallFunction(dump_callback, "s", "screen_alignment_display")); PyErr_Clear(); -#endif + REPORT_COMMAND0(screen_alignment_display); screen_alignment_display(screen); break; -#ifdef DUMP_COMMANDS default: - Py_XDECREF(PyObject_CallFunction(dump_callback, "sB", "Unknown char in sharp_dispatch: ", ch)); PyErr_Clear(); -#endif + REPORT_ERROR("%s%d", "Unknown char in sharp_dispatch: ", ch); } } HANDLER(esc) { -#ifdef DUMP_COMMANDS -#define ESC_DISPATCH(which, extra) which(screen, ch, extra); SET_STATE(NORMAL_STATE); \ - Py_XDECREF(PyObject_CallFunction(dump_callback, "sCC", #which, (int)ch, (int)(extra))); PyErr_Clear(); \ - return; -#else -#define ESC_DISPATCH(which, extra) which(screen, ch, extra); SET_STATE(NORMAL_STATE); return; -#endif +#define ESC_DISPATCH(which, extra) REPORT_COMMAND2(which, ch, extra); which(screen, ch, extra); SET_STATE(NORMAL_STATE); return; #ifdef DUMP_COMMANDS #define ESC_DELEGATE(which) which(screen, ch, dump_callback); SET_STATE(NORMAL_STATE); return; #else @@ -188,7 +211,58 @@ HANDLER(esc) { // Parse CSI {{{ HANDLER(csi) { - screen->parser_state = NORMAL_STATE; +#define CALL_BASIC_HANDLER(name) REPORT_COMMAND1(screen, ch); name(screen, ch); break; +#define HANDLE_BASIC_CH \ + case BEL: \ + CALL_BASIC_HANDLER(screen_bell); \ + case BS: \ + CALL_BASIC_HANDLER(screen_backspace); \ + case HT: \ + CALL_BASIC_HANDLER(screen_tab); \ + case LF: \ + case VT: \ + case FF: \ + CALL_BASIC_HANDLER(screen_linefeed); \ + case CR: \ + CALL_BASIC_HANDLER(screen_carriage_return); \ + case NUL: \ + case DEL: \ + break; // no-op + + uint8_t ch = buf[(*pos)++]; + switch(screen->parser_buf_pos) { + case 0: // CSI starting + screen->parser_buf[0] = 0; + screen->parser_buf[1] = 0; + switch(ch) { + IS_DIGIT + screen->parser_buf_pos = 2; + screen->parser_buf[1] = ch; + break; + case '?': + case '>': + case '!': + screen->parser_buf[0] = ch; screen->parser_buf_pos = 1; + break; + HANDLE_BASIC_CH + default: + REPORT_ERROR("%s%d", "Invalid first character for CSI: ", (int)ch); + SET_STATE(NORMAL_STATE); + return; + } + break; + default: // CSI started + switch(ch) { + IS_DIGIT + case ';': + if (screen->parser_buf_pos >= PARSER_BUF_SZ - 1) { + REPORT_ERROR("%s", "CSI sequence too long, ignoring."); + SET_STATE(NORMAL_STATE); + } else screen->parser_buf[screen->parser_buf_pos++] = ch; + break; + } + break; + } } // }}} diff --git a/kitty_tests/parser.py b/kitty_tests/parser.py index cb17b266c..81da952d4 100644 --- a/kitty_tests/parser.py +++ b/kitty_tests/parser.py @@ -12,6 +12,8 @@ from kitty.fast_data_types import parse_bytes, parse_bytes_dump class CmdDump(list): def __call__(self, *a): + if len(a) == 1: + a = a[0] self.append(a) @@ -33,7 +35,7 @@ class TestScreen(BaseTest): pb('3456') self.ae(str(s.line(0)), '12345') self.ae(str(s.line(1)), '6 ') - pb(b'\n123\n\r45', ('screen_linefeed', '\n'), ('screen_linefeed', '\n'), ('screen_carriage_return', '\r')) + pb(b'\n123\n\r45', ('screen_linefeed', ord('\n')), ('screen_linefeed', ord('\n')), ('screen_carriage_return', ord('\r'))) self.ae(str(s.line(1)), '6 ') self.ae(str(s.line(2)), ' 123 ') self.ae(str(s.line(3)), '45 ') @@ -47,9 +49,9 @@ class TestScreen(BaseTest): def test_esc_codes(self): s = self.create_screen() pb = partial(self.parse_buytes_dump, s) - pb('12\033Da', ('screen_index',)) + pb('12\033Da', 'screen_index') self.ae(str(s.line(0)), '12 ') self.ae(str(s.line(1)), ' a ') - pb('\033x', ('Unknown char in escape_dispatch: ', ord('x'))) - pb('\033c123', ('screen_reset',)) + pb('\033x', 'Unknown char in escape_dispatch: %d' % ord('x')) + pb('\033c123', 'screen_reset') self.ae(str(s.line(0)), '123 ')