From fa87ce72a89c4bfe19e19255af47dbbbb3208a1a Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 6 Feb 2018 17:29:05 +0530 Subject: [PATCH] A terminal input parse helper for the kittens --- kitty/data-types.c | 2 + kitty/kittens.c | 93 ++++++++++++++++++++++++++++++++++++++++ kitty_tests/datatypes.py | 22 +++++++++- 3 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 kitty/kittens.c diff --git a/kitty/data-types.c b/kitty/data-types.c index 5f9460ce1..37a16cac2 100644 --- a/kitty/data-types.c +++ b/kitty/data-types.c @@ -173,6 +173,7 @@ extern bool init_keys(PyObject *module); extern bool init_graphics(PyObject *module); extern bool init_shaders(PyObject *module); extern bool init_mouse(PyObject *module); +extern bool init_kittens(PyObject *module); #ifdef __APPLE__ extern int init_CoreText(PyObject *); extern bool init_cocoa(PyObject *module); @@ -206,6 +207,7 @@ PyInit_fast_data_types(void) { if (!init_graphics(m)) return NULL; if (!init_shaders(m)) return NULL; if (!init_mouse(m)) return NULL; + if (!init_kittens(m)) return NULL; #ifdef __APPLE__ if (!init_CoreText(m)) return NULL; if (!init_cocoa(m)) return NULL; diff --git a/kitty/kittens.c b/kitty/kittens.c new file mode 100644 index 000000000..407481c19 --- /dev/null +++ b/kitty/kittens.c @@ -0,0 +1,93 @@ +/* + * kittens.c + * Copyright (C) 2018 Kovid Goyal + * + * Distributed under terms of the GPL3 license. + */ + +#include "data-types.h" + +static PyObject* +parse_input_from_terminal(PyObject *self UNUSED, PyObject *args) { + enum State { NORMAL, ESC, CSI, ST, ESC_ST }; + enum State state = NORMAL; + PyObject *uo, *text_callback, *dcs_callback, *csi_callback, *osc_callback, *pm_callback, *apc_callback, *callback; + if (!PyArg_ParseTuple(args, "UOOOOOO", &uo, &text_callback, &dcs_callback, &csi_callback, &osc_callback, &pm_callback, &apc_callback)) return NULL; + Py_ssize_t sz = PyUnicode_GET_LENGTH(uo), pos = 0, start = 0, count = 0, consumed = 0; + callback = text_callback; + int kind = PyUnicode_KIND(uo); + void *data = PyUnicode_DATA(uo); +#define CALL(cb, s, num) {\ + if (num > 0) PyObject_CallFunction(cb, "N", PyUnicode_Substring(uo, s, s + num)); \ + consumed = s + num; \ + count = 0; \ +} + START_ALLOW_CASE_RANGE; + while (pos < sz) { + Py_UCS4 ch = PyUnicode_READ(kind, data, pos); + switch(state) { + case NORMAL: + if (ch == 0x1b) { + state = ESC; + CALL(text_callback, start, count); + } else count++; + break; + case ESC: + start = pos; + count = 0; + switch(ch) { + case 'P': + state = ST; callback = dcs_callback; break; + case '[': + state = CSI; callback = csi_callback; break; + case ']': + state = ST; callback = osc_callback; break; + case '^': + state = ST; callback = pm_callback; break; + case '_': + state = ST; callback = apc_callback; break; + default: + state = NORMAL; break; + } + break; + case CSI: + count++; + switch (ch) { + case 'a' ... 'z': + case 'A' ... 'Z': + CALL(callback, start + 1, count); + state = NORMAL; + start = pos + 1; + break; + } + break; + case ESC_ST: + if (ch == '\\') { + CALL(callback, start + 1, count); + state = NORMAL; start = pos + 1; + consumed += 2; + } else count += 2; + break; + case ST: + if (ch == 0x1b) { state = ESC_ST; } + else count++; + break; + } + pos++; + } + if (state == NORMAL && count > 0) CALL(text_callback, start, count); + return PyUnicode_Substring(uo, consumed, sz); + END_ALLOW_CASE_RANGE; +#undef CALL +} + +static PyMethodDef module_methods[] = { + METHODB(parse_input_from_terminal, METH_VARARGS), + {NULL, NULL, 0, NULL} /* Sentinel */ +}; + +bool +init_kittens(PyObject *module) { + if (PyModule_AddFunctions(module, module_methods) != 0) return false; + return true; +} diff --git a/kitty_tests/datatypes.py b/kitty_tests/datatypes.py index 19ea0d4c8..71371f8f7 100644 --- a/kitty_tests/datatypes.py +++ b/kitty_tests/datatypes.py @@ -4,7 +4,8 @@ from kitty.config import build_ansi_color_table, defaults from kitty.fast_data_types import ( - REVERSE, ColorProfile, Cursor as C, HistoryBuf, LineBuf, wcswidth, wcwidth + REVERSE, ColorProfile, Cursor as C, HistoryBuf, LineBuf, + parse_input_from_terminal, wcswidth, wcwidth ) from kitty.rgb import to_color from kitty.utils import sanitize_title @@ -338,6 +339,25 @@ class TestDataTypes(BaseTest): self.ae(wcswidth('\u2716\u2716\ufe0f\U0001f337'), 5) self.ae(sanitize_title('a\0\01 \t\n\f\rb'), 'a b') + def tp(*data, leftover='', text='', csi='', apc=''): + text_r, csi_r, apc_r, rest = [], [], [], [] + left = '' + for d in data: + left = parse_input_from_terminal(left + d, text_r.append, rest.append, csi_r.append, rest.append, rest.append, apc_r.append) + self.ae(left, leftover) + self.ae(text, ' '.join(text_r)) + self.ae(csi, ' '.join(csi_r)) + self.ae(apc, ' '.join(apc_r)) + self.assertFalse(rest) + + tp('abc', text='abc') + tp('a\033[38:5:12:32mb', text='a b', csi='38:5:12:32m') + tp('a\033_x,;(\033\\b', text='a b', apc='x,;(') + tp('a\033', '[', 'mb', text='a b', csi='m') + tp('a\033[', 'mb', text='a b', csi='m') + tp('a\033', '_', 'x\033', '\\b', text='a b', apc='x') + tp('a\033_', 'x', '\033', '\\', 'b', text='a b', apc='x') + def test_color_profile(self): c = ColorProfile() c.update_ansi_color_table(build_ansi_color_table())