Skip to content

fix: respect OS default editor when opening Ghostty config file#2149

Open
anthhub wants to merge 2 commits intomanaflow-ai:mainfrom
anthhub:fix/config-file-default-editor
Open

fix: respect OS default editor when opening Ghostty config file#2149
anthhub wants to merge 2 commits intomanaflow-ai:mainfrom
anthhub:fix/config-file-default-editor

Conversation

@anthhub
Copy link
Contributor

@anthhub anthhub commented Mar 25, 2026

Summary

  • Fixes the bug where opening Ghostty Settings from the cmux menu always opened the config file in TextEdit, ignoring the user's OS-configured default editor (e.g. Zed, VS Code, Sublime Text).
  • The editor URL was previously hard-coded to /System/Applications/TextEdit.app.
  • Now uses LSCopyDefaultApplicationURLForContentType (Launch Services) to resolve the correct editor at runtime, mirroring the implementation in Ghostty's own NSWorkspace+Extension.swift.

Resolution order:

  1. Default app for the file's UTI (e.g. Zed registered for .ghostty files)
  2. System default plain-text editor (UTType.plainText)
  3. Last resort: NSWorkspace.open() — let the OS pick any associated application

No new API was introduced; LSCopyDefaultApplicationURLForContentType and UTType are already available on macOS 14+, and UniformTypeIdentifiers was already imported in GhosttyTerminalView.swift.

Test plan

  • Set a non-TextEdit editor (e.g. Zed or VS Code) as the default text editor in macOS System Settings → Default Apps
  • Open cmux → menu → Ghostty Settings…
  • Verify the config file opens in the configured editor, not TextEdit
  • With no custom default set (TextEdit as default), verify it still opens correctly in TextEdit

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.

  • Bug Fixes
    • Removed hard-coded /System/Applications/TextEdit.app; resolve the editor via NSWorkspace.shared.urlForApplication(toOpen:) (replaces deprecated Launch Services lookup).
    • Resolution: default app for the file → fallback to NSWorkspace.open(fileURL) to let the OS choose.

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

Summary by CodeRabbit

  • Bug Fixes
    • Configuration files now open in your preferred system default editor instead of always launching in TextEdit.
    • Added an intelligent fallback so the OS-assigned editor is used when no specific default handler is configured for the file type.
    • Improves reliability when opening config files and aligns behavior with system preferences for a more consistent experience.

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>
@vercel
Copy link

vercel bot commented Mar 25, 2026

@anthhub is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link

coderabbitai bot commented Mar 25, 2026

📝 Walkthrough

Walkthrough

openConfigurationInTextEdit() now resolves the OS-configured application for the config file via NSWorkspace.shared.urlForApplication(toOpen:) and opens the file with that app; if unresolved, it falls back to NSWorkspace.shared.open(fileURL) instead of always launching TextEdit.

Changes

Cohort / File(s) Summary
Editor resolution
Sources/GhosttyTerminalView.swift
Removed hard-coded TextEdit launch. Now calls NSWorkspace.shared.urlForApplication(toOpen:) to get the preferred app for the config file and opens with that URL; falls back to NSWorkspace.shared.open(fileURL) when no app URL is resolved.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 I found the config, no forced TextEdit today,
I asked the Workspace which app should play.
It points to your choice, or lets macOS decide,
A hop, a click — your editor opens with pride. 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main change: replacing hard-coded TextEdit with OS-configured default editor for opening Ghostty config files.
Description check ✅ Passed The description covers all required template sections: summary (what/why), testing (test plan with specific steps), and checklist items. However, no demo video is provided, and not all checklist items are marked complete.
Linked Issues check ✅ Passed The code changes directly address both linked issues (#2071 and #2067): replacing hard-coded TextEdit with OS-configured default editor resolution using NSWorkspace API, matching the expected behavior described in both issues.
Out of Scope Changes check ✅ Passed All changes in GhosttyTerminalView.swift are focused on the core objective: modifying editor resolution logic to respect OS defaults instead of hard-coding TextEdit. No out-of-scope changes detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

@greptile-apps
Copy link

greptile-apps bot commented Mar 25, 2026

Greptile Summary

This 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 /System/Applications/TextEdit.app URL with a UTI-based dynamic resolution using LSCopyDefaultApplicationURLForContentType, with a clean three-tier fallback: (1) default app for the file's specific extension UTI, (2) system default plain-text editor, (3) NSWorkspace.open() as a last resort.

Changes:

  • openConfigurationInTextEdit() in GhosttyTerminalView.swift: removes the hard-coded TextEdit app URL and replaces it with runtime UTI resolution via Launch Services
  • The resolution order mirrors Ghostty's own NSWorkspace+Extension.swift implementation
  • UniformTypeIdentifiers was already imported, so no new framework dependencies are added

Issues found:

  • The function is still named openConfigurationInTextEdit — now a misnomer since it no longer opens in TextEdit specifically
  • Both LSCopyDefaultApplicationURLForContentType calls use a deprecated API (deprecated since macOS 12); since the project targets macOS 14+, the modern NSWorkspace.urlForApplication(toOpenContentType:) is available as a drop-in replacement with no deployment-target cost and without the manual CFRetained unwrapping

Confidence Score: 5/5

  • Safe to merge; the fix is correct and the fallback chain is sound. Remaining comments are non-blocking cleanup suggestions.
  • The core logic is correct — UTI-based resolution with a sensible three-level fallback is a well-established macOS pattern. The two flagged items (stale function name and deprecated-but-functional API) are style issues that don't affect runtime correctness or reliability on the targeted macOS 14+ platform. No new API surface, no data-loss or security risk.
  • No files require special attention

Important Files Changed

Filename Overview
Sources/GhosttyTerminalView.swift Replaces the hard-coded TextEdit URL with a dynamic UTI-based editor resolution that respects the OS default; logic is correct and fallback chain is sound. Two minor style issues: stale function name and use of a deprecated Launch Services API (replacement is available on the project's minimum deployment target).

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]
Loading

Comments Outside Diff (1)

  1. Sources/GhosttyTerminalView.swift, line 1914 (link)

    P2 Stale function name

    The function is still named openConfigurationInTextEdit, but it now dynamically resolves the user's default editor and no longer hard-codes TextEdit. The name is now misleading for anyone calling or reading this code.

Reviews (1): Last reviewed commit: "fix: respect OS default editor when open..." | Re-trigger Greptile

Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

No issues found across 1 file

Comment on lines +1928 to +1934
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
Copy link

Choose a reason for hiding this comment

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

P2 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.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 984ad2c and bd164e4.

📒 Files selected for processing (1)
  • Sources/GhosttyTerminalView.swift

Comment on lines +1925 to +1933
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)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, find and check the actual code in GhosttyTerminalView.swift at lines 1925-1933
fd GhosttyTerminalView.swift --type f

Repository: 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.swift

Repository: 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 swift

Repository: 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 swift

Repository: 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 8

Repository: manaflow-ai/cmux

Length of output: 22534


🏁 Script executed:

# Check for error handling patterns with NSWorkspace in the codebase
rg "NSWorkspace.*\{" --type swift -A 5

Repository: 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:


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.

Suggested change
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.

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.

Settings file opens in TextEdit instead of OS-configured default editor Respect configured text editor for Ghostty config files

1 participant