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: