From ac398130307a6a79f78977c8407fdecaa37e797c Mon Sep 17 00:00:00 2001 From: Johannes Altmanninger Date: Wed, 5 Mar 2025 13:24:35 +0100 Subject: [PATCH 1/5] Extract escape-sequence parsing logic into new module Following commits want to add more logic (and tests) for decoding terminal escape sequences, and implement a subset of the kitty keyboard protocol. It's probably not right to keep all of this in lib/util.c, so extract a new module. I called it "terminal" because it helps mc act as terminal. We also have lib/tty/tty.h though that's for talking to the actual terminal that mc is running inside. Signed-off-by: Johannes Altmanninger Signed-off-by: Yury V. Zaytsev --- lib/Makefile.am | 1 + lib/terminal.c | 216 +++++++++++++++++++++++++++++++++++++++ lib/util.c | 157 ---------------------------- lib/util.h | 6 -- src/filemanager/layout.c | 1 + src/learn.c | 3 +- src/setup.c | 1 + src/subshell/common.c | 1 + 8 files changed, 222 insertions(+), 164 deletions(-) create mode 100644 lib/terminal.c diff --git a/lib/Makefile.am b/lib/Makefile.am index 9e0bbbb77..060e7175d 100644 --- a/lib/Makefile.am +++ b/lib/Makefile.am @@ -26,6 +26,7 @@ SUBLIB_includes = \ widget.h SRC_mc_utils = \ + terminal.c terminal.h \ utilunix.c \ unixcompat.h \ util.c util.h diff --git a/lib/terminal.c b/lib/terminal.c new file mode 100644 index 000000000..271b51ee8 --- /dev/null +++ b/lib/terminal.c @@ -0,0 +1,216 @@ +/* + Terminal emulation. + + Copyright (C) 2025 + Free Software Foundation, Inc. + + This file is part of the Midnight Commander. + Authors: + Miguel de Icaza, 1994, 1995, 1996 + Johannes Altmanninger, 2025 + + The Midnight Commander is free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License as + published by the Free Software Foundation, either version 3 of the License, + or (at your option) any later version. + + The Midnight Commander is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +/** \file terminal.c + * \brief Source: terminal emulation. + * \author Johannes Altmanninger + * \date 2025 + * + * Subshells running inside Midnight Commander may assume they run inside + * a terminal. This module helps us act like a real terminal in relevant + * aspects. + */ + +#include + +#include "lib/util.h" +#include "lib/strutil.h" + +#include "lib/terminal.h" + +/*** global variables ****************************************************************************/ + +/*** file scope macro definitions ****************************************************************/ + +/*** file scope type declarations ****************************************************************/ + +/*** forward declarations (file scope functions) *************************************************/ + +/* --------------------------------------------------------------------------------------------- */ +/*** file scope functions ************************************************************************/ +/* --------------------------------------------------------------------------------------------- */ + +/* --------------------------------------------------------------------------------------------- */ +/*** public functions ****************************************************************************/ +/* --------------------------------------------------------------------------------------------- */ +/** + * Remove all control sequences from the argument string. We define + * "control sequence", in a sort of pidgin BNF, as follows: + * + * control-seq = Esc non-'[' + * | Esc '[' (parameter-byte)* (intermediate-byte)* final-byte + * parameter-byte = [\x30-\x3F] # one of "0-9;:<=>?" + * intermediate-byte = [\x20–\x2F] # one of " !\"#$%&'()*+,-./" + * final-byte = [\x40-\x7e] # one of "@A–Z[\]^_`a–z{|}~" + * + * The 256-color and true-color escape sequences should allow either ';' or ':' inside as separator, + * actually, ':' is the more correct according to ECMA-48. + * Some terminal emulators (e.g. xterm, gnome-terminal) support this. + * + * Non-printable characters are also removed. + */ + +char * +strip_ctrl_codes (char *s) +{ + char *w; // Current position where the stripped data is written + char *r; // Current position where the original data is read + + if (s == NULL) + return NULL; + + const char *end = s + strlen (s); + + for (w = s, r = s; *r != '\0';) + { + if (*r == ESC_CHAR) + { + // Skip the control sequence's arguments + // '(' need to avoid strange 'B' letter in *Suse (if mc runs under root user) + if (*(++r) == '[' || *r == '(') + { + // strchr() matches trailing binary 0 + while (*(++r) != '\0' && strchr ("0123456789;:<=>?", *r) != NULL) + ; + while (*r != '\0' && (*r < 0x40 || *r > 0x7E)) + ++r; + } + if (*r == ']') + { + /* + * Skip xterm's OSC (Operating System Command) + * https://www.xfree86.org/current/ctlseqs.html + * OSC P s ; P t ST + * OSC P s ; P t BEL + */ + char *new_r; + + for (new_r = r; *new_r != '\0'; new_r++) + { + switch (*new_r) + { + // BEL + case '\a': + r = new_r; + goto osc_out; + case ESC_CHAR: + // ST + if (new_r[1] == '\\') + { + r = new_r + 1; + goto osc_out; + } + break; + default: + break; + } + } + osc_out:; + } + + /* + * Now we are at the last character of the sequence. + * Skip it unless it's binary 0. + */ + if (*r != '\0') + r++; + } + else + { + char *n; + + n = str_cget_next_char (r); + if (str_isprint (r)) + { + memmove (w, r, n - r); + w += n - r; + } + r = n; + } + } + + *w = '\0'; + return s; +} + +/* --------------------------------------------------------------------------------------------- */ +/** + * Convert "\E" -> esc character and ^x to control-x key and ^^ to ^ key + * + * @param p pointer to string + * + * @return newly allocated string + */ + +char * +convert_controls (const char *p) +{ + char *valcopy; + char *q; + + valcopy = g_strdup (p); + + // Parse the escape special character + for (q = valcopy; *p != '\0';) + switch (*p) + { + case '\\': + p++; + + if (*p == 'e' || *p == 'E') + { + p++; + *q++ = ESC_CHAR; + } + break; + + case '^': + p++; + if (*p == '^') + *q++ = *p++; + else + { + char c; + + c = *p | 0x20; + if (c >= 'a' && c <= 'z') + { + *q++ = c - 'a' + 1; + p++; + } + else if (*p != '\0') + p++; + } + break; + + default: + *q++ = *p++; + } + + *q = '\0'; + return valcopy; +} + +/* --------------------------------------------------------------------------------------------- */ diff --git a/lib/util.c b/lib/util.c index 88fde2afe..76e4638bc 100644 --- a/lib/util.c +++ b/lib/util.c @@ -741,105 +741,6 @@ skip_numbers (const char *s) return su; } -/* --------------------------------------------------------------------------------------------- */ -/** - * Remove all control sequences from the argument string. We define - * "control sequence", in a sort of pidgin BNF, as follows: - * - * control-seq = Esc non-'[' - * | Esc '[' (parameter-byte)* (intermediate-byte)* final-byte - * parameter-byte = [\x30-\x3F] # one of "0-9;:<=>?" - * intermediate-byte = [\x20–\x2F] # one of " !\"#$%&'()*+,-./" - * final-byte = [\x40-\x7e] # one of "@A–Z[\]^_`a–z{|}~" - * - * The 256-color and true-color escape sequences should allow either ';' or ':' inside as separator, - * actually, ':' is the more correct according to ECMA-48. - * Some terminal emulators (e.g. xterm, gnome-terminal) support this. - * - * Non-printable characters are also removed. - */ - -char * -strip_ctrl_codes (char *s) -{ - char *w; // Current position where the stripped data is written - char *r; // Current position where the original data is read - - if (s == NULL) - return NULL; - - for (w = s, r = s; *r != '\0';) - { - if (*r == ESC_CHAR) - { - // Skip the control sequence's arguments - // '(' need to avoid strange 'B' letter in *Suse (if mc runs under root user) - if (*(++r) == '[' || *r == '(') - { - // strchr() matches trailing binary 0 - while (*(++r) != '\0' && strchr ("0123456789;:<=>?", *r) != NULL) - ; - while (*r != '\0' && (*r < 0x40 || *r > 0x7E)) - ++r; - } - else if (*r == ']') - { - /* - * Skip xterm's OSC (Operating System Command) - * https://www.xfree86.org/current/ctlseqs.html - * OSC P s ; P t ST - * OSC P s ; P t BEL - */ - char *new_r; - - for (new_r = r; *new_r != '\0'; new_r++) - { - switch (*new_r) - { - // BEL - case '\a': - r = new_r; - goto osc_out; - case ESC_CHAR: - // ST - if (new_r[1] == '\\') - { - r = new_r + 1; - goto osc_out; - } - break; - default: - break; - } - } - osc_out:; - } - - /* - * Now we are at the last character of the sequence. - * Skip it unless it's binary 0. - */ - if (*r != '\0') - r++; - } - else - { - char *n; - - n = str_get_next_char (r); - if (str_isprint (r)) - { - memmove (w, r, n - r); - w += n - r; - } - r = n; - } - } - - *w = '\0'; - return s; -} - /* --------------------------------------------------------------------------------------------- */ enum compression_type @@ -977,64 +878,6 @@ wipe_password (char *passwd) } } -/* --------------------------------------------------------------------------------------------- */ -/** - * Convert "\E" -> esc character and ^x to control-x key and ^^ to ^ key - * - * @param p pointer to string - * - * @return newly allocated string - */ - -char * -convert_controls (const char *p) -{ - char *valcopy; - char *q; - - valcopy = g_strdup (p); - - // Parse the escape special character - for (q = valcopy; *p != '\0';) - switch (*p) - { - case '\\': - p++; - - if (*p == 'e' || *p == 'E') - { - p++; - *q++ = ESC_CHAR; - } - break; - - case '^': - p++; - if (*p == '^') - *q++ = *p++; - else - { - char c; - - c = *p | 0x20; - if (c >= 'a' && c <= 'z') - { - *q++ = c - 'a' + 1; - p++; - } - else if (*p != '\0') - p++; - } - break; - - default: - *q++ = *p++; - } - - *q = '\0'; - return valcopy; -} - /* --------------------------------------------------------------------------------------------- */ /** * Finds out a relative path from first to second, i.e. goes as many .. diff --git a/lib/util.h b/lib/util.h index 1df2070fb..87505f3f1 100644 --- a/lib/util.h +++ b/lib/util.h @@ -191,12 +191,6 @@ const char *extension (const char *); const char *unix_error_string (int error_num); const char *skip_separators (const char *s); const char *skip_numbers (const char *s); -char *strip_ctrl_codes (char *s); - -/* Replaces "\\E" and "\\e" with "\033". Replaces "^" + [a-z] with - * ((char) 1 + (c - 'a')). The same goes for "^" + [A-Z]. - * Returns a newly allocated string. */ -char *convert_controls (const char *s); /* overwrites passwd with '\0's and frees it. */ void wipe_password (char *passwd); diff --git a/src/filemanager/layout.c b/src/filemanager/layout.c index 21db1420a..40aeed79b 100644 --- a/src/filemanager/layout.c +++ b/src/filemanager/layout.c @@ -41,6 +41,7 @@ #include #include "lib/global.h" +#include "lib/terminal.h" // strip_ctrl_codes() #include "lib/tty/tty.h" #include "lib/skin.h" #include "lib/tty/key.h" diff --git a/src/learn.c b/src/learn.c index cc06ddf85..d19e7bc46 100644 --- a/src/learn.c +++ b/src/learn.c @@ -38,7 +38,8 @@ #include "lib/tty/key.h" #include "lib/mcconfig.h" #include "lib/strutil.h" -#include "lib/util.h" // convert_controls() +#include "lib/terminal.h" // convert_controls() +#include "lib/util.h" // MC_PTR_FREE #include "lib/widget.h" #include "setup.h" diff --git a/src/setup.c b/src/setup.c index 2c8ed4866..ec4b4c24a 100644 --- a/src/setup.c +++ b/src/setup.c @@ -38,6 +38,7 @@ #include "lib/tty/key.h" #include "lib/mcconfig.h" // num_history_items_recorded #include "lib/fileloc.h" +#include "lib/terminal.h" // convert_controls() #include "lib/timefmt.h" #include "lib/util.h" diff --git a/src/subshell/common.c b/src/subshell/common.c index f4a4a9c7b..7a2775471 100644 --- a/src/subshell/common.c +++ b/src/subshell/common.c @@ -98,6 +98,7 @@ #include "lib/global.h" #include "lib/fileloc.h" +#include "lib/terminal.h" #include "lib/unixcompat.h" #include "lib/tty/tty.h" // LINES #include "lib/tty/key.h" // XCTRL From 27c3da7b81501efd6747eb285625954cd6944623 Mon Sep 17 00:00:00 2001 From: Johannes Altmanninger Date: Wed, 5 Mar 2025 11:12:01 +0100 Subject: [PATCH 2/5] Test for stripping control codes Take from my fish shell prompt. Signed-off-by: Johannes Altmanninger Signed-off-by: Yury V. Zaytsev --- tests/.gitignore | 1 + tests/lib/Makefile.am | 4 +++ tests/lib/terminal.c | 84 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+) create mode 100644 tests/lib/terminal.c diff --git a/tests/.gitignore b/tests/.gitignore index ddf11e3a7..d3b3fece1 100644 --- a/tests/.gitignore +++ b/tests/.gitignore @@ -53,6 +53,7 @@ lib/strutil/str_verscmp lib/strutil/str_verscmp.log lib/strutil/str_verscmp.trs lib/strutil/test-suite.log +lib/terminal lib/test-suite.log lib/utilinux__my_system-fork_child.log lib/utilinux__my_system-fork_child_shell.log diff --git a/tests/lib/Makefile.am b/tests/lib/Makefile.am index 256b2d18d..3ed6d1676 100644 --- a/tests/lib/Makefile.am +++ b/tests/lib/Makefile.am @@ -19,6 +19,7 @@ TESTS = \ mc_build_filename \ name_quote \ serialize \ + terminal \ tty \ utilunix__mc_pstream_get_string \ utilunix__my_system_fork_fail \ @@ -47,6 +48,9 @@ name_quote_SOURCES = \ serialize_SOURCES = \ serialize.c +terminal_SOURCES = \ + terminal.c + tty_SOURCES = \ tty.c diff --git a/tests/lib/terminal.c b/tests/lib/terminal.c new file mode 100644 index 000000000..cbc382ec6 --- /dev/null +++ b/tests/lib/terminal.c @@ -0,0 +1,84 @@ +/* + lib/terminal - tests for terminal emulation functions + + Copyright (C) 2013-2025 + Free Software Foundation, Inc. + + Written by: + Johannes Altmanninger, 2025 + + This file is part of the Midnight Commander. + + The Midnight Commander is free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License as + published by the Free Software Foundation, either version 3 of the License, + or (at your option) any later version. + + The Midnight Commander is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +#define TEST_SUITE_NAME "/lib/terminal" + +#include "tests/mctest.h" + +#include + +#include "lib/global.h" // include +#include "lib/terminal.h" +#include "lib/strutil.h" + +/* --------------------------------------------------------------------------------------------- */ + +static void +setup (void) +{ + str_init_strings (NULL); +} + +static void +teardown (void) +{ + str_uninit_strings (); +} + +/* --------------------------------------------------------------------------------------------- */ + +START_TEST (test_strip_ctrl_codes) +{ + char *s = strdup ( + "\033]0;~\a\033[30m\033(B\033[m\033]133;A;special_key=1\a$ " + "\033[K\033[?2004h\033[>4;1m\033[=5u\033=\033[?2004l\033[>4;0m\033[=0u\033>\033[?2004h" + "\033[>4;1m\033[=5u\033=\033[?2004l\033[>4;0m\033[=0u\033>\033[?2004h\033[>4;1m\033[=5u" + "\033="); + char *actual = strip_ctrl_codes (s); + const char *expected = "$ "; + ck_assert_str_eq (actual, expected); + free (s); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + +int +main (void) +{ + TCase *tc_core; + + tc_core = tcase_create ("Core"); + + tcase_add_checked_fixture (tc_core, setup, teardown); + + // Add new tests here: *************** + tcase_add_test (tc_core, test_strip_ctrl_codes); + // *********************************** + + return mctest_run_all (tc_core); +} + +/* --------------------------------------------------------------------------------------------- */ From d10b138a075c82a5abdab913232a960a6c467062 Mon Sep 17 00:00:00 2001 From: Johannes Altmanninger Date: Wed, 5 Mar 2025 11:12:01 +0100 Subject: [PATCH 3/5] const-correct strip_ctrl_codes Signed-off-by: Johannes Altmanninger Signed-off-by: Yury V. Zaytsev --- lib/terminal.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/terminal.c b/lib/terminal.c index 271b51ee8..71d2e1e99 100644 --- a/lib/terminal.c +++ b/lib/terminal.c @@ -75,8 +75,8 @@ char * strip_ctrl_codes (char *s) { - char *w; // Current position where the stripped data is written - char *r; // Current position where the original data is read + char *w; // Current position where the stripped data is written + const char *r; // Current position where the original data is read if (s == NULL) return NULL; @@ -105,7 +105,7 @@ strip_ctrl_codes (char *s) * OSC P s ; P t ST * OSC P s ; P t BEL */ - char *new_r; + const char *new_r; for (new_r = r; *new_r != '\0'; new_r++) { @@ -139,7 +139,7 @@ strip_ctrl_codes (char *s) } else { - char *n; + const char *n; n = str_cget_next_char (r); if (str_isprint (r)) From 931e1d6943a8d2d7589deadc8316fd2a2d06b186 Mon Sep 17 00:00:00 2001 From: Johannes Altmanninger Date: Wed, 5 Mar 2025 11:12:01 +0100 Subject: [PATCH 4/5] Recognize kitty keyboard protocol ctrl-o despite caps_lock/num_lock Commit 6e4510b55 (subshell: recognize CSI u encoding for ctrl-o, 2024-10-09) made us parse sequences like \e[111;5u (meaning ctrl-o). As reported in https://github.com/fish-shell/fish-shell/issues/10640#issuecomment-2698350151, the kitty keyboard protocol specifies various other sequences that also mean ctrl-o. Specifically, when numlock is active. Instead of matching raw byte values, let's teach our ECMA-48 CSI parser to decode parameter bytes and interpret them according to the kitty keyboard protocol. Tested with fish version 4 on kitty: 1. Turn on numlock (or capslock) 2. SHELL=/bin/fish src/mc 3. ctrl-o 4. Enter 5. ctrl-o -- confirmed that this one is recognized now Signed-off-by: Johannes Altmanninger Signed-off-by: Yury V. Zaytsev --- lib/terminal.c | 126 ++++++++++++++++++++++++++++++++++++++---- lib/terminal.h | 41 ++++++++++++++ src/subshell/common.c | 65 ++++++++++++++++++---- tests/lib/terminal.c | 13 +++++ 4 files changed, 221 insertions(+), 24 deletions(-) create mode 100644 lib/terminal.h diff --git a/lib/terminal.c b/lib/terminal.c index 71d2e1e99..e3589a55f 100644 --- a/lib/terminal.c +++ b/lib/terminal.c @@ -55,19 +55,122 @@ /* --------------------------------------------------------------------------------------------- */ /*** public functions ****************************************************************************/ /* --------------------------------------------------------------------------------------------- */ + /** - * Remove all control sequences from the argument string. We define - * "control sequence", in a sort of pidgin BNF, as follows: + * Parse a CSI command, starting from the third byte (i.e. the first + * parameter byte, if any). + * + * On success, *sptr will point to one-past the end of the sequence. + * On failure, *sptr will point to the first invalid byte. + * + * Here's the format in a sort of pidgin BNF: * - * control-seq = Esc non-'[' - * | Esc '[' (parameter-byte)* (intermediate-byte)* final-byte + * CSI-command = Esc '[' parameters (intermediate-byte)* final-byte + * parameters = [0-9;:]+ + * | [<=>?] (parameter-byte)* # private mode * parameter-byte = [\x30-\x3F] # one of "0-9;:<=>?" * intermediate-byte = [\x20–\x2F] # one of " !\"#$%&'()*+,-./" * final-byte = [\x40-\x7e] # one of "@A–Z[\]^_`a–z{|}~" + */ +gboolean +parse_csi (struct csi_command_t *out, const char **sptr, const char *end) +{ + gboolean ok = FALSE; + + const char *s = *sptr; + if (s == end) + goto invalid_sequence; + + char c = *s; + +#define NEXT_CHAR \ + do \ + { \ + if (++s == end) \ + goto invalid_sequence; \ + c = *s; \ + } \ + while (0) + + char private_mode = '\0'; + + if (c >= '<' && c <= '?') // "<=>?" + { + private_mode = c; + NEXT_CHAR; + } + + // parameter bytes + size_t param_count = 0; + + if (private_mode != '\0') + { + while (c >= 0x30 && c <= 0x3F) + NEXT_CHAR; + } + else + { + if (out != NULL) + // N.B. empty parameter strings are allowed. For our current use, + // treating them as zeroes happens to work. + memset (out->params, 0, sizeof (out->params)); + + uint32_t tmp = 0; + size_t sub_index = 0; + + while (c >= 0x30 && c <= 0x3F) + { + if (c >= '0' && c <= '9') + { + if (param_count == 0) + param_count = 1; + if (tmp * 10 < tmp) + goto invalid_sequence; // overflow + tmp *= 10; + if (tmp + c - '0' < tmp) + goto invalid_sequence; // overflow + tmp += c - '0'; + if (out != NULL) + out->params[param_count - 1][sub_index] = tmp; + } + else if (c == ':' && ++sub_index < G_N_ELEMENTS (out->params[0])) + tmp = 0; + else if (c == ';' && ++param_count <= G_N_ELEMENTS (out->params)) + tmp = 0, sub_index = 0; + else + goto invalid_sequence; + NEXT_CHAR; + } + } + + while (c >= 0x20 && c <= 0x2F) // intermediate bytes + NEXT_CHAR; +#undef NEXT_CHAR + + if (c < 0x40 || c > 0x7E) // final byte + goto invalid_sequence; + + ++s; + ok = TRUE; + + if (out != NULL) + { + out->private_mode = private_mode; + out->param_count = param_count; + } + +invalid_sequence: + *sptr = s; + return ok; +} + +/* --------------------------------------------------------------------------------------------- */ +/** + * Remove all control sequences (CSI, OSC) from the argument string. * - * The 256-color and true-color escape sequences should allow either ';' or ':' inside as separator, - * actually, ':' is the more correct according to ECMA-48. - * Some terminal emulators (e.g. xterm, gnome-terminal) support this. + * The 256-color and true-color escape sequences should allow either ';' or ':' inside as + * separator, actually, ':' is the more correct according to ECMA-48. Some terminal emulators + * (e.g. xterm, gnome-terminal) support this. * * Non-printable characters are also removed. */ @@ -91,11 +194,10 @@ strip_ctrl_codes (char *s) // '(' need to avoid strange 'B' letter in *Suse (if mc runs under root user) if (*(++r) == '[' || *r == '(') { - // strchr() matches trailing binary 0 - while (*(++r) != '\0' && strchr ("0123456789;:<=>?", *r) != NULL) - ; - while (*r != '\0' && (*r < 0x40 || *r > 0x7E)) - ++r; + ++r; + parse_csi (NULL, &r, end); + // We're already past the sequence, no need to increment. + continue; } if (*r == ']') { diff --git a/lib/terminal.h b/lib/terminal.h new file mode 100644 index 000000000..bd0f79cb7 --- /dev/null +++ b/lib/terminal.h @@ -0,0 +1,41 @@ +/** \file terminal.h + * \brief Header: terminal emulation logic. + */ + +#ifndef MC_TERMINAL_H +#define MC_TERMINAL_H + +#include +#include // uint32_t + +#include "lib/global.h" // include + +/*** typedefs(not structures) and defined constants **********************************************/ + +/*** enums ***************************************************************************************/ + +/*** structures declarations (and typedefs of structures)*****************************************/ + +struct csi_command_t +{ + char private_mode; + uint32_t params[16][4]; + size_t param_count; +}; + +/*** global variables defined in .c file *********************************************************/ + +/*** declarations of public functions ************************************************************/ + +gboolean parse_csi (struct csi_command_t *out, const char **sptr, const char *end); + +char *strip_ctrl_codes (char *s); + +/* Replaces "\\E" and "\\e" with "\033". Replaces "^" + [a-z] with + * ((char) 1 + (c - 'a')). The same goes for "^" + [A-Z]. + * Returns a newly allocated string. */ +char *convert_controls (const char *s); + +/*** inline functions ****************************************************************************/ + +#endif diff --git a/src/subshell/common.c b/src/subshell/common.c index 7a2775471..14d08f554 100644 --- a/src/subshell/common.c +++ b/src/subshell/common.c @@ -118,7 +118,7 @@ /* State of the subshell: * INACTIVE: the default state; awaiting a command - * ACTIVE: remain in the shell until the user hits 'subshell_switch_key' + * ACTIVE: remain in the shell until the user hits the subshell switch key * RUNNING_COMMAND: return to MC when the current command finishes */ enum subshell_state_enum subshell_state; @@ -178,12 +178,6 @@ static char tcsh_fifo[BUF_SMALL]; static int subshell_pty_slave = -1; -/* The key for switching back to MC from the subshell */ -static const char subshell_switch_key = XCTRL ('o') & 255; - -static const char subshell_switch_key_csi_u[] = "\x1b[111;5u"; -static const size_t subshell_switch_key_csi_u_len = sizeof (subshell_switch_key_csi_u) - 1; - /* For reading/writing on the subshell's pty */ static char pty_buffer[PTY_BUFFER_SIZE] = "\0"; @@ -779,6 +773,57 @@ set_prompt_string (void) setup_cmdline (); } +/* --------------------------------------------------------------------------------------------- */ + +enum kitty_keyboard_protocol_t +{ + MODIFIER_SHIFT = 0x01, + MODIFIER_CTRL = 0x04, + MODIFIER_CAPS_LOCK = 0x40, + MODIFIER_NUM_LOCK = 0x80, + + EVENT_TYPE_PRESS = 1, + EVENT_TYPE_REPEAT = 2 +}; + +static gboolean +peek_subshell_switch_key (const char *buffer, size_t len) +{ + if (len == 0) + return FALSE; + if (buffer[0] == (XCTRL ('o') & 255)) + return TRUE; + + // Also check if ctrl-o is encoded as per the kitty keyboard protocol. + if (len == 1) + return FALSE; + if (buffer[0] != ESC_CHAR || buffer[1] != '[') // CSI + return FALSE; + buffer += 2; + len -= 2; + + struct csi_command_t csi; + + if (!parse_csi (&csi, &buffer, buffer + len)) + return FALSE; + if (csi.private_mode != '\0' || buffer[-1] != 'u') + return FALSE; + if (csi.param_count != 2) // ctrl-o must have the modifier field + return FALSE; + if (csi.params[1][0] == 0) // Bad modifier. + return FALSE; + + const uint32_t codepoint = csi.params[0][0]; + const uint32_t modifiers = csi.params[1][0] - 1; + const uint32_t event = csi.params[1][1]; + + if (event != 0 && event != EVENT_TYPE_PRESS && event != EVENT_TYPE_REPEAT) + return FALSE; + + return codepoint == 'o' + && (modifiers & ~(MODIFIER_CAPS_LOCK | MODIFIER_NUM_LOCK)) == MODIFIER_CTRL; +} + /* --------------------------------------------------------------------------------------------- */ /** Feed the subshell our keyboard input until it says it's finished */ @@ -908,11 +953,7 @@ feed_subshell (int how, gboolean fail_on_error) } for (i = 0; i < bytes; ++i) - if (pty_buffer[i] == subshell_switch_key - || (subshell_switch_key_csi_u_len <= (size_t) bytes - i - && memcmp (&pty_buffer[i], subshell_switch_key_csi_u, - subshell_switch_key_csi_u_len) - == 0)) + if (peek_subshell_switch_key (pty_buffer + i, bytes - i)) { write_all (mc_global.tty.subshell_pty, pty_buffer, i); diff --git a/tests/lib/terminal.c b/tests/lib/terminal.c index cbc382ec6..4eed2821e 100644 --- a/tests/lib/terminal.c +++ b/tests/lib/terminal.c @@ -49,6 +49,18 @@ teardown (void) /* --------------------------------------------------------------------------------------------- */ +START_TEST (test_parse_csi) +{ + const char *s = &"\x1b[=5uRest"[2]; + const char *end = s + strlen (s); + gboolean ok = parse_csi (NULL, &s, end); + ck_assert_msg (ok, "failed to parse CSI"); + ck_assert_str_eq (s, "Rest"); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + START_TEST (test_strip_ctrl_codes) { char *s = strdup ( @@ -75,6 +87,7 @@ main (void) tcase_add_checked_fixture (tc_core, setup, teardown); // Add new tests here: *************** + tcase_add_test (tc_core, test_parse_csi); tcase_add_test (tc_core, test_strip_ctrl_codes); // *********************************** From 87a6a142e76fcb73073b786cdc2fc2b466165d58 Mon Sep 17 00:00:00 2001 From: Johannes Altmanninger Date: Wed, 5 Mar 2025 20:49:25 +0100 Subject: [PATCH 5/5] subshell: tell fish that we actually support the kitty keyboard protocol now __mc_csi_u was added so that fish shell knows when it's safe to enable the kitty keyboard protocol. As seen in the parent commit, it wasn't quite safe; so the upcoming patch release of fish will disable this protocol again -- until fish finds this commit, so add a new version marker. While at it, use a less misleading name. Dropping the old name will mean that fish < 4.0.1 will no longer enable the kitty keyboard protocol (and be safe from the hang on numlock). I guess we could avoid that but I don't think this is a big loss, especially because since fish 4.0.1 will enable it again. Signed-off-by: Johannes Altmanninger Signed-off-by: Yury V. Zaytsev --- src/subshell/common.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/subshell/common.c b/src/subshell/common.c index 14d08f554..994e490bd 100644 --- a/src/subshell/common.c +++ b/src/subshell/common.c @@ -461,7 +461,7 @@ init_subshell_child (const char *pty_name) case SHELL_FISH: execl (mc_global.shell->path, mc_global.shell->path, "--init-command", - "set --global __mc_csi_u 1", (char *) NULL); + "set --global __mc_kitty_keyboard 1", (char *) NULL); break; case SHELL_ASH_BUSYBOX: