fix: respect OS default editor when opening Ghostty config file#2149
fix: respect OS default editor when opening Ghostty config file#2149anthhub wants to merge 2 commits intomanaflow-ai:mainfrom
Conversation
Previously, opening Ghostty Settings from the cmux menu always used TextEdit because the editor URL was hard-coded to /System/Applications/TextEdit.app. Now we use LSCopyDefaultApplicationURLForContentType via Launch Services to resolve the editor in two steps: 1. The default app associated with the file's UTI (e.g. Zed for .ghostty) 2. Falling back to the system-wide default plain-text editor 3. Last resort: NSWorkspace.open() lets the OS pick any handler This mirrors the implementation in Ghostty's own NSWorkspace extension and ensures users who configure Zed, VS Code, or any other editor as their default text editor will have it used here too. Fixes manaflow-ai#2071, Fixes manaflow-ai#2067 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@anthhub is attempting to deploy a commit to the Manaflow Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthrough
Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Greptile SummaryThis PR fixes a longstanding bug where opening Ghostty Settings from the cmux menu always launched the config file in TextEdit, ignoring the user's OS-configured default editor. The fix replaces the hard-coded Changes:
Issues found:
Confidence Score: 5/5
Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[User selects Ghostty Settings…] --> B[openConfigurationInTextEdit]
B --> C{path empty?}
C -- yes --> Z[return — no-op]
C -- no --> D{file has extension?}
D -- yes --> E[UTType from extension\ne.g. .ghostty]
E --> F[LSCopyDefaultApplicationURLForContentType\nextension UTI]
F -- found --> G[open file with\nextension-specific editor]
F -- not found --> H[LSCopyDefaultApplicationURLForContentType\nUTType.plainText]
D -- no --> H
H -- found --> I[open file with\ndefault plain-text editor]
H -- not found --> J[NSWorkspace.open fileURL\nOS picks any associated app]
|
Sources/GhosttyTerminalView.swift
Outdated
| let url = LSCopyDefaultApplicationURLForContentType( | ||
| uti.identifier as CFString, .all, nil)?.takeRetainedValue() as? URL { | ||
| return url | ||
| } | ||
| // Fall back to the default plain-text editor (e.g. VS Code, Zed, etc.) | ||
| return LSCopyDefaultApplicationURLForContentType( | ||
| UTType.plainText.identifier as CFString, .all, nil)?.takeRetainedValue() as? URL |
There was a problem hiding this comment.
Deprecated Launch Services API
LSCopyDefaultApplicationURLForContentType was deprecated in macOS 12 (Monterey). Since the project already targets macOS 14+, the modern replacement NSWorkspace.urlForApplication(toOpenContentType:) is available without any deployment-target trade-off.
Consider replacing both calls:
// Try extension-specific UTI first
if !fileURL.pathExtension.isEmpty,
let uti = UTType(filenameExtension: fileURL.pathExtension),
let url = NSWorkspace.shared.urlForApplication(toOpenContentType: uti) {
return url
}
// Fall back to plain-text default
return NSWorkspace.shared.urlForApplication(toOpenContentType: .plainText)This avoids the CFRetained dance entirely and is the idiomatic Swift/AppKit approach on macOS 12+.
…h NSWorkspace API LSCopyDefaultApplicationURLForContentType was deprecated in macOS 12. Use NSWorkspace.shared.urlForApplication(toOpen:) instead, which achieves the same result (look up the default app for a given file) without relying on the deprecated Launch Services C API. The project minimum deployment target is macOS 14, so the replacement API is unconditionally available.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
Sources/GhosttyTerminalView.swift (1)
1914-1936: Rename method to reflect new behavior.
openConfigurationInTextEdit()now opens the OS-associated editor, not TextEdit specifically. The current name is misleading at the call site.♻️ Proposed rename
- func openConfigurationInTextEdit() { + func openConfigurationInDefaultEditor() { `#if` os(macOS) let path = ghosttyStringValue(ghostty_config_open_path()) guard !path.isEmpty else { return } let fileURL = URL(fileURLWithPath: path) ... `#endif` }--- a/Sources/cmuxApp.swift +++ b/Sources/cmuxApp.swift @@ - GhosttyApp.shared.openConfigurationInTextEdit() + GhosttyApp.shared.openConfigurationInDefaultEditor()🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/GhosttyTerminalView.swift` around lines 1914 - 1936, The method openConfigurationInTextEdit() is misnamed because it opens the OS-associated editor via NSWorkspace rather than TextEdit; rename the method (e.g., to openConfigurationInDefaultEditor or openConfigurationInAssociatedEditor) and update all call sites and any documentation/tests to use the new name; keep the existing implementation using NSWorkspace.shared.urlForApplication(toOpen:) and NSWorkspace.shared.open(...) unchanged, and ensure any references to openConfigurationInTextEdit() in the codebase are replaced to avoid breakage.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@Sources/GhosttyTerminalView.swift`:
- Around line 1925-1933: The open-with-resolved-editor call using
NSWorkspace.shared.open(withApplicationAt:configuration:) must handle launch
failures via its completion handler and trigger the fallback
NSWorkspace.shared.open(fileURL) if an error occurs; update the branch that
checks NSWorkspace.shared.urlForApplication(toOpen: fileURL) to call the
overload with a completionHandler, inspect the provided Error? and, on non-nil
error (or failed success flag), call the fallback
NSWorkspace.shared.open(fileURL) so stale Launch Services mappings still fall
back to the OS-chosen app.
---
Nitpick comments:
In `@Sources/GhosttyTerminalView.swift`:
- Around line 1914-1936: The method openConfigurationInTextEdit() is misnamed
because it opens the OS-associated editor via NSWorkspace rather than TextEdit;
rename the method (e.g., to openConfigurationInDefaultEditor or
openConfigurationInAssociatedEditor) and update all call sites and any
documentation/tests to use the new name; keep the existing implementation using
NSWorkspace.shared.urlForApplication(toOpen:) and NSWorkspace.shared.open(...)
unchanged, and ensure any references to openConfigurationInTextEdit() in the
codebase are replaced to avoid breakage.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 168c317e-0024-4ccc-9273-9f28b554f27d
📒 Files selected for processing (1)
Sources/GhosttyTerminalView.swift
| if let editorURL = NSWorkspace.shared.urlForApplication(toOpen: fileURL) { | ||
| NSWorkspace.shared.open( | ||
| [fileURL], | ||
| withApplicationAt: editorURL, | ||
| configuration: NSWorkspace.OpenConfiguration() | ||
| ) | ||
| } else { | ||
| // Last resort: let the OS pick any associated application | ||
| NSWorkspace.shared.open(fileURL) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, find and check the actual code in GhosttyTerminalView.swift at lines 1925-1933
fd GhosttyTerminalView.swift --type fRepository: manaflow-ai/cmux
Length of output: 93
🏁 Script executed:
# Once we find the file, let's examine the function and surrounding context
cat -n Sources/GhosttyTerminalView.swift | sed -n '1910,1945p'Repository: manaflow-ai/cmux
Length of output: 1994
🏁 Script executed:
# Search for the function openConfigurationInTextEdit to understand its full context
rg -A 30 "func openConfigurationInTextEdit" Sources/GhosttyTerminalView.swiftRepository: manaflow-ai/cmux
Length of output: 1615
🏁 Script executed:
# Check NSWorkspace.shared.open API usage patterns in the codebase
rg -B 3 -A 5 "withApplicationAt:" --type swiftRepository: manaflow-ai/cmux
Length of output: 1412
🏁 Script executed:
# Check the learnings context - look at similar patterns in ContentView.swift and AppDelegate.swift
rg -B 2 -A 10 "workspace\.newTextEditorSplit\|openWorkspaceForExternalDirectory" Sources/ --type swiftRepository: manaflow-ai/cmux
Length of output: 42
🏁 Script executed:
# Search for NSWorkspace.open patterns with completion handlers
rg "NSWorkspace.shared.open" --type swift -B 2 -A 8Repository: manaflow-ai/cmux
Length of output: 22534
🏁 Script executed:
# Check for error handling patterns with NSWorkspace in the codebase
rg "NSWorkspace.*\{" --type swift -A 5Repository: manaflow-ai/cmux
Length of output: 2261
🌐 Web query:
NSWorkspace.open withApplicationAt completion handler macOS Swift
💡 Result:
To open URLs (such as files) asynchronously with a specific application at a given URL on macOS using Swift, use the NSWorkspace.open(:withApplicationAt:configuration:completionHandler:) method. Signature: func open( urls: [URL], withApplicationAt applicationURL: URL, configuration: NSWorkspace.OpenConfiguration, completionHandler: (@Sendable (NSRunningApplication?, (any Error)?) -> Void)? = nil) Example usage: let workspace = NSWorkspace.shared let fileURL = URL(fileURLWithPath: "/path/to/file.txt") let appURL = URL(fileURLWithPath: "/Applications/TextEdit.app") let config = NSWorkspace.OpenConfiguration workspace.open([fileURL], withApplicationAt: appURL, configuration: config) { app, error in if let error = error { print("Failed to open: (error)") } else { print("Opened successfully with (app?.bundleIdentifier ?? "unknown app")") } } This method opens the specified URLs in the app at applicationURL. The completion handler is called on a concurrent queue with the launched NSRunningApplication (or nil) and any Error. It can be called from any thread. For launching the app without URLs, use openApplication(at:configuration:completionHandler:). Older synchronous methods like open(_:withApplicationAt:options:configuration:) exist but lack completion handlers.
Citations:
- 1: https://developer.apple.com/documentation/appkit/nsworkspace/open(_:withapplicationat:configuration:completionhandler:)?language=objc
- 2: https://developer.apple.com/documentation/appkit/nsworkspace/open(_:withapplicationat:configuration:completionhandler:)?changes=_7_1
- 3: https://developer.apple.com/documentation/AppKit/NSWorkspace
- 4: https://developer.apple.com/documentation/AppKit/NSWorkspace/open(_:withApplicationAt:configuration:completionHandler:)
Add error handling for fallback when the resolved editor fails to open.
The call to NSWorkspace.shared.open(...withApplicationAt:...) does not check for launch errors. If the app fails to open (e.g., stale Launch Services mapping), the fallback is never triggered. Use the completion handler to catch errors and invoke the fallback:
Proposed fix
- if let editorURL = NSWorkspace.shared.urlForApplication(toOpen: fileURL) {
- NSWorkspace.shared.open(
- [fileURL],
- withApplicationAt: editorURL,
- configuration: NSWorkspace.OpenConfiguration()
- )
- } else {
- // Last resort: let the OS pick any associated application
- NSWorkspace.shared.open(fileURL)
- }
+ if let editorURL = NSWorkspace.shared.urlForApplication(toOpen: fileURL) {
+ NSWorkspace.shared.open(
+ [fileURL],
+ withApplicationAt: editorURL,
+ configuration: NSWorkspace.OpenConfiguration()
+ ) { _, error in
+ if error != nil {
+ NSWorkspace.shared.open(fileURL)
+ }
+ }
+ } else {
+ // Last resort: let the OS pick any associated application
+ NSWorkspace.shared.open(fileURL)
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if let editorURL = NSWorkspace.shared.urlForApplication(toOpen: fileURL) { | |
| NSWorkspace.shared.open( | |
| [fileURL], | |
| withApplicationAt: editorURL, | |
| configuration: NSWorkspace.OpenConfiguration() | |
| ) | |
| } else { | |
| // Last resort: let the OS pick any associated application | |
| NSWorkspace.shared.open(fileURL) | |
| if let editorURL = NSWorkspace.shared.urlForApplication(toOpen: fileURL) { | |
| NSWorkspace.shared.open( | |
| [fileURL], | |
| withApplicationAt: editorURL, | |
| configuration: NSWorkspace.OpenConfiguration() | |
| ) { _, error in | |
| if error != nil { | |
| NSWorkspace.shared.open(fileURL) | |
| } | |
| } | |
| } else { | |
| // Last resort: let the OS pick any associated application | |
| NSWorkspace.shared.open(fileURL) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Sources/GhosttyTerminalView.swift` around lines 1925 - 1933, The
open-with-resolved-editor call using
NSWorkspace.shared.open(withApplicationAt:configuration:) must handle launch
failures via its completion handler and trigger the fallback
NSWorkspace.shared.open(fileURL) if an error occurs; update the branch that
checks NSWorkspace.shared.urlForApplication(toOpen: fileURL) to call the
overload with a completionHandler, inspect the provided Error? and, on non-nil
error (or failed success flag), call the fallback
NSWorkspace.shared.open(fileURL) so stale Launch Services mappings still fall
back to the OS-chosen app.
Summary
/System/Applications/TextEdit.app.LSCopyDefaultApplicationURLForContentType(Launch Services) to resolve the correct editor at runtime, mirroring the implementation in Ghostty's ownNSWorkspace+Extension.swift.Resolution order:
.ghosttyfiles)UTType.plainText)NSWorkspace.open()— let the OS pick any associated applicationNo new API was introduced;
LSCopyDefaultApplicationURLForContentTypeandUTTypeare already available on macOS 14+, andUniformTypeIdentifierswas already imported inGhosttyTerminalView.swift.Test plan
Fixes #2071, Fixes #2067
Summary by cubic
Open the Ghostty config with the macOS default editor instead of always using TextEdit. Works with Zed, VS Code, or any OS default.
/System/Applications/TextEdit.app; resolve the editor viaNSWorkspace.shared.urlForApplication(toOpen:)(replaces deprecated Launch Services lookup).NSWorkspace.open(fileURL)to let the OS choose.Written for commit bd164e4. Summary will update on new commits.
Summary by CodeRabbit