Skip to content

Honor libghostty mouse-cursor-shape (OSC 22) requests - #7318

Merged
lawrencecchen merged 3 commits into
mainfrom
feat-osc22-mouse-shape
Jul 4, 2026
Merged

lawrencecchen merged 3 commits into
mainfrom
feat-osc22-mouse-shape

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jul 4, 2026 •

Copy link
Copy Markdown
Contributor

What

cmux's action dispatch dropped GHOSTTY_ACTION_MOUSE_SHAPE, so the terminal
showed a static arrow and never reflected the mouse-cursor shape libghostty
requests: OSC 22 (\e]22;<shape> from apps), the pointer over OSC-8 links,
the crosshair during rectangle select, or even the default text/iBeam over the
grid.

How

GhosttyNSView now stores the requested shape (defaulting to text/iBeam),
maps it to a public NSCursor, and drives the terminal's base cursor rect from
it via resetCursorRects(). The GHOSTTY_ACTION_MOUSE_SHAPE case hops to main,
updates the shape (guarded against redundant same-shape events to avoid
cursor-rect thrash), and invalidates the cursor rects. The existing Cmd-hover
pointingHand push/pop overlays on top and reverts to the base shape on
release. No ghostty/submodule change (the C API was already present); public
NSCursor APIs only.

Test

Hover the terminal grid -> iBeam (was arrow). printf '\e]22;pointer\e\\' ->
pointer; \e]22;crosshair\e\\ -> crosshair; \e]22;wait\e\\ -> arrow fallback;
\e]22;\e\\ -> back to iBeam. Cmd-hover a real filename -> pointingHand, revert
to iBeam on release.


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Note

Low Risk
Localized AppKit cursor-rect behavior with no auth, data, or protocol changes beyond handling an existing Ghostty action.

Overview
The terminal view no longer ignores GHOSTTY_ACTION_MOUSE_SHAPE from libghostty, so the pointer can follow OSC 22, link hover, selection, and grid text states instead of staying a static arrow.

GhosttyNSView keeps the latest requested shape (default text/iBeam), maps each ghostty_action_mouse_shape_e to a public NSCursor, and applies it in resetCursorRects(). The action handler dispatches to the main thread, updates the shape only when it changes, and invalidates cursor rects. Existing Cmd-hover pointingHand push/pop still overlays on top and pops back to the base shape.

Reviewed by Cursor Bugbot for commit 7e80e00. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

Restore dynamic mouse cursor by honoring OSC 22 from libghostty. The terminal now shows the correct shape (iBeam, pointer, crosshair, etc.) instead of a static arrow.

  • Bug Fixes

    • Process GHOSTTY_ACTION_MOUSE_SHAPE; dispatch to main and update GhosttyNSView.
    • Map shapes to NSCursor and set the base cursor via resetCursorRects().
    • Deduplicate same-shape events; keep Cmd-hover pointer overlay.
    • Reset OSC 22 mouse shape on surface swap to avoid stale cursors.
  • Refactors

    • Refresh Swift file-length budget for Sources/GhosttyTerminalView.swift.

