From 9d48fa91262e1df7280dbee91c6c0120d556feb0 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 12 Dec 2024 13:11:12 +0530 Subject: [PATCH] hints/unicode_input kittens: Do not lose keypresses that are sent very rapidly via an automation tool immediately after the kitten is launched We now buffer the key events until the kitten tells us it is ready. Without this the key presses are delivered to the underlying window as the kitten's overlay window was not being focused until the kitten set the ready message. Fixes #7089 --- docs/changelog.rst | 2 + kittens/hints/main.go | 2 +- kitty/data-types.h | 2 +- kitty/fast_data_types.pyi | 2 + kitty/keys.c | 103 ++++++++++++++++++++++++++------------ kitty/layout/base.py | 5 +- kitty/state.c | 33 +++++++++++- kitty/state.h | 7 +++ kitty/tabs.py | 6 +++ kitty/window.py | 7 +++ 10 files changed, 131 insertions(+), 38 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 13feb193d..c0dacd748 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -111,6 +111,8 @@ Detailed list of changes - When re-attaching a detached tab preserve internal layout state such as biases and orientations (:iss:`8106`) +- hints/unicode_input kittens: Do not lose keypresses that are sent very rapidly via an automation tool immediately after the kitten is launched (:iss:`7089`) + 0.37.0 [2024-10-30] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/kittens/hints/main.go b/kittens/hints/main.go index 89eb0d1fe..c8c408c49 100644 --- a/kittens/hints/main.go +++ b/kittens/hints/main.go @@ -217,11 +217,11 @@ func main(_ *cli.Command, o *Options, args []string) (rc int, err error) { } lp.OnInitialize = func() (string, error) { - lp.SendOverlayReady() lp.SetCursorVisible(false) lp.SetWindowTitle(window_title) lp.AllowLineWrapping(false) draw_screen() + lp.SendOverlayReady() return "", nil } lp.OnFinalize = func() string { diff --git a/kitty/data-types.h b/kitty/data-types.h index 0eb47bdc2..4654096c5 100644 --- a/kitty/data-types.h +++ b/kitty/data-types.h @@ -321,7 +321,7 @@ void enter_event(void); void mouse_event(const int, int, int); void focus_in_event(void); void scroll_event(double, double, int, int); -void on_key_input(GLFWkeyevent *ev); +void on_key_input(const GLFWkeyevent *ev); void request_window_attention(id_type, bool); #ifndef __APPLE__ void play_canberra_sound(const char *which_sound, const char *event_id, bool is_path, const char *role, const char *theme_name); diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index 122ebb0a1..a530a0a49 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -1715,6 +1715,8 @@ def systemd_move_pid_into_new_scope(pid: int, scope_name: str, description: str) def play_desktop_sound_async(name: str, event_id: str = 'test sound', is_path: bool = False, theme_name: str = '') -> str: ... def cocoa_play_system_sound_by_id_async(sound_id: int) -> None: ... def glfw_get_system_color_theme(query_if_unintialized: bool = True) -> Literal['light', 'dark', 'no_preference']: ... +def set_redirect_keys_to_overlay(os_window_id: int, tab_id: int, window_id: int, overlay_window_id: int) -> None: ... +def buffer_keys_in_window(os_window_id: int, tab_id: int, window_id: int, enabled: bool = True) -> bool: ... class MousePosition(TypedDict): cell_x: int diff --git a/kitty/keys.c b/kitty/keys.c index ddbe2a8d7..42a393057 100644 --- a/kitty/keys.c +++ b/kitty/keys.c @@ -106,8 +106,13 @@ static Window* active_window(void) { Tab *t = global_state.callback_os_window->tabs + global_state.callback_os_window->active_tab; Window *w = t->windows + t->active_window; - if (w->render_data.screen) return w; - return NULL; + if (!w->render_data.screen) return NULL; + if (w->redirect_keys_to_overlay) { + for (unsigned i = 0; i < t->num_windows; i++) { + if (t->windows[i].id == w->redirect_keys_to_overlay && w->render_data.screen) return t->windows + i; + } + } + return w; } void @@ -162,8 +167,57 @@ format_mods(unsigned mods) { return buf; } +static void +send_key_to_child(id_type window_id, Screen *screen, const GLFWkeyevent *ev) { + const int action = ev->action; + const uint32_t key = ev->key, native_key = ev->native_key; + const char *text = ev->text ? ev->text : ""; + + if (action == GLFW_REPEAT && !screen->modes.mDECARM) { + debug("discarding repeat key event as DECARM is off\n"); + return; + } + if (screen->scrolled_by && action == GLFW_PRESS && !is_no_action_key(key, native_key)) { + screen_history_scroll(screen, SCROLL_FULL, false); // scroll back to bottom + } + char encoded_key[KEY_BUFFER_SIZE] = {0}; + int size = encode_glfw_key_event(ev, screen->modes.mDECCKM, screen_current_key_encoding_flags(screen), encoded_key); + if (size == SEND_TEXT_TO_CHILD) { + schedule_write_to_child(window_id, 1, text, strlen(text)); + debug("sent key as text to child (window_id: %llu): %s\n", window_id, text); + } else if (size > 0) { + if (size == 1 && screen->modes.mHANDLE_TERMIOS_SIGNALS) { + if (screen_send_signal_for_key(screen, *encoded_key)) return; + } + schedule_write_to_child(window_id, 1, encoded_key, size); + if (OPT(debug_keyboard)) { + debug("sent encoded key to child (window_id: %llu): ", window_id); + for (int ki = 0; ki < size; ki++) { + if (encoded_key[ki] == 27) { debug("^[ "); } + else if (encoded_key[ki] == ' ') { debug("SPC "); } + else if (isprint(encoded_key[ki])) { debug("%c ", encoded_key[ki]); } + else { debug("0x%x ", encoded_key[ki]); } + } + debug("\n"); + } + } else { + debug("ignoring as keyboard mode does not support encoding this event\n"); + } +} + void -on_key_input(GLFWkeyevent *ev) { +dispatch_buffered_keys(Window *w) { + if (!w->render_data.screen || !w->buffered_keys.count) return; + GLFWkeyevent *keys = w->buffered_keys.key_data; + for (size_t i = 0; i < w->buffered_keys.count; i++) { + debug("Sending previously buffered key "); + send_key_to_child(w->id, w->render_data.screen, keys + i); + } + free(w->buffered_keys.key_data); zero_at_ptr(&w->buffered_keys); +} + +void +on_key_input(const GLFWkeyevent *ev) { Window *w = active_window(); const int action = ev->action, mods = ev->mods; const uint32_t key = ev->key, native_key = ev->native_key; @@ -235,42 +289,25 @@ on_key_input(GLFWkeyevent *ev) { } } if (!w) return; + screen = w->render_data.screen; } else if (w->last_special_key_pressed == key) { w->last_special_key_pressed = 0; debug("ignoring release event for previous press that was handled as shortcut\n"); return; } + if (w->buffered_keys.enabled) { + if (w->buffered_keys.capacity < w->buffered_keys.count + 1) { + w->buffered_keys.capacity = MAX(16, w->buffered_keys.capacity + 8); + GLFWkeyevent *new = malloc(w->buffered_keys.capacity * sizeof(GLFWkeyevent)); + if (!new) fatal("Out of memory"); + memcpy(new, w->buffered_keys.key_data, w->buffered_keys.count * sizeof(new[0])); + w->buffered_keys.key_data = new; + } + GLFWkeyevent *k = w->buffered_keys.key_data; + k[w->buffered_keys.count++] = *ev; + debug("bufferring key until child is ready\n"); + } else send_key_to_child(w->id, screen, ev); #undef dispatch_key_event - if (action == GLFW_REPEAT && !screen->modes.mDECARM) { - debug("discarding repeat key event as DECARM is off\n"); - return; - } - if (screen->scrolled_by && action == GLFW_PRESS && !is_no_action_key(key, native_key)) { - screen_history_scroll(screen, SCROLL_FULL, false); // scroll back to bottom - } - char encoded_key[KEY_BUFFER_SIZE] = {0}; - int size = encode_glfw_key_event(ev, screen->modes.mDECCKM, screen_current_key_encoding_flags(screen), encoded_key); - if (size == SEND_TEXT_TO_CHILD) { - schedule_write_to_child(w->id, 1, text, strlen(text)); - debug("sent key as text to child: %s\n", text); - } else if (size > 0) { - if (size == 1 && screen->modes.mHANDLE_TERMIOS_SIGNALS) { - if (screen_send_signal_for_key(screen, *encoded_key)) return; - } - schedule_write_to_child(w->id, 1, encoded_key, size); - if (OPT(debug_keyboard)) { - debug("sent encoded key to child: "); - for (int ki = 0; ki < size; ki++) { - if (encoded_key[ki] == 27) { debug("^[ "); } - else if (encoded_key[ki] == ' ') { debug("SPC "); } - else if (isprint(encoded_key[ki])) { debug("%c ", encoded_key[ki]); } - else { debug("0x%x ", encoded_key[ki]); } - } - debug("\n"); - } - } else { - debug("ignoring as keyboard mode does not support encoding this event\n"); - } } void diff --git a/kitty/layout/base.py b/kitty/layout/base.py index e1b052b7f..cfb160ea6 100644 --- a/kitty/layout/base.py +++ b/kitty/layout/base.py @@ -294,16 +294,17 @@ class Layout: def add_window( self, all_windows: WindowList, window: WindowType, location: Optional[str] = None, overlay_for: Optional[int] = None, put_overlay_behind: bool = False, bias: Optional[float] = None, - ) -> None: + ) -> Optional[WindowType]: if overlay_for is not None: underlay = all_windows.id_map.get(overlay_for) if underlay is not None: window.margin, window.padding = underlay.margin.copy(), underlay.padding.copy() all_windows.add_window(window, group_of=overlay_for, head_of_group=put_overlay_behind) - return + return underlay if location == 'neighbor': location = 'after' self.add_non_overlay_window(all_windows, window, location, bias) + return None def add_non_overlay_window(self, all_windows: WindowList, window: WindowType, location: Optional[str], bias: Optional[float] = None) -> None: next_to: Optional[WindowType] = None diff --git a/kitty/state.c b/kitty/state.c index fc5340ab3..a7463bf8d 100644 --- a/kitty/state.c +++ b/kitty/state.c @@ -348,7 +348,8 @@ update_os_window_title(OSWindow *os_window) { static void destroy_window(Window *w) { - free(w->pending_clicks.clicks); w->pending_clicks.clicks = NULL; w->pending_clicks.num = 0; w->pending_clicks.capacity = 0; + free(w->pending_clicks.clicks); zero_at_ptr(&w->pending_clicks); + free(w->buffered_keys.key_data); zero_at_ptr(&w->buffered_keys); Py_CLEAR(w->render_data.screen); Py_CLEAR(w->title); Py_CLEAR(w->title_bar_data.last_drawn_title_object_id); free(w->title_bar_data.buf); w->title_bar_data.buf = NULL; @@ -532,6 +533,26 @@ set_active_window(id_type os_window_id, id_type tab_id, id_type window_id) { END_WITH_WINDOW; } +static bool +buffer_keys_in_window(id_type os_window_id, id_type tab_id, id_type window_id, bool enable) { + WITH_WINDOW(os_window_id, tab_id, window_id) + window->buffered_keys.enabled = enable; + if (!enable) dispatch_buffered_keys(window); + return true; + END_WITH_WINDOW; + return false; +} + +static bool +set_redirect_keys_to_overlay(id_type os_window_id, id_type tab_id, id_type window_id, id_type overlay_id) { + WITH_WINDOW(os_window_id, tab_id, window_id) + window->redirect_keys_to_overlay = overlay_id; + return true; + END_WITH_WINDOW; + return false; +} + + static void swap_tabs(id_type os_window_id, unsigned int a, unsigned int b) { WITH_OS_WINDOW(os_window_id) @@ -1359,6 +1380,13 @@ PYWRAP1(redirect_mouse_handling) { Py_RETURN_NONE; } +PYWRAP1(buffer_keys_in_window) { + int enabled = 1; + id_type a, b, c; PA("KKK|p", &a, &b, &c, &enabled); + if (buffer_keys_in_window(a, b, c, enabled)) Py_RETURN_TRUE; + Py_RETURN_FALSE; +} + THREE_ID_OBJ(update_window_title) THREE_ID(remove_window) THREE_ID(detach_window) @@ -1372,6 +1400,7 @@ K(mark_os_window_dirty) KKK(set_active_window) KII(swap_tabs) KK5I(add_borders_rect) +KKKK(set_redirect_keys_to_overlay) static PyObject* os_window_focus_counters(PyObject *self UNUSED, PyObject *args UNUSED) { @@ -1430,6 +1459,8 @@ static PyMethodDef module_methods[] = { MW(attach_window, METH_VARARGS), MW(set_active_tab, METH_VARARGS), MW(mark_os_window_dirty, METH_VARARGS), + MW(set_redirect_keys_to_overlay, METH_VARARGS), + MW(buffer_keys_in_window, METH_VARARGS), MW(set_active_window, METH_VARARGS), MW(swap_tabs, METH_VARARGS), MW(add_borders_rect, METH_VARARGS), diff --git a/kitty/state.h b/kitty/state.h index 628d026a4..1ea6311e4 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -198,6 +198,12 @@ typedef struct { monotonic_t last_drag_scroll_at; uint32_t last_special_key_pressed; WindowBarData title_bar_data, url_target_bar_data; + id_type redirect_keys_to_overlay; + struct { + bool enabled; + void *key_data; + size_t count, capacity; + } buffered_keys; struct { PendingClick *clicks; size_t num, capacity; @@ -417,3 +423,4 @@ void change_live_resize_state(OSWindow*, bool); bool render_os_window(OSWindow *w, monotonic_t now, bool ignore_render_frames, bool scan_for_animated_images); void update_mouse_pointer_shape(void); void adjust_window_size_for_csd(OSWindow *w, int width, int height, int *adjusted_width, int *adjusted_height); +void dispatch_buffered_keys(Window *w); diff --git a/kitty/tabs.py b/kitty/tabs.py index f16aa7d0b..9ca7a5948 100644 --- a/kitty/tabs.py +++ b/kitty/tabs.py @@ -30,6 +30,7 @@ from .fast_data_types import ( GLFW_RELEASE, add_tab, attach_window, + buffer_keys_in_window, current_focused_os_window_id, detach_window, focus_os_window, @@ -45,6 +46,7 @@ from .fast_data_types import ( ring_bell, set_active_tab, set_active_window, + set_redirect_keys_to_overlay, swap_tabs, sync_os_window_title, ) @@ -518,6 +520,10 @@ class Tab: # {{{ overlay_behind: bool = False, bias: Optional[float] = None ) -> None: self.current_layout.add_window(self.windows, window, location, overlay_for, put_overlay_behind=overlay_behind, bias=bias) + if overlay_behind and (w := self.active_window): + set_redirect_keys_to_overlay(self.os_window_id, self.id, w.id, window.id) + buffer_keys_in_window(self.os_window_id, self.id, window.id, True) + window.keys_redirected_till_ready_from = w.id self.mark_tab_bar_dirty() self.relayout() diff --git a/kitty/window.py b/kitty/window.py index 18140e1e3..bdf6ef93c 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -58,6 +58,7 @@ from .fast_data_types import ( add_timer, add_window, base64_decode, + buffer_keys_in_window, cell_size_for_window, click_mouse_cmd_output, click_mouse_url, @@ -77,6 +78,7 @@ from .fast_data_types import ( pointer_name_to_css_name, pt_to_px, replace_c0_codes_except_nl_space_tab, + set_redirect_keys_to_overlay, set_window_logo, set_window_padding, set_window_render_data, @@ -622,6 +624,7 @@ class Window: self.watchers.add(global_watchers()) else: self.watchers = global_watchers().copy() + self.keys_redirected_till_ready_from: int = 0 self.last_focused_at = 0. self.is_focused: bool = False self.last_resized_at = 0. @@ -1407,6 +1410,10 @@ class Window: tab = boss.tab_for_window(self) if tab is not None: tab.move_window_to_top_of_group(self) + if self.keys_redirected_till_ready_from: + set_redirect_keys_to_overlay(self.os_window_id, self.tab_id, self.keys_redirected_till_ready_from, 0) + buffer_keys_in_window(self.os_window_id, self.tab_id, self.id, False) + self.keys_redirected_till_ready_from = 0 def append_remote_data(self, msgb: memoryview) -> str: if not msgb: