Repository navigation
Add markdown viewer font size controls #5153
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -79,6 +79,12 @@ public struct AppCatalogSection: SettingCatalogSection { | |||||||||||||||||||||||
| userDefaultsKey: "openMarkdownInCmuxViewer" | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| public let markdownViewerFontSize = DefaultsKey<Double>( | ||||||||||||||||||||||||
| id: "app.markdownViewerFontSize", | ||||||||||||||||||||||||
| defaultValue: 15, | ||||||||||||||||||||||||
| userDefaultsKey: "markdownViewerFontSize" | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
|
Comment on lines
+82
to
+86
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win Document the new public settings key.
📝 Proposed fix+ /// Default font size, in points, for markdown viewer panels.
public let markdownViewerFontSize = DefaultsKey<Double>(
id: "app.markdownViewerFontSize",
defaultValue: 15,
userDefaultsKey: "markdownViewerFontSize"
)As per coding guidelines, “Every public symbol in new Swift packages under Packages/ must be documented with Swift-DocC triple-slash comment at time of writing.” 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| public let iMessageMode = DefaultsKey<Bool>( | ||||||||||||||||||||||||
| id: "app.iMessageMode", | ||||||||||||||||||||||||
| defaultValue: false, | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import Foundation | ||
|
|
||
| public enum CmuxSettingsRuntimeNotifications { | ||
| public static let markdownViewerFontSizeDidChange = Notification.Name("cmux.markdownViewerFontSizeDidChange") | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -87,6 +87,9 @@ public enum ShortcutAction: String, CaseIterable, Sendable, Hashable, SettingCod | |
| case browserZoomIn | ||
| case browserZoomOut | ||
| case browserZoomReset | ||
| case markdownZoomIn | ||
| case markdownZoomOut | ||
| case markdownZoomReset | ||
| case find | ||
| case findInDirectory | ||
| case findNext | ||
|
|
@@ -106,6 +109,7 @@ extension ShortcutAction { | |
| case navigation | ||
| case panes | ||
| case browser | ||
| case markdown | ||
|
|
||
| public var title: String { | ||
| switch self { | ||
|
|
@@ -114,6 +118,7 @@ extension ShortcutAction { | |
| case .navigation: return "Navigation" | ||
| case .panes: return "Panes" | ||
| case .browser: return "Browser & Find" | ||
| case .markdown: return "Markdown Viewer" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Localize the new shortcut metadata. These new As per coding guidelines, "For production user-facing text, fail partial localization: Swift text must use localized APIs with matching translated string-catalog entries." Also applies to: 231-233 🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
| } | ||
|
|
@@ -148,6 +153,8 @@ extension ShortcutAction { | |
| .hideFind, .useSelectionForFind, .toggleBrowserDeveloperTools, | ||
| .showBrowserJavaScriptConsole, .toggleReactGrab: | ||
| return .browser | ||
| case .markdownZoomIn, .markdownZoomOut, .markdownZoomReset: | ||
| return .markdown | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -221,6 +228,9 @@ extension ShortcutAction { | |
| case .browserZoomIn: return "Zoom In" | ||
| case .browserZoomOut: return "Zoom Out" | ||
| case .browserZoomReset: return "Actual Size" | ||
| case .markdownZoomIn: return "Markdown Viewer: Zoom In" | ||
| case .markdownZoomOut: return "Markdown Viewer: Zoom Out" | ||
| case .markdownZoomReset: return "Markdown Viewer: Actual Size" | ||
| case .find: return "Find…" | ||
| case .findInDirectory: return "Find in Directory…" | ||
| case .findNext: return "Find Next" | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -41,6 +41,7 @@ extension Array where Element == CuratedSettingEntry { | |
| .init(section: .app, id: "preferred-editor", title: "Open Files With", synonyms: "app.preferredEditor editor open file code vscode visual studio zed sublime subl cursor"), | ||
| .init(section: .app, id: "supported-file-previews", title: "Open Supported Files in cmux", synonyms: "app.openSupportedFilesInCmux cmd click file preview pdf image video audio quicklook quick look editor external"), | ||
| .init(section: .app, id: "markdown-viewer", title: "Open Markdown in cmux Viewer", synonyms: "app.openMarkdownInCmuxViewer md markdown mdx viewer preview readme"), | ||
| .init(section: .app, id: "markdown-font-size", title: "Markdown Viewer Font Size", synonyms: "app.markdownViewerFontSize markdown viewer font size text scale zoom points preview"), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't add English-only curated setting entries. This new curated entry hard-codes both the visible search-result title and its search terms in English, so localized users will see an untranslated result here and won't get locale-appropriate search matches for the new setting. As per coding guidelines, "For production user-facing text, fail partial localization: Swift text must use localized APIs with matching translated string-catalog entries." 🤖 Prompt for AI Agents |
||
| .init(section: .app, id: "terminal-config", title: "Terminal Config", synonyms: "ghostty config merged generated preview terminal configuration window open config"), | ||
| .init(section: .app, id: "imessage-mode", title: "iMessage Mode", synonyms: "app.iMessageMode imessage message messages chat prompt prompts submitted texting reorder move workspace top agent send"), | ||
| .init(section: .app, id: "reorder-notification", title: "Reorder on Notification", synonyms: "app.reorderOnNotification notification reorder move workspace top unread sort"), | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,7 @@ import UniformTypeIdentifiers | |
| @MainActor | ||
| public struct AppSection: View { | ||
| private let catalog: SettingCatalog | ||
| private let defaultsStore: UserDefaultsSettingsStore | ||
| private let hostActions: SettingsHostActions | ||
|
|
||
| // Every bound value-model lives here as view state, constructed once | ||
|
|
@@ -35,6 +36,7 @@ public struct AppSection: View { | |
| @State private var preferredEditor: DefaultsValueModel<String> | ||
| @State private var openSupported: DefaultsValueModel<Bool> | ||
| @State private var openMarkdown: DefaultsValueModel<Bool> | ||
| @State private var markdownFontSize: DefaultsValueModel<Double> | ||
| @State private var iMessage: DefaultsValueModel<Bool> | ||
| @State private var reorder: DefaultsValueModel<Bool> | ||
| @State private var dockBadge: DefaultsValueModel<Bool> | ||
|
|
@@ -62,6 +64,7 @@ public struct AppSection: View { | |
| hostActions: SettingsHostActions | ||
| ) { | ||
| self.catalog = catalog | ||
| self.defaultsStore = defaultsStore | ||
| self.hostActions = hostActions | ||
| _language = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.app.language)) | ||
| _appearance = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.app.appearance)) | ||
|
|
@@ -75,6 +78,7 @@ public struct AppSection: View { | |
| _preferredEditor = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.app.preferredEditor)) | ||
| _openSupported = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.app.openSupportedFilesInCmux)) | ||
| _openMarkdown = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.app.openMarkdownInCmuxViewer)) | ||
| _markdownFontSize = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.app.markdownViewerFontSize)) | ||
| _iMessage = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.app.iMessageMode)) | ||
| _reorder = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.app.reorderOnNotification)) | ||
| _dockBadge = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.notifications.dockBadge)) | ||
|
|
@@ -96,6 +100,7 @@ public struct AppSection: View { | |
|
|
||
| private static let columnWidth: CGFloat = 196 | ||
| private static let notificationSoundControlWidth: CGFloat = 280 | ||
| private static let markdownFontSizeRange: ClosedRange<Double> = 1...96 | ||
|
|
||
| /// Languages legacy `AppLanguage` exposes (cmuxApp.swift line | ||
| /// 4338). The shared `CmuxSettings.AppLanguage` adds `.vi` for a | ||
|
|
@@ -311,6 +316,35 @@ public struct AppSection: View { | |
| } | ||
| SettingsCardDivider() | ||
|
|
||
| // Markdown Viewer Font Size | ||
| SettingsCardRow( | ||
| configurationReview: .json("app.markdownViewerFontSize"), | ||
| String(localized: "settings.app.markdownViewerFontSize", defaultValue: "Markdown Viewer Font Size"), | ||
| subtitle: String(localized: "settings.app.markdownViewerFontSize.subtitle", defaultValue: "Default rendered markdown size for new viewer panels. Panel zoom shortcuts override this per panel until reset."), | ||
| controlWidth: 140 | ||
| ) { | ||
| Stepper( | ||
| value: Binding( | ||
| get: { Self.clampedMarkdownFontSize(markdownFontSize.current) }, | ||
| set: { setMarkdownFontSize($0) } | ||
| ), | ||
| in: Self.markdownFontSizeRange, | ||
| step: 1 | ||
| ) { | ||
| Text( | ||
| String.localizedStringWithFormat( | ||
| String(localized: "settings.fontSize.valuePoints", defaultValue: "%@ pt"), | ||
| Self.formatMarkdownFontSize(markdownFontSize.current) | ||
| ) | ||
| ) | ||
| .foregroundStyle(.secondary) | ||
| .monospacedDigit() | ||
| .frame(minWidth: 44, alignment: .trailing) | ||
| } | ||
| .accessibilityIdentifier("SettingsMarkdownViewerFontSizeStepper") | ||
| } | ||
| SettingsCardDivider() | ||
|
|
||
| // iMessage Mode | ||
| SettingsCardRow( | ||
| configurationReview: .json("app.iMessageMode"), | ||
|
|
@@ -369,6 +403,7 @@ public struct AppSection: View { | |
| Toggle("", isOn: Binding(get: { showInMenuBar.current }, set: { showInMenuBar.set($0) })) | ||
| .labelsHidden() | ||
| .controlSize(.small) | ||
| .accessibilityIdentifier("SettingsShowInMenuBarToggle") | ||
| } | ||
| .disabled(menuBarOnly.current) | ||
| SettingsCardDivider() | ||
|
|
@@ -715,6 +750,33 @@ public struct AppSection: View { | |
| } | ||
| } | ||
|
|
||
| private static func clampedMarkdownFontSize(_ value: Double) -> Double { | ||
| guard value.isFinite else { return 15 } | ||
| return min(max(value, markdownFontSizeRange.lowerBound), markdownFontSizeRange.upperBound) | ||
| } | ||
|
|
||
| private static func formatMarkdownFontSize(_ value: Double) -> String { | ||
| let clamped = clampedMarkdownFontSize(value) | ||
| if clamped.rounded() == clamped { | ||
| return "\(Int(clamped))" | ||
| } | ||
| return String(format: "%.2f", clamped) | ||
| } | ||
|
Comment on lines
+758
to
+764
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: The Swift initializer String(format: "%.2f", value) does not automatically respect the user's current locale settings [1]. It uses a C-style printf-based formatting engine that defaults to the "C" locale, meaning it will consistently use a period (.) as the decimal separator regardless of the user's regional preferences, which may expect a comma (,) or other locale-specific conventions [1]. To make String(format:) locale-aware, you must explicitly provide a locale, such as by using the initializer String(format:locale:_:arguments:) [1][2]. For SwiftUI settings labels and general UI output, the recommended approach is to use the modern Swift FormatStyle APIs [3][4]. These APIs are natively locale-aware and handle formatting details automatically [5][6]. Recommended API: The.formatted method with a specific format style is the standard for SwiftUI [5][3]. For example, to format a number to two decimal places while respecting the current user's locale, you can use: Text(value, format:.number.precision(.fractionLength(2))) This approach is preferred because it is declarative, type-safe, and automatically adapts to the user's environment (e.g., decimal separators, grouping separators) without requiring manual locale management [5][6][4]. Citations:
Use locale-aware number formatting for displayed font size
🤖 Prompt for AI Agents |
||
|
|
||
| private func setMarkdownFontSize(_ value: Double) { | ||
| let clamped = Self.clampedMarkdownFontSize(value) | ||
| let key = catalog.app.markdownViewerFontSize | ||
| Task { [defaultsStore] in | ||
| await defaultsStore.set(clamped, for: key) | ||
| await MainActor.run { | ||
| NotificationCenter.default.post( | ||
| name: CmuxSettingsRuntimeNotifications.markdownViewerFontSizeDidChange, | ||
| object: nil | ||
| ) | ||
| } | ||
| } | ||
|
Comment on lines
+766
to
+777
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Update the bound value model synchronously. This setter bypasses 🤖 Prompt for AI Agents |
||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Stepper setter bypasses model causing async update lagMedium Severity
Reviewed by Cursor Bugbot for commit 622ad8b. Configure here. |
||
|
|
||
| private func confirmQuitSubtitle(_ mode: ConfirmQuitMode) -> String { | ||
| // Mirrors legacy confirmQuitModeSubtitle keys/text. | ||
| switch mode { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -290,6 +290,44 @@ public struct KeyboardShortcutsSection: View { | |
|
|
||
| // MARK: - Conflict helpers | ||
|
|
||
| private enum ShortcutConflictContext: Equatable { | ||
| case application | ||
| case nonBrowserPanel | ||
| case browserPanel | ||
| case markdownPanel | ||
| case rightSidebarFocus | ||
|
|
||
| func overlaps(_ other: ShortcutConflictContext) -> Bool { | ||
| if self == .application || other == .application { | ||
| return true | ||
| } | ||
| if self == .nonBrowserPanel && other == .markdownPanel { | ||
| return true | ||
| } | ||
| if self == .markdownPanel && other == .nonBrowserPanel { | ||
| return true | ||
| } | ||
| return self == other | ||
| } | ||
| } | ||
|
|
||
| private static func shortcutContext(for action: ShortcutAction) -> ShortcutConflictContext { | ||
| switch action { | ||
| case .switchRightSidebarToFiles, .switchRightSidebarToFind, .switchRightSidebarToSessions, | ||
| .switchRightSidebarToFeed, .switchRightSidebarToDock: | ||
| return .rightSidebarFocus | ||
| case .renameTab, .renameWorkspace: | ||
| return .nonBrowserPanel | ||
| case .browserBack, .browserForward, .browserReload, .toggleBrowserDeveloperTools, | ||
| .showBrowserJavaScriptConsole, .browserZoomIn, .browserZoomOut, .browserZoomReset: | ||
| return .browserPanel | ||
| case .markdownZoomIn, .markdownZoomOut, .markdownZoomReset: | ||
| return .markdownPanel | ||
| default: | ||
| return .application | ||
| } | ||
| } | ||
|
Comment on lines
+293
to
+329
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Run restores through the same conflict gate. The new context-aware 🔧 Suggested fix private func restoreBinding(_ shortcut: StoredShortcut, for action: ShortcutAction) async {
+ if let conflict = detectConflict(for: action, stroke: shortcut) {
+ conflictRejections[action.rawValue] = conflict
+ bareKeyRejections.remove(action.rawValue)
+ return
+ }
var updated = bindings
updated[action.rawValue] = shortcut
restoreShortcuts.removeValue(forKey: action.rawValue)
conflictRejections.removeValue(forKey: action.rawValue)
await write(updated)Also applies to: 346-348 🤖 Prompt for AI Agents |
||
|
|
||
| /// Mirrors legacy `KeyboardShortcutSettings.Action.conflicts(with:proposedAction:configuredShortcut:)` | ||
| /// at a coarser grain: only treat two actions as conflicting when the | ||
| /// *configured* (effective) shortcut of the other action is not | ||
|
|
@@ -305,7 +343,9 @@ public struct KeyboardShortcutsSection: View { | |
| } | ||
|
|
||
| private func detectConflict(for action: ShortcutAction, stroke: StoredShortcut) -> ShortcutAction? { | ||
| let context = Self.shortcutContext(for: action) | ||
| for other in ShortcutAction.allCases where other != action { | ||
| guard context.overlaps(Self.shortcutContext(for: other)) else { continue } | ||
| let override = bindings[other.rawValue] | ||
| let effective = override ?? other.defaultStroke.map { StoredShortcut(first: $0) } | ||
| guard let effective, !effective.isUnbound else { continue } | ||
|
|
||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Align
--font-sizevalidation with the new 1–96pt contract.This helper currently accepts any finite value greater than 0, so
cmux markdown open --font-size 0.5passes here even though the new markdown viewer font-size setting is documented as 1–96 pt. That makes the CLI surface inconsistent with the settings/schema layer for the same feature.Suggested fix
private func parseMarkdownViewerFontSize(_ rawValue: String) throws -> Double { let trimmed = rawValue.trimmingCharacters(in: .whitespacesAndNewlines) guard let size = Double(trimmed), isUsableMarkdownViewerFontSize(size) else { - throw CLIError(message: "--font-size must be a positive number no larger than 96") + throw CLIError(message: "--font-size must be between 1 and 96") } return roundedMarkdownViewerMetric(size) } private func isUsableMarkdownViewerFontSize(_ size: Double) -> Bool { - size.isFinite && size > 0 && size <= 96 + size.isFinite && size >= 1 && size <= 96 }🤖 Prompt for AI Agents