diff --git a/configure.ac b/configure.ac
index 755e4a75e3..04fc3fe7b5 100644
--- a/configure.ac
+++ b/configure.ac
@@ -727,6 +727,7 @@ tests/lib/vfs/mc.charsets
tests/lib/widget/Makefile
tests/src/Makefile
tests/src/filemanager/Makefile
+tests/src/subshell/Makefile
tests/src/editor/Makefile
tests/src/editor/edit_complete_word_cmd_test_data.txt
tests/src/vfs/Makefile
diff --git a/src/subshell/common.c b/src/subshell/common.c
index 8aab520450..340be6dc1a 100644
--- a/src/subshell/common.c
+++ b/src/subshell/common.c
@@ -180,7 +180,9 @@ typedef enum
* the current command buffer and the location of the cursor. */
#define SHELL_BUFFER_KEYBINDING "_"
-/*** forward declarations (file scope functions) *************************************************/
+/*** file scope functions ************************************************************************/
+
+static int subshell_init_ready (int fd, void *info);
/*** file scope variables ************************************************************************/
@@ -1702,8 +1704,6 @@ do_subshell_chdir (const vfs_path_t *vpath, gboolean force, gboolean update_prom
void
init_subshell (void)
{
- vfs_path_t *vfs_subshell_cwd;
-
// This must be remembered across calls to init_subshell()
static char pty_name[BUF_SMALL];
@@ -1823,7 +1823,19 @@ init_subshell (void)
g_free (precmd);
}
- // Wait until the subshell has started up and processed the command
+ // Finish the handshake asynchronously from the main loop once the shell is ready
+ subshell_initialized = FALSE;
+ add_select_channel (subshell_pipe[READ], subshell_init_ready, NULL);
+}
+
+/* --------------------------------------------------------------------------------------------- */
+// Second half of init_subshell(): consume the shell's first CWD report, probe the persistent
+// command buffer and get a fresh prompt printed. Runs from the main loop as soon as the shell
+// is ready, or right away from invoke_subshell() if the user got there first.
+
+static void
+subshell_finish_init (void)
+{
subshell_state = RUNNING_COMMAND;
tty_enable_interrupt_key ();
if (!feed_subshell (QUIETLY, TRUE))
@@ -1841,19 +1853,63 @@ init_subshell (void)
/* Force an initial `cd` command, even if the subshell is already in the target directory.
* Testing the persistent command feature might have read and discarded the prompt. Just get
- * a new one printed. See #4784#issuecomment-3435834623. */
- vfs_subshell_cwd = vfs_path_from_str (subshell_cwd);
- do_subshell_chdir (vfs_subshell_cwd, TRUE, FALSE);
- vfs_path_free (vfs_subshell_cwd, TRUE);
+ * a new one printed. See #4784#issuecomment-3435834623.
+ *
+ * The panel may have moved on while the shell was starting up (subshell_chdir() is a no-op
+ * until we get here), so aim at the panel's directory rather than the shell's. */
+ do_subshell_chdir (subshell_get_cwd (), TRUE, FALSE);
subshell_initialized = TRUE;
}
+/* --------------------------------------------------------------------------------------------- */
+// Remove both of the subshell's file descriptors from the main event loop; safe even if
+// unregistered.
+
+static void
+subshell_close_select_channels (void)
+{
+ delete_select_channel (mc_global.tty.subshell_pty);
+ delete_select_channel (subshell_pipe[READ]);
+}
+
+/* --------------------------------------------------------------------------------------------- */
+// Event-loop callback: fires once the shell has written its CWD, i.e. reached its first prompt.
+
+static int
+subshell_init_ready (int fd, void *info)
+{
+ (void) info;
+
+ delete_select_channel (fd);
+
+ if (subshell_alive)
+ subshell_finish_init ();
+
+ return 0;
+}
+
+/* --------------------------------------------------------------------------------------------- */
+// Finish the handshake synchronously if it isn't done yet. Called from
+// invoke_subshell(), so Ctrl-O and running a command always get a ready subshell.
+
+static void
+subshell_ensure_initialized (void)
+{
+ if (!subshell_initialized)
+ {
+ delete_select_channel (subshell_pipe[READ]);
+ subshell_finish_init ();
+ }
+}
+
/* --------------------------------------------------------------------------------------------- */
int
invoke_subshell (const char *command, int how, vfs_path_t **new_dir_vpath)
{
+ subshell_ensure_initialized ();
+
// Make the MC terminal transparent
tcsetattr (STDOUT_FILENO, TCSANOW, &raw_mode);
@@ -2099,7 +2155,8 @@ exit_subshell (void)
void
subshell_chdir (const vfs_path_t *vpath)
{
- if (mc_global.tty.use_subshell && vfs_current_is_local ())
+ // Before the handshake is done, subshell_finish_init() takes care of the panel's directory
+ if (mc_global.tty.use_subshell && subshell_initialized && vfs_current_is_local ())
do_subshell_chdir (vpath, FALSE, FALSE);
}
@@ -2149,7 +2206,9 @@ sigchld_handler (MC_UNUSED int sig)
{
// The subshell has either exited normally or been killed
subshell_alive = FALSE;
- delete_select_channel (mc_global.tty.subshell_pty);
+ subshell_close_select_channels ();
+ if (!subshell_initialized)
+ mc_global.tty.use_subshell = FALSE; // Subshell died instantly, so don't use it
if (WIFEXITED (status) && WEXITSTATUS (status) != FORK_FAILURE)
{
const int subshell_quit =
diff --git a/tests/src/Makefile.am b/tests/src/Makefile.am
index abba5ad1f2..5b5d202920 100644
--- a/tests/src/Makefile.am
+++ b/tests/src/Makefile.am
@@ -6,6 +6,10 @@ if USE_INTERNAL_EDIT
SUBDIRS += editor
endif
+if ENABLE_SUBSHELL
+SUBDIRS += subshell
+endif
+
AM_CPPFLAGS = \
$(GLIB_CFLAGS) \
-I$(top_srcdir) \
diff --git a/tests/src/subshell/Makefile.am b/tests/src/subshell/Makefile.am
new file mode 100644
index 0000000000..74a2e85f6a
--- /dev/null
+++ b/tests/src/subshell/Makefile.am
@@ -0,0 +1,30 @@
+PACKAGE_STRING = "/src/subshell"
+
+AM_CPPFLAGS = \
+ $(GLIB_CFLAGS) \
+ -I$(top_srcdir) \
+ -I$(top_srcdir)/lib/vfs \
+ @CHECK_CFLAGS@
+
+LIBS = @CHECK_LIBS@ \
+ $(top_builddir)/src/libinternal.la \
+ $(top_builddir)/lib/libmc.la
+
+if ENABLE_MCLIB
+LIBS += $(GLIB_LIBS)
+endif
+
+EXTRA_DIST = \
+ subshell__common.c
+
+TESTS = \
+ subshell__peek_subshell_switch_key \
+ subshell__async_init
+
+check_PROGRAMS = $(TESTS)
+
+subshell__peek_subshell_switch_key_SOURCES = \
+ subshell__peek_subshell_switch_key.c
+
+subshell__async_init_SOURCES = \
+ subshell__async_init.c
diff --git a/tests/src/subshell/subshell__async_init.c b/tests/src/subshell/subshell__async_init.c
new file mode 100644
index 0000000000..bb1c91f969
--- /dev/null
+++ b/tests/src/subshell/subshell__async_init.c
@@ -0,0 +1,401 @@
+/*
+ src/subshell - tests for subshell_init_ready() and subshell_ensure_initialized()
+
+ Copyright (C) 2026
+ Free Software Foundation, Inc.
+
+ 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 "/src/subshell"
+
+#include "tests/mctest.h"
+
+#include
+#include
+#include
+#include
+
+#include "src/vfs/local/local.h"
+
+#include "subshell__common.c"
+
+/* --------------------------------------------------------------------------------------------- */
+
+static int
+delete_select_channel__call_count_for_fd (int fd)
+{
+ int n = 0;
+
+ for (guint i = 0; i < delete_select_channel__calls->len; i++)
+ if (g_array_index (delete_select_channel__calls, int, i) == fd)
+ n++;
+
+ return n;
+}
+
+static void
+write_fake_cwd (const char *cwd)
+{
+ char line[MC_MAXPATHLEN + 2];
+ const int n = g_snprintf (line, sizeof (line), "%s\n", cwd);
+
+ write_all (subshell_pipe[WRITE], line, (size_t) n);
+}
+
+static gboolean
+fd_readable (int fd)
+{
+ fd_set read_set;
+ struct timeval no_wait = { 0 };
+
+ FD_ZERO (&read_set);
+ FD_SET (fd, &read_set);
+
+ return select (fd + 1, &read_set, NULL, NULL, &no_wait) > 0;
+}
+
+/* --------------------------------------------------------------------------------------------- */
+
+static int fake_pty[2] = { -1, -1 };
+static pid_t fake_shell_pid = -1;
+static mc_shell_t test_shell;
+
+static void
+kill_fake_shell (void)
+{
+ if (fake_shell_pid > 0)
+ {
+ kill (fake_shell_pid, SIGKILL);
+ waitpid (fake_shell_pid, NULL, 0);
+ fake_shell_pid = -1;
+ }
+}
+
+/* Let the fake shell die and deliver its SIGCHLD to sigchld_handler() synchronously. */
+static void
+kill_fake_shell_and_report (void)
+{
+ sigset_t chld_set, old_set;
+ siginfo_t info;
+
+ sigemptyset (&chld_set);
+ sigaddset (&chld_set, SIGCHLD);
+ sigprocmask (SIG_BLOCK, &chld_set, &old_set);
+
+ kill (fake_shell_pid, SIGKILL);
+ waitid (P_PID, (id_t) fake_shell_pid, &info, WEXITED | WNOWAIT);
+ test_sigchld_handler (SIGCHLD);
+ fake_shell_pid = -1;
+
+ sigprocmask (SIG_SETMASK, &old_set, NULL);
+}
+
+/* @Before */
+static void
+setup (void)
+{
+ str_init_strings (NULL);
+ vfs_init ();
+ vfs_init_localfs ();
+ vfs_setup_work_dir ();
+
+ // Skip prompt/cmdline widgets
+ mc_global.mc_run_mode = MC_RUN_VIEWER;
+
+ test_shell.type = SHELL_SH;
+ test_shell.name = "sh";
+ test_shell.path = (char *) "/bin/sh";
+ test_shell.real_path = (char *) "/bin/sh";
+ mc_global.shell = &test_shell;
+
+ mc_global.tty.use_subshell = TRUE;
+ subshell_alive = TRUE;
+ subshell_stopped = FALSE;
+ subshell_initialized = FALSE;
+ subshell_state = INACTIVE;
+ use_persistent_buffer = FALSE;
+ subshell_cwd[0] = '\0';
+
+ subshell_pipe[READ] = -1;
+ subshell_pipe[WRITE] = -1;
+
+ if (socketpair (AF_UNIX, SOCK_STREAM, 0, fake_pty) != 0)
+ {
+ fake_pty[0] = fake_pty[1] = -1;
+ ck_abort_msg ("Cannot create test pty");
+ }
+ // SOCK_DGRAM keeps one CWD report per read(); a stream pipe can merge writes
+ if (socketpair (AF_UNIX, SOCK_DGRAM, 0, subshell_pipe) != 0)
+ {
+ subshell_pipe[READ] = -1;
+ subshell_pipe[WRITE] = -1;
+ ck_abort_msg ("Cannot create test pipe");
+ }
+ mc_global.tty.subshell_pty = fake_pty[0];
+
+ fake_shell_pid = fork ();
+ if (fake_shell_pid == -1)
+ ck_abort_msg ("Cannot fork fake shell child");
+ if (fake_shell_pid == 0)
+ {
+ for (;;)
+ raise (SIGSTOP);
+ }
+
+ {
+ int status;
+
+ // Reap the first SIGSTOP so synchronize() does not depend on catching SIGCHLD
+ if (waitpid (fake_shell_pid, &status, WUNTRACED) == -1 || !WIFSTOPPED (status))
+ {
+ kill_fake_shell ();
+ ck_abort_msg ("Fake shell child did not stop");
+ }
+ }
+ subshell_pid = fake_shell_pid;
+ subshell_stopped = TRUE;
+
+ {
+ struct sigaction sa = { 0 };
+
+ sa.sa_handler = test_sigchld_handler;
+ sigemptyset (&sa.sa_mask);
+ sa.sa_flags = SA_RESTART;
+ sigaction (SIGCHLD, &sa, NULL);
+ }
+
+ subshell_get_cwd__return_value = vfs_path_from_str ("/tmp");
+
+ delete_select_channel__calls = g_array_new (FALSE, FALSE, sizeof (int));
+}
+
+/* --------------------------------------------------------------------------------------------- */
+
+/* @After */
+static void
+teardown (void)
+{
+ signal (SIGCHLD, SIG_DFL);
+ kill_fake_shell ();
+
+ if (delete_select_channel__calls != NULL)
+ {
+ g_array_free (delete_select_channel__calls, TRUE);
+ delete_select_channel__calls = NULL;
+ }
+
+ vfs_path_free (subshell_get_cwd__return_value, TRUE);
+ subshell_get_cwd__return_value = NULL;
+
+ if (fake_pty[0] >= 0)
+ close (fake_pty[0]);
+ if (fake_pty[1] >= 0)
+ close (fake_pty[1]);
+ fake_pty[0] = fake_pty[1] = -1;
+
+ if (subshell_pipe[READ] >= 0)
+ close (subshell_pipe[READ]);
+ if (subshell_pipe[WRITE] >= 0)
+ close (subshell_pipe[WRITE]);
+ subshell_pipe[READ] = subshell_pipe[WRITE] = -1;
+
+ vfs_shut ();
+ str_uninit_strings ();
+}
+
+/* --------------------------------------------------------------------------------------------- */
+
+/* @Test */
+START_TEST (init_ready_finishes_handshake_when_cwd_available)
+{
+ // given
+ write_fake_cwd ("/tmp"); // handshake
+ write_fake_cwd ("/tmp"); // forced initial cd in subshell_finish_init()
+
+ // when
+ subshell_init_ready (subshell_pipe[READ], NULL);
+
+ // then
+ mctest_assert_true (subshell_initialized);
+ mctest_assert_str_eq (subshell_cwd, "/tmp");
+ ck_assert_int_eq (delete_select_channel__call_count_for_fd (subshell_pipe[READ]), 1);
+}
+END_TEST
+
+/* --------------------------------------------------------------------------------------------- */
+
+/* @Test */
+START_TEST (init_ready_bails_out_when_subshell_not_alive)
+{
+ // given
+ subshell_alive = FALSE;
+
+ // when
+ subshell_init_ready (subshell_pipe[READ], NULL);
+
+ // then
+ mctest_assert_false (subshell_initialized);
+ ck_assert_int_eq (delete_select_channel__call_count_for_fd (subshell_pipe[READ]), 1);
+}
+END_TEST
+
+/* --------------------------------------------------------------------------------------------- */
+
+/* @Test */
+START_TEST (ensure_initialized_finishes_pending_init)
+{
+ // given
+ write_fake_cwd ("/tmp"); // handshake
+ write_fake_cwd ("/tmp"); // forced initial cd in subshell_finish_init()
+
+ mctest_assert_false (subshell_initialized);
+
+ // when
+ subshell_ensure_initialized ();
+
+ // then
+ mctest_assert_true (subshell_initialized);
+ mctest_assert_str_eq (subshell_cwd, "/tmp");
+ ck_assert_int_eq (delete_select_channel__call_count_for_fd (subshell_pipe[READ]), 1);
+}
+END_TEST
+
+/* --------------------------------------------------------------------------------------------- */
+
+/* @Test */
+START_TEST (ensure_initialized_is_a_noop_once_already_initialized)
+{
+ // given
+ subshell_initialized = TRUE;
+
+ // when
+ subshell_ensure_initialized ();
+
+ // then
+ mctest_assert_true (subshell_initialized);
+ ck_assert_int_eq (delete_select_channel__call_count_for_fd (subshell_pipe[READ]), 0);
+}
+END_TEST
+
+/* --------------------------------------------------------------------------------------------- */
+
+/* @Test */
+START_TEST (chdir_is_ignored_until_initialized)
+{
+ // when
+ test_subshell_chdir (subshell_get_cwd__return_value);
+
+ // then
+ mctest_assert_false (fd_readable (fake_pty[1]));
+ mctest_assert_str_eq (subshell_cwd, "");
+}
+END_TEST
+
+/* --------------------------------------------------------------------------------------------- */
+
+/* @Test */
+START_TEST (chdir_is_sent_once_initialized)
+{
+ // given
+ subshell_initialized = TRUE;
+ g_strlcpy (subshell_cwd, "/", sizeof (subshell_cwd));
+ write_fake_cwd ("/tmp"); // response to the command line clearing
+ write_fake_cwd ("/tmp"); // response to cd
+
+ // when
+ test_subshell_chdir (subshell_get_cwd__return_value);
+
+ // then
+ mctest_assert_true (fd_readable (fake_pty[1]));
+ mctest_assert_str_eq (subshell_cwd, "/tmp");
+}
+END_TEST
+
+/* --------------------------------------------------------------------------------------------- */
+
+/* @Test */
+START_TEST (shell_death_before_handshake_disables_subshell)
+{
+ // when
+ kill_fake_shell_and_report ();
+
+ // then
+ mctest_assert_false (subshell_alive);
+ mctest_assert_false (mc_global.tty.use_subshell);
+ ck_assert_int_eq (delete_select_channel__call_count_for_fd (mc_global.tty.subshell_pty), 1);
+ ck_assert_int_eq (delete_select_channel__call_count_for_fd (subshell_pipe[READ]), 1);
+}
+END_TEST
+
+/* --------------------------------------------------------------------------------------------- */
+
+/* @Test */
+START_TEST (shell_death_after_handshake_keeps_subshell_enabled)
+{
+ // given
+ subshell_initialized = TRUE;
+
+ // when
+ kill_fake_shell_and_report ();
+
+ // then
+ mctest_assert_false (subshell_alive);
+ mctest_assert_true (mc_global.tty.use_subshell);
+}
+END_TEST
+
+/* --------------------------------------------------------------------------------------------- */
+
+/* @Test */
+START_TEST (close_select_channels_removes_both_fds)
+{
+ // when
+ subshell_close_select_channels ();
+
+ // then
+ ck_assert_int_eq (delete_select_channel__call_count_for_fd (mc_global.tty.subshell_pty), 1);
+ ck_assert_int_eq (delete_select_channel__call_count_for_fd (subshell_pipe[READ]), 1);
+}
+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, init_ready_finishes_handshake_when_cwd_available);
+ tcase_add_test (tc_core, init_ready_bails_out_when_subshell_not_alive);
+ tcase_add_test (tc_core, ensure_initialized_finishes_pending_init);
+ tcase_add_test (tc_core, ensure_initialized_is_a_noop_once_already_initialized);
+ tcase_add_test (tc_core, chdir_is_ignored_until_initialized);
+ tcase_add_test (tc_core, chdir_is_sent_once_initialized);
+ tcase_add_test (tc_core, shell_death_before_handshake_disables_subshell);
+ tcase_add_test (tc_core, shell_death_after_handshake_keeps_subshell_enabled);
+ tcase_add_test (tc_core, close_select_channels_removes_both_fds);
+ // ***********************************
+
+ return mctest_run_all (tc_core);
+}
+
+/* --------------------------------------------------------------------------------------------- */
diff --git a/tests/src/subshell/subshell__common.c b/tests/src/subshell/subshell__common.c
new file mode 100644
index 0000000000..08c8250903
--- /dev/null
+++ b/tests/src/subshell/subshell__common.c
@@ -0,0 +1,109 @@
+/*
+ Common code for testing functions in src/subshell/common.c file.
+
+ Copyright (C) 2026
+ Free Software Foundation, Inc.
+
+ 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 .
+ */
+
+/* Rename symbols that collide with libinternal.la / libmc.la. */
+#define init_subshell test_init_subshell
+#define invoke_subshell test_invoke_subshell
+#define flush_subshell test_flush_subshell
+#define read_subshell_prompt test_read_subshell_prompt
+#define do_update_prompt test_do_update_prompt
+#define exit_subshell test_exit_subshell
+#define subshell_chdir test_subshell_chdir
+#define subshell_get_console_attributes test_subshell_get_console_attributes
+#define sigchld_handler test_sigchld_handler
+#define subshell_state test_subshell_state
+#define subshell_prompt test_subshell_prompt
+#define update_subshell_prompt test_update_subshell_prompt
+#define should_read_new_subshell_prompt test_should_read_new_subshell_prompt
+#define add_select_channel test_add_select_channel
+#define delete_select_channel test_delete_select_channel
+#define subshell_get_cwd test_subshell_get_cwd
+#define subshell_handle_cons_saver test_subshell_handle_cons_saver
+#define subshell_get_mainloop_quit test_subshell_get_mainloop_quit
+#define subshell_set_mainloop_quit test_subshell_set_mainloop_quit
+#define cmdline test_cmdline
+#define setup_cmdline test_setup_cmdline
+
+#include "src/subshell/common.c"
+
+/* --------------------------------------------------------------------------------------------- */
+
+/* @ThenReturnValue */
+static vfs_path_t *subshell_get_cwd__return_value;
+
+/* @Mock */
+const vfs_path_t *
+test_subshell_get_cwd (void)
+{
+ return subshell_get_cwd__return_value;
+}
+
+/* @Mock */
+void
+test_subshell_handle_cons_saver (void)
+{
+}
+
+WInput *test_cmdline = NULL;
+
+/* @Mock */
+void
+test_setup_cmdline (void)
+{
+}
+
+/* @Mock */
+int
+test_subshell_get_mainloop_quit (void)
+{
+ return 0;
+}
+
+/* @Mock */
+void
+test_subshell_set_mainloop_quit (const int param_quit)
+{
+ (void) param_quit;
+}
+
+/* --------------------------------------------------------------------------------------------- */
+
+/* @CapturedValue */
+static GArray *delete_select_channel__calls;
+
+/* @Mock */
+void
+test_add_select_channel (int fd, select_fn callback, void *info)
+{
+ (void) fd;
+ (void) callback;
+ (void) info;
+}
+
+/* @Mock */
+void
+test_delete_select_channel (int fd)
+{
+ g_array_append_val (delete_select_channel__calls, fd);
+}
+
+/* --------------------------------------------------------------------------------------------- */
diff --git a/tests/src/subshell/subshell__peek_subshell_switch_key.c b/tests/src/subshell/subshell__peek_subshell_switch_key.c
new file mode 100644
index 0000000000..e504e9582b
--- /dev/null
+++ b/tests/src/subshell/subshell__peek_subshell_switch_key.c
@@ -0,0 +1,102 @@
+/*
+ src/subshell - tests for peek_subshell_switch_key()
+
+ Copyright (C) 2026
+ Free Software Foundation, Inc.
+
+ 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 "/src/subshell"
+
+#include "tests/mctest.h"
+
+#include "subshell__common.c"
+
+/* --------------------------------------------------------------------------------------------- */
+
+/* @Test */
+START_TEST (raw_ctrl_o_is_recognized)
+{
+ const char buf[] = { XCTRL ('o') & 255 };
+
+ mctest_assert_true (peek_subshell_switch_key (buf, 1));
+}
+END_TEST
+
+/* --------------------------------------------------------------------------------------------- */
+
+/* @Test */
+START_TEST (other_byte_is_not_recognized)
+{
+ const char buf[] = { 'x' };
+
+ mctest_assert_false (peek_subshell_switch_key (buf, 1));
+}
+END_TEST
+
+/* --------------------------------------------------------------------------------------------- */
+
+/* @Test */
+START_TEST (empty_buffer_is_not_recognized)
+{
+ mctest_assert_false (peek_subshell_switch_key ("", 0));
+}
+END_TEST
+
+/* --------------------------------------------------------------------------------------------- */
+
+/* @Test */
+START_TEST (kitty_ctrl_o_is_recognized)
+{
+ const char buf[] = ESC_STR "[111;5u";
+
+ mctest_assert_true (peek_subshell_switch_key (buf, sizeof (buf) - 1));
+}
+END_TEST
+
+/* --------------------------------------------------------------------------------------------- */
+
+/* @Test */
+START_TEST (kitty_plain_o_is_not_recognized)
+{
+ const char buf[] = ESC_STR "[111;1u";
+
+ mctest_assert_false (peek_subshell_switch_key (buf, sizeof (buf) - 1));
+}
+END_TEST
+
+/* --------------------------------------------------------------------------------------------- */
+
+int
+main (void)
+{
+ TCase *tc_core;
+
+ tc_core = tcase_create ("Core");
+
+ // Add new tests here: ***************
+ tcase_add_test (tc_core, raw_ctrl_o_is_recognized);
+ tcase_add_test (tc_core, other_byte_is_not_recognized);
+ tcase_add_test (tc_core, empty_buffer_is_not_recognized);
+ tcase_add_test (tc_core, kitty_ctrl_o_is_recognized);
+ tcase_add_test (tc_core, kitty_plain_o_is_not_recognized);
+ // ***********************************
+
+ return mctest_run_all (tc_core);
+}
+
+/* --------------------------------------------------------------------------------------------- */