1 /*
2 * kittens.c
3 * Copyright (C) 2018 Kovid Goyal <kovid at kovidgoyal.net>
4 *
5 * Distributed under terms of the GPL3 license.
6 */
7
8 #include "data-types.h"
9 #include "monotonic.h"
10
11 #define CMD_BUF_SZ 2048
12
13
14 static bool
append_buf(char buf[CMD_BUF_SZ],size_t * pos,PyObject * ans)15 append_buf(char buf[CMD_BUF_SZ], size_t *pos, PyObject *ans) {
16 if (*pos) {
17 PyObject *bytes = PyBytes_FromStringAndSize(buf, *pos);
18 if (!bytes) { PyErr_NoMemory(); return false; }
19 int ret = PyList_Append(ans, bytes);
20 Py_CLEAR(bytes);
21 if (ret != 0) return false;
22 *pos = 0;
23 }
24 return true;
25 }
26
27 static bool
add_char(char buf[CMD_BUF_SZ],size_t * pos,char ch,PyObject * ans)28 add_char(char buf[CMD_BUF_SZ], size_t *pos, char ch, PyObject *ans) {
29 if (*pos >= CMD_BUF_SZ) {
30 if (!append_buf(buf, pos, ans)) return false;
31 }
32 buf[*pos] = ch;
33 *pos += 1;
34 return true;
35 }
36
37 static bool
read_response(int fd,monotonic_t timeout,PyObject * ans)38 read_response(int fd, monotonic_t timeout, PyObject *ans) {
39 static char buf[CMD_BUF_SZ];
40 size_t pos = 0;
41 enum ReadState {START, STARTING_ESC, P, AT, K, I, T, T2, Y, HYPHEN, C, M, BODY, TRAILING_ESC};
42 enum ReadState state = START;
43 char ch;
44 monotonic_t end_time = monotonic() + timeout;
45 while(monotonic() <= end_time) {
46 ssize_t len = read(fd, &ch, 1);
47 if (len == 0) continue;
48 if (len < 0) {
49 if (errno == EINTR || errno == EAGAIN) continue;
50 PyErr_SetFromErrno(PyExc_OSError);
51 return false;
52 }
53 end_time = monotonic() + timeout;
54 switch(state) {
55 case START:
56 if (ch == 0x1b) state = STARTING_ESC;
57 break;
58 #define CASE(curr, q, next) case curr: state = ch == q ? next : START; break;
59 CASE(STARTING_ESC, 'P', P);
60 CASE(P, '@', AT);
61 CASE(AT, 'k', K);
62 CASE(K, 'i', I);
63 CASE(I, 't', T);
64 CASE(T, 't', T2);
65 CASE(T2, 'y', Y);
66 CASE(Y, '-', HYPHEN);
67 CASE(HYPHEN, 'c', C);
68 CASE(C, 'm', M);
69 CASE(M, 'd', BODY);
70 case BODY:
71 if (ch == 0x1b) { state = TRAILING_ESC; }
72 else {
73 if (!add_char(buf, &pos, ch, ans)) return false;
74 }
75 break;
76 case TRAILING_ESC:
77 if (ch == '\\') return append_buf(buf, &pos, ans);
78 if (!add_char(buf, &pos, 0x1b, ans)) return false;
79 if (!add_char(buf, &pos, ch, ans)) return false;
80 state = BODY;
81 break;
82 }
83 }
84 PyErr_SetString(PyExc_TimeoutError,
85 "Timed out while waiting to read command response."
86 " Make sure you are running this command from within the kitty terminal."
87 " If you want to run commands from outside, then you have to setup a"
88 " socket with the --listen-on command line flag.");
89 return false;
90 }
91
92 static PyObject*
read_command_response(PyObject * self UNUSED,PyObject * args)93 read_command_response(PyObject *self UNUSED, PyObject *args) {
94 double timeout;
95 int fd;
96 PyObject *ans;
97 if (!PyArg_ParseTuple(args, "idO!", &fd, &timeout, &PyList_Type, &ans)) return NULL;
98 if (!read_response(fd, s_double_to_monotonic_t(timeout), ans)) return NULL;
99 Py_RETURN_NONE;
100 }
101
102 static PyObject*
parse_input_from_terminal(PyObject * self UNUSED,PyObject * args)103 parse_input_from_terminal(PyObject *self UNUSED, PyObject *args) {
104 enum State { NORMAL, ESC, CSI, ST, ESC_ST };
105 enum State state = NORMAL;
106 PyObject *uo, *text_callback, *dcs_callback, *csi_callback, *osc_callback, *pm_callback, *apc_callback, *callback;
107 int inbp = 0;
108 if (!PyArg_ParseTuple(args, "OOOOOOUp", &text_callback, &dcs_callback, &csi_callback, &osc_callback, &pm_callback, &apc_callback, &uo, &inbp)) return NULL;
109 Py_ssize_t sz = PyUnicode_GET_LENGTH(uo), pos = 0, start = 0, count = 0, consumed = 0;
110 callback = text_callback;
111 int kind = PyUnicode_KIND(uo);
112 void *data = PyUnicode_DATA(uo);
113 bool in_bracketed_paste_mode = inbp != 0;
114 #define CALL(cb, s_, num_) {\
115 PyObject *fcb = cb; \
116 Py_ssize_t s = s_, num = num_; \
117 if (in_bracketed_paste_mode && fcb != text_callback) { \
118 fcb = text_callback; num += 2; s -= 2; \
119 } \
120 if (num > 0) { \
121 PyObject *ret = PyObject_CallFunction(fcb, "N", PyUnicode_Substring(uo, s, s + num)); \
122 if (ret == NULL) return NULL; \
123 Py_DECREF(ret); \
124 } \
125 consumed = s_ + num_; \
126 count = 0; \
127 }
128 START_ALLOW_CASE_RANGE;
129 while (pos < sz) {
130 Py_UCS4 ch = PyUnicode_READ(kind, data, pos);
131 switch(state) {
132 case NORMAL:
133 if (ch == 0x1b) {
134 state = ESC;
135 CALL(text_callback, start, count);
136 start = pos;
137 } else count++;
138 break;
139 case ESC:
140 start = pos;
141 count = 0;
142 switch(ch) {
143 case 'P':
144 state = ST; callback = dcs_callback; break;
145 case '[':
146 state = CSI; callback = csi_callback; break;
147 case ']':
148 state = ST; callback = osc_callback; break;
149 case '^':
150 state = ST; callback = pm_callback; break;
151 case '_':
152 state = ST; callback = apc_callback; break;
153 default:
154 state = NORMAL; break;
155 }
156 break;
157 case CSI:
158 count++;
159 switch (ch) {
160 case 'a' ... 'z':
161 case 'A' ... 'Z':
162 case '@':
163 case '`':
164 case '{':
165 case '|':
166 case '}':
167 case '~':
168 #define IBP(w) ch == '~' && PyUnicode_READ(kind, data, start + 1) == '2' && PyUnicode_READ(kind, data, start + 2) == '0' && PyUnicode_READ(kind, data, start + 3) == w
169 if (IBP('1')) in_bracketed_paste_mode = false;
170 CALL(callback, start + 1, count);
171 if (IBP('0')) in_bracketed_paste_mode = true;
172 #undef IBP
173 state = NORMAL;
174 start = pos + 1;
175 break;
176 }
177 break;
178 case ESC_ST:
179 if (ch == '\\') {
180 CALL(callback, start + 1, count);
181 state = NORMAL; start = pos + 1;
182 consumed += 2;
183 } else count += 2;
184 break;
185 case ST:
186 if (ch == 0x1b) { state = ESC_ST; }
187 else count++;
188 break;
189 }
190 pos++;
191 }
192 if (state == NORMAL && count > 0) CALL(text_callback, start, count);
193 return PyUnicode_Substring(uo, consumed, sz);
194 END_ALLOW_CASE_RANGE;
195 #undef CALL
196 }
197
198 static PyMethodDef module_methods[] = {
199 METHODB(parse_input_from_terminal, METH_VARARGS),
200 METHODB(read_command_response, METH_VARARGS),
201 {NULL, NULL, 0, NULL} /* Sentinel */
202 };
203
204 bool
init_kittens(PyObject * module)205 init_kittens(PyObject *module) {
206 if (PyModule_AddFunctions(module, module_methods) != 0) return false;
207 return true;
208 }
209