Skip to content

Commit 92c19b8

Browse files
authored
Merge pull request #4663 from krobelus/kitty-protocol-subshell-switch-key
Fix kitty keyboard protocol parsing of ctrl-o
2 parents bcc8074 + 87a6a14 commit 92c19b8

File tree

12 files changed

+521
-177
lines changed

12 files changed

+521
-177
lines changed

lib/Makefile.am

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ SUBLIB_includes = \
2626
widget.h
2727
2828
SRC_mc_utils = \
29+
terminal.c terminal.h \
2930
utilunix.c \
3031
unixcompat.h \
3132
util.c util.h

lib/terminal.c

+318
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
1+
/*
2+
Terminal emulation.
3+
4+
Copyright (C) 2025
5+
Free Software Foundation, Inc.
6+
7+
This file is part of the Midnight Commander.
8+
Authors:
9+
Miguel de Icaza, 1994, 1995, 1996
10+
Johannes Altmanninger, 2025
11+
12+
The Midnight Commander is free software: you can redistribute it
13+
and/or modify it under the terms of the GNU General Public License as
14+
published by the Free Software Foundation, either version 3 of the License,
15+
or (at your option) any later version.
16+
17+
The Midnight Commander is distributed in the hope that it will be useful,
18+
but WITHOUT ANY WARRANTY; without even the implied warranty of
19+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20+
GNU General Public License for more details.
21+
22+
You should have received a copy of the GNU General Public License
23+
along with this program. If not, see <https://www.gnu.org/licenses/>.
24+
*/
25+
26+
/** \file terminal.c
27+
* \brief Source: terminal emulation.
28+
* \author Johannes Altmanninger
29+
* \date 2025
30+
*
31+
* Subshells running inside Midnight Commander may assume they run inside
32+
* a terminal. This module helps us act like a real terminal in relevant
33+
* aspects.
34+
*/
35+
36+
#include <config.h>
37+
38+
#include "lib/util.h"
39+
#include "lib/strutil.h"
40+
41+
#include "lib/terminal.h"
42+
43+
/*** global variables ****************************************************************************/
44+
45+
/*** file scope macro definitions ****************************************************************/
46+
47+
/*** file scope type declarations ****************************************************************/
48+
49+
/*** forward declarations (file scope functions) *************************************************/
50+
51+
/* --------------------------------------------------------------------------------------------- */
52+
/*** file scope functions ************************************************************************/
53+
/* --------------------------------------------------------------------------------------------- */
54+
55+
/* --------------------------------------------------------------------------------------------- */
56+
/*** public functions ****************************************************************************/
57+
/* --------------------------------------------------------------------------------------------- */
58+
59+
/**
60+
* Parse a CSI command, starting from the third byte (i.e. the first
61+
* parameter byte, if any).
62+
*
63+
* On success, *sptr will point to one-past the end of the sequence.
64+
* On failure, *sptr will point to the first invalid byte.
65+
*
66+
* Here's the format in a sort of pidgin BNF:
67+
*
68+
* CSI-command = Esc '[' parameters (intermediate-byte)* final-byte
69+
* parameters = [0-9;:]+
70+
* | [<=>?] (parameter-byte)* # private mode
71+
* parameter-byte = [\x30-\x3F] # one of "0-9;:<=>?"
72+
* intermediate-byte = [\x20–\x2F] # one of " !\"#$%&'()*+,-./"
73+
* final-byte = [\x40-\x7e] # one of "@A–Z[\]^_`a–z{|}~"
74+
*/
75+
gboolean
76+
parse_csi (struct csi_command_t *out, const char **sptr, const char *end)
77+
{
78+
gboolean ok = FALSE;
79+
80+
const char *s = *sptr;
81+
if (s == end)
82+
goto invalid_sequence;
83+
84+
char c = *s;
85+
86+
#define NEXT_CHAR \
87+
do \
88+
{ \
89+
if (++s == end) \
90+
goto invalid_sequence; \
91+
c = *s; \
92+
} \
93+
while (0)
94+
95+
char private_mode = '\0';
96+
97+
if (c >= '<' && c <= '?') // "<=>?"
98+
{
99+
private_mode = c;
100+
NEXT_CHAR;
101+
}
102+
103+
// parameter bytes
104+
size_t param_count = 0;
105+
106+
if (private_mode != '\0')
107+
{
108+
while (c >= 0x30 && c <= 0x3F)
109+
NEXT_CHAR;
110+
}
111+
else
112+
{
113+
if (out != NULL)
114+
// N.B. empty parameter strings are allowed. For our current use,
115+
// treating them as zeroes happens to work.
116+
memset (out->params, 0, sizeof (out->params));
117+
118+
uint32_t tmp = 0;
119+
size_t sub_index = 0;
120+
121+
while (c >= 0x30 && c <= 0x3F)
122+
{
123+
if (c >= '0' && c <= '9')
124+
{
125+
if (param_count == 0)
126+
param_count = 1;
127+
if (tmp * 10 < tmp)
128+
goto invalid_sequence; // overflow
129+
tmp *= 10;
130+
if (tmp + c - '0' < tmp)
131+
goto invalid_sequence; // overflow
132+
tmp += c - '0';
133+
if (out != NULL)
134+
out->params[param_count - 1][sub_index] = tmp;
135+
}
136+
else if (c == ':' && ++sub_index < G_N_ELEMENTS (out->params[0]))
137+
tmp = 0;
138+
else if (c == ';' && ++param_count <= G_N_ELEMENTS (out->params))
139+
tmp = 0, sub_index = 0;
140+
else
141+
goto invalid_sequence;
142+
NEXT_CHAR;
143+
}
144+
}
145+
146+
while (c >= 0x20 && c <= 0x2F) // intermediate bytes
147+
NEXT_CHAR;
148+
#undef NEXT_CHAR
149+
150+
if (c < 0x40 || c > 0x7E) // final byte
151+
goto invalid_sequence;
152+
153+
++s;
154+
ok = TRUE;
155+
156+
if (out != NULL)
157+
{
158+
out->private_mode = private_mode;
159+
out->param_count = param_count;
160+
}
161+
162+
invalid_sequence:
163+
*sptr = s;
164+
return ok;
165+
}
166+
167+
/* --------------------------------------------------------------------------------------------- */
168+
/**
169+
* Remove all control sequences (CSI, OSC) from the argument string.
170+
*
171+
* The 256-color and true-color escape sequences should allow either ';' or ':' inside as
172+
* separator, actually, ':' is the more correct according to ECMA-48. Some terminal emulators
173+
* (e.g. xterm, gnome-terminal) support this.
174+
*
175+
* Non-printable characters are also removed.
176+
*/
177+
178+
char *
179+
strip_ctrl_codes (char *s)
180+
{
181+
char *w; // Current position where the stripped data is written
182+
const char *r; // Current position where the original data is read
183+
184+
if (s == NULL)
185+
return NULL;
186+
187+
const char *end = s + strlen (s);
188+
189+
for (w = s, r = s; *r != '\0';)
190+
{
191+
if (*r == ESC_CHAR)
192+
{
193+
// Skip the control sequence's arguments
194+
// '(' need to avoid strange 'B' letter in *Suse (if mc runs under root user)
195+
if (*(++r) == '[' || *r == '(')
196+
{
197+
++r;
198+
parse_csi (NULL, &r, end);
199+
// We're already past the sequence, no need to increment.
200+
continue;
201+
}
202+
if (*r == ']')
203+
{
204+
/*
205+
* Skip xterm's OSC (Operating System Command)
206+
* https://www.xfree86.org/current/ctlseqs.html
207+
* OSC P s ; P t ST
208+
* OSC P s ; P t BEL
209+
*/
210+
const char *new_r;
211+
212+
for (new_r = r; *new_r != '\0'; new_r++)
213+
{
214+
switch (*new_r)
215+
{
216+
// BEL
217+
case '\a':
218+
r = new_r;
219+
goto osc_out;
220+
case ESC_CHAR:
221+
// ST
222+
if (new_r[1] == '\\')
223+
{
224+
r = new_r + 1;
225+
goto osc_out;
226+
}
227+
break;
228+
default:
229+
break;
230+
}
231+
}
232+
osc_out:;
233+
}
234+
235+
/*
236+
* Now we are at the last character of the sequence.
237+
* Skip it unless it's binary 0.
238+
*/
239+
if (*r != '\0')
240+
r++;
241+
}
242+
else
243+
{
244+
const char *n;
245+
246+
n = str_cget_next_char (r);
247+
if (str_isprint (r))
248+
{
249+
memmove (w, r, n - r);
250+
w += n - r;
251+
}
252+
r = n;
253+
}
254+
}
255+
256+
*w = '\0';
257+
return s;
258+
}
259+
260+
/* --------------------------------------------------------------------------------------------- */
261+
/**
262+
* Convert "\E" -> esc character and ^x to control-x key and ^^ to ^ key
263+
*
264+
* @param p pointer to string
265+
*
266+
* @return newly allocated string
267+
*/
268+
269+
char *
270+
convert_controls (const char *p)
271+
{
272+
char *valcopy;
273+
char *q;
274+
275+
valcopy = g_strdup (p);
276+
277+
// Parse the escape special character
278+
for (q = valcopy; *p != '\0';)
279+
switch (*p)
280+
{
281+
case '\\':
282+
p++;
283+
284+
if (*p == 'e' || *p == 'E')
285+
{
286+
p++;
287+
*q++ = ESC_CHAR;
288+
}
289+
break;
290+
291+
case '^':
292+
p++;
293+
if (*p == '^')
294+
*q++ = *p++;
295+
else
296+
{
297+
char c;
298+
299+
c = *p | 0x20;
300+
if (c >= 'a' && c <= 'z')
301+
{
302+
*q++ = c - 'a' + 1;
303+
p++;
304+
}
305+
else if (*p != '\0')
306+
p++;
307+
}
308+
break;
309+
310+
default:
311+
*q++ = *p++;
312+
}
313+
314+
*q = '\0';
315+
return valcopy;
316+
}
317+
318+
/* --------------------------------------------------------------------------------------------- */

