Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import Foundation

/// Settings under the dotted-id prefix `links.*`.
public struct LinksCatalogSection: SettingCatalogSection {
/// Whether terminal-emitted links are captured into the workspace Links pane.
public let enabled = DefaultsKey<Bool>(
id: "links.enabled",
defaultValue: true,
userDefaultsKey: "links.enabled"
)

/// Comma-separated hosts ignored during link ingest.
public let ignoreHosts = DefaultsKey<String>(
id: "links.ignoreHosts",
defaultValue: "localhost:31034",
userDefaultsKey: "links.ignoreHosts"
)

/// Whether `file://` URLs are retained in link history.
public let includeFilePaths = DefaultsKey<Bool>(
id: "links.includeFilePaths",
defaultValue: false,
userDefaultsKey: "links.includeFilePaths"
)

/// Maximum captured links retained per workspace.
public let retentionLimit = DefaultsKey<Int>(
id: "links.retentionLimit",
defaultValue: 500,
userDefaultsKey: "links.retentionLimit"
)

/// Whether cmux fetches remote page titles for captured links.
public let fetchTitles = DefaultsKey<Bool>(
id: "links.fetchTitles",
defaultValue: false,
userDefaultsKey: "links.fetchTitles"
)

/// Creates the links settings section with its default keys.
public init() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ public struct SettingCatalog: SettingCatalogSection {
public let browser = BrowserCatalogSection()
/// Settings for the built-in markdown viewer (the `markdown.*` keys).
public let markdown = MarkdownCatalogSection()
/// Settings for terminal-emitted workspace link capture (the `links.*` keys).
public let links = LinksCatalogSection()
/// Settings for the freeform canvas workspace layout (the `canvas.*` keys).
public let canvas = CanvasCatalogSection()
/// Settings for the built-in plain-text file editor (the `fileEditor.*` keys).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ extension ShortcutAction {
case .fileExplorerOpenSelection: return ShortcutStroke(key: "\r")
case .fileExplorerOpenSelectionFinderAlias: return ShortcutStroke(key: "↓", command: true)
case .openDiffViewer: return ShortcutStroke(key: "d", command: true, shift: true, control: true)
case .openLinksPanel: return ShortcutStroke(key: "l", command: true, shift: true, control: true)
case .saveFilePreview: return ShortcutStroke(key: "s", command: true)
case .openBrowser: return ShortcutStroke(key: "l", command: true, shift: true)
case .focusBrowserAddressBar: return ShortcutStroke(key: "l", command: true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ extension ShortcutAction {
case .canvasDistributeVertically:
return String(localized: "shortcut.canvasDistributeVertically.label", defaultValue: "Canvas: Distribute Vertically")
case .openDiffViewer: return "Open Diff Viewer"
case .openLinksPanel: return String(localized: "shortcut.openLinksPanel.label", defaultValue: "Open Links Panel")
case .saveFilePreview: return "Save File Preview"
case .openBrowser: return "Open Browser"
case .focusBrowserAddressBar: return "Focus Address Bar"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ extension ShortcutAction {
.simulatorHome, .simulatorRotateLeft, .simulatorRotateRight,
.simulatorToggleAppearance, .simulatorToggleSoftwareKeyboard:
return .panes
case .openDiffViewer, .saveFilePreview, .openBrowser, .focusBrowserAddressBar,
case .openDiffViewer, .openLinksPanel, .saveFilePreview, .openBrowser, .focusBrowserAddressBar,
.browserBack, .browserForward, .browserReload, .browserHardReload,
.browserZoomIn, .browserZoomOut, .browserZoomReset,
.markdownZoomIn, .markdownZoomOut, .markdownZoomReset,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ public enum ShortcutAction: String, CaseIterable, Sendable, Hashable, SettingCod

// MARK: Browser & Find
case openDiffViewer
case openLinksPanel
case saveFilePreview
case openBrowser
case focusBrowserAddressBar
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ public struct AppSection: View {
@State private var markdownFontSize: DefaultsValueModel<Int>
@State private var markdownFontFamily: DefaultsValueModel<String>
@State private var markdownMaxWidth: DefaultsValueModel<Int>
@State private var linksEnabled: DefaultsValueModel<Bool>
@State private var linksIgnoreHosts: DefaultsValueModel<String>
@State private var linksIncludeFilePaths: DefaultsValueModel<Bool>
@State private var linksRetentionLimit: DefaultsValueModel<Int>
@State private var linksFetchTitles: DefaultsValueModel<Bool>
@State private var canvasPaneGap: DefaultsValueModel<Int>
@State private var canvasSnapping: DefaultsValueModel<Bool>
@State private var fileEditorWordWrap: DefaultsValueModel<Bool>
Expand Down Expand Up @@ -94,6 +99,11 @@ public struct AppSection: View {
_markdownFontSize = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.markdown.fontSize))
_markdownFontFamily = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.markdown.fontFamily))
_markdownMaxWidth = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.markdown.maxWidth))
_linksEnabled = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.links.enabled))
_linksIgnoreHosts = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.links.ignoreHosts))
_linksIncludeFilePaths = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.links.includeFilePaths))
_linksRetentionLimit = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.links.retentionLimit))
_linksFetchTitles = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.links.fetchTitles))
_canvasPaneGap = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.canvas.paneGap))
_canvasSnapping = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.canvas.snappingEnabled))
_fileEditorWordWrap = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.fileEditor.wordWrap))
Expand Down Expand Up @@ -140,7 +150,7 @@ public struct AppSection: View {
mainCard
}
.task {
startSettingsObservation([language, appearance, appIcon, placement, inheritDir, minimalMode, keepWorkspaceOpen, firstClick, focusHistoryIncludesPanesAndTabs, fileDrop, preferredEditor, openSupported, openMarkdown, globalFontMagnification, markdownFontSize, markdownFontFamily, markdownMaxWidth, canvasPaneGap, canvasSnapping, fileEditorWordWrap, iMessage, reorder, dockBadge, menuBarOnly, showInMenuBar, paneRing, paneFlash, desktopNotifications, agentPermissionPrompt, agentTurnComplete, agentIdleReminder, soundName, soundCommand, customSoundFile, telemetry, confirmQuit, warnCloseTab, warnCloseX, hideCloseButton, renameSelects, paletteAllSurfaces])
startSettingsObservation([language, appearance, appIcon, placement, inheritDir, minimalMode, keepWorkspaceOpen, firstClick, focusHistoryIncludesPanesAndTabs, fileDrop, preferredEditor, openSupported, openMarkdown, globalFontMagnification, markdownFontSize, markdownFontFamily, markdownMaxWidth, linksEnabled, linksIgnoreHosts, linksIncludeFilePaths, linksRetentionLimit, linksFetchTitles, canvasPaneGap, canvasSnapping, fileEditorWordWrap, iMessage, reorder, dockBadge, menuBarOnly, showInMenuBar, paneRing, paneFlash, desktopNotifications, agentPermissionPrompt, agentTurnComplete, agentIdleReminder, soundName, soundCommand, customSoundFile, telemetry, confirmQuit, warnCloseTab, warnCloseX, hideCloseButton, renameSelects, paletteAllSurfaces])
if languageAtAppear == nil { languageAtAppear = language.current }; if telemetryAtAppear == nil { telemetryAtAppear = telemetry.current }
}
}
Expand Down Expand Up @@ -500,6 +510,78 @@ public struct AppSection: View {
}
SettingsCardDivider()

// Links Capture
SettingsCardRow(
configurationReview: .json("links.enabled"),
String(localized: "settings.app.linksEnabled", defaultValue: "Capture Terminal Links"),
subtitle: String(localized: "settings.app.linksEnabled.subtitle", defaultValue: "Record URLs emitted by terminals into each workspace's Links pane.")
) {
Toggle("", isOn: Binding(get: { linksEnabled.current }, set: { linksEnabled.set($0) }))
.labelsHidden()
.controlSize(.small)
.accessibilityIdentifier("SettingsLinksEnabledToggle")
}
SettingsCardDivider()

SettingsCardRow(
configurationReview: .json("links.ignoreHosts"),
String(localized: "settings.app.linksIgnoreHosts", defaultValue: "Ignored Link Hosts"),
subtitle: String(localized: "settings.app.linksIgnoreHosts.subtitle", defaultValue: "Comma-separated hosts to skip. Use host, host:port, or *.example.com.")
) {
TextField(
String(localized: "settings.app.linksIgnoreHosts.placeholder", defaultValue: "localhost:31034"),
text: Binding(get: { linksIgnoreHosts.current }, set: { linksIgnoreHosts.set($0) })
)
.textFieldStyle(.roundedBorder)
.frame(width: 220)
.accessibilityIdentifier("SettingsLinksIgnoreHostsTextField")
}
SettingsCardDivider()

SettingsCardRow(
configurationReview: .json("links.includeFilePaths"),
String(localized: "settings.app.linksIncludeFilePaths", defaultValue: "Include File URLs"),
subtitle: String(localized: "settings.app.linksIncludeFilePaths.subtitle", defaultValue: "Keep file:// links in captured link history.")
) {
Toggle("", isOn: Binding(get: { linksIncludeFilePaths.current }, set: { linksIncludeFilePaths.set($0) }))
.labelsHidden()
.controlSize(.small)
.accessibilityIdentifier("SettingsLinksIncludeFilePathsToggle")
}
SettingsCardDivider()

SettingsCardRow(
configurationReview: .json("links.retentionLimit"),
String(localized: "settings.app.linksRetentionLimit", defaultValue: "Link Retention Limit"),
subtitle: String(localized: "settings.app.linksRetentionLimit.subtitle", defaultValue: "Maximum captured links retained per workspace."),
controlWidth: Self.columnWidth
) {
Stepper(
value: Binding(get: { linksRetentionLimit.current }, set: { linksRetentionLimit.set(min(max($0, 10), 10_000)) }),
in: 10...10_000,
step: 10
) {
Text(verbatim: "\(linksRetentionLimit.current)")
.monospacedDigit()
.frame(width: 52, alignment: .trailing)
}
.controlSize(.small)
.accessibilityIdentifier("SettingsLinksRetentionLimitStepper")
}
SettingsCardDivider()

SettingsCardRow(
configurationReview: .json("links.fetchTitles"),
String(localized: "settings.app.linksFetchTitles", defaultValue: "Fetch Link Titles"),
subtitle: String(localized: "settings.app.linksFetchTitles.subtitle", defaultValue: "Fetch page titles for public http(s) links. Private and local hosts are never fetched.")
) {
Toggle("", isOn: Binding(get: { linksFetchTitles.current }, set: { linksFetchTitles.set($0) }))
.labelsHidden()
.controlSize(.small)
.accessibilityIdentifier("SettingsLinksFetchTitlesToggle")
}
SettingsCardDivider()

// File Editor Word Wrap
SettingsCardRow(
configurationReview: .json("fileEditor.wordWrap"),
Expand Down
Loading