From 7e80e00429d65dd2fcb879ff4b44ea99b872b6ce Mon Sep 17 00:00:00 2001 From: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> Date: Sat, 4 Jul 2026 00:07:04 -0700 Subject: [PATCH 1/3] Honor libghostty mouse-cursor-shape (OSC 22) requests cmux's action dispatch dropped GHOSTTY_ACTION_MOUSE_SHAPE, so the terminal showed a static arrow and never reflected the pointer shape libghostty requests: OSC 22 (`\e]22;`), the pointer over OSC-8 links, the crosshair during rectangle select, or even the default text/iBeam over the grid. Handle the action in GhosttyNSView: store the requested shape (defaulting to text/iBeam), map it to a public NSCursor, and drive the terminal's base cursor rect from it via resetCursorRects. The existing Cmd-hover pointingHand push/pop overlays on top and reverts to the base shape on release. --- Sources/GhosttyTerminalView.swift | 60 +++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/Sources/GhosttyTerminalView.swift b/Sources/GhosttyTerminalView.swift index 79cfab793b16..7d63b2588f87 100644 --- a/Sources/GhosttyTerminalView.swift +++ b/Sources/GhosttyTerminalView.swift @@ -2818,6 +2818,12 @@ class GhosttyApp { } case GHOSTTY_ACTION_RENDER: return false + case GHOSTTY_ACTION_MOUSE_SHAPE: + let shape = action.action.mouse_shape + DispatchQueue.main.async { + surfaceView.updateGhosttyMouseShape(shape) + } + return true case GHOSTTY_ACTION_SCROLLBAR: let scrollbar = GhosttyScrollbar(c: action.action.scrollbar) surfaceView.enqueueScrollbarUpdate(scrollbar) @@ -3415,6 +3421,7 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations { private let _renderedFrameLock = NSLock() var cellSize: CGSize = .zero private var lastKnownMousePointInView: NSPoint? + private var ghosttyMouseShape: ghostty_action_mouse_shape_e = GHOSTTY_MOUSE_SHAPE_TEXT static func retainRenderedFrameNotifications() -> () -> Void { // See GhosttyApp.retainTickNotifications() on the idempotent release. @@ -3422,6 +3429,54 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations { return { retention.release() } } + private static func ghosttyMouseCursor(for shape: ghostty_action_mouse_shape_e) -> NSCursor { + switch shape { + case GHOSTTY_MOUSE_SHAPE_DEFAULT: + return .arrow + case GHOSTTY_MOUSE_SHAPE_TEXT: + return .iBeam + case GHOSTTY_MOUSE_SHAPE_VERTICAL_TEXT: + return .iBeamCursorForVerticalLayout + case GHOSTTY_MOUSE_SHAPE_POINTER: + return .pointingHand + case GHOSTTY_MOUSE_SHAPE_CROSSHAIR, + GHOSTTY_MOUSE_SHAPE_CELL: + return .crosshair + case GHOSTTY_MOUSE_SHAPE_GRAB: + return .openHand + case GHOSTTY_MOUSE_SHAPE_GRABBING, + GHOSTTY_MOUSE_SHAPE_MOVE: + return .closedHand + case GHOSTTY_MOUSE_SHAPE_NOT_ALLOWED, + GHOSTTY_MOUSE_SHAPE_NO_DROP: + return .operationNotAllowed + case GHOSTTY_MOUSE_SHAPE_CONTEXT_MENU: + return .contextualMenu + case GHOSTTY_MOUSE_SHAPE_COPY: + return .dragCopy + case GHOSTTY_MOUSE_SHAPE_ALIAS: + return .dragLink + case GHOSTTY_MOUSE_SHAPE_W_RESIZE, + GHOSTTY_MOUSE_SHAPE_E_RESIZE, + GHOSTTY_MOUSE_SHAPE_EW_RESIZE, + GHOSTTY_MOUSE_SHAPE_COL_RESIZE: + return .resizeLeftRight + case GHOSTTY_MOUSE_SHAPE_N_RESIZE, + GHOSTTY_MOUSE_SHAPE_S_RESIZE, + GHOSTTY_MOUSE_SHAPE_NS_RESIZE, + GHOSTTY_MOUSE_SHAPE_ROW_RESIZE: + return .resizeUpDown + default: + return .arrow + } + } + + fileprivate func updateGhosttyMouseShape(_ shape: ghostty_action_mouse_shape_e) { + guard ghosttyMouseShape != shape else { return } + ghosttyMouseShape = shape + window?.invalidateCursorRects(for: self) + } + /// Coalesce high-frequency scrollbar updates into a single main-thread /// dispatch. The action callback (which may fire thousands of times per /// second during bulk output like `seq 1 100000`) stores the latest value @@ -3920,6 +3975,11 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations { invalidateTextInputCoordinates() } + override func resetCursorRects() { + super.resetCursorRects() + addCursorRect(bounds, cursor: Self.ghosttyMouseCursor(for: ghosttyMouseShape)) + } + override var isOpaque: Bool { false } private func resolvedSurfaceSize(preferred size: CGSize?) -> CGSize { From 1821142fdc2e307cafcbb324a0bcbe5ce13cc4de Mon Sep 17 00:00:00 2001 From: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> Date: Sat, 4 Jul 2026 00:34:33 -0700 Subject: [PATCH 2/3] Refresh Swift file-length budget for GhosttyTerminalView.swift growth --- .github/swift-file-length-budget.tsv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/swift-file-length-budget.tsv b/.github/swift-file-length-budget.tsv index 3b81deaa264c..81a92e76ebfe 100644 --- a/.github/swift-file-length-budget.tsv +++ b/.github/swift-file-length-budget.tsv @@ -7,7 +7,7 @@ 14270 Sources/TerminalController.swift 13172 Sources/Workspace.swift 12348 cmuxTests/AppDelegateShortcutRoutingTests.swift -12212 Sources/GhosttyTerminalView.swift +12272 Sources/GhosttyTerminalView.swift 11669 Sources/Panels/BrowserPanel.swift 9497 cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift 8032 Sources/Panels/BrowserPanelView.swift From ab1288cf552da0824c1a3f5609a585f314318c90 Mon Sep 17 00:00:00 2001 From: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> Date: Sat, 4 Jul 2026 00:40:10 -0700 Subject: [PATCH 3/3] Reset OSC 22 mouse shape on surface swap Bugbot flagged that ghosttyMouseShape persists across attachSurface when a different TerminalSurface binds to the view, so cursor rects could show the previous surface's OSC 22 shape until libghostty emits a new GHOSTTY_ACTION_MOUSE_SHAPE. Reset to the default (GHOSTTY_MOUSE_SHAPE_TEXT) when a new surface attaches; the new surface re-emits its own shape if any. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/swift-file-length-budget.tsv | 2 +- Sources/GhosttyTerminalView.swift | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/swift-file-length-budget.tsv b/.github/swift-file-length-budget.tsv index 81a92e76ebfe..ba31ad7200b4 100644 --- a/.github/swift-file-length-budget.tsv +++ b/.github/swift-file-length-budget.tsv @@ -7,7 +7,7 @@ 14270 Sources/TerminalController.swift 13172 Sources/Workspace.swift 12348 cmuxTests/AppDelegateShortcutRoutingTests.swift -12272 Sources/GhosttyTerminalView.swift +12276 Sources/GhosttyTerminalView.swift 11669 Sources/Panels/BrowserPanel.swift 9497 cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift 8032 Sources/Panels/BrowserPanelView.swift diff --git a/Sources/GhosttyTerminalView.swift b/Sources/GhosttyTerminalView.swift index 7d63b2588f87..bae634033d28 100644 --- a/Sources/GhosttyTerminalView.swift +++ b/Sources/GhosttyTerminalView.swift @@ -3849,6 +3849,10 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations { let isAlreadyAttached = surface.isAttached(to: self) if !isSameSurface { appliedColorScheme = nil + // Reset any OSC 22 mouse shape carried over from the previous + // surface; the newly-bound surface re-emits its own shape via + // GHOSTTY_ACTION_MOUSE_SHAPE if it has one. + updateGhosttyMouseShape(GHOSTTY_MOUSE_SHAPE_TEXT) } terminalSurface = surface tabId = surface.tabId