The pointer_button_count check added in dc36e2165 uses the client side
view of the implicit grab, which is stale when the button release is
still in flight: glfwStartDrag passes the check, but by the time the
compositor processes wl_data_device.start_drag the implicit grab is
gone and the request is silently dropped. The data source then never
receives any event, so the tab drag state leaks forever, hijacking
mouse handling again, and the already created xdg_toplevel_drag
toplevel gets mapped as a stray undecorated window showing the drag
thumbnail that nothing ever destroys.
Detect this deterministically using protocol ordering: issue a
wl_display.sync right after start_drag. An accepted start_drag
synchronously produces events (wl_pointer.leave from the DND grab
taking over, wl_data_device.enter, wl_data_source events) that are
ordered before the sync callback. If the callback fires with none of
them seen, the request was dropped: cancel the drag, which notifies
the application (clearing the tab drag state) and destroys the drag
toplevel and data source.
Also defer mapping the drag toplevel until the session is confirmed,
so the stray window can never appear, not even for one frame.
Verified against headless GNOME Shell 50.2 (mutter) with injected
pointer timing scans: the unpatched build deadlocks with an orphaned
drag toplevel within ~13 fast flicks, the patched build catches every
race hit and cancels cleanly, with no deadlock, no stray window, and
normal slow drag/reorder/detach behaviour preserved.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
No significant feedback received post release of protocol in 0.47.0
almost a month ago.
Protocol can now only change in backward compatible ways, barring
security issues.
Fixes#9984
The choose-fonts kitten queries the terminal for the user's foreground and
background colors at startup by sending a DCS request. The terminal responds
with separate DCS response strings — one per field — which arrive
asynchronously through on_query_response.
The background handler unconditionally called draw_screen() after storing its
value. If the terminal's DCS response for "background" arrived before the
response for "foreground", draw_screen() fired with text_style.Foreground
still at its Go zero value ("").
The font list preview (list.go) only guarded against empty Background, not
empty Foreground, so it passed the check and sent a render_family_samples
command to the Python backend with foreground="". The backend's to_color('')
raised ValueError: Invalid color name: '', crashing the kitten.
The faces pane (faces.go) launched a render_family_samples goroutine
unconditionally when its preview cache was empty, with no guard at all.
Fix
---
1. ui.go — Only trigger draw_screen() from the foreground or background
handler when the counterpart field is already populated, so the UI
never renders with a partial TextStyle.
2. list.go — Require both Foreground and Background to be non-empty
before rendering the font preview, not just Background alone.
3. faces.go — Skip the render_family_samples goroutine if either color
field is still empty. The preview cache stays empty, so the next
redraw (triggered when the counterpart query response arrives, or
by on_wakeup) retries with full colors.
A quick click-and-flick on a tab could leave all of kitty with mouse
input permanently redirected to the tab bar, making every window
unclickable and text selection impossible.
Starting a tab drag is asynchronous: the drag thumbnail is rendered on
the next frame before glfwStartDrag is called. If the button is
released in that window, wl_data_device_start_drag is sent with a stale
serial that no longer matches an active pointer implicit grab, so the
compositor silently ignores it. The wl_data_source then never receives
any event, on_drag_source_finished never runs, and the
tab_being_dragged state is stuck forever, hijacking all mouse events.
Fix in layers:
- glfw/Wayland: track the implicit grab (serial of the first button
press and pressed-button count), use that serial for start_drag and
refuse with EAGAIN when there is no active implicit grab instead of
letting the compositor silently drop the request
- mouse.c: a left button release arriving while a tab drag is marked
started but no system DND is active means the drag never launched
(an active DND consumes the release on all platforms), so clear the
drag state instead of waiting for DND events that will never come
- tabs.py: handle OSError from start_drag_with_data for tab drags the
same way window drags already do; clear the potential-drag state when
the release lands on the new-tab button or empty tab bar area
- tabs.py/boss.py: clear drag state on drag finish/drop even when the
dragged tab has already been closed
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When focus_follows_mouse is enabled, returning to a desktop/space fired an
enter event that switched the active window to whichever one was under the
cursor, even though the mouse had not crossed a window boundary.
Distinguish genuine mouse motion into a window from a window reappearing
under a stationary cursor by checking, in cursor_enter_callback, whether the
cursor position actually changed. focus_follows_mouse now switches focus only
when the cursor moved, so motion into a window still switches focus while a
stationary reappearance does not.
- Merge TestWatchForConfigChangesIncludeAdded and
TestWatchForConfigChangesIncludeRemoved into the main
TestWatchForConfigChanges function, which now starts the watcher
once and shares it across all integration subtests.
- Add prime_watcher helper that retries writes until an action fires,
replacing the blind time.Sleep(200ms) watcher-startup wait.
- Add new subtest "include added to already-included file adds its
parent dir": writes an include directive into sub/included.conf
(itself included from kitty.conf), then verifies that changes to
the newly referenced file trigger the action, confirming its parent
directory was added to the watch set.
- Fix TestWatchForConfigChangesDebounce to use prime_watcher instead
of time.Sleep for startup; capture before_burst baseline before the
burst loop so the burst-action count is computed correctly.