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>
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>
inform_compositor_of_window_geometry() calls xdg_surface_set_window_geometry()
unconditionally, but layer-shell surfaces (e.g. those from `kitten panel`) have
no xdg_surface. With hide_window_decorations set to titlebar-only, the panel hits
this geometry call during creation and dereferences the NULL proxy, crashing in
wl_proxy_get_version() with SIGSEGV.
Guard on window->wl.xdg.surface: layer-shell surfaces manage geometry via the
layer surface, so the xdg call is skipped. Normal toplevels are unaffected.
Wayland (glfw/wl_window.c):
- Fix out-of-bounds access in send_drag_data: look up item by MIME type
instead of using the data-request index i to index _glfw.drag.items[].
The compositor calls drag_source_send once per target window entered,
so _glfw.wl.drag.count grows independently of item_count, causing
_glfw.drag.items[i] to be out-of-bounds on the second drag, yielding a
garbage optional_data pointer that made write() fail with EFAULT.
- Fix protocol error "Drag has not ended": change on_fail and the
GLFW_DRAG_DATA_REQUEST error path to call finish_drag_write(i)+return
instead of cancel_drag(), which was calling wl_data_source_destroy()
before the compositor ended the drag, violating the Wayland protocol.
- Fix double-free of dr.pending_data: null the pointer after free and
add cleanup to finish_drag_write().
- Fix missing finish_drag_write() after a full write in data-request
mode, which left the pipe open causing the target to wait for EOF.
X11 (glfw/x11_window.c):
- Wrap XSendEvent() calls in send_xdnd_enter/position/leave/drop with
_glfwGrabErrorHandlerX11()/_glfwReleaseErrorHandlerX11(). A target
window destroyed between discovery and message delivery produced a
BadWindow error that hit the default X11 abort handler. Now handled
gracefully by clearing current_target or cancelling the drag."
Fixes#9677Fixes#9683
Hide the CSD titlebar subsurface while keeping shadow borders for
resizing. On SSD compositors (GNOME), forces CSD mode to draw
kitty's own shadows without a titlebar.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>