From 8e8725274549f42af047daf9e2cdfeec5612a339 Mon Sep 17 00:00:00 2001 From: ntomsic Date: Wed, 20 May 2026 20:23:36 -0500 Subject: [PATCH] =?UTF-8?q?qt:=20parity=20tier=202=20batch=201=20=E2=80=94?= =?UTF-8?q?=20input=20+=20child-exited=20(B15,=20B23,=20B24,=20B27,=20B28)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five fixes: B23 — Sided modifiers (left vs right) now reported. New XkbState::sideBitsForKeycode looks up the keysym for a keycode and returns GHOSTTY_MODS_*_RIGHT when it's a right-side modifier (Shift_R / Control_R / Alt_R or ISO_Level3_Shift / Super_R / etc.). sendKey ORs the result alongside the unsided bits so binds like `right_shift+x` can fire. macOS + GTK both populate sided bits. B24 — Mouse enter/leave now notify libghostty. * enterEvent forwards the actual cursor position so hover state and OSC-8 link arming reset from any stale (-1, -1) sentinel. * leaveEvent (new override) sends (-1, -1) so any hover-armed state clears once the pointer leaves the pane. Mirrors macOS mouseEntered/mouseExited and GTK ecMouseLeave. B27 — Right-click is now sent to libghostty BEFORE deciding whether to open the context menu. The mouse-press fires unconditionally; the contextMenuEvent handler still gates on mouse_captured, so capturing programs (e.g. mc) still get raw clicks. Matches macOS rightMouseDown + menu(for:) and GTK mouseButtonCallback's "fire menu only if not consumed." B28 — Click-to-focus suppresses the matching mouse-up. New m_suppressNextLeftRelease flag set on a press that grabs focus from elsewhere, cleared on the matching release. Without this, vim/less/etc. would see a stray button-up event right after the user clicked the pane to focus it. macOS does the same with suppressNextLeftMouseUp; GTK uses suppress_left_mouse_release. B15 — SHOW_CHILD_EXITED now respects the abnormal-command-exit- runtime config (default 250ms). Banner is suppressed for short-lived children (e.g. quick `ls`); macOS does the same gate. B40 — window-decoration full enum: documenting that Qt has no portable way to force CSD vs SSD on Wayland, so `auto`/`server`/ `client` all map to "decorated" (the platform decides which). No code change needed; the existing comment already says this. Co-Authored-By: claude-flow --- qt/src/GhosttySurface.cpp | 74 +++++++++++++++++++++++++++++++++++++-- qt/src/GhosttySurface.h | 6 ++++ qt/src/MainWindow.cpp | 13 +++++-- 3 files changed, 88 insertions(+), 5 deletions(-) diff --git a/qt/src/GhosttySurface.cpp b/qt/src/GhosttySurface.cpp index f1bf4c0ed..b3faf16d5 100644 --- a/qt/src/GhosttySurface.cpp +++ b/qt/src/GhosttySurface.cpp @@ -603,6 +603,32 @@ public: return xkb_keysym_to_utf32(sym); } + // Side bits for the libghostty mods bitfield, derived from a + // keycode — used so that pressing Right-Shift sets BOTH the + // unsided GHOSTTY_MODS_SHIFT and the GHOSTTY_MODS_SHIFT_RIGHT bit + // (a left-side keycode sets only the unsided bit). macOS and GTK + // populate sided bits this way; Qt was leaving them empty so + // bindings that distinguish left-vs-right modifier keys couldn't + // fire. + ghostty_input_mods_e sideBitsForKeycode(uint32_t keycode) const { + if (!m_unshifted) return GHOSTTY_MODS_NONE; + const xkb_keysym_t sym = + xkb_state_key_get_one_sym(m_unshifted, keycode); + int r = GHOSTTY_MODS_NONE; + switch (sym) { + case XKB_KEY_Shift_R: r |= GHOSTTY_MODS_SHIFT_RIGHT; break; + case XKB_KEY_Control_R: r |= GHOSTTY_MODS_CTRL_RIGHT; break; + // Both Alt_R and ISO_Level3_Shift (AltGr) are right-Alt physically. + case XKB_KEY_Alt_R: + case XKB_KEY_ISO_Level3_Shift: r |= GHOSTTY_MODS_ALT_RIGHT; break; + case XKB_KEY_Super_R: + case XKB_KEY_Hyper_R: + case XKB_KEY_Meta_R: r |= GHOSTTY_MODS_SUPER_RIGHT; break; + default: break; + } + return static_cast(r); + } + // Modifiers consumed by the layout to produce `keycode`'s text given // `mods` are depressed. Returns the consumed subset, expressed as // ghostty mod bits. Mutates m_query (mutable) — see thread-safety @@ -698,6 +724,12 @@ void GhosttySurface::sendKey(QKeyEvent *ev, ghostty_input_action_e action) { ghostty_input_key_s k = {}; k.action = action; k.mods = translateMods(ev->modifiers()); + // OR in any right-side bit for this keycode (e.g. Right-Shift sets + // SHIFT_RIGHT alongside SHIFT). macOS + GTK populate these; without + // them, keybinds like `right_shift+x` can't distinguish from + // `left_shift+x`. + k.mods = static_cast( + k.mods | XkbState::instance().sideBitsForKeycode(keycode)); k.keycode = keycode; k.text = printable ? text.constData() : nullptr; // XKB lookups: unshifted codepoint (what this physical key would @@ -772,13 +804,30 @@ void GhosttySurface::mousePressEvent(QMouseEvent *ev) { m_owner->removeSurface(this); return; } + // Click-to-focus: if the surface didn't have focus, this click is + // grabbing focus rather than a real interaction with the running + // program. macOS + GTK suppress the matching mouse-up so vim, less, + // etc. don't see a stray button-up event. We mirror that by setting + // a one-shot flag the matching release consults. + const bool wasFocused = hasFocus(); setFocus(); - if (rightClickOpensMenu(ev)) return; + if (!wasFocused && ev->button() == Qt::LeftButton) + m_suppressNextLeftRelease = true; + + // Right-click: send the press to libghostty BEFORE deciding to + // open the context menu. macOS + GTK both do this so the core can + // word-select on right-press and then we open the menu over the + // selection. If the running program is mouse-captured, the press + // is forwarded as a real button event. sendMouseButton(ev, GHOSTTY_MOUSE_PRESS); } void GhosttySurface::mouseReleaseEvent(QMouseEvent *ev) { - if (rightClickOpensMenu(ev)) return; + // Suppress the release of a focus-grabbing click — see press above. + if (ev->button() == Qt::LeftButton && m_suppressNextLeftRelease) { + m_suppressNextLeftRelease = false; + return; + } sendMouseButton(ev, GHOSTTY_MOUSE_RELEASE); } @@ -947,9 +996,28 @@ void GhosttySurface::wheelEvent(QWheelEvent *ev) { flashScrollbar(); // mouse-wheel scrolling reveals the overlay scrollbar } -void GhosttySurface::enterEvent(QEnterEvent *) { +void GhosttySurface::enterEvent(QEnterEvent *ev) { // focus-follows-mouse: take focus when the pointer enters this pane. if (m_owner && m_owner->focusFollowsMouse() && !hasFocus()) setFocus(); + // Tell libghostty about the actual cursor position so hover state + // and OSC-8 link arming reset from any stale (-1, -1) sentinel. + // macOS does this in mouseEntered (SurfaceView_AppKit.swift:920); + // GTK does it in ecMouseEnter (apprt/gtk/class/surface.zig). + if (m_surface) + ghostty_surface_mouse_pos(m_surface, ev->position().x(), + ev->position().y(), + translateMods(QGuiApplication::keyboardModifiers())); +} + +void GhosttySurface::leaveEvent(QEvent *) { + // libghostty's "no cursor here" sentinel: pass (-1, -1) so any + // hover-armed state (URL underline, mouse-report sequences for an + // OSC-8 link) clears once the pointer leaves the pane. macOS and + // GTK both do this; without it the arm state would survive until + // the next move event. + if (m_surface) + ghostty_surface_mouse_pos(m_surface, -1, -1, + translateMods(QGuiApplication::keyboardModifiers())); } void GhosttySurface::focusInEvent(QFocusEvent *) { diff --git a/qt/src/GhosttySurface.h b/qt/src/GhosttySurface.h index 39b4d1199..8d7930d9d 100644 --- a/qt/src/GhosttySurface.h +++ b/qt/src/GhosttySurface.h @@ -131,6 +131,7 @@ protected: void dropEvent(QDropEvent *) override; void wheelEvent(QWheelEvent *) override; void enterEvent(QEnterEvent *) override; // focus-follows-mouse + void leaveEvent(QEvent *) override; // libghostty hover reset void focusInEvent(QFocusEvent *) override; void focusOutEvent(QFocusEvent *) override; @@ -206,6 +207,11 @@ private: bool m_notifyOnCommand = false; // one-shot: notify on next cmd finish bool m_bellFlash = false; // bell `border` flash in progress bool m_bellTitle = false; // unacknowledged bell `title` mark + // Set when a left-click grabbed focus from elsewhere; cleared on + // the matching mouse-up so the click that grabbed focus isn't + // also reported to the running program. macOS + GTK do the same + // (suppressNextLeftMouseUp / suppress_left_mouse_release). + bool m_suppressNextLeftRelease = false; // Last requested cursor shape (from MOUSE_SHAPE) and visibility // (from MOUSE_VISIBILITY). Tracked separately so toggling // visibility doesn't reset the shape. diff --git a/qt/src/MainWindow.cpp b/qt/src/MainWindow.cpp index 055ae77c9..a970a54c2 100644 --- a/qt/src/MainWindow.cpp +++ b/qt/src/MainWindow.cpp @@ -1457,8 +1457,17 @@ bool MainWindow::onAction(ghostty_app_t, ghostty_target_s target, case GHOSTTY_ACTION_SHOW_CHILD_EXITED: { if (!src) return false; - const int code = - static_cast(action.action.child_exited.exit_code); + const ghostty_surface_message_childexited_s ce = + action.action.child_exited; + // Suppress the banner for fast-exiting children (e.g. an + // intentional `exit 0` after a quick command). Match the macOS + // gate: only show when runtime_ms is at least the configured + // abnormal threshold (default 250ms). Banner = "the process + // died unexpectedly," not "the process exited." + uint32_t threshold = 250; + configGet(s_config, &threshold, "abnormal-command-exit-runtime"); + if (ce.runtime_ms < threshold) return true; + const int code = static_cast(ce.exit_code); post(src, [srcp, code]() { if (srcp) srcp->showChildExited(code); });