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
This commit is contained in:
Kovid Goyal 2024-12-12 13:11:12 +05:30
parent 9c1324e9d0
commit 9d48fa9126
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
10 changed files with 131 additions and 38 deletions

View file

@ -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]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View file

@ -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 {

View file

@ -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);

View file

@ -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

View file

@ -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

View file

@ -294,16 +294,17 @@ def move_window_to_group(self, all_windows: WindowList, group: int) -> bool:
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

View file

@ -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),

View file

@ -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);

View file

@ -30,6 +30,7 @@
GLFW_RELEASE,
add_tab,
attach_window,
buffer_keys_in_window,
current_focused_os_window_id,
detach_window,
focus_os_window,
@ -45,6 +46,7 @@
ring_bell,
set_active_tab,
set_active_window,
set_redirect_keys_to_overlay,
swap_tabs,
sync_os_window_title,
)
@ -518,6 +520,10 @@ def _add_window(
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()

View file

@ -58,6 +58,7 @@
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 @@
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 @@ def __init__(
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 @@ def handle_overlay_ready(self, msg: memoryview) -> None:
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: