Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -87714,6 +87714,124 @@
}
}
}
},
"settings.app.openMarkdownInCmuxViewer": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Open Markdown in cmux Viewer"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "cmux ビューアで Markdown を開く"
}
},
"ko": {
"stringUnit": {
"state": "translated",
"value": "cmux 뷰어에서 Markdown 열기"
}
},
"zh-Hans": {
"stringUnit": {
"state": "translated",
"value": "在 cmux 查看器中打开 Markdown"
}
},
"zh-Hant": {
"stringUnit": {
"state": "translated",
"value": "在 cmux 檢視器中開啟 Markdown"
}
},
"de": {
"stringUnit": {
"state": "translated",
"value": "Markdown im cmux-Viewer öffnen"
}
},
"es": {
"stringUnit": {
"state": "translated",
"value": "Abrir Markdown en el visor cmux"
}
},
"fr": {
"stringUnit": {
"state": "translated",
"value": "Ouvrir Markdown dans la visionneuse cmux"
}
},
"it": {
"stringUnit": {
"state": "translated",
"value": "Apri Markdown nel visualizzatore cmux"
}
}
}
},
"settings.app.openMarkdownInCmuxViewer.subtitle": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Cmd-clicking .md/.markdown/.mkd/.mdx files opens the cmux markdown viewer panel instead of the preferred editor."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": ".md/.markdown/.mkd/.mdx ファイルを Cmd+クリックすると、優先エディタの代わりに cmux マークダウンビューアパネルが開きます。"
}
},
"ko": {
"stringUnit": {
"state": "translated",
"value": ".md/.markdown/.mkd/.mdx 파일을 Cmd+클릭하면 기본 에디터 대신 cmux 마크다운 뷰어 패널이 열립니다."
}
},
"zh-Hans": {
"stringUnit": {
"state": "translated",
"value": "Cmd+点击 .md/.markdown/.mkd/.mdx 文件将打开 cmux 标记查看器面板,而不是使用首选编辑器。"
}
},
"zh-Hant": {
"stringUnit": {
"state": "translated",
"value": "Cmd+點擊 .md/.markdown/.mkd/.mdx 檔案會開啟 cmux Markdown 檢視器面板,而不是使用偏好編輯器。"
}
},
"de": {
"stringUnit": {
"state": "translated",
"value": "Cmd-Klick auf .md/.markdown/.mkd/.mdx-Dateien öffnet das cmux-Markdown-Viewer-Panel statt des bevorzugten Editors."
}
},
"es": {
"stringUnit": {
"state": "translated",
"value": "Hacer Cmd+clic en archivos .md/.markdown/.mkd/.mdx abre el panel del visor de markdown de cmux en lugar del editor preferido."
}
},
"fr": {
"stringUnit": {
"state": "translated",
"value": "Cmd+clic sur les fichiers .md/.markdown/.mkd/.mdx ouvre le panneau de la visionneuse markdown cmux au lieu de l'éditeur préféré."
}
},
"it": {
"stringUnit": {
"state": "translated",
"value": "Cmd+clic sui file .md/.markdown/.mkd/.mdx apre il pannello del visualizzatore markdown cmux invece dell'editor preferito."
}
}
}
}
}
}
46 changes: 46 additions & 0 deletions Sources/GhosttyTerminalView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3272,6 +3272,40 @@ class GhosttyApp {
#endif

@cubic-dev-ai cubic-dev-ai Bot Apr 15, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Markdown open-or-focus routing logic is duplicated across two click handlers, increasing drift risk and maintenance cost.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/GhosttyTerminalView.swift, line 7881:

<comment>Markdown open-or-focus routing logic is duplicated across two click handlers, increasing drift risk and maintenance cost.</comment>

<file context>
@@ -7831,6 +7873,28 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
+        if let termSurface = terminalSurface,
+           let workspace = termSurface.owningWorkspace(),
+           !workspace.isRemoteTerminalSurface(termSurface.id),
+           CmdClickMarkdownRouteSettings.shouldRoute(path: resolution.path) {
+            if let existingId = workspace.panels.first(where: { _, panel in
+                (panel as? MarkdownPanel)?.filePath == resolution.path
</file context>
Fix with Cubic

return false
}
// Route markdown file URLs into the cmux viewer when the toggle is
// on AND the link is local + has no anchor/query. Anything else
// (toggle off, hosted file URL, #fragment, ?query, non-markdown,
// remote workspace, unreadable file, split creation failure) falls
// through to the existing NSWorkspace path below so the default-off
// behavior and URL semantics are preserved.
let fileURLHost = target.url.host
if CmdClickMarkdownRouteSettings.isEnabled(),
target.url.isFileURL,
target.url.fragment == nil,
target.url.query == nil,
fileURLHost == nil || fileURLHost?.isEmpty == true || fileURLHost == "localhost",
CmdClickMarkdownRouteSettings.isMarkdownPath(target.url.path) {
let fileURL = target.url
let routed: Bool = performOnMain {
// Remote-surface guard runs before shouldRoute so we never
// stat a local path on the main thread for a remote workspace.
guard let termSurface = surfaceView.terminalSurface,
let workspace = termSurface.owningWorkspace(),
!workspace.isRemoteTerminalSurface(termSurface.id),
CmdClickMarkdownRouteSettings.shouldRoute(path: fileURL.path) else {
return false
}
return workspace.openOrFocusMarkdownSplit(
from: termSurface.id,
filePath: fileURL.path
) != nil
}
if routed {
return true
}
// Fall through to the existing NSWorkspace path below.
}

if !BrowserLinkOpenSettings.openTerminalLinksInCmuxBrowser() {
#if DEBUG
dlog("link.openURL cmuxBrowser=disabled, opening externally url=\(target.url)")
Expand Down Expand Up @@ -7846,6 +7880,18 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
)
#endif

// Remote-surface guard runs before shouldRoute so we never stat a local
// path on the main thread for a remote workspace. When the viewer path
// is applicable but split creation fails, fall back to the preferred
// editor so the click never silently no-ops.
if let termSurface = terminalSurface,
let workspace = termSurface.owningWorkspace(),
!workspace.isRemoteTerminalSurface(termSurface.id),
CmdClickMarkdownRouteSettings.shouldRoute(path: resolution.path),
workspace.openOrFocusMarkdownSplit(from: termSurface.id, filePath: resolution.path) != nil {
return resolution
}

PreferredEditorSettings.open(URL(fileURLWithPath: resolution.path))
return resolution
}
Expand Down
5 changes: 5 additions & 0 deletions Sources/KeyboardShortcutSettingsFileStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ final class CmuxSettingsFileStore {
"app.keepWorkspaceOpenWhenClosingLastSurface",
"app.focusPaneOnFirstClick",
"app.preferredEditor",
"app.openMarkdownInCmuxViewer",
"app.reorderOnNotification",
"app.sendAnonymousTelemetry",
"app.warnBeforeQuit",
Expand Down Expand Up @@ -430,6 +431,9 @@ final class CmuxSettingsFileStore {
if let value = jsonString(section["preferredEditor"]) {
snapshot.managedUserDefaults[PreferredEditorSettings.key] = .string(value)
}
if let value = jsonBool(section["openMarkdownInCmuxViewer"]) {
snapshot.managedUserDefaults[CmdClickMarkdownRouteSettings.key] = .bool(value)
}
if let value = jsonBool(section["reorderOnNotification"]) {
snapshot.managedUserDefaults[WorkspaceAutoReorderSettings.key] = .bool(value)
}
Expand Down Expand Up @@ -1357,6 +1361,7 @@ final class CmuxSettingsFileStore {
"keepWorkspaceOpenWhenClosingLastSurface": !LastSurfaceCloseShortcutSettings.defaultValue,
"focusPaneOnFirstClick": PaneFirstClickFocusSettings.defaultEnabled,
"preferredEditor": "",
"openMarkdownInCmuxViewer": CmdClickMarkdownRouteSettings.defaultValue,
"reorderOnNotification": WorkspaceAutoReorderSettings.defaultValue,
"sendAnonymousTelemetry": TelemetrySettings.defaultSendAnonymousTelemetry,
"warnBeforeQuit": QuitWarningSettings.defaultWarnBeforeQuit,
Expand Down
28 changes: 28 additions & 0 deletions Sources/Workspace.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9178,6 +9178,34 @@ final class Workspace: Identifiable, ObservableObject {
return browserPanel
}

/// Open the markdown viewer for `filePath`, reusing an existing
/// `MarkdownPanel` in this workspace that already shows the same file.
/// Paths are compared after symlink resolution so `./README.md` and a
/// symlink pointing at the same file focus the same viewer.
/// Returns `nil` when no existing viewer matches and split creation
/// fails, so callers can fall back to the preferred editor / system opener.
@discardableResult
func openOrFocusMarkdownSplit(
from panelId: UUID,
filePath: String
) -> MarkdownPanel? {
let canonical = (filePath as NSString).resolvingSymlinksInPath
for (existingId, panel) in panels {
guard let md = panel as? MarkdownPanel else { continue }
if (md.filePath as NSString).resolvingSymlinksInPath == canonical {
focusPanel(existingId)
return md
}
}
return newMarkdownSplit(
from: panelId,
orientation: .horizontal,
insertFirst: false,
filePath: filePath,
focus: true
)
}

func newMarkdownSplit(
from panelId: UUID,
orientation: SplitOrientation,
Expand Down
48 changes: 48 additions & 0 deletions Sources/cmuxApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4128,6 +4128,37 @@ enum TelemetrySettings {
static let enabledForCurrentLaunch = isEnabled()
}

enum CmdClickMarkdownRouteSettings {
static let key = "openMarkdownInCmuxViewer"
static let defaultValue = false

static func isEnabled(defaults: UserDefaults = .standard) -> Bool {
defaults.object(forKey: key) == nil ? defaultValue : defaults.bool(forKey: key)
}

/// Cheap extension check. Safe to call off the main thread before any
/// filesystem probe so remote/non-markdown paths can be filtered early.
static func isMarkdownPath(_ path: String) -> Bool {
let ext = (path as NSString).pathExtension.lowercased()
return ext == "md" || ext == "markdown" || ext == "mkd" || ext == "mdx"
}

static func shouldRoute(path: String) -> Bool {
guard isEnabled(), isMarkdownPath(path) else { return false }
// Match the `markdown.open` socket path: only route real, readable
// files. Rejects FIFOs, device nodes, sockets, symlinks to non-regular
// targets, and permission-denied paths so the viewer never opens into
// an unavailable state.
let resolved = (path as NSString).resolvingSymlinksInPath
guard FileManager.default.isReadableFile(atPath: resolved),
let attrs = try? FileManager.default.attributesOfItem(atPath: resolved),
(attrs[.type] as? FileAttributeType) == .typeRegular else {
return false
}
return true
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}

enum PreferredEditorSettings {
static let key = "preferredEditorCommand"

Expand Down Expand Up @@ -4362,6 +4393,7 @@ struct SettingsView: View {
@AppStorage(TelemetrySettings.sendAnonymousTelemetryKey)
private var sendAnonymousTelemetry = TelemetrySettings.defaultSendAnonymousTelemetry
@AppStorage(PreferredEditorSettings.key) private var preferredEditorCommand = ""
@AppStorage(CmdClickMarkdownRouteSettings.key) private var openMarkdownInCmuxViewer = CmdClickMarkdownRouteSettings.defaultValue
@AppStorage("cmuxPortBase") private var cmuxPortBase = 9100
@AppStorage("cmuxPortRange") private var cmuxPortRange = 10
@AppStorage(BrowserSearchSettings.searchEngineKey) private var browserSearchEngine = BrowserSearchSettings.defaultSearchEngine.rawValue
Expand Down Expand Up @@ -5068,6 +5100,21 @@ struct SettingsView: View {

SettingsCardDivider()

SettingsCardRow(
configurationReview: .json("app.openMarkdownInCmuxViewer"),
String(localized: "settings.app.openMarkdownInCmuxViewer", defaultValue: "Open Markdown in cmux Viewer"),
subtitle: String(localized: "settings.app.openMarkdownInCmuxViewer.subtitle", defaultValue: "Cmd-clicking .md/.markdown/.mkd/.mdx files opens the cmux markdown viewer panel instead of the preferred editor.")
) {
Toggle("", isOn: $openMarkdownInCmuxViewer)
.labelsHidden()
.controlSize(.small)
.accessibilityLabel(
String(localized: "settings.app.openMarkdownInCmuxViewer", defaultValue: "Open Markdown in cmux Viewer")
)
}

SettingsCardDivider()

SettingsCardRow(
configurationReview: .json("app.reorderOnNotification"),
String(localized: "settings.app.reorderOnNotification", defaultValue: "Reorder on Notification"),
Expand Down Expand Up @@ -6479,6 +6526,7 @@ struct SettingsView: View {
geminiHooksEnabled = GeminiIntegrationSettings.defaultHooksEnabled
sendAnonymousTelemetry = TelemetrySettings.defaultSendAnonymousTelemetry
preferredEditorCommand = ""
openMarkdownInCmuxViewer = CmdClickMarkdownRouteSettings.defaultValue
browserSearchEngine = BrowserSearchSettings.defaultSearchEngine.rawValue
browserSearchSuggestionsEnabled = BrowserSearchSettings.defaultSearchSuggestionsEnabled
browserThemeMode = BrowserThemeSettings.defaultMode.rawValue
Expand Down
Loading
Loading