Written for commit ab1288c. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Added support for updating the terminal mouse cursor shape in response to app-driven actions.
    • Cursor appearance now updates automatically based on the most recent Ghostty-provided mouse shape while moving within the terminal.
  • Bug Fixes

    • Cursor shape state is now cleared when a new terminal surface is attached, preventing stale cursor behavior.
    • Cursor regions are now properly registered when the terminal view resets its cursor rectangles.

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;<shape>`), 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.
@vercel

vercel Bot commented Jul 4, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jul 4, 2026 2:56pm

@coderabbitai

coderabbitai Bot commented Jul 4, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds handling for GHOSTTY_ACTION_MOUSE_SHAPE in the Ghostty action dispatcher and wires mouse-shape state through GhosttyNSView so the view can map Ghostty shapes to NSCursor, reset stale shape state on surface attach, and register cursor rects from the current shape.

Changes

Mouse cursor shape support

Layer / File(s) Summary
Cursor shape action handling and rendering
Sources/GhosttyTerminalView.swift
Adds a GHOSTTY_ACTION_MOUSE_SHAPE branch that forwards the shape to updateGhosttyMouseShape on the main queue, stores and maps Ghostty mouse shapes in GhosttyNSView, clears carried shape state during attachSurface, and applies the current cursor in resetCursorRects().

Estimated code review effort: 2 (Simple) | ~10 minutes

Sequence Diagram(s)

sequenceDiagram
  participant GhosttyCore
  participant ActionDispatcher
  participant GhosttyNSView
  participant AppKit
  GhosttyCore->>ActionDispatcher: GHOSTTY_ACTION_MOUSE_SHAPE(shape)
  ActionDispatcher->>GhosttyNSView: updateGhosttyMouseShape(shape)
  GhosttyNSView->>GhosttyNSView: ghosttyMouseCursor(for: shape)
  GhosttyNSView->>AppKit: invalidate cursor rects
  AppKit->>GhosttyNSView: resetCursorRects()
  GhosttyNSView->>AppKit: addCursorRect(bounds, cursor)
Loading

Possibly related issues

🚥 Pre-merge checks | ✅ 25
✅ Passed checks (25 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed PASS: The new cursor state lives in the MainActor UI view/protocol, and the background action callback hops to DispatchQueue.main before mutating it; no new isolation debt.
Cmux Swift Blocking Runtime ✅ Passed PASS: the PR only adds main-queue async cursor updates and state resets; no semaphores, waits, sleeps, delayed dispatch, main.sync, or new locks were introduced.
Cmux Browser Automation Off-Main ✅ Passed HEAD only resets GhosttyNSView OSC 22 cursor state on surface swap; it doesn’t add or reroute any browser.* command or worker/main routing.
Cmux Expensive Synchronous Load ✅ Passed Diff only adds mouse-cursor shape handling in GhosttyNSView and a cursor-reset tweak; no RestorableAgentSessionIndex/load, JSON/JSONL parsing, or agent-history scans were added.
Cmux Cache Substitution Correctness ✅ Passed ghosttyMouseShape is view-local transient cursor UI state, not persisted/history data; it has a default text fallback and event-driven updates.
Cmux No Hacky Sleeps ✅ Passed Only changed files are Swift and a file-length budget TSV; no TS/JS/shell/runtime scripts and no introduced fixed sleeps, timers, polling, or delays.
Cmux Algorithmic Complexity ✅ Passed Sources/GhosttyTerminalView.swift only adds constant-time mouse-shape handling and a cursor-rect override; no scalable collection scans or batch rescans.
Cmux Swift Concurrency ✅ Passed PASS: The actual diff only resets the cursor shape on surface reattach; no new background queues, Combine, completion handlers, or fire-and-forget Tasks were introduced.
Cmux Swift @Concurrent ✅ Passed The diff only adds a synchronous mouse-shape reset in NSView; no new async helpers or @concurrent annotations were introduced.
Cmux Swift File And Package Boundaries ✅ Passed PASS: Existing 12.3k-line GhosttyTerminalView got a small AppKit/Ghostty glue fix (+4-line follow-up); no new mixed responsibilities or package-boundary violation, budget tweak is incidental.
Cmux Swiftpm Lockfiles ✅ Passed No Package.swift, Package.resolved, .xcodeproj, .xcworkspace, .gitignore, or workflow files changed; only Sources/GhosttyTerminalView.swift and a budget TSV were modified.
Cmux Swift Logging ✅ Passed No logging was added or changed in the Swift diff; only a cursor-shape reset call/comment and a file-length budget update.
Cmux User-Facing Error Privacy ✅ Passed The patch only adds cursor-shape handling in GhosttyNSView; it adds no user-facing errors, alerts, command output, or sensitive upstream details.
Cmux Full Internationalization ✅ Passed Diff only adds cursor-shape behavior and a comment; no user-facing Swift copy, catalogs, Info.plist, or web i18n files changed.
Cmux Swiftui State Layout ✅ Passed PASS: The commit only resets legacy cursor-shape state inside GhosttyNSView (an AppKit bridge); it adds no new ObservableObject/@published, GeometryReader, lazy-row store refs, or body-time mutations.
Cmux Architecture Rethink ✅ Passed mouse-shape state is owned by GhosttyNSView, reset on attachSurface, and main-thread dispatch is a required AppKit bridge—not a timing workaround.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed Diff only changes GhosttyNSView cursor handling and a file-length budget; no NSWindow/NSPanel/WindowGroup or cmuxAuxiliaryWindowIdentifiers changes.
Cmux Source Artifacts ✅ Passed All changed paths are source/tests/config; no artifact-like dirs/files (DerivedData, build output, logs, scratch dirs) were added, and the budget TSV is intentional config.
Cmux No Test Or Debug Seam In Production Source ✅ Passed The PR only adds production cursor-shape handling and a surface-reset in GhosttyTerminalView.swift; no new #if DEBUG, ForTesting, or test-hook seam appears in the diff.
Cmux No Ambient Global State ✅ Passed All new behavior is owned by GhosttyNSView; no new file-scope API, mutable globals, or singletons were introduced.
Title check ✅ Passed The title clearly matches the main change: honoring libghostty mouse-cursor-shape (OSC 22) requests.
Description check ✅ Passed The description covers Summary, How, and Test well; it omits Demo Video, Review Trigger, and checklist details but is otherwise complete.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-osc22-mouse-shape

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jul 4, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a long-standing omission where GHOSTTY_ACTION_MOUSE_SHAPE was never dispatched, leaving the terminal cursor permanently as a static arrow regardless of OSC 22 requests, link hover, or selection mode.

  • GhosttyNSView gains a ghosttyMouseShape property (defaulting to GHOSTTY_MOUSE_SHAPE_TEXT/iBeam), a private static mapping from ghostty_action_mouse_shape_e to NSCursor, and a resetCursorRects() override that registers the base cursor rect from the stored shape.
  • The action dispatch handler picks up GHOSTTY_ACTION_MOUSE_SHAPE, captures the shape, hops to main via DispatchQueue.main.async, and calls updateGhosttyMouseShape, which guards against no-op same-shape updates before calling window?.invalidateCursorRects(for:).
  • attachSurface resets the shape to TEXT on surface replacement so no stale OSC 22 state leaks from one pane to another; the existing Cmd-hover pointingHand push/pop overlay is unaffected.

Confidence Score: 5/5

Safe to merge — the change is confined to AppKit cursor-rect management on the main thread with no auth, persistence, or concurrency concerns.

All mutations to ghosttyMouseShape happen exclusively on the main thread (via DispatchQueue.main.async from the action handler, or directly inside attachSurface which is an AppKit main-thread call). The resetCursorRects() override is driven by AppKit and reads the property on the same actor. The guard against same-shape updates prevents cursor-rect thrash. The static cursor-mapping function introduces no global state. The file-length budget is correctly updated.

No files require special attention.

Important Files Changed

Filename Overview
Sources/GhosttyTerminalView.swift Adds GHOSTTY_ACTION_MOUSE_SHAPE handling: stores requested shape on GhosttyNSView, maps it to NSCursor via a private static switch, drives cursor rects through resetCursorRects(), dispatches updates to main, and resets shape on surface switch. Implementation is correct and follows existing dispatch patterns in the file.
.github/swift-file-length-budget.tsv Budget entry for GhosttyTerminalView.swift raised from 12212 to 12276 to account for the 64 lines added by this PR. Consistent with the budget-tracking convention.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant L as libghostty (I/O thread)
    participant D as GhosttyApp.handleAction
    participant M as Main thread
    participant V as GhosttyNSView
    participant A as AppKit cursor system

    L->>D: GHOSTTY_ACTION_MOUSE_SHAPE(shape)
    D->>M: DispatchQueue.main.async
    M->>V: updateGhosttyMouseShape(shape)
    V->>V: "guard ghosttyMouseShape != shape"
    V->>V: "ghosttyMouseShape = shape"
    V->>A: window?.invalidateCursorRects(for: self)
    A->>V: resetCursorRects()
    V->>A: addCursorRect(bounds, cursor: NSCursor mapped from shape)

    Note over V,A: On surface switch
    V->>V: attachSurface(newSurface)
    V->>V: updateGhosttyMouseShape(GHOSTTY_MOUSE_SHAPE_TEXT)
    V->>A: invalidateCursorRects → resetCursorRects() → iBeam
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant L as libghostty (I/O thread)
    participant D as GhosttyApp.handleAction
    participant M as Main thread
    participant V as GhosttyNSView
    participant A as AppKit cursor system

    L->>D: GHOSTTY_ACTION_MOUSE_SHAPE(shape)
    D->>M: DispatchQueue.main.async
    M->>V: updateGhosttyMouseShape(shape)
    V->>V: "guard ghosttyMouseShape != shape"
    V->>V: "ghosttyMouseShape = shape"
    V->>A: window?.invalidateCursorRects(for: self)
    A->>V: resetCursorRects()
    V->>A: addCursorRect(bounds, cursor: NSCursor mapped from shape)

    Note over V,A: On surface switch
    V->>V: attachSurface(newSurface)
    V->>V: updateGhosttyMouseShape(GHOSTTY_MOUSE_SHAPE_TEXT)
    V->>A: invalidateCursorRects → resetCursorRects() → iBeam
Loading

Reviews (3): Last reviewed commit: "Reset OSC 22 mouse shape on surface swap" | Re-trigger Greptile

Comment thread Sources/GhosttyTerminalView.swift
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) <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ab1288c. Configure here.

let shape = action.action.mouse_shape
DispatchQueue.main.async {
surfaceView.updateGhosttyMouseShape(shape)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale shape after surface swap

Medium Severity

GHOSTTY_ACTION_MOUSE_SHAPE applies the shape on the main queue without checking that the emitting surface is still the one bound to surfaceView. After attachSurface resets the cursor for a new surface, a queued update from the previous surface can overwrite it and show the wrong cursor until another shape event arrives.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ab1288c. Configure here.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Sources/GhosttyTerminalView.swift (1)

2821-2826: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Guard the mouse-shape update against surface reattachment. The DispatchQueue.main.async hop can still run after attachSurface(_:) resets the cursor, so a stale GHOSTTY_ACTION_MOUSE_SHAPE from the previous surface can reapply the old shape. Check the surface identity/generation in the closure before calling updateGhosttyMouseShape.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/GhosttyTerminalView.swift` around lines 2821 - 2826, The mouse-shape
update path in GhosttyTerminalView’s GHOSTTY_ACTION_MOUSE_SHAPE handling can run
after attachSurface(_:) has already switched surfaces, letting a stale async
callback reapply an old cursor. Update the DispatchQueue.main.async closure to
verify the current surface identity or generation before calling
surfaceView.updateGhosttyMouseShape, and skip the update when it no longer
matches the surface that scheduled it.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@Sources/GhosttyTerminalView.swift`:
- Around line 2821-2826: The mouse-shape update path in GhosttyTerminalView’s
GHOSTTY_ACTION_MOUSE_SHAPE handling can run after attachSurface(_:) has already
switched surfaces, letting a stale async callback reapply an old cursor. Update
the DispatchQueue.main.async closure to verify the current surface identity or
generation before calling surfaceView.updateGhosttyMouseShape, and skip the
update when it no longer matches the surface that scheduled it.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1f1cc00e-e3e1-4bfb-b34b-1a4a5e15114a

📥 Commits

Reviewing files that changed from the base of the PR and between 7e80e00 and ab1288c.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (1)
  • Sources/GhosttyTerminalView.swift

@lawrencecchen
lawrencecchen merged commit 657f82a into main Jul 4, 2026
18 of 30 checks passed
@lawrencecchen
lawrencecchen deleted the feat-osc22-mouse-shape branch July 4, 2026 09:29
azooz2003-bit added a commit that referenced this pull request Jul 4, 2026
…ng, themes)

Delta: #7318/#7319 mouse cursor-shape + right/middle drag forwarding, #7320 ghostty
upstream, #6664 iOS themes, #7196 iOS scroll, #7257/#7307 web, #7329 CLI suggestions,
#7222 codex resume update-suppression. Conflicts resolved keeping HEAD structure:
- GhosttyTerminalView.swift: took main's viewDidEndLiveResize + resetCursorRects
  (cursor-shape #7318; ghosttyMouseShape/ghosttyMouseCursor already in HEAD).
- Mobile/MobileHostService.swift: union (kept HEAD sharedRequestActivity +
  main publicStatusPayload theme payload #6664).
- project.pbxproj: union-dedup, normalized; test-wiring green.
- swift-file-length-budget.tsv regenerated.

#7222 (codex update-check suppression) partially landed: the CMUXAgentLaunch package
part (AgentResumeArgv.codexUpdateCheckSuppressionOverride) auto-merged and is present;
the app-target files (+CodexUpdateCheck.swift + test) extend SurfaceResumeCommandCanonicalizer,
a type HEAD dissolved into the CmuxWorkspaces package, so they can't compile and the
suppression was never wired (call site was in the HEAD-deleted +PortableAgentExecutable).
Removed those two files; re-wire recorded in merge-deferred-gaps.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
azooz2003-bit added a commit that referenced this pull request Jul 4, 2026
…n HEAD's resize model)

main's #7318 viewDidEndLiveResize called updateSurfaceSize(bypassLiveResizeCoalescing:),
but HEAD's refactored updateSurfaceSize lacks the coalescePixelOnlyResize model (#6386);
HEAD applies resizes continuously during live resize. Call HEAD's updateSurfaceSize()
(kept the new viewDidEndLiveResize + resetCursorRects cursor-shape). #6386 divergence
recorded in merge-deferred-gaps.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lawrencecchen
lawrencecchen restored the feat-osc22-mouse-shape branch July 18, 2026 10:24

This branch was successfully deployed

1 active deployment
Preview – cmux — ab1288cf Deployed Jul 4, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant