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