fix(tui): ensure all pending escape sequences are processed before exiting #32151

Problem:
Neovim disables a number of terminal modes when it exits, some of which
cause the terminal to send asynchronous events to Neovim. It's possible
that Neovim exits before the terminal has received and processed all of
the sequences to disable these modes, causing the terminal to emit one
of these asynchronous sequences after Neovim has already exited. If this
happens, then the sequence is received by the user's shell (or some
other program that is not Neovim).

Solution:
When Neovim exits, it now emits a Device Attributes request (DA1)
after disabling all of the different modes. When the terminal responds
to this request we know that it has already received all of our other
sequences disabling the other modes. At that point, it should not be
emitting any further asynchronous sequences. This means the process of
exiting Neovim is now asynchronous as well since it depends on receiving
the DA1 response from the terminal.

(cherry picked from commit 82f08f33c1)
This commit is contained in:
Gregory Anders
2025-04-22 07:18:34 -05:00
committed by github-actions[bot]
parent b9c9b15ad7
commit 646a8f663e
3 changed files with 96 additions and 49 deletions

View File

@ -642,24 +642,17 @@ static void handle_unknown_csi(TermInput *input, const TermKeyKey *key)
switch (initial) { switch (initial) {
case '?': case '?':
// Kitty keyboard protocol query response. // Kitty keyboard protocol query response.
if (input->waiting_for_kkp_response) {
input->waiting_for_kkp_response = false;
input->key_encoding = kKeyEncodingKitty; input->key_encoding = kKeyEncodingKitty;
tui_set_key_encoding(input->tui_data);
}
break; break;
} }
break; break;
case 'c': case 'c':
switch (initial) { switch (initial) {
case '?': case '?':
// Primary Device Attributes response // Primary Device Attributes (DA1) response
if (input->waiting_for_kkp_response) { if (input->callbacks.primary_device_attr) {
input->waiting_for_kkp_response = false; input->callbacks.primary_device_attr(input->tui_data);
input->callbacks.primary_device_attr = NULL;
// Enable the fallback key encoding (if any)
tui_set_key_encoding(input->tui_data);
} }
break; break;

View File

@ -17,6 +17,10 @@ typedef enum {
kKeyEncodingXterm, ///< Xterm's modifyOtherKeys encoding (XTMODKEYS) kKeyEncodingXterm, ///< Xterm's modifyOtherKeys encoding (XTMODKEYS)
} KeyEncoding; } KeyEncoding;
typedef struct {
void (*primary_device_attr)(TUIData *tui);
} TermInputCallbacks;
#define KEY_BUFFER_SIZE 0x1000 #define KEY_BUFFER_SIZE 0x1000
typedef struct { typedef struct {
int in_fd; int in_fd;
@ -24,8 +28,7 @@ typedef struct {
int8_t paste; int8_t paste;
bool ttimeout; bool ttimeout;
bool waiting_for_kkp_response; ///< True if we are expecting to receive a response to a query for TermInputCallbacks callbacks;
///< Kitty keyboard protocol support
KeyEncoding key_encoding; ///< The key encoding used by the terminal emulator KeyEncoding key_encoding; ///< The key encoding used by the terminal emulator

View File

@ -18,6 +18,7 @@
#include "nvim/cursor_shape.h" #include "nvim/cursor_shape.h"
#include "nvim/event/defs.h" #include "nvim/event/defs.h"
#include "nvim/event/loop.h" #include "nvim/event/loop.h"
#include "nvim/event/multiqueue.h"
#include "nvim/event/signal.h" #include "nvim/event/signal.h"
#include "nvim/event/stream.h" #include "nvim/event/stream.h"
#include "nvim/globals.h" #include "nvim/globals.h"
@ -46,6 +47,10 @@
# include "nvim/os/os_win_console.h" # include "nvim/os/os_win_console.h"
#endif #endif
// Maximum amount of time (in ms) to wait to receive a Device Attributes
// response before exiting.
#define EXIT_TIMEOUT_MS 1000
#define OUTBUF_SIZE 0xffff #define OUTBUF_SIZE 0xffff
#define TOO_MANY_EVENTS 1000000 #define TOO_MANY_EVENTS 1000000
@ -100,6 +105,7 @@ struct TUIData {
bool bce; bool bce;
bool mouse_enabled; bool mouse_enabled;
bool mouse_move_enabled; bool mouse_move_enabled;
bool mouse_enabled_save;
bool title_enabled; bool title_enabled;
bool sync_output; bool sync_output;
bool busy, is_invisible, want_invisible; bool busy, is_invisible, want_invisible;
@ -287,7 +293,8 @@ void tui_enable_extended_underline(TUIData *tui)
static void tui_query_kitty_keyboard(TUIData *tui) static void tui_query_kitty_keyboard(TUIData *tui)
FUNC_ATTR_NONNULL_ALL FUNC_ATTR_NONNULL_ALL
{ {
tui->input.waiting_for_kkp_response = true; // Set the key encoding whenever the Device Attributes (DA1) response is received.
tui->input.callbacks.primary_device_attr = tui_set_key_encoding;
out(tui, S_LEN("\x1b[?u\x1b[c")); out(tui, S_LEN("\x1b[?u\x1b[c"));
} }
@ -505,8 +512,8 @@ static void terminfo_start(TUIData *tui)
flush_buf(tui); flush_buf(tui);
} }
/// Disable the alternate screen and prepare for the TUI to close. /// Disable various terminal modes and other features.
static void terminfo_stop(TUIData *tui) static void terminfo_disable(TUIData *tui)
{ {
// Disable theme update notifications. We do this first to avoid getting any // Disable theme update notifications. We do this first to avoid getting any
// more notifications after we reset the cursor and any color palette changes. // more notifications after we reset the cursor and any color palette changes.
@ -531,16 +538,6 @@ static void terminfo_stop(TUIData *tui)
// May restore old title before exiting alternate screen. // May restore old title before exiting alternate screen.
tui_set_title(tui, NULL_STRING); tui_set_title(tui, NULL_STRING);
if (ui_client_exit_status == 0) {
ui_client_exit_status = tui->seen_error_exit;
}
// if nvim exited with nonzero status, without indicated this was an
// intentional exit (like `:1cquit`), it likely was an internal failure.
// Don't clobber the stderr error message in this case.
if (ui_client_exit_status == tui->seen_error_exit) {
// Exit alternate screen.
unibi_out(tui, unibi_exit_ca_mode);
}
if (tui->cursor_has_color) { if (tui->cursor_has_color) {
unibi_out_ext(tui, tui->unibi_ext.reset_cursor_color); unibi_out_ext(tui, tui->unibi_ext.reset_cursor_color);
} }
@ -549,6 +546,29 @@ static void terminfo_stop(TUIData *tui)
// Disable focus reporting // Disable focus reporting
unibi_out_ext(tui, tui->unibi_ext.disable_focus_reporting); unibi_out_ext(tui, tui->unibi_ext.disable_focus_reporting);
// Send a DA1 request. When the terminal responds we know that it has
// processed all of our requests and won't be emitting anymore sequences.
out(tui, S_LEN("\x1b[c"));
// Immediately flush the buffer and wait for the DA1 response.
flush_buf(tui);
}
/// Disable the alternate screen and prepare for the TUI to close.
static void terminfo_stop(TUIData *tui)
{
if (ui_client_exit_status == 0) {
ui_client_exit_status = tui->seen_error_exit;
}
// if nvim exited with nonzero status, without indicated this was an
// intentional exit (like `:1cquit`), it likely was an internal failure.
// Don't clobber the stderr error message in this case.
if (ui_client_exit_status == tui->seen_error_exit) {
// Exit alternate screen.
unibi_out(tui, unibi_exit_ca_mode);
}
flush_buf(tui); flush_buf(tui);
uv_tty_reset_mode(); uv_tty_reset_mode();
uv_close((uv_handle_t *)&tui->output_handle, NULL); uv_close((uv_handle_t *)&tui->output_handle, NULL);
@ -583,8 +603,14 @@ static void tui_terminal_after_startup(TUIData *tui)
flush_buf(tui); flush_buf(tui);
} }
/// Stop the terminal but allow it to restart later (like after suspend) void tui_error_exit(TUIData *tui, Integer status)
static void tui_terminal_stop(TUIData *tui) FUNC_ATTR_NONNULL_ALL
{
tui->seen_error_exit = (int)status;
}
void tui_stop(TUIData *tui)
FUNC_ATTR_NONNULL_ALL
{ {
if (uv_is_closing((uv_handle_t *)&tui->output_handle)) { if (uv_is_closing((uv_handle_t *)&tui->output_handle)) {
// Race between SIGCONT (tui.c) and SIGHUP (os/signal.c)? #8075 // Race between SIGCONT (tui.c) and SIGHUP (os/signal.c)? #8075
@ -592,28 +618,42 @@ static void tui_terminal_stop(TUIData *tui)
tui->stopped = true; tui->stopped = true;
return; return;
} }
tui->input.callbacks.primary_device_attr = tui_stop_cb;
terminfo_disable(tui);
// Wait until DA1 response is received
LOOP_PROCESS_EVENTS_UNTIL(tui->loop, tui->loop->events, EXIT_TIMEOUT_MS, tui->stopped);
tui_terminal_stop(tui);
stream_set_blocking(tui->input.in_fd, true); // normalize stream (#2598)
tinput_destroy(&tui->input);
signal_watcher_stop(&tui->winch_handle);
signal_watcher_close(&tui->winch_handle, NULL);
uv_close((uv_handle_t *)&tui->startup_delay_timer, NULL);
}
/// Callback function called when the response to the Device Attributes (DA1)
/// request is sent during shutdown.
static void tui_stop_cb(TUIData *tui)
FUNC_ATTR_NONNULL_ALL
{
tui->stopped = true;
}
/// Stop the terminal but allow it to restart later (like after suspend)
///
/// This is called after we receive the response to the DA1 request sent from
/// terminfo_disable.
static void tui_terminal_stop(TUIData *tui)
FUNC_ATTR_NONNULL_ALL
{
tinput_stop(&tui->input); tinput_stop(&tui->input);
// Position the cursor on the last screen line, below all the text // Position the cursor on the last screen line, below all the text
cursor_goto(tui, tui->height - 1, 0); cursor_goto(tui, tui->height - 1, 0);
terminfo_stop(tui); terminfo_stop(tui);
} }
void tui_error_exit(TUIData *tui, Integer status)
{
tui->seen_error_exit = (int)status;
}
void tui_stop(TUIData *tui)
{
tui_terminal_stop(tui);
stream_set_blocking(tui->input.in_fd, true); // normalize stream (#2598)
tinput_destroy(&tui->input);
tui->stopped = true;
signal_watcher_stop(&tui->winch_handle);
signal_watcher_close(&tui->winch_handle, NULL);
uv_close((uv_handle_t *)&tui->startup_delay_timer, NULL);
}
/// Returns true if UI `ui` is stopped. /// Returns true if UI `ui` is stopped.
bool tui_is_stopped(TUIData *tui) bool tui_is_stopped(TUIData *tui)
{ {
@ -1572,7 +1612,16 @@ void tui_suspend(TUIData *tui)
// on a non-UNIX system, this is a no-op // on a non-UNIX system, this is a no-op
#ifdef UNIX #ifdef UNIX
ui_client_detach(); ui_client_detach();
bool enable_mouse = tui->mouse_enabled; tui->mouse_enabled_save = tui->mouse_enabled;
tui->input.callbacks.primary_device_attr = tui_suspend_cb;
terminfo_disable(tui);
#endif
}
#ifdef UNIX
static void tui_suspend_cb(TUIData *tui)
FUNC_ATTR_NONNULL_ALL
{
tui_terminal_stop(tui); tui_terminal_stop(tui);
stream_set_blocking(tui->input.in_fd, true); // normalize stream (#2598) stream_set_blocking(tui->input.in_fd, true); // normalize stream (#2598)
@ -1580,13 +1629,13 @@ void tui_suspend(TUIData *tui)
tui_terminal_start(tui); tui_terminal_start(tui);
tui_terminal_after_startup(tui); tui_terminal_after_startup(tui);
if (enable_mouse) { if (tui->mouse_enabled_save) {
tui_mouse_on(tui); tui_mouse_on(tui);
} }
stream_set_blocking(tui->input.in_fd, false); // libuv expects this stream_set_blocking(tui->input.in_fd, false); // libuv expects this
ui_client_attach(tui->width, tui->height, tui->term, tui->rgb); ui_client_attach(tui->width, tui->height, tui->term, tui->rgb);
#endif
} }
#endif
void tui_set_title(TUIData *tui, String title) void tui_set_title(TUIData *tui, String title)
{ {
@ -2464,7 +2513,9 @@ static void augment_terminfo(TUIData *tui, const char *term, int vte_version, in
if (!kitty && (vte_version == 0 || vte_version >= 5400)) { if (!kitty && (vte_version == 0 || vte_version >= 5400)) {
// Fallback to Xterm's modifyOtherKeys if terminal does not support the // Fallback to Xterm's modifyOtherKeys if terminal does not support the
// Kitty keyboard protocol // Kitty keyboard protocol. We don't actually enable the key encoding here
// though: it won't be enabled until the terminal responds to our query for
// kitty keyboard support.
tui->input.key_encoding = kKeyEncodingXterm; tui->input.key_encoding = kKeyEncodingXterm;
} }
} }