lib/terminal.h

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/** \file terminal.h
2+
* \brief Header: terminal emulation logic.
3+
*/
4+
5+
#ifndef MC_TERMINAL_H
6+
#define MC_TERMINAL_H
7+
8+
#include <sys/types.h>
9+
#include <inttypes.h> // uint32_t
10+
11+
#include "lib/global.h" // include <glib.h>
12+
13+
/*** typedefs(not structures) and defined constants **********************************************/
14+
15+
/*** enums ***************************************************************************************/
16+
17+
/*** structures declarations (and typedefs of structures)*****************************************/
18+
19+
struct csi_command_t
20+
{
21+
char private_mode;
22+
uint32_t params[16][4];
23+
size_t param_count;
24+
};
25+
26+
/*** global variables defined in .c file *********************************************************/
27+
28+
/*** declarations of public functions ************************************************************/
29+
30+
gboolean parse_csi (struct csi_command_t *out, const char **sptr, const char *end);
31+
32+
char *strip_ctrl_codes (char *s);
33+
34+
/* Replaces "\\E" and "\\e" with "\033". Replaces "^" + [a-z] with
35+
* ((char) 1 + (c - 'a')). The same goes for "^" + [A-Z].
36+
* Returns a newly allocated string. */
37+
char *convert_controls (const char *s);
38+
39+
/*** inline functions ****************************************************************************/
40+
41+
#endif

0 commit comments

Comments
 (0)