Add global font magnification (#6554)

* [cmux] add global font magnification across terminals and chrome

* [cmux] address PR review for global font magnification

* [cmux] cap global font magnification at 200% to fit chrome budgets

* [cmux] keep About title bold, label magnification stepper, robust percent parse

* [cmux] scale magnification control text and unclip settings subtitles

* fix: make font magnification cover AppKit chrome

* fix: address magnification review findings

* fix: apply magnification to terminal runtime config

* fix: keep sidebar font size unscaled in config

* fix: make browser import font actions void

* fix: make font magnification helper instantiable

* fix: update updater UI package lockfile

* fix: scale magnified chrome row metrics

* fix: address magnification review feedback

* fix: close magnification review gates

* fix: close app-host magnification gates

* fix: localize magnification setting catalog

* fix: hoist font magnification environment

* fix: close magnification review follow-ups

* fix: restore file explorer keyboard activation

* fix: keep inherited terminal font sizes unscaled

* fix: restore close and search regressions

* fix: keep file explorer header height in sync

* fix: keep right sidebar keyboard tests compiling

* fix: scale fallback ghostty config font size

* fix: keep ghostty config parsing deterministic

* fix: address magnification review regressions

* fix: satisfy magnification policy gates

* fix: satisfy font helper package policy

* fix: update sidebar drop metrics fixture

* fix: satisfy cmux font policy check

---------

Co-authored-by: Gigi Sayfan <[email protected]>
This commit is contained in:
Austin Wang
2026-06-22 00:27:00 -07:00
committed by GitHub
co-authored by Gigi Sayfan
parent cd09255f41
commit a6255a1d5f
162 changed files with 3008 additions and 944 deletions
+26 -25
View File
@@ -2,13 +2,13 @@
# Format: max_lines<TAB>relative path
# Reduce counts as files shrink. CI fails if tracked files exceed this budget.
34336 CLI/cmux.swift
17697 Sources/AppDelegate.swift
16094 Sources/ContentView.swift
17717 Sources/AppDelegate.swift
16112 Sources/ContentView.swift
13950 Sources/TerminalController.swift
12758 Sources/Workspace.swift
12144 cmuxTests/AppDelegateShortcutRoutingTests.swift
11865 Sources/Panels/BrowserPanel.swift
11841 Sources/GhosttyTerminalView.swift
11924 Sources/Panels/BrowserPanel.swift
11867 Sources/GhosttyTerminalView.swift
9335 cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift
8017 CLI/cmux_open.swift
7988 Sources/Panels/BrowserPanelView.swift
@@ -16,25 +16,25 @@
7218 cmuxTests/WorkspaceRemoteConnectionTests.swift
6317 cmuxTests/SessionPersistenceTests.swift
6220 cmuxTests/GhosttyConfigTests.swift
6163 Sources/TabManager.swift
6072 Sources/TextBoxInput.swift
6165 Sources/TabManager.swift
6084 Sources/TextBoxInput.swift
5915 cmuxTests/TerminalAndGhosttyTests.swift
5573 cmuxTests/BrowserConfigTests.swift
5566 Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
4475 Sources/cmuxApp.swift
4467 Sources/Panels/FilePreviewPanel.swift
4487 Sources/Panels/FilePreviewPanel.swift
4478 Sources/cmuxApp.swift
4401 cmuxTests/BrowserPanelTests.swift
4225 Sources/BrowserWindowPortal.swift
3937 Sources/Feed/FeedPanelView.swift
3934 Sources/Feed/FeedPanelView.swift
3926 cmuxTests/TabManagerUnitTests.swift
3896 cmuxTests/WindowAndDragTests.swift
3734 Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift
3699 cmuxTests/CLIGenericHookPersistenceTests.swift
3397 Sources/CmuxConfig.swift
3364 cmuxTests/TabManagerSessionSnapshotTests.swift
3055 Sources/Update/UpdateTitlebarAccessory.swift
2878 Sources/SessionIndexView.swift
3058 Sources/Update/UpdateTitlebarAccessory.swift
2876 cmuxTests/CMUXOpenCommandTests.swift
2875 Sources/SessionIndexView.swift
2608 Sources/KeyboardShortcutSettings.swift
2565 Sources/Panels/CmuxWebView.swift
2546 cmuxTests/WorkspaceManualUnreadTests.swift
@@ -46,31 +46,31 @@
2126 cmuxTests/CmuxConfigTests.swift
2103 Sources/SessionPersistence.swift
2091 cmuxTests/ShortcutAndCommandPaletteTests.swift
1952 Sources/KeyboardShortcutSettingsFileStore.swift
1949 Sources/Panels/BrowserWebAuthnSupport.swift
1945 Sources/RestorableAgentSession.swift
1940 Sources/KeyboardShortcutSettingsFileStore.swift
1900 cmuxTests/NotificationAndMenuBarTests.swift
1810 Sources/SessionIndexStore.swift
1748 Sources/WindowDragHandleView.swift
1732 cmuxTests/WorkspacePullRequestSidebarTests.swift
1687 cmuxTests/MarkdownPanelTests.swift
1680 cmuxUITests/BrowserPaneNavigationKeybindUITests.swift
1656 Sources/FileExplorerView.swift
1652 cmuxTests/CMUXCLIErrorOutputRegressionTests.swift
1637 Sources/FileExplorerView.swift
1581 Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/TerminalInputTextView.swift
1560 cmuxTests/TextBoxMentionCompletionTests.swift
1547 cmuxTests/TerminalControllerSocketSecurityTests.swift
1500 cmuxUITests/MultiWindowNotificationsUITests.swift
1499 cmuxTests/OmnibarAndToolsTests.swift
1445 Sources/FileExplorerStore.swift
1447 Sources/FileExplorerStore.swift
1412 Sources/RemoteTmuxControlConnection.swift
1384 cmuxTests/KeyboardShortcutSettingsFileStoreStartupTests.swift
1380 cmuxUITests/MenuKeyEquivalentRoutingUITests.swift
1373 cmuxTests/AppDelegateIssue2907RoutingTests.swift
1362 Sources/CMUXInstalledExtensionSidebarHostView.swift
1363 Sources/CMUXInstalledExtensionSidebarHostView.swift
1360 Sources/Feed/FeedButtonStyleDebugWindowController.swift
1295 Packages/macOS/CmuxTerminalCore/Sources/CmuxTerminalCore/Config/GhosttyConfig.swift
1295 cmuxTests/MobileHostAuthorizationTests.swift
1292 Packages/macOS/CmuxTerminalCore/Sources/CmuxTerminalCore/Config/GhosttyConfig.swift
1285 cmuxUITests/SidebarHelpMenuUITests.swift
1277 cmuxTests/RestorableAgentSessionIndexTests.swift
1258 Sources/Feed/FeedCoordinator.swift
@@ -81,7 +81,7 @@
1161 cmuxTests/SidebarOrderingTests.swift
1144 cmuxTests/PiVaultAgentPersistenceTests.swift
1121 cmuxTests/AgentHibernationTests.swift
1107 Sources/AppDelegate+CmuxSSHURL.swift
1108 Sources/AppDelegate+CmuxSSHURL.swift
1093 cmuxUITests/BonsplitTabDragUITests.swift
1087 Packages/macOS/CmuxCommandPalette/Sources/CmuxCommandPalette/Search/CommandPaletteFuzzyMatcher.swift
1037 Sources/RemoteTmuxController.swift
@@ -92,12 +92,12 @@
947 Sources/TerminalNotificationPolicy.swift
945 Sources/SessionIndexRegisteredAgents.swift
937 Sources/TextBoxMentionIndexStore.swift
935 Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/AppSection.swift
934 Sources/App/ShortcutRoutingSupport.swift
925 Sources/DockPanelView.swift
926 Sources/DockPanelView.swift
920 Sources/CommandPalette/CommandPaletteSettingsToggle.swift
918 cmuxTests/WorkspaceGroupTests.swift
905 Sources/CmuxSSHURLRequest.swift
901 Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/AppSection.swift
899 Sources/Panels/MarkdownWebRenderer.swift
885 Sources/Panels/TerminalPanel.swift
877 Packages/Shared/CmuxAgentChat/Tests/CmuxAgentChatTests/ChatConversationStoreTests.swift
@@ -118,10 +118,10 @@
768 cmuxUITests/BrowserFixtureInteractionUITests.swift
762 Packages/iOS/CmuxMobileTransport/Sources/CmuxMobileTransport/CmxNetworkByteTransport.swift
760 Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/AgentLaunchSanitizerTests.swift
759 Sources/App/MenuBarExtraController.swift
756 Sources/Panels/AgentSessionWebRendererCoordinator.swift
754 Sources/TerminalController+ControlWorkspaceContext.swift
752 cmuxUITests/CloseWorkspaceCmdDUITests.swift
746 Sources/App/MenuBarExtraController.swift
738 Packages/macOS/CMUXProjectModel/Sources/CMUXProjectModel/XcodeProjectAdapter.swift
736 Packages/Shared/CmuxAuthRuntime/Sources/CmuxAuthRuntime/Coordinator/AuthCoordinator.swift
726 cmuxTests/CLICodexHookTimeoutRegressionTests.swift
@@ -131,10 +131,10 @@
710 Sources/TerminalSSHSessionDetector.swift
707 CLI/CMUXCLI+AgentHookDefinitions.swift
706 CLI/CMUXCLI+Config.swift
705 Sources/Panels/BrowserPopupWindowController.swift
699 cmuxTests/TerminalNotificationClearAllTests.swift
698 cmuxTests/RestorableAgentHookProviderResumeTests.swift
696 cmuxTests/UpdatePillReleaseVisibilityTests.swift
693 Sources/Panels/BrowserPopupWindowController.swift
691 Sources/NotificationSoundSettings.swift
691 cmuxTests/TaskManagerResourcesTests.swift
688 cmuxTests/KeyboardShortcutContextTests.swift
@@ -154,7 +154,7 @@
641 cmuxTests/CommandPaletteNucleoFFITests.swift
636 Sources/TerminalController+ControlPaneContext.swift
630 Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/ShortcutWhenClause.swift
628 Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/KeyboardShortcutsSection.swift
629 Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/KeyboardShortcutsSection.swift
621 cmuxUITests/RightSidebarChromeHeightUITests.swift
620 cmuxTests/FinderFileDropRegressionTests.swift
620 cmuxTests/TerminalNotificationQueueTests.swift
@@ -179,7 +179,7 @@
580 cmuxTests/CLIHookNoResponseTests.swift
578 Sources/RightSidebarPanelView.swift
577 cmuxTests/AppearanceSettingsTests.swift
571 Sources/Feed/FeedTextEditorDebugWindowController.swift
572 Sources/Feed/FeedTextEditorDebugWindowController.swift
568 Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/MobileTerminalRenderGrid.swift
567 Packages/macOS/CmuxTerminalCore/Sources/CmuxTerminalCore/ConfigDiscovery/GhosttyConfigDiscovery.swift
562 Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Transcript/ChatTranscriptTableView.swift
@@ -187,7 +187,7 @@
561 cmuxTests/GhosttyConfigPathResolverTests.swift
560 cmuxTests/CLISSHPTYResizeInputTests.swift
558 Packages/macOS/CmuxGit/Sources/CmuxGit/Parsing/GitMetadataService+Config.swift
551 Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/BrowserSection.swift
552 Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/BrowserSection.swift
549 Sources/Panels/BrowserAutomation.swift
541 Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Pane/ControlCommandCoordinator+Pane.swift
540 Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceReorderCoordinator.swift
@@ -204,7 +204,7 @@
527 CLI/CLISocketPathResolver.swift
526 Packages/macOS/CmuxSettings/Sources/CmuxSettings/SocketControl/SocketControlSettings.swift
524 CLI/CMUXCLI+AutoNaming.swift
522 Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/AutomationSection.swift
523 Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/AutomationSection.swift
520 CLI/CMUXCLI+AmpExtension.swift
520 cmuxTests/MainWindowVisibilityControllerTests.swift
519 Packages/macOS/CmuxSwiftRender/Tests/CmuxSwiftRenderTests/Corpus/stress-two-column-cockpit-sidebar.swift
@@ -224,3 +224,4 @@
503 Sources/Settings/ConfigSource.swift
502 Sources/CmuxEventPublishing.swift
502 Sources/KeyboardShortcutContext.swift
500 Sources/KeyboardShortcutRecorder.swift
1 # cmux-owned Swift file length budget.
2 # Format: max_lines<TAB>relative path
3 # Reduce counts as files shrink. CI fails if tracked files exceed this budget.
4 34336
5 17697 17717
6 16094 16112
7 13950
8 12758
9 12144
10 11865 11924
11 11841 11867
12 9335
13 8017
14 7988
16 7218
17 6317
18 6220
19 6163 6165
20 6072 6084
21 5915
22 5573
23 5566
24 4475 4487
25 4467 4478
26 4401
27 4225
28 3937 3934
29 3926
30 3896
31 3734
32 3699
33 3397
34 3364
35 3055 3058
2878
36 2876
37 2875
38 2608
39 2565
40 2546
46 2126
47 2103
48 2091
49 1952
50 1949
51 1945
1940
52 1900
53 1810
54 1748
55 1732
56 1687
57 1680
58 1656
59 1652
1637
60 1581
61 1560
62 1547
63 1500
64 1499
65 1445 1447
66 1412
67 1384
68 1380
69 1373
70 1362 1363
71 1360
72 1295
73 1295
1292
74 1285
75 1277
76 1258
81 1161
82 1144
83 1121
84 1107 1108
85 1093
86 1087
87 1037
92 947
93 945
94 937
95 935
96 934
97 925 926
98 920
99 918
100 905
901
101 899
102 885
103 877
118 768
119 762
120 760
121 759
122 756
123 754
124 752
746
125 738
126 736
127 726
131 710
132 707
133 706
134 705
135 699
136 698
137 696
693
138 691
139 691
140 688
154 641
155 636
156 630
157 628 629
158 621
159 620
160 620
179 580
180 578
181 577
182 571 572
183 568
184 567
185 562
187 561
188 560
189 558
190 551 552
191 549
192 541
193 540
204 527
205 526
206 524
207 522 523
208 520
209 520
210 519
224 503
225 502
226 502
227 500
@@ -1,3 +1,4 @@
import CmuxFoundation
#if canImport(AppKit)
public import SwiftUI
@@ -20,7 +21,7 @@ public struct AboutTitlebarDebugView: View {
ScrollView {
VStack(alignment: .leading, spacing: 14) {
Text(String(localized: "debug.aboutTitlebarDebug.title", defaultValue: "About Titlebar Debug"))
.font(.headline)
.cmuxFont(.headline)
editor(for: .about)
@@ -56,7 +57,7 @@ public struct AboutTitlebarDebugView: View {
Toggle("Enable Debug Overrides", isOn: overridesEnabled)
Text("When disabled, cmux uses normal default titlebar behavior for this window.")
.font(.caption)
.cmuxFont(.caption)
.foregroundColor(.secondary)
Divider()
@@ -87,7 +88,7 @@ public struct AboutTitlebarDebugView: View {
Divider()
Text("Style Mask")
.font(.caption)
.cmuxFont(.caption)
.foregroundColor(.secondary)
Toggle("Titled", isOn: binding(for: kind, keyPath: \.titled))
@@ -15,12 +15,14 @@ let package = Package(
],
dependencies: [
.package(path: "../CmuxCanvas"),
.package(path: "../CmuxFoundation"),
],
targets: [
.target(
name: "CmuxCanvasUI",
dependencies: [
"CmuxCanvas",
"CmuxFoundation",
],
swiftSettings: [
.swiftLanguageMode(.v6),
@@ -1,3 +1,4 @@
import CmuxFoundation
import SwiftUI
/// The one-time discovery hint that appears, with a soft scale+fade, after a
@@ -14,11 +15,11 @@ struct CanvasCommandScrollHint: View {
HStack(spacing: 8) {
textKeycap("")
Image(systemName: "plus")
.font(.system(size: 9, weight: .bold))
.cmuxFont(size: 9, weight: .bold)
.foregroundStyle(.secondary)
symbolKeycap("arrow.up.and.down.and.arrow.left.and.right")
Text(text)
.font(.system(size: 12, weight: .medium))
.cmuxFont(size: 12, weight: .medium)
.foregroundStyle(.primary)
.fixedSize()
}
@@ -41,14 +42,14 @@ struct CanvasCommandScrollHint: View {
private func textKeycap(_ text: String) -> some View {
keycapContainer {
Text(text)
.font(.system(size: 12, weight: .semibold))
.cmuxFont(size: 12, weight: .semibold)
}
}
private func symbolKeycap(_ systemName: String) -> some View {
keycapContainer {
Image(systemName: systemName)
.font(.system(size: 12, weight: .semibold))
.cmuxFont(size: 12, weight: .semibold)
}
}
@@ -1,3 +1,4 @@
import CmuxFoundation
import SwiftUI
/// Hit rects for one rendered tab, in the tab bar's local coordinates.
@@ -102,7 +103,7 @@ private struct CanvasPaneTabItem: View {
HStack(spacing: 6) {
iconOrClose
Text(tab.title)
.font(.system(size: 11))
.cmuxFont(size: 11)
.foregroundStyle(textColor)
.lineLimit(1)
.truncationMode(.middle)
@@ -131,7 +132,7 @@ private struct CanvasPaneTabItem: View {
ZStack {
if isHovered {
Image(systemName: "xmark")
.font(.system(size: 9, weight: .bold))
.cmuxFont(size: 9, weight: .bold)
.foregroundStyle(Color(nsColor: .labelColor))
.frame(width: 16, height: 16)
.background(
@@ -146,7 +147,7 @@ private struct CanvasPaneTabItem: View {
)
} else if let iconSystemName = tab.iconSystemName {
Image(systemName: iconSystemName)
.font(.system(size: 11, weight: .medium))
.cmuxFont(size: 11, weight: .medium)
.foregroundStyle(textColor)
}
}
@@ -14,9 +14,15 @@ let package = Package(
targets: ["CmuxFeedback"]
),
],
dependencies: [
.package(path: "../CmuxFoundation"),
],
targets: [
.target(
name: "CmuxFeedback",
dependencies: [
"CmuxFoundation",
],
resources: [
// Folded from CmuxFeedbackUI: the composer's localized strings.
.process("ComposerUI/Resources"),
@@ -1,19 +1,23 @@
public import AppKit
import CmuxFoundation
/// A self-sizing, scrollable multiline message editor used by the feedback
/// composer. Grows its document height with content (with an overlay scroller
/// once it exceeds the visible area) and shows a placeholder while empty.
public final class FeedbackComposerMessageEditorView: NSView {
private static let font = NSFont.systemFont(ofSize: 12)
private static var font: NSFont {
GlobalFontMagnification.systemFont(ofSize: 12)
}
private static let textInset = NSSize(width: 10, height: 10)
private static let minimumDocumentHeight: CGFloat = {
private static var minimumDocumentHeight: CGFloat {
let lineHeight = ceil(font.ascender - font.descender + font.leading)
return lineHeight + textInset.height * 2
}()
}
public let scrollView = FeedbackComposerMessageScrollView()
public let textView = NSTextView()
private let placeholderField = FeedbackComposerPassthroughLabel(labelWithString: "")
private var fontMagnificationObserver: GlobalFontMagnificationChangeObserver?
public var placeholder: String = "" {
didSet {
@@ -51,7 +55,7 @@ public final class FeedbackComposerMessageEditorView: NSView {
textView.autoresizingMask = [.width]
textView.backgroundColor = .clear
textView.drawsBackground = false
textView.font = Self.font
applyFonts()
textView.textColor = .labelColor
textView.insertionPointColor = .labelColor
textView.textContainerInset = Self.textInset
@@ -69,7 +73,6 @@ public final class FeedbackComposerMessageEditorView: NSView {
addSubview(scrollView)
placeholderField.translatesAutoresizingMaskIntoConstraints = false
placeholderField.font = Self.font
placeholderField.textColor = .secondaryLabelColor
placeholderField.lineBreakMode = .byWordWrapping
placeholderField.maximumNumberOfLines = 0
@@ -81,6 +84,10 @@ public final class FeedbackComposerMessageEditorView: NSView {
name: NSText.didChangeNotification,
object: textView
)
fontMagnificationObserver = GlobalFontMagnificationChangeObserver { [weak self] in
self?.applyFonts()
self?.refreshTextLayout()
}
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: topAnchor),
@@ -127,6 +134,13 @@ public final class FeedbackComposerMessageEditorView: NSView {
placeholderField.isHidden = textView.string.isEmpty == false
}
private func applyFonts() {
let font = Self.font
textView.font = font
placeholderField.font = font
textView.minSize = NSSize(width: 0, height: Self.minimumDocumentHeight)
}
public func refreshTextLayout(scrollSelection: Bool = false) {
updatePlaceholderVisibility()
needsLayout = true
@@ -1,3 +1,4 @@
import CmuxFoundation
public import SwiftUI
import AppKit
@@ -39,7 +40,7 @@ public struct SidebarFeedbackComposerSheet: View {
public var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text(String(localized: "sidebar.help.feedback.title", defaultValue: "Send Feedback", bundle: .module))
.font(.title3.weight(.semibold))
.cmuxFont(.title3, weight: .semibold)
if didSend {
successView
@@ -60,7 +61,7 @@ public struct SidebarFeedbackComposerSheet: View {
private var successView: some View {
VStack(alignment: .leading, spacing: 12) {
Text(String(localized: "sidebar.help.feedback.successTitle", defaultValue: "Thanks for the feedback.", bundle: .module))
.font(.headline)
.cmuxFont(.headline)
Text(
String(
localized: "sidebar.help.feedback.successBody",
@@ -68,7 +69,7 @@ public struct SidebarFeedbackComposerSheet: View {
bundle: .module
)
)
.font(.system(size: 12))
.cmuxFont(size: 12)
.foregroundStyle(.secondary)
.textSelection(.enabled)
@@ -91,12 +92,12 @@ public struct SidebarFeedbackComposerSheet: View {
bundle: .module
)
)
.font(.system(size: 12))
.cmuxFont(size: 12)
.foregroundStyle(.secondary)
VStack(alignment: .leading, spacing: 6) {
Text(String(localized: "sidebar.help.feedback.email", defaultValue: "Your Email", bundle: .module))
.font(.system(size: 12, weight: .medium))
.cmuxFont(size: 12, weight: .medium)
TextField(
String(localized: "sidebar.help.feedback.emailPlaceholder", defaultValue: "[email protected]", bundle: .module),
text: $email
@@ -109,10 +110,10 @@ public struct SidebarFeedbackComposerSheet: View {
VStack(alignment: .leading, spacing: 6) {
HStack(alignment: .firstTextBaseline) {
Text(String(localized: "sidebar.help.feedback.message", defaultValue: "Message", bundle: .module))
.font(.system(size: 12, weight: .medium))
.cmuxFont(size: 12, weight: .medium)
Spacer(minLength: 0)
Text("\(message.count)/\(Self.settings.maxMessageLength)")
.font(.system(size: 11))
.cmuxFont(size: 11)
.foregroundStyle(
message.count > Self.settings.maxMessageLength
? Color.red
@@ -152,7 +153,7 @@ public struct SidebarFeedbackComposerSheet: View {
bundle: .module
)
)
.font(.system(size: 11))
.cmuxFont(size: 11)
.foregroundStyle(.secondary)
}
@@ -163,12 +164,12 @@ public struct SidebarFeedbackComposerSheet: View {
Image(systemName: "photo")
.foregroundStyle(.secondary)
Text(attachment.fileName)
.font(.system(size: 12))
.cmuxFont(size: 12)
.lineLimit(1)
.truncationMode(.middle)
Spacer(minLength: 0)
Text(attachment.displaySize)
.font(.system(size: 11))
.cmuxFont(size: 11)
.foregroundStyle(.secondary)
Button(
String(localized: "sidebar.help.feedback.removeAttachment", defaultValue: "Remove", bundle: .module)
@@ -189,7 +190,7 @@ public struct SidebarFeedbackComposerSheet: View {
if let submissionErrorMessage, submissionErrorMessage.isEmpty == false {
Text(submissionErrorMessage)
.font(.system(size: 12))
.cmuxFont(size: 12)
.foregroundStyle(.red)
}
@@ -0,0 +1,10 @@
public import SwiftUI
/// Injects the stored cmux font magnification percent into a SwiftUI subtree.
struct CmuxFontMagnificationEnvironmentModifier: ViewModifier {
@AppStorage(GlobalFontMagnification.percentKey) private var percent = GlobalFontMagnification.defaultPercent
func body(content: Content) -> some View {
content.environment(\.cmuxGlobalFontMagnificationPercent, percent)
}
}
@@ -0,0 +1,25 @@
import SwiftUI
struct CmuxFontModifier: ViewModifier {
@Environment(\.cmuxGlobalFontMagnificationPercent) private var percent
let baseSize: CGFloat
let weight: Font.Weight
let design: Font.Design
var monospacedDigit: Bool = false
func body(content: Content) -> some View {
content.font(resolvedFont)
}
private var resolvedFont: Font {
var font = Font.system(size: scaledSize, weight: weight, design: design)
if monospacedDigit {
font = font.monospacedDigit()
}
return font
}
private var scaledSize: CGFloat {
GlobalFontMagnification.scaledSize(baseSize, percent: percent)
}
}
@@ -0,0 +1,6 @@
import SwiftUI
/// Stores the current global font magnification percent in the SwiftUI environment.
struct CmuxGlobalFontMagnificationPercentKey: EnvironmentKey {
static var defaultValue: Int { GlobalFontMagnification.storedPercent }
}
@@ -0,0 +1,29 @@
import SwiftUI
struct CmuxTextStyleMetrics {
let style: Font.TextStyle
var baseSize: CGFloat {
switch style {
case .largeTitle: return 26
case .title: return 22
case .title2: return 17
case .title3: return 15
case .headline: return 13
case .subheadline: return 11
case .body: return 13
case .callout: return 12
case .footnote: return 10
case .caption: return 10
case .caption2: return 9
@unknown default: return 13
}
}
var baseWeight: Font.Weight {
switch style {
case .headline: return .semibold
default: return .regular
}
}
}
@@ -0,0 +1,15 @@
public import SwiftUI
/// Adds cmux font magnification values to SwiftUI environment lookups.
public extension EnvironmentValues {
/// The current clamped global font magnification percent.
///
/// cmux scene roots should inject this value with
/// ``View/cmuxFontMagnificationEnvironment()`` so repeated row labels can
/// read a pure environment value instead of each subscribing to
/// `UserDefaults`.
var cmuxGlobalFontMagnificationPercent: Int {
get { self[CmuxGlobalFontMagnificationPercentKey.self] }
set { self[CmuxGlobalFontMagnificationPercentKey.self] = GlobalFontMagnification.clamp(newValue) }
}
}
@@ -0,0 +1,254 @@
public import AppKit
public import Foundation
/// App-wide magnification for cmux-owned chrome and terminal configuration.
///
/// Stored as an integer percent (100 = default, 150 = 1.5x, 200 = 2x).
/// SwiftUI call sites should use ``View/cmuxFont(size:weight:design:monospacedDigit:)``
/// or ``View/cmuxFont(_:weight:design:)``. AppKit call sites should use the
/// `GlobalFontMagnification` font helpers and reapply them from
/// ``didChangeNotification`` via ``GlobalFontMagnificationChangeObserver``.
public struct GlobalFontMagnification {
private let userDefaults: UserDefaults
private let notificationCenter: NotificationCenter
/// Creates a font magnification helper backed by injectable storage.
///
/// - Parameters:
/// - userDefaults: The defaults domain used for persisted magnification.
/// - notificationCenter: The center that receives live-update notifications.
public init(userDefaults: UserDefaults = .standard, notificationCenter: NotificationCenter = .default) {
self.userDefaults = userDefaults
self.notificationCenter = notificationCenter
}
/// UserDefaults key storing the global font magnification percent.
public static let percentKey = "globalFontMagnificationPercent"
/// Default magnification percent.
public static let defaultPercent: Int = 100
/// Minimum supported magnification percent.
public static let minimumPercent: Int = 50
/// Capped at 200% so cmux fixed-size chrome does not clip or overflow.
public static let maximumPercent: Int = 200
/// Percent increment used by settings UI and schema validation.
public static let stepPercent: Int = 10
/// Notification posted after the global font magnification percent changes.
public static let didChangeNotification = Notification.Name("cmux.globalFontMagnification.didChange")
/// Raw percent stored in UserDefaults. If the key is unset, treat as 100%.
/// Accepts numeric storage and string-encoded integers so values written
/// from `defaults write` resolve cleanly.
public var storedPercent: Int {
let raw = userDefaults.object(forKey: Self.percentKey)
let resolved: Int
if let number = raw as? NSNumber {
resolved = number.intValue
} else if let string = raw as? String, let parsed = Int(string) {
resolved = parsed
} else {
resolved = Self.defaultPercent
}
return Self.clamp(resolved)
}
/// Multiplier (1.0 for 100%, 1.5 for 150%, etc.).
public var scale: CGFloat {
CGFloat(storedPercent) / CGFloat(Self.defaultPercent)
}
/// Whether the current stored percent is the default magnification.
public var isDefault: Bool { storedPercent == Self.defaultPercent }
/// Scale a design-time point size by the current magnification.
public func scaled(_ base: CGFloat) -> CGFloat {
max(1, base * scale)
}
/// Scales a design-time point size by the current magnification.
///
/// - Parameter baseSize: The unscaled design point size.
/// - Returns: A point size clamped to at least 1 point.
public func scaledSize(_ baseSize: CGFloat) -> CGFloat {
scaled(baseSize)
}
/// Creates a magnified AppKit system font.
///
/// - Parameters:
/// - baseSize: The unscaled design point size.
/// - weight: The AppKit system font weight.
/// - Returns: A system font at the magnified point size.
public func systemFont(ofSize baseSize: CGFloat, weight: NSFont.Weight = .regular) -> NSFont {
NSFont.systemFont(ofSize: scaledSize(baseSize), weight: weight)
}
/// Creates a magnified AppKit monospaced system font.
///
/// - Parameters:
/// - baseSize: The unscaled design point size.
/// - weight: The AppKit system font weight.
/// - Returns: A monospaced system font at the magnified point size.
public func monospacedSystemFont(ofSize baseSize: CGFloat, weight: NSFont.Weight = .regular) -> NSFont {
NSFont.monospacedSystemFont(ofSize: scaledSize(baseSize), weight: weight)
}
/// Creates a magnified AppKit system font with monospaced digits.
///
/// - Parameters:
/// - baseSize: The unscaled design point size.
/// - weight: The AppKit system font weight.
/// - Returns: A monospaced-digit system font at the magnified point size.
public func monospacedDigitSystemFont(ofSize baseSize: CGFloat, weight: NSFont.Weight = .regular) -> NSFont {
NSFont.monospacedDigitSystemFont(ofSize: scaledSize(baseSize), weight: weight)
}
/// Creates a magnified AppKit menu font.
///
/// - Parameter baseSize: The unscaled design point size.
/// - Returns: A menu font at the magnified point size.
public func menuFont(ofSize baseSize: CGFloat = NSFont.systemFontSize) -> NSFont {
NSFont.menuFont(ofSize: scaledSize(baseSize))
}
/// Creates a magnified named AppKit font.
///
/// - Parameters:
/// - name: The AppKit font name.
/// - baseSize: The unscaled design point size.
/// - Returns: A named font at the magnified point size, or `nil` if unavailable.
public func font(name: String, size baseSize: CGFloat) -> NSFont? {
NSFont(name: name, size: scaledSize(baseSize))
}
/// Normalizes a requested magnification to the supported range and step.
public static func clamp(_ percent: Int) -> Int {
let bounded = Swift.max(minimumPercent, Swift.min(maximumPercent, percent))
let stepped = Int((Double(bounded) / Double(stepPercent)).rounded()) * stepPercent
return Swift.max(minimumPercent, Swift.min(maximumPercent, stepped))
}
/// Stores a new percent and posts the live-update notification.
///
/// - Parameter percent: The requested percent; values outside the supported
/// range are clamped before storage.
public func setPercent(_ percent: Int) {
userDefaults.set(Self.clamp(percent), forKey: Self.percentKey)
notificationCenter.post(name: Self.didChangeNotification, object: nil)
}
/// Restores the default magnification and posts the live-update notification.
public func resetToDefault() {
userDefaults.set(Self.defaultPercent, forKey: Self.percentKey)
notificationCenter.post(name: Self.didChangeNotification, object: nil)
}
/// Raw percent stored in `UserDefaults.standard`.
public static var storedPercent: Int {
Self().storedPercent
}
/// Multiplier for the percent stored in `UserDefaults.standard`.
public static var scale: CGFloat {
Self().scale
}
/// Whether `UserDefaults.standard` stores the default magnification.
public static var isDefault: Bool {
Self().isDefault
}
/// Scale a design-time point size by the standard stored magnification.
public static func scaled(_ base: CGFloat) -> CGFloat {
Self().scaled(base)
}
/// Returns the multiplier for a magnification percent.
///
/// - Parameter percent: The requested percent; values outside the supported
/// range are clamped before the multiplier is calculated.
public static func scale(for percent: Int) -> CGFloat {
CGFloat(clamp(percent)) / CGFloat(defaultPercent)
}
/// Scales a design-time point size by a specific magnification percent.
///
/// - Parameters:
/// - baseSize: The unscaled design point size.
/// - percent: The requested percent; values outside the supported range
/// are clamped before scaling.
/// - Returns: A point size clamped to at least 1 point.
public static func scaledSize(_ baseSize: CGFloat, percent: Int) -> CGFloat {
max(1, baseSize * scale(for: percent))
}
/// Scales a design-time point size by the standard stored magnification.
///
/// - Parameter baseSize: The unscaled design point size.
/// - Returns: A point size clamped to at least 1 point.
public static func scaledSize(_ baseSize: CGFloat) -> CGFloat {
Self().scaledSize(baseSize)
}
/// Creates a system font using the standard stored magnification.
///
/// - Parameters:
/// - baseSize: The unscaled design point size.
/// - weight: The AppKit system font weight.
/// - Returns: A system font at the magnified point size.
public static func systemFont(ofSize baseSize: CGFloat, weight: NSFont.Weight = .regular) -> NSFont {
Self().systemFont(ofSize: baseSize, weight: weight)
}
/// Creates a monospaced system font using the standard stored magnification.
///
/// - Parameters:
/// - baseSize: The unscaled design point size.
/// - weight: The AppKit system font weight.
/// - Returns: A monospaced system font at the magnified point size.
public static func monospacedSystemFont(ofSize baseSize: CGFloat, weight: NSFont.Weight = .regular) -> NSFont {
Self().monospacedSystemFont(ofSize: baseSize, weight: weight)
}
/// Creates a monospaced-digit system font using the standard stored magnification.
///
/// - Parameters:
/// - baseSize: The unscaled design point size.
/// - weight: The AppKit system font weight.
/// - Returns: A monospaced-digit system font at the magnified point size.
public static func monospacedDigitSystemFont(ofSize baseSize: CGFloat, weight: NSFont.Weight = .regular) -> NSFont {
Self().monospacedDigitSystemFont(ofSize: baseSize, weight: weight)
}
/// Creates a menu font using the standard stored magnification.
///
/// - Parameter baseSize: The unscaled design point size.
/// - Returns: A menu font at the magnified point size.
public static func menuFont(ofSize baseSize: CGFloat = NSFont.systemFontSize) -> NSFont {
Self().menuFont(ofSize: baseSize)
}
/// Creates a named font using the standard stored magnification.
///
/// - Parameters:
/// - name: The AppKit font name.
/// - baseSize: The unscaled design point size.
/// - Returns: A named font at the magnified point size, or `nil` if unavailable.
public static func font(name: String, size baseSize: CGFloat) -> NSFont? {
Self().font(name: name, size: baseSize)
}
/// Stores a new standard percent and posts the live-update notification.
///
/// - Parameter percent: The requested percent; values outside the supported
/// range are clamped before storage.
public static func setPercent(_ percent: Int) {
Self().setPercent(percent)
}
/// Restores the standard stored magnification and posts the live-update notification.
public static func resetToDefault() {
Self().resetToDefault()
}
}
@@ -0,0 +1,37 @@
public import Foundation
/// Observes global font magnification changes for AppKit-backed views.
///
/// Retain this object for as long as the observed view or controller needs
/// live font updates. The handler is delivered on the main actor after
/// ``GlobalFontMagnification/didChangeNotification`` posts, and observation is
/// automatically removed when the observer deinitializes.
public final class GlobalFontMagnificationChangeObserver {
private let notificationCenter: NotificationCenter
private var notificationObserver: (any NSObjectProtocol)?
/// Creates an observer that invokes `handler` when the global percent changes.
///
/// - Parameters:
/// - notificationCenter: The notification center that posts
/// ``GlobalFontMagnification/didChangeNotification``.
/// - handler: Main-actor work that reapplies derived fonts or layout.
public init(notificationCenter: NotificationCenter = .default, handler: @MainActor @escaping () -> Void) {
self.notificationCenter = notificationCenter
notificationObserver = notificationCenter.addObserver(
forName: GlobalFontMagnification.didChangeNotification,
object: nil,
queue: .main
) { _ in
MainActor.assumeIsolated {
handler()
}
}
}
deinit {
if let notificationObserver {
notificationCenter.removeObserver(notificationObserver)
}
}
}
@@ -9,9 +9,6 @@ public import AppKit
/// faithful minimal guard (no actor needed; callers are synchronous layout
/// code). Instances are interchangeable; they all share the same memo.
public struct SidebarWorkspaceShortcutHintMetrics {
// Immutable measurement font; NSFont is not Sendable but this constant is
// never mutated and is only read under `lock` during measurement.
nonisolated(unsafe) private static let measurementFont = NSFont.systemFont(ofSize: 10, weight: .semibold)
private static let minimumSlotWidth: CGFloat = 28
private static let horizontalPadding: CGFloat = 12
// Pure layout memo guarded by a lock; see type doc for the lock rationale.
@@ -33,18 +30,22 @@ public struct SidebarWorkspaceShortcutHintMetrics {
/// Cached rendered width of a hint `label`.
public func hintWidth(for label: String) -> CGFloat {
let percent = GlobalFontMagnification.storedPercent
let cacheKey = "\(percent)\u{0}\(label)"
Self.lock.lock()
if let cached = Self.cachedHintWidths[label] {
if let cached = Self.cachedHintWidths[cacheKey] {
Self.lock.unlock()
return cached
}
Self.lock.unlock()
let textWidth = (label as NSString).size(withAttributes: [.font: Self.measurementFont]).width
let pointSize = max(1, 10 * CGFloat(percent) / CGFloat(GlobalFontMagnification.defaultPercent))
let measurementFont = NSFont.systemFont(ofSize: pointSize, weight: .semibold)
let textWidth = (label as NSString).size(withAttributes: [.font: measurementFont]).width
let measuredWidth = ceil(textWidth) + Self.horizontalPadding
Self.lock.lock()
Self.cachedHintWidths[label] = measuredWidth
Self.cachedHintWidths[cacheKey] = measuredWidth
#if DEBUG
Self.measurementCount += 1
#endif
@@ -0,0 +1,62 @@
public import SwiftUI
/// Adds cmux-owned font scaling modifiers to SwiftUI views.
public extension View {
/// Injects the global font magnification percent into this view subtree.
///
/// Apply this once near each cmux-owned SwiftUI root. Descendant
/// ``cmuxFont(size:weight:design:monospacedDigit:)`` calls then read the
/// environment value without creating per-label `UserDefaults`
/// subscriptions.
///
/// - Returns: A view that supplies the current magnification percent to descendants.
func cmuxFontMagnificationEnvironment() -> some View {
modifier(CmuxFontMagnificationEnvironmentModifier())
}
/// Apply a system font at `size` points, scaled by the global magnification.
///
/// - Parameters:
/// - size: The unscaled base point size.
/// - weight: The system font weight to apply.
/// - design: The system font design to apply.
/// - monospacedDigit: Whether numeric glyphs should use tabular widths.
/// - Returns: A view with a magnification-aware system font.
func cmuxFont(
size: CGFloat,
weight: Font.Weight = .regular,
design: Font.Design = .default,
monospacedDigit: Bool = false
) -> some View {
modifier(
CmuxFontModifier(
baseSize: size,
weight: weight,
design: design,
monospacedDigit: monospacedDigit
)
)
}
/// Apply a text-style-sized system font, scaled by the global magnification.
///
/// - Parameters:
/// - style: The SwiftUI text style whose cmux base metrics should be used.
/// - weight: An optional weight override. When `nil`, cmux uses the style's default weight.
/// - design: The system font design to apply.
/// - monospacedDigit: Whether numeric glyphs should use tabular widths.
/// - Returns: A view with a magnification-aware font for the requested text style.
func cmuxFont(
_ style: Font.TextStyle,
weight: Font.Weight? = nil,
design: Font.Design = .default,
monospacedDigit: Bool = false
) -> some View {
cmuxFont(
size: CmuxTextStyleMetrics(style: style).baseSize,
weight: weight ?? CmuxTextStyleMetrics(style: style).baseWeight,
design: design,
monospacedDigit: monospacedDigit
)
}
}
@@ -1,3 +1,4 @@
import CmuxFoundation
import Foundation
/// Settings under the dotted-id prefix `app.*` user-facing app behavior.
@@ -91,6 +92,13 @@ public struct AppCatalogSection: SettingCatalogSection {
userDefaultsKey: "openMarkdownInCmuxViewer"
)
/// App-wide magnification percent for cmux-owned fonts and chrome.
public let globalFontMagnification = DefaultsKey<Int>(
id: "app.globalFontMagnification",
defaultValue: GlobalFontMagnification.defaultPercent,
userDefaultsKey: GlobalFontMagnification.percentKey
)
public let iMessageMode = DefaultsKey<Bool>(
id: "app.iMessageMode",
defaultValue: false,
@@ -14,12 +14,14 @@ let package = Package(
),
],
dependencies: [
.package(path: "../CmuxFoundation"),
.package(path: "../CmuxSettings"),
],
targets: [
.target(
name: "CmuxSettingsUI",
dependencies: [
.product(name: "CmuxFoundation", package: "CmuxFoundation"),
.product(name: "CmuxSettings", package: "CmuxSettings"),
]
),
@@ -1,3 +1,4 @@
import CmuxFoundation
import CmuxSettings
import SwiftUI
@@ -21,10 +22,10 @@ struct AccountIdentityCard: View {
HStack(alignment: .center, spacing: 12) {
VStack(alignment: .leading, spacing: 2) {
Text(titleText)
.font(.system(size: 13, weight: .medium))
.cmuxFont(size: 13, weight: .medium)
if let subtitle = subtitleText {
Text(subtitle)
.font(.system(size: 11))
.cmuxFont(size: 11)
.foregroundColor(.secondary)
}
}
@@ -60,7 +61,7 @@ struct AccountIdentityCard: View {
localized: "settings.account.signIn.slowHint",
defaultValue: "The system sign-in window may stop responding. If nothing happens, open sign-in in your default browser instead."
))
.font(.system(size: 11))
.cmuxFont(size: 11)
.foregroundColor(.secondary)
.fixedSize(horizontal: false, vertical: true)
Spacer(minLength: 8)
@@ -18,7 +18,9 @@ import Observation
/// replaying every intermediate back through ``current``.
/// 2. SwiftUI views read ``current`` synchronously and write via ``set(_:)``.
/// 3. ``set(_:)`` updates ``current`` optimistically (immediate UI) and
/// persists the write in a fire-and-forget `Task`.
/// persists the write in a fire-and-forget `Task`. ``set(_:afterCommit:)``
/// uses the same store path, then runs a main-actor side effect after the
/// async write has committed.
///
/// Lifecycle: the observation is owned by a ``SettingReadDriver`` held by the
/// model. When the model deallocates, the driver's `deinit` cancels the
@@ -104,6 +106,24 @@ public final class DefaultsValueModel<Value: SettingCodable> {
}
}
/// Persists the value, then runs `afterCommit` after storage accepts it.
///
/// Use this when a setting has host-side live-update work that must observe
/// the committed defaults value. The write still goes through the injected
/// ``UserDefaultsSettingsStore`` instead of assuming `UserDefaults.standard`.
///
/// - Parameters:
/// - value: The new value to persist.
/// - afterCommit: Main-actor work to run after ``UserDefaultsSettingsStore``
/// has completed the write.
public func set(_ value: Value, afterCommit: @escaping @MainActor @Sendable () -> Void) {
current = value
Task { [store, key, afterCommit] in
await store.set(value, for: key)
await afterCommit()
}
}
/// Updates ``current`` after another owner has already persisted `value`.
///
/// Use this for settings whose committed write spans multiple backing keys
@@ -1,3 +1,4 @@
import CmuxFoundation
import SwiftUI
/// Small caption-style note rendered inside a ``SettingsCard``
@@ -13,7 +14,7 @@ public struct SettingsCardNote: View {
public var body: some View {
Text(text)
.font(.caption)
.cmuxFont(.caption)
.foregroundColor(.secondary)
.padding(.horizontal, 14)
.padding(.vertical, 8)
@@ -1,3 +1,4 @@
import CmuxFoundation
import SwiftUI
/// One row inside a ``SettingsCard``: a left-aligned title (and
@@ -61,10 +62,10 @@ public struct SettingsCardRow<Trailing: View>: View {
HStack(alignment: .center, spacing: 12) {
VStack(alignment: .leading, spacing: subtitle == nil ? 0 : 3) {
Text(title)
.font(.system(size: 13, weight: .medium))
.cmuxFont(size: 13, weight: .medium)
if let subtitle {
Text(subtitle)
.font(.caption)
.cmuxFont(.caption)
.foregroundColor(.secondary)
.lineLimit(2)
}
@@ -1,3 +1,4 @@
import CmuxFoundation
import SwiftUI
/// Section header rendered above a ``SettingsCard``.
@@ -23,7 +24,7 @@ public struct SettingsSectionHeader: View {
public var body: some View {
Text(title)
.font(.system(size: 13, weight: .semibold))
.cmuxFont(size: 13, weight: .semibold)
.foregroundColor(.secondary)
.padding(.leading, 2)
.padding(.bottom, -2)
@@ -42,6 +42,7 @@ extension Array where Element == CuratedSettingEntry {
.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: "file-editor-word-wrap", title: "File Editor Word Wrap", synonyms: "fileEditor.wordWrap file editor word wrap soft wrap reflow lines text horizontal scroll preview"),
.init(section: .app, id: "terminal-config", title: "Terminal Config", synonyms: "ghostty config merged generated preview terminal configuration window open config macos-option-as-alt option as alt left option right option alt key meta"),
.init(section: .app, id: "global-font-magnification", title: String(localized: "settings.app.globalFontMagnification", defaultValue: "Global Font Magnification"), synonyms: "app.globalFontMagnification global font magnification scale text zoom terminals tabs chrome bigger smaller accessibility"),
.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"),
.init(section: .app, id: "menu-bar-only", title: "Menu Bar Only", synonyms: "app.menuBarOnly menubar menu bar dockless hide dock app switcher cmd-tab command-tab"),
@@ -1,3 +1,4 @@
import CmuxFoundation
import SwiftUI
/// Sidebar row used inside the settings window's `List`.
@@ -25,7 +26,7 @@ struct SettingsSidebarEntryRow: View {
if let subtitle {
Text(subtitle)
.font(.caption)
.cmuxFont(.caption)
.foregroundStyle(.secondary)
.lineLimit(1)
}
@@ -1,3 +1,4 @@
import CmuxFoundation
import CmuxSettings
import SwiftUI
@@ -23,9 +24,9 @@ struct AppIconPickerRow: View {
HStack(alignment: .top, spacing: 12) {
VStack(alignment: .leading, spacing: 3) {
Text(String(localized: "settings.app.appIcon", defaultValue: "App Icon"))
.font(.system(size: 13, weight: .medium))
.cmuxFont(size: 13, weight: .medium)
Text(String(localized: "settings.app.appIcon.subtitle", defaultValue: "Dock and app switcher"))
.font(.caption)
.cmuxFont(.caption)
.foregroundColor(.secondary)
}
.frame(maxWidth: .infinity, alignment: .leading)
@@ -64,7 +65,7 @@ struct AppIconPickerRow: View {
}
Text(iconDisplayName(mode))
.font(.system(size: 10))
.cmuxFont(size: 10)
.foregroundColor(isSelected ? .primary : .secondary)
}
.padding(.vertical, 8)
@@ -1,3 +1,4 @@
import CmuxFoundation
import CmuxSettings
import SwiftUI
@@ -42,7 +43,7 @@ public struct SettingsDefaultsTextFieldRow: View {
}
if let subtitle {
Text(subtitle)
.font(.caption)
.cmuxFont(.caption)
.foregroundStyle(.secondary)
}
}
@@ -1,3 +1,4 @@
import CmuxFoundation
import CmuxSettings
import SwiftUI
@@ -40,7 +41,7 @@ public struct SettingsToggleRow: View {
Text(title)
if let subtitle {
Text(subtitle)
.font(.caption)
.cmuxFont(.caption)
.foregroundStyle(.secondary)
}
}
@@ -1,4 +1,5 @@
import AppKit
import CmuxFoundation
import CmuxSettings
import SwiftUI
@@ -184,6 +185,7 @@ public final class RecorderHostButton: NSButton {
// token is set/cleared only on the main thread (this is a main-thread
// AppKit view), so reading it from the nonisolated deinit is safe.
private nonisolated(unsafe) var eventMonitor: Any?
private var fontMagnificationObserver: GlobalFontMagnificationChangeObserver?
public override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
@@ -223,11 +225,20 @@ public final class RecorderHostButton: NSButton {
// package recorder renders byte-for-byte like legacy regardless
// of the surrounding SwiftUI environment.
controlSize = .regular
font = NSFont.systemFont(ofSize: NSFont.systemFontSize(for: .regular))
applyFont()
if fontMagnificationObserver == nil {
fontMagnificationObserver = GlobalFontMagnificationChangeObserver { [weak self] in
self?.applyFont()
}
}
target = self
action = #selector(buttonClicked)
}
private func applyFont() {
font = GlobalFontMagnification.systemFont(ofSize: NSFont.systemFontSize(for: .regular))
}
public override var acceptsFirstResponder: Bool { true }
public override func becomeFirstResponder() -> Bool {
@@ -1,3 +1,4 @@
import CmuxFoundation
import CmuxSettings
import SwiftUI
@@ -19,7 +20,7 @@ struct ThemePickerRow: View {
var body: some View {
HStack(alignment: .top, spacing: 12) {
Text(String(localized: "settings.app.theme", defaultValue: "Theme"))
.font(.system(size: 13, weight: .medium))
.cmuxFont(size: 13, weight: .medium)
.frame(maxWidth: .infinity, alignment: .leading)
HStack(spacing: 8) {
@@ -62,7 +63,7 @@ struct ThemePickerRow: View {
.frame(width: thumbWidth, height: thumbHeight)
Text(themeDisplayName(mode))
.font(.system(size: 10))
.cmuxFont(size: 10)
.fontWeight(isSelected ? .semibold : .regular)
.foregroundColor(isSelected ? .primary : .secondary)
}
@@ -1,3 +1,4 @@
import CmuxFoundation
import SwiftUI
/// Tiny stylized desktop+windows thumbnail used by ``ThemePickerRow``.
@@ -50,7 +51,7 @@ struct ThemeWindowThumbnail: View {
VStack(spacing: 0) {
HStack {
Image(systemName: "applelogo")
.font(.system(size: max(height * 0.08, 6)))
.cmuxFont(size: max(height * 0.08, 6))
.foregroundColor(isDark ? .white : .black)
.opacity(0.8)
Spacer()
@@ -1,4 +1,5 @@
import AppKit
import CmuxFoundation
import CmuxSettings
import SwiftUI
import UniformTypeIdentifiers
@@ -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 globalFontMagnification: DefaultsValueModel<Int>
@State private var markdownFontSize: DefaultsValueModel<Int>
@State private var markdownFontFamily: DefaultsValueModel<String>
@State private var markdownMaxWidth: DefaultsValueModel<Int>
@@ -81,6 +83,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))
_globalFontMagnification = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.app.globalFontMagnification))
_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))
@@ -126,11 +129,29 @@ public struct AppSection: View {
mainCard
}
.task {
startSettingsObservation([language, appearance, appIcon, placement, inheritDir, minimalMode, keepWorkspaceOpen, firstClick, fileDrop, preferredEditor, openSupported, openMarkdown, markdownFontSize, markdownFontFamily, markdownMaxWidth, canvasPaneGap, canvasSnapping, fileEditorWordWrap, iMessage, reorder, dockBadge, menuBarOnly, showInMenuBar, paneRing, paneFlash, soundName, soundCommand, customSoundFile, telemetry, confirmQuit, warnCloseTab, warnCloseX, hideCloseButton, renameSelects, paletteAllSurfaces])
startSettingsObservation([language, appearance, appIcon, placement, inheritDir, minimalMode, keepWorkspaceOpen, firstClick, fileDrop, preferredEditor, openSupported, openMarkdown, globalFontMagnification, markdownFontSize, markdownFontFamily, markdownMaxWidth, canvasPaneGap, canvasSnapping, fileEditorWordWrap, iMessage, reorder, dockBadge, menuBarOnly, showInMenuBar, paneRing, paneFlash, soundName, soundCommand, customSoundFile, telemetry, confirmQuit, warnCloseTab, warnCloseX, hideCloseButton, renameSelects, paletteAllSurfaces])
if languageAtAppear == nil { languageAtAppear = language.current }; if telemetryAtAppear == nil { telemetryAtAppear = telemetry.current }
}
}
private var globalFontMagnificationSubtitle: String {
if globalFontMagnification.current != GlobalFontMagnification.defaultPercent {
return String(
localized: "settings.app.globalFontMagnification.subtitleOn",
defaultValue: "Terminals, tabs, and chrome all render at this magnification. Per-pane zoom (Cmd= / Cmd-) still overrides for the focused pane."
)
}
return String(
localized: "settings.app.globalFontMagnification.subtitleOff",
defaultValue: "Scale every font in cmux by the same percentage. 100% = design size."
)
}
private func setGlobalFontMagnification(_ percent: Int) {
let clamped = GlobalFontMagnification.clamp(percent)
globalFontMagnification.set(clamped) { NotificationCenter.default.post(name: GlobalFontMagnification.didChangeNotification, object: nil) }
}
@ViewBuilder
private var mainCard: some View {
SettingsCard {
@@ -311,6 +332,19 @@ public struct AppSection: View {
}
SettingsCardDivider()
// Global Font Magnification
SettingsCardRow(
configurationReview: .json("app.globalFontMagnification"),
String(localized: "settings.app.globalFontMagnification", defaultValue: "Global Font Magnification"),
subtitle: globalFontMagnificationSubtitle
) {
GlobalFontMagnificationControl(
percent: globalFontMagnification.current,
onChange: setGlobalFontMagnification
)
}
SettingsCardDivider()
// Open Markdown in cmux Viewer
SettingsCardRow(
configurationReview: .json("app.openMarkdownInCmuxViewer"),
@@ -542,7 +576,7 @@ public struct AppSection: View {
) {
HStack(spacing: 6) {
Text(String(localized: "settings.notifications.desktop.status.unknown", defaultValue: "Permission unknown"))
.font(.system(size: 11, weight: .semibold))
.cmuxFont(size: 11, weight: .semibold)
.foregroundStyle(.secondary)
.frame(width: 98, alignment: .trailing)
Button(String(localized: "settings.notifications.desktop.action.enable", defaultValue: "Enable")) {
@@ -728,7 +762,7 @@ public struct AppSection: View {
hostActions.previewNotificationSound(value: model.current, customFilePath: customFile.current)
} label: {
Image(systemName: "play.fill")
.font(.system(size: 9))
.cmuxFont(size: 9)
}
.buttonStyle(.bordered)
.controlSize(.small)
@@ -740,7 +774,7 @@ public struct AppSection: View {
// display name slot, with a "No file selected"
// fallback when the path is empty.
Text(customSoundFileDisplayName(path: customFile.current))
.font(.system(size: 11))
.cmuxFont(size: 11)
.foregroundStyle(.secondary)
.lineLimit(1)
.truncationMode(.middle)
@@ -1,3 +1,4 @@
import CmuxFoundation
import CmuxSettings
import SwiftUI
@@ -201,7 +202,7 @@ public struct AutomationSection: View {
}
if let status = socketPasswordStatus {
Text(status.message)
.font(.caption)
.cmuxFont(.caption)
.foregroundStyle(status.isError ? Color.red : Color.secondary)
.padding(.horizontal, 14)
.padding(.bottom, 8)
@@ -211,7 +212,7 @@ public struct AutomationSection: View {
if isAllowAll {
SettingsCardDivider()
Text(String(localized: "settings.automation.openAccessWarning", defaultValue: "Warning: Full open access makes the control socket world-readable/writable on this Mac and disables auth checks. Use only for local debugging."))
.font(.caption)
.cmuxFont(.caption)
.foregroundStyle(.red)
.padding(.horizontal, 14)
.padding(.vertical, 8)
@@ -317,7 +318,7 @@ public struct AutomationSection: View {
@ViewBuilder
private func autoNamingFootnote(_ text: String) -> some View {
Text(text)
.font(.caption)
.cmuxFont(.caption)
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 14)
@@ -1,3 +1,4 @@
import CmuxFoundation
import CmuxSettings
import SwiftUI
@@ -153,12 +154,12 @@ private struct BetaFeaturesWarningNote: View {
var body: some View {
HStack(alignment: .top, spacing: 8) {
Image(systemName: "exclamationmark.triangle.fill")
.font(.system(size: 12, weight: .semibold))
.cmuxFont(size: 12, weight: .semibold)
.foregroundStyle(.yellow)
.accessibilityHidden(true)
Text(text)
.font(.caption)
.cmuxFont(.caption)
.foregroundColor(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
@@ -1,3 +1,4 @@
import CmuxFoundation
import AppKit
import CmuxSettings
import SwiftUI
@@ -197,7 +198,7 @@ public struct BrowserSection: View {
) {
HStack(spacing: 8) {
Text(formatDiscardDelay(discardDelay.current))
.font(.system(.body, design: .monospaced))
.cmuxFont(.body, design: .monospaced)
.monospacedDigit()
.frame(width: 56, alignment: .trailing)
Stepper(
@@ -281,7 +282,7 @@ public struct BrowserSection: View {
TextField("", text: Binding(get: { reactGrab.current }, set: { reactGrab.set($0) }))
.textFieldStyle(.roundedBorder)
.frame(width: 100)
.font(.system(.body, design: .monospaced))
.cmuxFont(.body, design: .monospaced)
.accessibilityIdentifier("SettingsReactGrabVersionField")
}
@@ -314,7 +315,7 @@ public struct BrowserSection: View {
EmptyView()
}
TextEditor(text: Binding(get: { model.current }, set: { model.set($0) }))
.font(.system(.body, design: .monospaced))
.cmuxFont(.body, design: .monospaced)
.frame(minHeight: 60, maxHeight: 120)
.scrollContentBackground(.hidden)
.padding(6)
@@ -333,12 +334,12 @@ public struct BrowserSection: View {
private func httpAllowlistRow(model: DefaultsValueModel<String>) -> some View {
VStack(alignment: .leading, spacing: 8) {
Text(String(localized: "settings.browser.httpAllowlist", defaultValue: "HTTP Hosts Allowed in Embedded Browser"))
.font(.system(size: 13, weight: .semibold))
.cmuxFont(size: 13, weight: .semibold)
Text(String(localized: "settings.browser.httpAllowlist.description", defaultValue: "Controls which HTTP (non-HTTPS) hosts can open in cmux without a warning prompt. Defaults include localhost, *.localhost, 127.0.0.1, ::1, 0.0.0.0, and *.localtest.me."))
.font(.caption)
.cmuxFont(.caption)
.foregroundStyle(.secondary)
TextEditor(text: $httpAllowlistDraft)
.font(.system(size: 12, weight: .regular, design: .monospaced))
.cmuxFont(size: 12, weight: .regular, design: .monospaced)
.frame(minHeight: 86)
.padding(6)
.background(
@@ -353,7 +354,7 @@ public struct BrowserSection: View {
ViewThatFits(in: .horizontal) {
HStack(alignment: .center, spacing: 10) {
Text(String(localized: "settings.browser.httpAllowlist.hint", defaultValue: "One host or wildcard per line (for example: localhost, *.localhost, 127.0.0.1, ::1, 0.0.0.0, *.localtest.me)."))
.font(.caption)
.cmuxFont(.caption)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
Spacer(minLength: 0)
@@ -369,7 +370,7 @@ public struct BrowserSection: View {
VStack(alignment: .leading, spacing: 8) {
Text(String(localized: "settings.browser.httpAllowlist.hint", defaultValue: "One host or wildcard per line (for example: localhost, *.localhost, 127.0.0.1, ::1, 0.0.0.0, *.localtest.me)."))
.font(.caption)
.cmuxFont(.caption)
.foregroundStyle(.secondary)
HStack {
Spacer(minLength: 0)
@@ -417,17 +418,17 @@ public struct BrowserSection: View {
VStack(alignment: .leading, spacing: 12) {
VStack(alignment: .leading, spacing: 8) {
Text(String(localized: "settings.browser.import", defaultValue: "Import Browser Data"))
.font(.system(size: 13, weight: .semibold))
.cmuxFont(size: 13, weight: .semibold)
VStack(alignment: .leading, spacing: 6) {
Text(String(localized: "browser.import.hint.title", defaultValue: "Import browser data"))
.font(.system(size: 12.5, weight: .semibold))
.cmuxFont(size: 12.5, weight: .semibold)
Text(String(localized: "browser.import.hint.subtitle", defaultValue: "Import bookmarks, history, and cookies from Safari, Chrome, Firefox, Brave, Edge, or Arc. Already-imported entries are deduped automatically."))
.font(.caption)
.cmuxFont(.caption)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
.accessibilityIdentifier("SettingsBrowserImportSummary")
Text(String(localized: "browser.import.hint.settingsFootnote", defaultValue: "You can always find this in Settings > Browser."))
.font(.system(size: 10.5))
.cmuxFont(size: 10.5)
.foregroundStyle(.tertiary)
.fixedSize(horizontal: false, vertical: true)
}
@@ -462,7 +463,7 @@ public struct BrowserSection: View {
.accessibilityIdentifier("SettingsBrowserImportHintToggle")
.settingsSearchAnchors(["setting:browserImport:import-hint"])
Text(String(localized: "settings.browser.import.hint.settingsNote", defaultValue: "Shown until you import or dismiss it on a blank tab."))
.font(.caption)
.cmuxFont(.caption)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
@@ -0,0 +1,50 @@
import CmuxFoundation
import SwiftUI
struct GlobalFontMagnificationControl: View {
let percent: Int
let onChange: (Int) -> Void
private let minimum = GlobalFontMagnification.minimumPercent
private let maximum = GlobalFontMagnification.maximumPercent
private let step = GlobalFontMagnification.stepPercent
private var clampedPercent: Int {
GlobalFontMagnification.clamp(percent)
}
private var isAtDefault: Bool {
clampedPercent == GlobalFontMagnification.defaultPercent
}
var body: some View {
HStack(spacing: 10) {
Text(String(format: "%d%%", clampedPercent))
.monospacedDigit()
.foregroundStyle(.primary)
.frame(minWidth: 48, alignment: .trailing)
Stepper(
"",
value: Binding(
get: { clampedPercent },
set: { onChange(GlobalFontMagnification.clamp($0)) }
),
in: minimum...maximum,
step: step
)
.labelsHidden()
.accessibilityLabel(String(localized: "settings.app.globalFontMagnification", defaultValue: "Global Font Magnification"))
.accessibilityValue(Text("\(clampedPercent)%"))
.accessibilityIdentifier("SettingsGlobalFontMagnificationStepper")
Button(String(localized: "settings.app.globalFontMagnification.reset", defaultValue: "Reset")) {
onChange(GlobalFontMagnification.defaultPercent)
}
.buttonStyle(.bordered)
.controlSize(.small)
.disabled(isAtDefault)
}
.cmuxFont(.body)
}
}
@@ -1,3 +1,4 @@
import CmuxFoundation
import CmuxSettings
import SwiftUI
@@ -85,7 +86,7 @@ public struct KeyboardShortcutsSection: View {
}
.settingsSearchAnchors(["setting:keyboardShortcuts:shortcuts"])
Text(String(localized: "settings.shortcuts.recordHint", defaultValue: "Click a shortcut value to record. Use X to unbind; it changes to restore after a clear."))
.font(.caption)
.cmuxFont(.caption)
.foregroundColor(.secondary)
.padding(.leading, 2)
.accessibilityIdentifier("ShortcutRecordingHint")
@@ -108,7 +109,7 @@ public struct KeyboardShortcutsSection: View {
String(localized: "settings.shortcuts.chords.docsButton", defaultValue: "Chord docs"),
destination: URL(string: "https://cmux.com/docs/keyboard-shortcuts#shortcut-chords")!
)
.font(.caption)
.cmuxFont(.caption)
.accessibilityIdentifier("SettingsKeyboardShortcutsChordDocsLink")
Button(String(localized: "settings.app.settingsFile.openButton", defaultValue: "Open cmux.json")) {
@@ -196,7 +197,7 @@ public struct KeyboardShortcutsSection: View {
Text(action.displayName)
if let subtitle {
Text(subtitle)
.font(.caption)
.cmuxFont(.caption)
.foregroundStyle(.secondary)
}
}
@@ -249,11 +250,11 @@ public struct KeyboardShortcutsSection: View {
if let validationMessage {
HStack(alignment: .firstTextBaseline, spacing: 8) {
Image(systemName: "exclamationmark.triangle.fill")
.font(.caption)
.cmuxFont(.caption)
.foregroundStyle(.red)
Text(validationMessage)
.font(.caption)
.cmuxFont(.caption)
.foregroundStyle(.red)
.fixedSize(horizontal: false, vertical: true)
@@ -269,7 +270,7 @@ public struct KeyboardShortcutsSection: View {
conflictRejections.removeValue(forKey: action.rawValue)
}
.buttonStyle(.link)
.font(.caption)
.cmuxFont(.caption)
}
.padding(.horizontal, 8)
.padding(.vertical, 6)
@@ -1,3 +1,4 @@
import CmuxFoundation
import CmuxSettings
import SwiftUI
@@ -224,7 +225,7 @@ public struct MobileSection: View {
@ViewBuilder
private func statusCaption(@ViewBuilder _ content: () -> some View) -> some View {
content()
.font(.caption)
.cmuxFont(.caption)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 14)
.padding(.bottom, 8)
@@ -288,7 +289,7 @@ public struct MobileSection: View {
subtitle: String(localized: "settings.mobile.connections.subtitle", defaultValue: "iOS devices currently attached to this Mac.")
) {
Text("\(snapshot?.activeConnectionCount ?? 0)")
.font(.system(size: 13, weight: .medium))
.cmuxFont(size: 13, weight: .medium)
.monospacedDigit()
.foregroundStyle(.secondary)
}
@@ -306,16 +307,16 @@ public struct MobileSection: View {
} else {
VStack(alignment: .leading, spacing: 4) {
Text(String(localized: "settings.mobile.routes.title", defaultValue: "Reachable at"))
.font(.caption)
.cmuxFont(.caption)
.foregroundStyle(.secondary)
ForEach(snapshot.routes) { route in
HStack(spacing: 8) {
Text(route.kindLabel)
.font(.caption)
.cmuxFont(.caption)
.foregroundStyle(.secondary)
Spacer(minLength: 8)
Text(route.endpoint)
.font(.caption.monospaced())
.cmuxFont(.caption, design: .monospaced)
.foregroundStyle(.primary)
.textSelection(.enabled)
}
@@ -1,3 +1,4 @@
import CmuxFoundation
import CmuxSettings
import SwiftUI
@@ -42,5 +43,6 @@ public struct ResetSection: View {
for key in catalog.all {
await key.resetInJSON(jsonStore)
}
NotificationCenter.default.post(name: GlobalFontMagnification.didChangeNotification, object: nil)
}
}
@@ -1,3 +1,4 @@
import CmuxFoundation
import CmuxSettings
import SwiftUI
@@ -37,7 +38,7 @@ public struct SettingsJSONSection: View {
) {
HStack(spacing: 8) {
Text(displayPath)
.font(.system(size: 11, weight: .medium, design: .monospaced))
.cmuxFont(size: 11, weight: .medium, design: .monospaced)
.foregroundStyle(.secondary)
.lineLimit(1)
.truncationMode(.middle)
@@ -66,7 +67,7 @@ public struct SettingsJSONSection: View {
String(localized: "settings.settingsJSON.docsButton", defaultValue: "Open Docs"),
destination: URL(string: "https://cmux.com/docs/configuration#cmux-json")!
)
.font(.caption)
.cmuxFont(.caption)
.accessibilityIdentifier("SettingsJSONDocsLink")
}
}
@@ -1,3 +1,4 @@
import CmuxFoundation
import CmuxSettings
import SwiftUI
@@ -200,7 +201,7 @@ public struct SidebarSection: View {
.accessibilityIdentifier("SettingsSidebarFontSizeSlider")
Text(String.localizedStringWithFormat(String(localized: "settings.fontSize.valuePoints", defaultValue: "%@ pt"), hostActions.formattedFontSize(sidebarFont.points)))
.font(.system(size: 12, weight: .medium, design: .rounded))
.cmuxFont(size: 12, weight: .medium, design: .rounded)
.monospacedDigit()
.frame(width: 44, alignment: .trailing)
@@ -215,7 +216,7 @@ public struct SidebarSection: View {
if fontSaveFailed {
Text(String(localized: "settings.sidebarAppearance.fontSize.saveFailed", defaultValue: "Couldn't save sidebar font size. Please try again."))
.font(.caption)
.cmuxFont(.caption)
.foregroundStyle(.red)
.multilineTextAlignment(.trailing)
.fixedSize(horizontal: false, vertical: true)
@@ -1,3 +1,4 @@
import CmuxFoundation
import CmuxSettings
import SwiftUI
@@ -113,7 +114,7 @@ public struct TerminalSection: View {
) {
HStack(spacing: 8) {
Text(verbatim: "0")
.font(.caption.monospacedDigit())
.cmuxFont(.caption, monospacedDigit: true)
.foregroundColor(.secondary)
Button(String(localized: "settings.settingsJSON.openButton", defaultValue: "Open")) {
hostActions.openConfigInExternalEditor()
@@ -147,7 +148,7 @@ public struct TerminalSection: View {
.accessibilityIdentifier("SettingsTabBarFontSizeSlider")
Text(String.localizedStringWithFormat(String(localized: "settings.fontSize.valuePoints", defaultValue: "%@ pt"), hostActions.formattedFontSize(surfaceTabBarFont.points)))
.font(.system(size: 12, weight: .medium, design: .rounded))
.cmuxFont(size: 12, weight: .medium, design: .rounded)
.monospacedDigit()
.frame(width: 44, alignment: .trailing)
@@ -162,7 +163,7 @@ public struct TerminalSection: View {
if fontSaveFailed {
Text(String(localized: "settings.terminal.tabBarFontSize.saveFailed", defaultValue: "Couldn't save tab bar font size. Please try again."))
.font(.caption)
.cmuxFont(.caption)
.foregroundStyle(.red)
.multilineTextAlignment(.trailing)
.fixedSize(horizontal: false, vertical: true)
@@ -188,7 +189,7 @@ public struct TerminalSection: View {
.accessibilityIdentifier("SettingsTerminalScrollSpeedSlider")
Text(String.localizedStringWithFormat(String(localized: "settings.terminal.scrollSpeed.value", defaultValue: "%.2f×"), displayedScrollSpeed))
.font(.system(size: 12, weight: .medium, design: .rounded))
.cmuxFont(size: 12, weight: .medium, design: .rounded)
.monospacedDigit()
.frame(width: 44, alignment: .trailing)
@@ -1,3 +1,4 @@
import CmuxFoundation
import CmuxSettings
import SwiftUI
@@ -120,12 +121,12 @@ private struct TextBoxBetaWarningNote: View {
var body: some View {
HStack(alignment: .top, spacing: 8) {
Image(systemName: "exclamationmark.triangle.fill")
.font(.system(size: 12, weight: .semibold))
.cmuxFont(size: 12, weight: .semibold)
.foregroundStyle(.yellow)
.accessibilityHidden(true)
Text(text)
.font(.caption)
.cmuxFont(.caption)
.foregroundColor(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
@@ -1,3 +1,4 @@
import CmuxFoundation
import AppKit
import CmuxSettings
import SwiftUI
@@ -163,7 +164,7 @@ public struct WorkspaceColorsSection: View {
.labelsHidden()
.frame(width: 38)
Text(isCustom ? model.current : String(localized: "settings.sidebarAppearance.defaultLabel", defaultValue: "Default"))
.font(.system(size: 12, weight: .medium, design: .monospaced))
.cmuxFont(size: 12, weight: .medium, design: .monospaced)
.foregroundStyle(.secondary)
.frame(width: 76, alignment: .trailing)
}
@@ -203,7 +204,7 @@ public struct WorkspaceColorsSection: View {
.labelsHidden()
.frame(width: 38)
Text(entry.hex)
.font(.system(size: 12, weight: .medium, design: .monospaced))
.cmuxFont(size: 12, weight: .medium, design: .monospaced)
.foregroundStyle(.secondary)
.frame(width: 76, alignment: .trailing)
if baseHex == nil {
@@ -131,4 +131,27 @@ import Testing
#expect(model.current == true)
#expect(UserDefaults(suiteName: suiteName)?.object(forKey: key.userDefaultsKey) == nil)
}
@Test func setAfterCommitRunsAfterStoreWrite() async {
let suiteName = "defaults-value-model-after-commit"
UserDefaults(suiteName: suiteName)?.removePersistentDomain(forName: suiteName)
let store = UserDefaultsSettingsStore(
defaults: UserDefaults(suiteName: suiteName)!
)
let key = SettingCatalog().betaFeatures.extensions
let (stream, _) = AsyncStream<Bool>.makeStream()
let model = DefaultsValueModel(
store: store,
key: key,
makeStream: { stream }
)
let valueObservedAfterCommit = await withCheckedContinuation { continuation in
model.set(true) {
continuation.resume(returning: store.initialValue(for: key))
}
}
#expect(valueObservedAfterCommit == true)
}
}
@@ -39,6 +39,7 @@ struct SettingsRowAnchorResolutionTests {
"app.commandPaletteSearchesAllSurfaces",
"app.confirmQuit",
"app.focusPaneOnFirstClick",
"app.globalFontMagnification",
"app.hideTabCloseButton",
"app.iMessageMode",
"app.keepWorkspaceOpenWhenClosingLastSurface",
@@ -1,3 +1,4 @@
import CmuxFoundation
import SwiftUI
/// Renders a declarative JSON ``DSLNode`` tree as native SwiftUI.
@@ -27,7 +28,7 @@ struct DSLSidebarRenderer: View {
ZStack { childViews }
case .text:
Text(node.text ?? "")
.font(resolvedFont)
.modifier(OptionalDSLFont(spec: resolvedFontSpec))
.fontWeight(dslFontWeight(node.weight))
case .button:
Button(node.title ?? "") {
@@ -36,7 +37,7 @@ struct DSLSidebarRenderer: View {
.reportTapTarget(node.action?.buttonAction)
case .image:
Image(systemName: node.systemName ?? "questionmark.square.dashed")
.font(resolvedFont)
.modifier(OptionalDSLFont(spec: resolvedFontSpec))
case .spacer:
Spacer(minLength: node.size.map { CGFloat($0) })
case .divider:
@@ -51,8 +52,8 @@ struct DSLSidebarRenderer: View {
}
}
private var resolvedFont: Font? {
dslFont(named: node.font, size: node.size)
private var resolvedFontSpec: DSLFontSpec? {
dslFontSpec(named: node.font, size: node.size)
}
@ViewBuilder
@@ -0,0 +1,7 @@
import SwiftUI
struct DSLFontSpec {
let baseSize: CGFloat
let weight: Font.Weight?
let design: Font.Design
}
@@ -0,0 +1,18 @@
import CmuxFoundation
import SwiftUI
struct OptionalDSLFont: ViewModifier {
let spec: DSLFontSpec?
func body(content: Content) -> some View {
if let spec {
content.cmuxFont(
size: spec.baseSize,
weight: spec.weight ?? .regular,
design: spec.design
)
} else {
content
}
}
}
@@ -1,3 +1,4 @@
import CmuxFoundation
import CmuxSwiftRender
import SwiftUI
@@ -51,7 +52,7 @@ struct RenderNodeView: View {
VStack(alignment: .leading, spacing: 4) {
if let header = node.text, !header.isEmpty {
Text(header)
.font(.caption)
.cmuxFont(.caption)
.fontWeight(.semibold)
.foregroundStyle(.secondary)
}
@@ -167,7 +168,7 @@ struct RenderNodeView: View {
let token = clean(modifier.firstValue)
switch modifier.name {
case "font":
return AnyView(view.font(resolveFont(token)))
return AnyView(view.modifier(OptionalDSLFont(spec: resolveFontSpec(token))))
case "bold":
return AnyView(view.fontWeight(.bold))
case "strikethrough":
@@ -444,24 +445,32 @@ struct RenderNodeView: View {
/// Resolves a font token, including the `.system(size:weight:design:)` /
/// `.system(.style, design:)` forms (size, monospaced design, named style).
private func resolveFont(_ token: String?) -> Font? {
private func resolveFontSpec(_ token: String?) -> DSLFontSpec? {
guard let token else { return nil }
guard token.hasPrefix("system") else { return dslFont(named: token, size: nil) }
guard token.hasPrefix("system") else { return dslFontSpec(named: token, size: nil) }
let design: Font.Design = token.contains("monospaced") ? .monospaced : .default
let weight = resolveFontWeight(in: token)
if let range = token.range(of: "size:") {
let digits = token[range.upperBound...].drop(while: { $0 == " " })
.prefix(while: { $0.isNumber || $0 == "." })
if let n = Double(digits) { return .system(size: CGFloat(n), design: design) }
if let n = Double(digits) { return dslFontSpec(named: nil, size: n, weight: weight, design: design) }
}
let styles: [(String, Font.TextStyle)] = [
("largeTitle", .largeTitle), ("title3", .title3), ("title2", .title2), ("title", .title),
("headline", .headline), ("subheadline", .subheadline), ("body", .body), ("callout", .callout),
("footnote", .footnote), ("caption2", .caption2), ("caption", .caption),
let styleNames = [
"largeTitle", "title3", "title2", "title",
"headline", "subheadline", "body", "callout",
"footnote", "caption2", "caption",
]
for (name, style) in styles where token.contains(name) {
return .system(style, design: design)
for name in styleNames where token.contains(name) {
return dslFontSpec(named: name, size: nil, weight: weight, design: design)
}
return .system(size: 13, design: design)
return dslFontSpec(named: nil, size: 13, weight: weight, design: design)
}
private func resolveFontWeight(in token: String) -> Font.Weight? {
guard let range = token.range(of: "weight:") else { return nil }
let rawWeight = token[range.upperBound...].drop(while: { $0 == " " || $0 == "." })
.prefix(while: { $0.isLetter })
return dslFontWeight(String(rawWeight))
}
/// Strips a leading `.` (member token) or surrounding quotes from a raw
@@ -1,3 +1,4 @@
import CmuxFoundation
import SwiftUI
/// Resolves a style token to a SwiftUI `Color`.
@@ -51,22 +52,22 @@ func dslColor(_ token: String?) -> Color? {
return Color(.sRGB, red: r, green: g, blue: b, opacity: a)
}
/// Resolves a font token (or explicit size) to a SwiftUI `Font`.
func dslFont(named token: String?, size: Double?) -> Font? {
if let size { return .system(size: size) }
/// Resolves a font token (or explicit size) to a magnification-aware font spec.
func dslFontSpec(named token: String?, size: Double?, weight: Font.Weight? = nil, design: Font.Design = .default) -> DSLFontSpec? {
if let size { return DSLFontSpec(baseSize: CGFloat(size), weight: weight, design: design) }
guard let token else { return nil }
switch token.lowercased() {
case "largetitle": return .largeTitle
case "title": return .title
case "title2": return .title2
case "title3": return .title3
case "headline": return .headline
case "subheadline": return .subheadline
case "body": return .body
case "callout": return .callout
case "footnote": return .footnote
case "caption": return .caption
case "caption2": return .caption2
case "largetitle": return DSLFontSpec(baseSize: 26, weight: weight, design: design)
case "title": return DSLFontSpec(baseSize: 22, weight: weight, design: design)
case "title2": return DSLFontSpec(baseSize: 17, weight: weight, design: design)
case "title3": return DSLFontSpec(baseSize: 15, weight: weight, design: design)
case "headline": return DSLFontSpec(baseSize: 13, weight: weight ?? .semibold, design: design)
case "subheadline": return DSLFontSpec(baseSize: 11, weight: weight, design: design)
case "body": return DSLFontSpec(baseSize: 13, weight: weight, design: design)
case "callout": return DSLFontSpec(baseSize: 12, weight: weight, design: design)
case "footnote": return DSLFontSpec(baseSize: 10, weight: weight, design: design)
case "caption": return DSLFontSpec(baseSize: 10, weight: weight, design: design)
case "caption2": return DSLFontSpec(baseSize: 9, weight: weight, design: design)
default: return nil
}
}
@@ -1,3 +1,4 @@
import CmuxFoundation
import CmuxSwiftRender
import SwiftUI
@@ -56,7 +57,7 @@ public struct CustomSidebarContentView: View {
case .missing:
scrollWrap(
Text(String(localized: "sidebar.custom.missing", defaultValue: "Sidebar file is empty or missing.", bundle: .module))
.font(.caption)
.cmuxFont(.caption)
.foregroundStyle(.secondary)
)
case let .json(document):
@@ -119,10 +120,10 @@ public struct CustomSidebarContentView: View {
String(localized: "sidebar.custom.error", defaultValue: "Sidebar error", bundle: .module),
systemImage: "exclamationmark.triangle.fill"
)
.font(.caption.bold())
.cmuxFont(.caption, weight: .bold)
.foregroundStyle(.orange)
Text(message)
.font(.caption)
.cmuxFont(.caption)
.foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity, alignment: .leading)
@@ -119,13 +119,13 @@ struct CustomSidebarValidationTests {
}
private func examplesDirectory() -> URL {
URL(fileURLWithPath: #filePath)
.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent()
.appendingPathComponent("Examples/CustomSidebars", isDirectory: true)
var directory = URL(fileURLWithPath: #filePath).deletingLastPathComponent()
for _ in 0..<8 {
let candidate = directory.appendingPathComponent("Examples/CustomSidebars", isDirectory: true)
if FileManager.default.fileExists(atPath: candidate.path) { return candidate }
directory.deleteLastPathComponent()
}
return directory.appendingPathComponent("Examples/CustomSidebars", isDirectory: true)
}
private static let richSidebarContext: [String: SwiftValue] = [
@@ -51,6 +51,9 @@ public struct TerminalSurfaceRuntimeDependencies {
/// surface strips it after the first runtime spawn.
public let scrollbackReplayEnvironmentKey: String
/// Provides the app's current global font magnification percent.
public let globalFontMagnificationPercent: @Sendable () -> Int
/// Creates the dependency bundle.
public init(
registry: any TerminalSurfaceRegistering,
@@ -65,7 +68,8 @@ public struct TerminalSurfaceRuntimeDependencies {
runtimeFilesystem: TerminalSurfaceRuntimeFilesystem,
sessionPortBase: Int,
sessionPortRangeSize: Int,
scrollbackReplayEnvironmentKey: String
scrollbackReplayEnvironmentKey: String,
globalFontMagnificationPercent: @escaping @Sendable () -> Int = { 100 }
) {
self.registry = registry
self.engine = engine
@@ -80,5 +84,6 @@ public struct TerminalSurfaceRuntimeDependencies {
self.sessionPortBase = sessionPortBase
self.sessionPortRangeSize = sessionPortRangeSize
self.scrollbackReplayEnvironmentKey = scrollbackReplayEnvironmentKey
self.globalFontMagnificationPercent = globalFontMagnificationPercent
}
}
@@ -603,18 +603,18 @@ extension TerminalSurface {
flushPendingRemoteOutput(to: createdSurface)
// Some GhosttyKit builds can drop inherited font_size during post-create
// config/scale reconciliation. If runtime points don't match the inherited
// template points, re-apply via binding action so all creation paths
// (new surface, split, new workspace) preserve zoom from the source terminal.
if let inheritedFontPoints = configTemplate?.fontSize,
inheritedFontPoints > 0 {
// config/scale reconciliation. Re-apply runtime points so all creation
// paths preserve zoom from the source terminal.
if let inheritedBaseFontPoints = configTemplate?.fontSize,
inheritedBaseFontPoints > 0 {
let inheritedRuntimeFontPoints = CmuxSurfaceConfigTemplate.runtimeFontSize(fromBasePoints: inheritedBaseFontPoints, percent: globalFontMagnificationPercent())
let currentFontPoints = GhosttySurfaceRuntimeProbe.currentSurfaceFontSizePoints(createdSurface)
let shouldReapply = {
guard let currentFontPoints else { return true }
return abs(currentFontPoints - inheritedFontPoints) > 0.05
return abs(currentFontPoints - inheritedRuntimeFontPoints) > 0.05
}()
if shouldReapply {
let action = String(format: "set_font_size:%.3f", inheritedFontPoints)
let action = String(format: "set_font_size:%.3f", inheritedRuntimeFontPoints)
_ = performBindingAction(action)
}
}
@@ -20,7 +20,11 @@ extension TerminalSurface {
) -> (createdSurface: ghostty_surface_t?, runtimeInitialInput: String?) {
var baseConfig = configTemplate ?? CmuxSurfaceConfigTemplate()
var surfaceConfig = ghostty_surface_config_new()
surfaceConfig.font_size = baseConfig.fontSize
let magnificationPercent = globalFontMagnificationPercent()
surfaceConfig.font_size = CmuxSurfaceConfigTemplate.runtimeFontSize(
fromBasePoints: baseConfig.fontSize,
percent: magnificationPercent
)
surfaceConfig.wait_after_command = baseConfig.waitAfterCommand
surfaceConfig.platform_tag = GHOSTTY_PLATFORM_MACOS
surfaceConfig.platform = ghostty_platform_u(macos: ghostty_platform_macos_s(
@@ -46,10 +50,11 @@ extension TerminalSurface {
surfaceConfig.io_write_userdata = box.toOpaque()
}
#if DEBUG
let templateFontText = String(format: "%.2f", surfaceConfig.font_size)
let templateFontText = String(format: "%.2f", baseConfig.fontSize)
let runtimeFontText = String(format: "%.2f", surfaceConfig.font_size)
logDebugEvent(
"zoom.create surface=\(id.uuidString.prefix(5)) context=\(GhosttySurfaceRuntimeProbe.contextName(surfaceContext)) " +
"templateFont=\(templateFontText)"
"templateFont=\(templateFontText) runtimeFont=\(runtimeFontText)"
)
#endif
var envVars: [ghostty_env_var_s] = []
@@ -70,12 +70,11 @@ public final class TerminalSurface: Identifiable, ObservableObject {
let runtimeTeardown: TerminalSurfaceRuntimeTeardownCoordinator
let restoreSpawnScheduler: any TerminalSurfaceRuntimeSpawnScheduling
let runtimeFilesystem: TerminalSurfaceRuntimeFilesystem
/// Port ordinal base/range for CMUX_PORT assignment, snapshotted once per
/// app session by the composition root so every runtime startup path uses
/// the same immutable workspace port range.
/// Port ordinal base/range for CMUX_PORT assignment, snapshotted by the app composition root.
let sessionPortBase: Int
let sessionPortRangeSize: Int
let scrollbackReplayEnvironmentKey: String
let globalFontMagnificationPercent: @Sendable () -> Int
/// cmux renderer reclamation: whether the current runtime surface's GPU
/// renderer (Metal swap chain / IOSurface, ~40MB) is realized. A freshly
@@ -414,6 +413,7 @@ public final class TerminalSurface: Identifiable, ObservableObject {
self.sessionPortBase = dependencies.sessionPortBase
self.sessionPortRangeSize = dependencies.sessionPortRangeSize
self.scrollbackReplayEnvironmentKey = dependencies.scrollbackReplayEnvironmentKey
self.globalFontMagnificationPercent = dependencies.globalFontMagnificationPercent
// Match Ghostty's own SurfaceView: ensure a non-zero initial frame so the backing layer
// has non-zero bounds and the renderer can initialize without presenting a blank/stretched
// intermediate frame on the first real resize.
@@ -27,11 +27,9 @@ public struct GhosttyConfig {
public static let cmuxDefaultDarkThemeName = "Apple System Colors"
private static let loadCacheLock = NSLock()
// Every read/write of this cache is serialized by `loadCacheLock` (see
// `cachedLoad`/`storeCachedLoad`/`invalidateLoadCache`), so the mutable
// static is data-race-safe despite being nonisolated. Faithful lift of the
// app-target lock-guarded cache; the lock contract is unchanged.
nonisolated(unsafe) private static var cachedConfigsByColorScheme: [ColorSchemePreference: GhosttyConfig] = [:]
// Every read/write of this cache is serialized by `loadCacheLock`; the
// nonisolated static mirrors the app-target lock-guarded cache contract.
nonisolated(unsafe) private static var cachedConfigsByColorScheme: [String: GhosttyConfig] = [:]
/// The default sidebar font size, in points.
public static let defaultSidebarFontSize = CGFloat(CmuxGhosttyConfigSettingEditor.defaultSidebarFontSize)
/// The minimum sidebar font size the parser will clamp to.
@@ -162,48 +160,46 @@ public struct GhosttyConfig {
}
/// Loads the resolved terminal config for `preferredColorScheme` (or the
/// current system/app preference when `nil`), caching per color scheme when
/// `useCache` is set. `loadFromDisk` is injectable for tests.
/// current system/app preference when `nil`), caching per color scheme plus
/// optional app-injected magnification percent. `loadFromDisk` is injectable
/// for tests; pass `nil` for deterministic base config values.
public static func load(
preferredColorScheme: ColorSchemePreference? = nil,
useCache: Bool = true,
globalFontMagnificationPercent: Int? = nil,
loadFromDisk: (_ preferredColorScheme: ColorSchemePreference) -> GhosttyConfig = Self.loadFromDisk
) -> GhosttyConfig {
let resolvedColorScheme = preferredColorScheme ?? currentColorSchemePreference()
if useCache, let cached = cachedLoad(for: resolvedColorScheme) {
return cached
let magnificationPercent = globalFontMagnificationPercent.map(GlobalFontMagnification.clamp)
let cacheKey = "\(resolvedColorScheme)#\(magnificationPercent ?? -1)"
if useCache {
let cached: GhosttyConfig? = {
loadCacheLock.lock()
defer { loadCacheLock.unlock() }
return cachedConfigsByColorScheme[cacheKey]
}()
if let cached {
return cached
}
}
let loaded = loadFromDisk(resolvedColorScheme)
var loaded = loadFromDisk(resolvedColorScheme)
if let magnificationPercent { loaded.applyGlobalMagnification(percent: magnificationPercent) }
if useCache {
storeCachedLoad(loaded, for: resolvedColorScheme)
loadCacheLock.lock()
cachedConfigsByColorScheme[cacheKey] = loaded
loadCacheLock.unlock()
}
return loaded
}
/// Drops every cached per-color-scheme config so the next ``load(preferredColorScheme:useCache:loadFromDisk:)``
/// re-reads from disk.
/// Drops every cached config so the next ``load(preferredColorScheme:useCache:globalFontMagnificationPercent:loadFromDisk:)`` re-reads from disk.
public static func invalidateLoadCache() {
loadCacheLock.lock()
cachedConfigsByColorScheme.removeAll()
loadCacheLock.unlock()
}
private static func cachedLoad(for colorScheme: ColorSchemePreference) -> GhosttyConfig? {
loadCacheLock.lock()
defer { loadCacheLock.unlock() }
return cachedConfigsByColorScheme[colorScheme]
}
private static func storeCachedLoad(
_ config: GhosttyConfig,
for colorScheme: ColorSchemePreference
) {
loadCacheLock.lock()
cachedConfigsByColorScheme[colorScheme] = config
loadCacheLock.unlock()
}
private static func cmuxConfigPaths(
fileManager: FileManager = .default,
currentBundleIdentifier: String? = Bundle.main.bundleIdentifier
@@ -349,6 +345,13 @@ public struct GhosttyConfig {
return config
}
mutating func applyGlobalMagnification(percent: Int) {
let scale = GlobalFontMagnification.scale(for: percent)
guard scale != 1 else { return }
fontSize = max(1, fontSize * scale)
surfaceTabBarFontSize = max(1, surfaceTabBarFontSize * scale)
}
/// Applies cmux's managed default theme for `preferredColorScheme` by parsing
/// the bundled (or fallback) theme config contents into this config.
public mutating func applyCmuxDefaultAppearance(
@@ -1,3 +1,4 @@
internal import CmuxFoundation
public import GhosttyKit
/// The Swift-side template for a new runtime surface's `ghostty_surface_config_s`.
@@ -6,7 +7,7 @@ public import GhosttyKit
/// command, environment, initial input) either empty for a fresh surface or
/// copied from a source surface's inherited C config when splitting.
public struct CmuxSurfaceConfigTemplate: Sendable {
/// The font size in points; `0` means the runtime default.
/// The unscaled base font size in points; `0` means the runtime default.
public var fontSize: Float32 = 0
/// The working directory the spawned shell starts in.
@@ -29,10 +30,19 @@ public struct CmuxSurfaceConfigTemplate: Sendable {
/// Creates a template from a ghostty inherited surface config.
///
/// - Parameter cConfig: The C config returned by
/// `ghostty_surface_inherited_config`.
public init(cConfig: ghostty_surface_config_s) {
fontSize = cConfig.font_size
/// - Parameters:
/// - cConfig: The C config returned by `ghostty_surface_inherited_config`.
/// - globalFontMagnificationPercent: The magnification percent that was
/// applied to the runtime font size. The default keeps package parsing
/// deterministic at 100%.
public init(
cConfig: ghostty_surface_config_s,
globalFontMagnificationPercent: Int = 100
) {
fontSize = Self.baseFontSize(
fromRuntimePoints: cConfig.font_size,
percent: globalFontMagnificationPercent
)
if let workingDirectory = cConfig.working_directory {
self.workingDirectory = String(cString: workingDirectory, encoding: .utf8)
}
@@ -53,4 +63,46 @@ public struct CmuxSurfaceConfigTemplate: Sendable {
}
waitAfterCommand = cConfig.wait_after_command
}
/// Converts a runtime Ghostty font size back into an unscaled base point size
/// at 100% magnification.
///
/// - Parameter runtimePoints: The current runtime point size reported by Ghostty.
/// - Returns: The corresponding base point size at 100% magnification.
public static func baseFontSize(fromRuntimePoints runtimePoints: Float32) -> Float32 {
baseFontSize(fromRuntimePoints: runtimePoints, percent: GlobalFontMagnification.defaultPercent)
}
/// Converts a runtime Ghostty font size back into an unscaled base point size.
///
/// - Parameters:
/// - runtimePoints: The current runtime point size reported by Ghostty.
/// - percent: The global magnification percent used for the conversion.
/// - Returns: The corresponding base point size for `percent`.
public static func baseFontSize(fromRuntimePoints runtimePoints: Float32, percent: Int) -> Float32 {
guard runtimePoints.isFinite, runtimePoints > 0 else { return runtimePoints }
let scale = Float32(GlobalFontMagnification.scale(for: percent))
guard scale > 0 else { return runtimePoints }
return max(1, runtimePoints / scale)
}
/// Converts an unscaled base font size into the runtime point size Ghostty
/// expects at 100% magnification.
///
/// - Parameter basePoints: The unscaled base point size.
/// - Returns: The runtime point size at 100% magnification.
public static func runtimeFontSize(fromBasePoints basePoints: Float32) -> Float32 {
runtimeFontSize(fromBasePoints: basePoints, percent: GlobalFontMagnification.defaultPercent)
}
/// Converts an unscaled base font size into the runtime point size Ghostty expects.
///
/// - Parameters:
/// - basePoints: The unscaled base point size.
/// - percent: The global magnification percent used for the conversion.
/// - Returns: The runtime point size for `percent`.
public static func runtimeFontSize(fromBasePoints basePoints: Float32, percent: Int) -> Float32 {
guard basePoints.isFinite, basePoints > 0 else { return basePoints }
return max(1, basePoints * Float32(GlobalFontMagnification.scale(for: percent)))
}
}
@@ -0,0 +1,23 @@
import Testing
import CmuxTerminalCore
@Suite struct SurfaceConfigTemplateFontSizeTests {
@Test func convertsRuntimeFontSizeToBasePoints() {
let basePoints = CmuxSurfaceConfigTemplate.baseFontSize(fromRuntimePoints: 24, percent: 200)
#expect(abs(basePoints - 12) < 0.001)
}
@Test func convertsBaseFontSizeToRuntimePoints() {
let runtimePoints = CmuxSurfaceConfigTemplate.runtimeFontSize(fromBasePoints: 12, percent: 200)
#expect(abs(runtimePoints - 24) < 0.001)
}
@Test func inheritedRuntimeFontSizeRoundTripsWithoutCompounding() {
let basePoints = CmuxSurfaceConfigTemplate.baseFontSize(fromRuntimePoints: 24, percent: 200)
let runtimePoints = CmuxSurfaceConfigTemplate.runtimeFontSize(fromBasePoints: basePoints, percent: 200)
#expect(abs(runtimePoints - 24) < 0.001)
}
}
@@ -1,5 +1,5 @@
{
"originHash" : "b507e60be0c085bcad34f84a1c16ab14c57c06f9ea436ac4aa35fdadba4767e0",
"originHash" : "8b92d33938d2b6f63a7f8b0c435ec437965cd977753f08ae6d08479317784251",
"pins" : [
{
"identity" : "sparkle",
@@ -14,6 +14,7 @@ let package = Package(
),
],
dependencies: [
.package(path: "../CmuxFoundation"),
.package(path: "../CmuxUpdater"),
.package(url: "https://github.com/sparkle-project/Sparkle", from: "2.5.1"),
],
@@ -21,6 +22,7 @@ let package = Package(
.target(
name: "CmuxUpdaterUI",
dependencies: [
"CmuxFoundation",
"CmuxUpdater",
.product(name: "Sparkle", package: "Sparkle"),
],
@@ -1,3 +1,4 @@
import CmuxFoundation
public import SwiftUI
public import CmuxUpdater
import AppKit
@@ -25,13 +26,13 @@ struct UpdateErrorView: View {
HStack(spacing: 8) {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundColor(.orange)
.font(.system(size: 13))
.cmuxFont(size: 13)
Text(title)
.font(.system(size: 13, weight: .semibold))
.cmuxFont(size: 13, weight: .semibold)
}
Text(message)
.font(.system(size: 11))
.cmuxFont(size: 11)
.foregroundColor(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
@@ -52,10 +53,10 @@ struct UpdateErrorView: View {
VStack(alignment: .leading, spacing: 6) {
Text(String(localized: "update.popover.details", defaultValue: "Details"))
.font(.system(size: 11, weight: .semibold))
.cmuxFont(size: 11, weight: .semibold)
ScrollView(.vertical) {
Text(details)
.font(.system(size: 10, design: .monospaced))
.cmuxFont(size: 10, design: .monospaced)
.foregroundColor(.secondary)
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: .infinity, alignment: .leading)
@@ -1,4 +1,5 @@
public import SwiftUI
import CmuxFoundation
public import CmuxUpdater
import AppKit
@@ -8,8 +9,11 @@ public struct UpdatePill: View {
private let appearance: UpdateAppearance
private let actions: any UpdateActionsHost
@State private var showPopover = false
@Environment(\.cmuxGlobalFontMagnificationPercent) private var globalFontPercent
private let textFont = NSFont.systemFont(ofSize: 11, weight: .medium)
private var textFont: NSFont {
GlobalFontMagnification.systemFont(ofSize: 11, weight: .medium)
}
/// Creates the pill.
///
@@ -49,7 +53,7 @@ public struct UpdatePill: View {
.frame(width: 14, height: 14)
Text(model.text)
.font(Font(textFont))
.cmuxFont(size: 11, weight: .medium)
.lineLimit(1)
.truncationMode(.tail)
.frame(maxWidth: textWidth, alignment: .leading)
@@ -91,6 +95,7 @@ public struct UpdatePill: View {
}
private var textWidth: CGFloat? {
_ = globalFontPercent
let attributes: [NSAttributedString.Key: Any] = [.font: textFont]
let size = (model.maxWidthText as NSString).size(withAttributes: attributes)
return size.width
@@ -1,3 +1,4 @@
import CmuxFoundation
public import SwiftUI
public import CmuxUpdater
import AppKit
@@ -76,7 +77,7 @@ private struct UpdateMetadataView: View {
.frame(width: labelWidth, alignment: .trailing)
Text(item.displayVersionString)
}
.font(.system(size: 11))
.cmuxFont(size: 11)
if item.contentLength > 0 {
HStack(spacing: 6) {
@@ -85,7 +86,7 @@ private struct UpdateMetadataView: View {
.frame(width: labelWidth, alignment: .trailing)
Text(ByteCountFormatter.string(fromByteCount: Int64(item.contentLength), countStyle: .file))
}
.font(.system(size: 11))
.cmuxFont(size: 11)
}
if let date = item.date {
@@ -95,7 +96,7 @@ private struct UpdateMetadataView: View {
.frame(width: labelWidth, alignment: .trailing)
Text(date.formatted(date: .abbreviated, time: .omitted))
}
.font(.system(size: 11))
.cmuxFont(size: 11)
}
}
.textSelection(.enabled)
@@ -109,12 +110,12 @@ private struct UpdateReleaseNotesLink: View {
Link(destination: notes.url) {
HStack {
Image(systemName: "doc.text")
.font(.system(size: 11))
.cmuxFont(size: 11)
Text(notes.label)
.font(.system(size: 11, weight: .medium))
.cmuxFont(size: 11, weight: .medium)
Spacer()
Image(systemName: "arrow.up.right")
.font(.system(size: 10))
.cmuxFont(size: 10)
}
.foregroundColor(.primary)
.padding(12)
@@ -138,7 +139,7 @@ private struct DetectedBackgroundUpdateView: View {
VStack(alignment: .leading, spacing: 12) {
VStack(alignment: .leading, spacing: 8) {
Text(String(localized: "update.popover.updateAvailable", defaultValue: "Update Available"))
.font(.system(size: 13, weight: .semibold))
.cmuxFont(size: 13, weight: .semibold)
UpdateMetadataView(item: item, labelWidth: labelWidth)
}
@@ -178,7 +179,7 @@ private struct DetectedBackgroundUpdatePendingView: View {
VStack(alignment: .leading, spacing: 16) {
VStack(alignment: .leading, spacing: 8) {
Text(String(localized: "update.popover.updateAvailable", defaultValue: "Update Available"))
.font(.system(size: 13, weight: .semibold))
.cmuxFont(size: 13, weight: .semibold)
HStack(spacing: 6) {
Text(String(localized: "update.popover.version", defaultValue: "Version:"))
@@ -186,14 +187,14 @@ private struct DetectedBackgroundUpdatePendingView: View {
.frame(width: 60, alignment: .trailing)
Text(version)
}
.font(.system(size: 11))
.cmuxFont(size: 11)
}
HStack(spacing: 10) {
ProgressView()
.controlSize(.small)
Text(String(localized: "update.popover.checking", defaultValue: "Checking for updates…"))
.font(.system(size: 13))
.cmuxFont(size: 13)
}
}
.padding(16)
@@ -208,10 +209,10 @@ private struct PermissionRequestView: View {
VStack(alignment: .leading, spacing: 16) {
VStack(alignment: .leading, spacing: 8) {
Text(String(localized: "update.popover.enableAutoUpdates", defaultValue: "Enable automatic updates?"))
.font(.system(size: 13, weight: .semibold))
.cmuxFont(size: 13, weight: .semibold)
Text(String(localized: "update.popover.autoUpdatesDescription", defaultValue: "cmux can automatically check for updates in the background."))
.font(.system(size: 11))
.cmuxFont(size: 11)
.foregroundColor(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
@@ -253,7 +254,7 @@ private struct CheckingView: View {
ProgressView()
.controlSize(.small)
Text(String(localized: "update.popover.checking", defaultValue: "Checking for updates…"))
.font(.system(size: 13))
.cmuxFont(size: 13)
}
HStack {
@@ -281,7 +282,7 @@ private struct UpdateAvailableView: View {
VStack(alignment: .leading, spacing: 12) {
VStack(alignment: .leading, spacing: 8) {
Text(String(localized: "update.popover.updateAvailable", defaultValue: "Update Available"))
.font(.system(size: 13, weight: .semibold))
.cmuxFont(size: 13, weight: .semibold)
UpdateMetadataView(item: update.appcastItem, labelWidth: labelWidth)
}
@@ -329,14 +330,14 @@ private struct DownloadingView: View {
VStack(alignment: .leading, spacing: 16) {
VStack(alignment: .leading, spacing: 8) {
Text(String(localized: "update.popover.downloadingUpdate", defaultValue: "Downloading Update"))
.font(.system(size: 13, weight: .semibold))
.cmuxFont(size: 13, weight: .semibold)
if let expectedLength = download.expectedLength, expectedLength > 0 {
let progress = min(1, max(0, Double(download.progress) / Double(expectedLength)))
VStack(alignment: .leading, spacing: 6) {
ProgressView(value: progress)
Text(String(format: "%.0f%%", progress * 100))
.font(.system(size: 11))
.cmuxFont(size: 11)
.foregroundColor(.secondary)
}
} else {
@@ -365,12 +366,12 @@ private struct ExtractingView: View {
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text(String(localized: "update.popover.preparingUpdate", defaultValue: "Preparing Update"))
.font(.system(size: 13, weight: .semibold))
.cmuxFont(size: 13, weight: .semibold)
VStack(alignment: .leading, spacing: 6) {
ProgressView(value: min(1, max(0, extracting.progress)), total: 1.0)
Text(String(format: "%.0f%%", min(1, max(0, extracting.progress)) * 100))
.font(.system(size: 11))
.cmuxFont(size: 11)
.foregroundColor(.secondary)
}
}
@@ -386,10 +387,10 @@ private struct InstallingView: View {
VStack(alignment: .leading, spacing: 16) {
VStack(alignment: .leading, spacing: 8) {
Text(String(localized: "update.popover.restartRequired", defaultValue: "Restart Required"))
.font(.system(size: 13, weight: .semibold))
.cmuxFont(size: 13, weight: .semibold)
Text(String(localized: "update.popover.restartRequired.message", defaultValue: "The update is ready. Please restart the application to complete the installation."))
.font(.system(size: 11))
.cmuxFont(size: 11)
.foregroundColor(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
@@ -425,10 +426,10 @@ private struct NotFoundView: View {
VStack(alignment: .leading, spacing: 16) {
VStack(alignment: .leading, spacing: 8) {
Text(String(localized: "update.popover.noUpdatesFound", defaultValue: "No Updates Found"))
.font(.system(size: 13, weight: .semibold))
.cmuxFont(size: 13, weight: .semibold)
Text(String(localized: "update.popover.noUpdatesFound.message", defaultValue: "You're already running the latest version."))
.font(.system(size: 11))
.cmuxFont(size: 11)
.foregroundColor(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
+772
View File
@@ -92588,6 +92588,778 @@
}
}
},
"emptyPanel.title" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Empty Panel"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "空のパネル"
}
}
}
},
"projectSchemes.badge.personal" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "personal"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "個人"
}
}
}
},
"projectSchemes.badge.shared" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "shared"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "共有"
}
}
}
},
"projectSchemes.detail.archiveTarget" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Archive target"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "アーカイブターゲット"
}
}
}
},
"projectSchemes.detail.environment" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Environment"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "環境"
}
}
}
},
"projectSchemes.detail.launchArguments" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Launch arguments"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "起動引数"
}
}
}
},
"projectSchemes.detail.profileTarget" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Profile target"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "プロファイルターゲット"
}
}
}
},
"projectSchemes.detail.runTarget" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Run target"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "実行ターゲット"
}
}
}
},
"projectSchemes.detail.testTargets" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Test targets"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "テストターゲット"
}
}
}
},
"projectSchemes.detail.visibility" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Visibility"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "表示範囲"
}
}
}
},
"projectSchemes.detail.visibilityPersonal" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Personal (current user)"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "個人(現在のユーザー)"
}
}
}
},
"projectSchemes.detail.visibilityShared" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Shared"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "共有"
}
}
}
},
"projectSchemes.empty.hint" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Pick a scheme on the left to inspect its run / test / profile / archive targets and launch settings."
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "左側のスキームを選択して、実行 / テスト / プロファイル / アーカイブのターゲットと起動設定を確認します。"
}
}
}
},
"projectSchemes.empty.title" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Select a scheme"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "スキームを選択"
}
}
}
},
"projectSchemes.row.runFormat" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "run: %@"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "実行: %@"
}
}
}
},
"projectSchemes.row.testFormat" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "test: %d"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "テスト: %d"
}
}
}
},
"settings.app.globalFontMagnification": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Global Font Magnification"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "グローバルフォント倍率"
}
},
"zh-Hans": {
"stringUnit": {
"state": "translated",
"value": "全局字体放大"
}
},
"zh-Hant": {
"stringUnit": {
"state": "translated",
"value": "全域字型放大"
}
},
"ko": {
"stringUnit": {
"state": "translated",
"value": "전역 글꼴 확대"
}
},
"de": {
"stringUnit": {
"state": "translated",
"value": "Globale Schriftvergrößerung"
}
},
"es": {
"stringUnit": {
"state": "translated",
"value": "Ampliación global de fuente"
}
},
"fr": {
"stringUnit": {
"state": "translated",
"value": "Agrandissement global de la police"
}
},
"it": {
"stringUnit": {
"state": "translated",
"value": "Ingrandimento globale del font"
}
},
"da": {
"stringUnit": {
"state": "translated",
"value": "Global skriftforstørrelse"
}
},
"pl": {
"stringUnit": {
"state": "translated",
"value": "Globalne powiększenie czcionki"
}
},
"ru": {
"stringUnit": {
"state": "translated",
"value": "Глобальное увеличение шрифта"
}
},
"bs": {
"stringUnit": {
"state": "translated",
"value": "Globalno uvećanje fonta"
}
},
"ar": {
"stringUnit": {
"state": "translated",
"value": "تكبير الخط العام"
}
},
"nb": {
"stringUnit": {
"state": "translated",
"value": "Global skriftforstørrelse"
}
},
"pt-BR": {
"stringUnit": {
"state": "translated",
"value": "Ampliação global da fonte"
}
},
"th": {
"stringUnit": {
"state": "translated",
"value": "การขยายแบบอักษรทั่วทั้งแอป"
}
},
"tr": {
"stringUnit": {
"state": "translated",
"value": "Genel yazı tipi büyütme"
}
},
"uk": {
"stringUnit": {
"state": "translated",
"value": "Глобальне збільшення шрифту"
}
},
"km": {
"stringUnit": {
"state": "translated",
"value": "ការពង្រីកពុម្ពអក្សរសកល"
}
}
}
},
"settings.app.globalFontMagnification.subtitleOff": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Scale every font in cmux by the same percentage. 100% = design size."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "cmux のすべてのフォントを同じ割合でスケーリングします。100% = 設計サイズ。"
}
},
"zh-Hans": {
"stringUnit": {
"state": "translated",
"value": "按相同比例缩放 cmux 中的所有字体。100% = 设计尺寸。"
}
},
"zh-Hant": {
"stringUnit": {
"state": "translated",
"value": "以相同比例縮放 cmux 中的所有字型。100% = 設計尺寸。"
}
},
"ko": {
"stringUnit": {
"state": "translated",
"value": "cmux의 모든 글꼴을 같은 비율로 조정합니다. 100% = 디자인 크기."
}
},
"de": {
"stringUnit": {
"state": "translated",
"value": "Skaliert jede Schrift in cmux um denselben Prozentsatz. 100 % = Designgröße."
}
},
"es": {
"stringUnit": {
"state": "translated",
"value": "Escala todas las fuentes de cmux con el mismo porcentaje. 100% = tamaño de diseño."
}
},
"fr": {
"stringUnit": {
"state": "translated",
"value": "Met à l'échelle toutes les polices de cmux avec le même pourcentage. 100 % = taille de conception."
}
},
"it": {
"stringUnit": {
"state": "translated",
"value": "Ridimensiona ogni font in cmux della stessa percentuale. 100% = dimensione di progetto."
}
},
"da": {
"stringUnit": {
"state": "translated",
"value": "Skalerer alle skrifter i cmux med samme procent. 100 % = designstørrelse."
}
},
"pl": {
"stringUnit": {
"state": "translated",
"value": "Skaluje każdą czcionkę w cmux o ten sam procent. 100% = rozmiar projektowy."
}
},
"ru": {
"stringUnit": {
"state": "translated",
"value": "Масштабирует все шрифты в cmux на один и тот же процент. 100% = проектный размер."
}
},
"bs": {
"stringUnit": {
"state": "translated",
"value": "Skalira svaki font u cmux-u istim procentom. 100% = projektovana veličina."
}
},
"ar": {
"stringUnit": {
"state": "translated",
"value": "يضبط حجم كل خط في cmux بالنسبة نفسها. 100% = حجم التصميم."
}
},
"nb": {
"stringUnit": {
"state": "translated",
"value": "Skalerer alle skrifter i cmux med samme prosent. 100 % = designstørrelse."
}
},
"pt-BR": {
"stringUnit": {
"state": "translated",
"value": "Escala todas as fontes no cmux pela mesma porcentagem. 100% = tamanho de design."
}
},
"th": {
"stringUnit": {
"state": "translated",
"value": "ปรับขนาดแบบอักษรทุกตัวใน cmux ด้วยเปอร์เซ็นต์เดียวกัน 100% = ขนาดตามดีไซน์"
}
},
"tr": {
"stringUnit": {
"state": "translated",
"value": "cmux'taki her yazı tipini aynı yüzdeyle ölçekler. %100 = tasarım boyutu."
}
},
"uk": {
"stringUnit": {
"state": "translated",
"value": "Масштабує всі шрифти в cmux на однаковий відсоток. 100% = проєктний розмір."
}
},
"km": {
"stringUnit": {
"state": "translated",
"value": "ពង្រីក/បង្រួមពុម្ពអក្សរទាំងអស់ក្នុង cmux តាមភាគរយដូចគ្នា។ 100% = ទំហំរចនា។"
}
}
}
},
"settings.app.globalFontMagnification.subtitleOn": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Terminals, tabs, and chrome all render at this magnification. Per-pane zoom (⌘= / ⌘-) still overrides for the focused pane."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "ターミナル、タブ、UI をこの倍率で描画します。ペインごとのズーム (⌘= / ⌘-) はフォーカス中のペインに対して引き続き優先されます。"
}
},
"zh-Hans": {
"stringUnit": {
"state": "translated",
"value": "终端、标签页和 chrome 都以此放大比例渲染。单个窗格缩放(⌘= / ⌘-)仍会覆盖当前聚焦窗格。"
}
},
"zh-Hant": {
"stringUnit": {
"state": "translated",
"value": "終端機、分頁和 chrome 都會以此放大倍率算繪。單一窗格縮放(⌘= / ⌘-)仍會覆蓋目前聚焦窗格。"
}
},
"ko": {
"stringUnit": {
"state": "translated",
"value": "터미널, 탭, chrome이 모두 이 확대율로 렌더링됩니다. 패널별 확대/축소(⌘= / ⌘-)는 포커스된 패널에서 계속 우선합니다."
}
},
"de": {
"stringUnit": {
"state": "translated",
"value": "Terminals, Tabs und Chrome werden alle mit dieser Vergrößerung dargestellt. Der Zoom pro Bereich (⌘= / ⌘-) hat für den fokussierten Bereich weiterhin Vorrang."
}
},
"es": {
"stringUnit": {
"state": "translated",
"value": "Terminales, pestañas y chrome se renderizan con esta ampliación. El zoom por panel (⌘= / ⌘-) sigue teniendo prioridad en el panel enfocado."
}
},
"fr": {
"stringUnit": {
"state": "translated",
"value": "Les terminaux, onglets et le chrome s'affichent tous avec cet agrandissement. Le zoom par volet (⌘= / ⌘-) reste prioritaire pour le volet actif."
}
},
"it": {
"stringUnit": {
"state": "translated",
"value": "Terminali, schede e chrome vengono renderizzati con questo ingrandimento. Lo zoom per riquadro (⌘= / ⌘-) continua a prevalere per il riquadro attivo."
}
},
"da": {
"stringUnit": {
"state": "translated",
"value": "Terminaler, faner og chrome gengives alle med denne forstørrelse. Zoom pr. rude (⌘= / ⌘-) tilsidesætter stadig for den fokuserede rude."
}
},
"pl": {
"stringUnit": {
"state": "translated",
"value": "Terminale, karty i chrome renderują się z tym powiększeniem. Powiększenie dla panelu (⌘= / ⌘-) nadal ma pierwszeństwo dla aktywnego panelu."
}
},
"ru": {
"stringUnit": {
"state": "translated",
"value": "Терминалы, вкладки и chrome отображаются с этим увеличением. Масштаб отдельной панели (⌘= / ⌘-) по-прежнему имеет приоритет для панели в фокусе."
}
},
"bs": {
"stringUnit": {
"state": "translated",
"value": "Terminali, kartice i chrome prikazuju se s ovim uvećanjem. Zumiranje po oknu (⌘= / ⌘-) i dalje ima prednost za fokusirano okno."
}
},
"ar": {
"stringUnit": {
"state": "translated",
"value": "تُعرض الطرفيات والتبويبات وواجهة التطبيق بهذا التكبير. يظل تكبير كل جزء (⌘= / ⌘-) متجاوزًا للجزء المركّز عليه."
}
},
"nb": {
"stringUnit": {
"state": "translated",
"value": "Terminaler, faner og chrome gjengis alle med denne forstørrelsen. Zoom per rute (⌘= / ⌘-) overstyrer fortsatt for den fokuserte ruten."
}
},
"pt-BR": {
"stringUnit": {
"state": "translated",
"value": "Terminais, abas e chrome são renderizados com esta ampliação. O zoom por painel (⌘= / ⌘-) ainda prevalece no painel em foco."
}
},
"th": {
"stringUnit": {
"state": "translated",
"value": "เทอร์มินัล แท็บ และ chrome จะแสดงผลด้วยการขยายนี้ทั้งหมด การซูมเฉพาะพาเนล (⌘= / ⌘-) ยังคงมีผลเหนือกว่าสำหรับพาเนลที่โฟกัสอยู่"
}
},
"tr": {
"stringUnit": {
"state": "translated",
"value": "Terminaller, sekmeler ve chrome bu büyütmeyle işlenir. Bölme başına yakınlaştırma (⌘= / ⌘-) odaktaki bölme için hâlâ önceliklidir."
}
},
"uk": {
"stringUnit": {
"state": "translated",
"value": "Термінали, вкладки та chrome відображаються з цим збільшенням. Масштаб окремої панелі (⌘= / ⌘-) і далі має пріоритет для панелі у фокусі."
}
},
"km": {
"stringUnit": {
"state": "translated",
"value": "ស្ថានីយ ផ្ទាំង និង chrome ត្រូវបានបង្ហាញតាមការពង្រីកនេះទាំងអស់។ ការពង្រីកតាមផ្ទាំង (⌘= / ⌘-) នៅតែជំនួសសម្រាប់ផ្ទាំងដែលកំពុងផ្តោត។"
}
}
}
},
"settings.app.globalFontMagnification.reset": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Reset"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "リセット"
}
},
"zh-Hans": {
"stringUnit": {
"state": "translated",
"value": "重置"
}
},
"zh-Hant": {
"stringUnit": {
"state": "translated",
"value": "重設"
}
},
"ko": {
"stringUnit": {
"state": "translated",
"value": "재설정"
}
},
"de": {
"stringUnit": {
"state": "translated",
"value": "Zurücksetzen"
}
},
"es": {
"stringUnit": {
"state": "translated",
"value": "Restablecer"
}
},
"fr": {
"stringUnit": {
"state": "translated",
"value": "Réinitialiser"
}
},
"it": {
"stringUnit": {
"state": "translated",
"value": "Ripristina"
}
},
"da": {
"stringUnit": {
"state": "translated",
"value": "Nulstil"
}
},
"pl": {
"stringUnit": {
"state": "translated",
"value": "Resetuj"
}
},
"ru": {
"stringUnit": {
"state": "translated",
"value": "Сбросить"
}
},
"bs": {
"stringUnit": {
"state": "translated",
"value": "Resetuj"
}
},
"ar": {
"stringUnit": {
"state": "translated",
"value": "إعادة تعيين"
}
},
"nb": {
"stringUnit": {
"state": "translated",
"value": "Tilbakestill"
}
},
"pt-BR": {
"stringUnit": {
"state": "translated",
"value": "Redefinir"
}
},
"th": {
"stringUnit": {
"state": "translated",
"value": "รีเซ็ต"
}
},
"tr": {
"stringUnit": {
"state": "translated",
"value": "Sıfırla"
}
},
"uk": {
"stringUnit": {
"state": "translated",
"value": "Скинути"
}
},
"km": {
"stringUnit": {
"state": "translated",
"value": "កំណត់ឡើងវិញ"
}
}
}
},
"settings.app.newWorkspacePlacement": {
"extractionState": "manual",
"localizations": {
+16 -3
View File
@@ -1,5 +1,6 @@
import AppKit
import Combine
import CmuxFoundation
import Foundation
@MainActor
@@ -17,6 +18,7 @@ final class MenuBarExtraController: NSObject, NSMenuDelegate {
private let onOpenPreferences: () -> Void
private let onQuitApp: () -> Void
private var notificationMenuSnapshotCancellable: AnyCancellable?
private var globalFontObserver: NSObjectProtocol?
private let buildHintTitle: String?
private let stateHintItem = NSMenuItem(title: String(localized: "statusMenu.noUnread", defaultValue: "No unread notifications"), action: nil, keyEquivalent: "")
@@ -75,6 +77,13 @@ final class MenuBarExtraController: NSObject, NSMenuDelegate {
.sink { [weak self] snapshot in
self?.refreshUI(snapshot: snapshot)
}
globalFontObserver = NotificationCenter.default.addObserver(
forName: GlobalFontMagnification.didChangeNotification,
object: nil,
queue: .main
) { [weak self] _ in
MainActor.assumeIsolated { self?.refreshUI() }
}
refreshUI()
}
@@ -153,6 +162,10 @@ final class MenuBarExtraController: NSObject, NSMenuDelegate {
func removeFromMenuBar() {
notificationMenuSnapshotCancellable?.cancel()
notificationMenuSnapshotCancellable = nil
if let globalFontObserver {
NotificationCenter.default.removeObserver(globalFontObserver)
self.globalFontObserver = nil
}
statusItem.menu = nil
NSStatusBar.system.removeStatusItem(statusItem)
}
@@ -392,7 +405,7 @@ enum MenuBarNotificationLineFormatter {
return NSAttributedString(
string: menuTitle(notification: notification, tabTitle: tabTitle),
attributes: [
.font: NSFont.menuFont(ofSize: NSFont.systemFontSize),
.font: GlobalFontMagnification.menuFont(ofSize: NSFont.systemFontSize),
.foregroundColor: NSColor.labelColor,
.paragraphStyle: paragraph,
]
@@ -406,7 +419,7 @@ enum MenuBarNotificationLineFormatter {
private static func wrappedAndTruncated(_ text: String, maxWidth: CGFloat, maxLines: Int) -> String {
let width = max(60, maxWidth)
let lines = max(1, maxLines)
let font = NSFont.menuFont(ofSize: NSFont.systemFontSize)
let font = GlobalFontMagnification.menuFont(ofSize: NSFont.systemFontSize)
let wrapped = wrappedLines(for: text, maxWidth: width, font: font)
guard wrapped.count > lines else { return wrapped.joined(separator: "\n") }
@@ -729,7 +742,7 @@ enum MenuBarIconRenderer {
paragraph.alignment = .center
let fontSize: CGFloat = text.count > 1 ? config.multiDigitFontSize : config.singleDigitFontSize
let attrs: [NSAttributedString.Key: Any] = [
.font: NSFont.systemFont(ofSize: fontSize, weight: .bold),
.font: NSFont.systemFont(ofSize: fontSize, weight: .bold), // Fixed 18x18 status-item bitmap.
.foregroundColor: NSColor.systemBlue,
.paragraphStyle: paragraph,
]
+4 -3
View File
@@ -1,4 +1,5 @@
import AppKit
import CmuxFoundation
import CmuxSettings
import Bonsplit
import Foundation
@@ -807,7 +808,7 @@ extension AppDelegate {
localized: "dialog.sshURL.commandLabel",
defaultValue: "Command preview:"
))
commandLabel.font = .systemFont(ofSize: NSFont.smallSystemFontSize, weight: .semibold)
commandLabel.font = GlobalFontMagnification.systemFont(ofSize: NSFont.smallSystemFontSize, weight: .semibold)
let socketPath = CmuxSSHURLProcessLauncher.shared.resolvedSocketPath()
let commandScrollView = cmuxSSHURLTextPreview(request.cliPreview(socketPath: socketPath), height: 80)
@@ -873,7 +874,7 @@ extension AppDelegate {
localized: "dialog.textURL.previewLabel",
defaultValue: "Text preview:"
))
previewLabel.font = .systemFont(ofSize: NSFont.smallSystemFontSize, weight: .semibold)
previewLabel.font = GlobalFontMagnification.systemFont(ofSize: NSFont.smallSystemFontSize, weight: .semibold)
let preview = cmuxSSHURLTextPreview(request.pasteText, height: 180)
@@ -909,7 +910,7 @@ extension AppDelegate {
textView.drawsBackground = true
textView.backgroundColor = NSColor.textBackgroundColor
textView.textColor = NSColor.labelColor
textView.font = NSFont.monospacedSystemFont(ofSize: NSFont.smallSystemFontSize, weight: .regular)
textView.font = GlobalFontMagnification.monospacedSystemFont(ofSize: NSFont.smallSystemFontSize, weight: .regular)
textView.textContainerInset = NSSize(width: 8, height: 8)
textView.isHorizontallyResizable = false
textView.isVerticallyResizable = true
+20
View File
@@ -831,6 +831,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
var activeConfiguredShortcutChordPrefixForCurrentEvent: ShortcutStroke?
var shortcutEventFocusContextCache: ShortcutEventFocusContextCache?
private var ghosttyConfigObserver: NSObjectProtocol?
private var globalFontMagnificationObserver: NSObjectProtocol?
private var ghosttyGotoSplitLeftShortcut: StoredShortcut?
private var ghosttyGotoSplitRightShortcut: StoredShortcut?
private var ghosttyGotoSplitUpShortcut: StoredShortcut?
@@ -1450,6 +1451,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
installMainWindowKeyObserver()
refreshGhosttyGotoSplitShortcuts()
installGhosttyConfigObserver()
installGlobalFontMagnificationObserver()
installWindowResponderSwizzles()
installBrowserAddressBarFocusObservers()
installShortcutMonitor()
@@ -8377,6 +8379,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
// optional, so a nil runtime just leaves reads at their seeded
// catalog default.
.environment(\.settingsRuntime, settingsRuntime)
.cmuxFontMagnificationEnvironment()
// Use the current key window's size for new windows so Cmd+Shift+N
// creates a window matching the previous one's dimensions.
@@ -12320,6 +12323,23 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
}
}
private func installGlobalFontMagnificationObserver() {
guard globalFontMagnificationObserver == nil else { return }
globalFontMagnificationObserver = NotificationCenter.default.addObserver(
forName: GlobalFontMagnification.didChangeNotification,
object: nil,
queue: .main
) { [weak self] _ in
GhosttyConfig.invalidateLoadCache()
MainActor.assumeIsolated {
self?.reloadConfiguration(
source: "globalFontMagnificationDidChange",
reloadSettingsFromFile: false
)
}
}
}
@objc func reloadConfigurationMenuItem(_ sender: Any?) {
reloadConfiguration(source: "menu.reload_configuration")
}
+4 -3
View File
@@ -1,3 +1,4 @@
import CmuxFoundation
import AppKit
import Bonsplit
import Foundation
@@ -229,7 +230,7 @@ private struct BonsplitTabBarDebugView: View {
var body: some View {
VStack(alignment: .leading, spacing: 14) {
Text(String(localized: "debug.bonsplitTabBarDebug.heading", defaultValue: "Bonsplit Tab Bar"))
.font(.headline)
.cmuxFont(.headline)
GroupBox(String(localized: "debug.bonsplitTabBarDebug.actionLaneGeometry", defaultValue: "Action Lane Geometry")) {
VStack(alignment: .leading, spacing: 10) {
@@ -280,7 +281,7 @@ private struct BonsplitTabBarDebugView: View {
}
Text(verbatim: BonsplitTabBarDebugSettings.currentTuningDescription())
.font(.system(.caption, design: .monospaced))
.cmuxFont(.caption, design: .monospaced)
.textSelection(.enabled)
.lineLimit(3)
.frame(maxWidth: .infinity, alignment: .leading)
@@ -356,7 +357,7 @@ private struct BonsplitTabBarDebugSliderRow: View {
step: setting.step
)
Text(pixelValueText)
.font(.caption)
.cmuxFont(.caption)
.monospacedDigit()
.frame(width: 76, alignment: .trailing)
}
@@ -1,3 +1,4 @@
import CmuxFoundation
@_spi(CmuxHostTransport) import CmuxSidebar
@_spi(CmuxHostTransport) import CmuxExtensionKit
import AppKit
@@ -208,7 +209,7 @@ struct CMUXInstalledExtensionSidebarHostView: View {
ProgressView()
.controlSize(.small)
Text(String(localized: "sidebar.extensions.loading", defaultValue: "Loading sidebar extensions"))
.font(.system(size: 12))
.cmuxFont(size: 12)
.foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
@@ -217,7 +218,7 @@ struct CMUXInstalledExtensionSidebarHostView: View {
} else {
VStack(spacing: 16) {
Image(systemName: "puzzlepiece.extension")
.font(.system(size: 26, weight: .regular))
.cmuxFont(size: 26, weight: .regular)
.foregroundStyle(.secondary)
.frame(width: 60, height: 60)
.background(
@@ -226,17 +227,17 @@ struct CMUXInstalledExtensionSidebarHostView: View {
)
VStack(spacing: 6) {
Text(emptyStateTitle)
.font(.system(size: 14, weight: .semibold))
.cmuxFont(size: 14, weight: .semibold)
.foregroundStyle(.primary)
.multilineTextAlignment(.center)
Text(errorText ?? emptyStateDetail)
.font(.system(size: 12))
.cmuxFont(size: 12)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.fixedSize(horizontal: false, vertical: true)
if disabledExtensionCount > 0 || unapprovedExtensionCount > 0 {
Text(extensionAvailabilityDetail)
.font(.system(size: 12))
.cmuxFont(size: 12)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.fixedSize(horizontal: false, vertical: true)
@@ -418,13 +419,13 @@ struct CMUXInstalledExtensionSidebarHostView: View {
VStack(alignment: .leading, spacing: 12) {
HStack(spacing: 8) {
Image(systemName: "puzzlepiece.extension")
.font(.system(size: 18, weight: .medium))
.cmuxFont(size: 18, weight: .medium)
VStack(alignment: .leading, spacing: 2) {
Text(activeIdentity?.localizedName ?? String(localized: "sidebar.provider.extensions.title", defaultValue: "Extension Sidebar"))
.font(.system(size: 13, weight: .semibold))
.cmuxFont(size: 13, weight: .semibold)
.lineLimit(1)
Text(String(localized: "sidebar.extensions.details.runtime", defaultValue: "Secure extension connection"))
.font(.system(size: 11))
.cmuxFont(size: 11)
.foregroundStyle(.secondary)
}
}
@@ -456,7 +457,7 @@ struct CMUXInstalledExtensionSidebarHostView: View {
} else if let blockedManifestReason {
Divider()
Text(blockedDetailText(reason: blockedManifestReason))
.font(.system(size: 11))
.cmuxFont(size: 11)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
@@ -502,12 +503,12 @@ struct CMUXInstalledExtensionSidebarHostView: View {
private func blockedExtensionView(reason: String) -> some View {
VStack(alignment: .leading, spacing: 8) {
Image(systemName: "exclamationmark.triangle")
.font(.system(size: 20, weight: .regular))
.cmuxFont(size: 20, weight: .regular)
.foregroundStyle(.secondary)
Text(String(localized: "sidebar.extensions.blocked.title", defaultValue: "Extension Blocked"))
.font(.system(size: 13, weight: .semibold))
.cmuxFont(size: 13, weight: .semibold)
Text(blockedDetailText(reason: reason))
.font(.system(size: 12))
.cmuxFont(size: 12)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
ViewThatFits(in: .horizontal) {
@@ -593,11 +594,11 @@ struct CMUXInstalledExtensionSidebarHostView: View {
private func detailRow(title: String, value: String) -> some View {
HStack(alignment: .firstTextBaseline, spacing: 8) {
Text(title)
.font(.system(size: 11, weight: .medium))
.cmuxFont(size: 11, weight: .medium)
.foregroundStyle(.secondary)
.frame(width: 64, alignment: .leading)
Text(value)
.font(.system(size: 11))
.cmuxFont(size: 11)
.foregroundStyle(.primary)
.lineLimit(2)
.textSelection(.enabled)
@@ -607,7 +608,7 @@ struct CMUXInstalledExtensionSidebarHostView: View {
private func permissionSection(effectiveGrant: CMUXSidebarExtensionEffectiveGrant) -> some View {
VStack(alignment: .leading, spacing: 8) {
Text(String(localized: "sidebar.extensions.details.permissions", defaultValue: "Permissions"))
.font(.system(size: 12, weight: .semibold))
.cmuxFont(size: 12, weight: .semibold)
ForEach(effectiveGrant.manifest.readScopes, id: \.self) { scope in
permissionRow(
title: scope.displayName,
@@ -628,14 +629,14 @@ struct CMUXInstalledExtensionSidebarHostView: View {
private func permissionRow(title: String, detail: String, isGranted: Bool) -> some View {
HStack(alignment: .top, spacing: 6) {
Image(systemName: isGranted ? "checkmark.circle.fill" : "circle")
.font(.system(size: 11, weight: .medium))
.cmuxFont(size: 11, weight: .medium)
.foregroundStyle(isGranted ? .green : .secondary)
.padding(.top, 1)
VStack(alignment: .leading, spacing: 1) {
Text(title)
.font(.system(size: 11, weight: .medium))
.cmuxFont(size: 11, weight: .medium)
Text(detail)
.font(.system(size: 10))
.cmuxFont(size: 10)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
@@ -643,7 +644,7 @@ struct CMUXInstalledExtensionSidebarHostView: View {
Text(isGranted
? String(localized: "sidebar.extensions.details.granted", defaultValue: "Granted")
: String(localized: "sidebar.extensions.details.pending", defaultValue: "Pending"))
.font(.system(size: 10, weight: .medium))
.cmuxFont(size: 10, weight: .medium)
.foregroundStyle(.secondary)
}
}
@@ -676,7 +677,7 @@ struct CMUXInstalledExtensionSidebarHostView: View {
activeIdentity?.localizedName ?? String(localized: "sidebar.provider.extensions.title", defaultValue: "Extension Sidebar"),
systemImage: "puzzlepiece.extension"
)
.font(.system(size: 12, weight: .semibold))
.cmuxFont(size: 12, weight: .semibold)
.foregroundStyle(.secondary)
.lineLimit(1)
}
@@ -701,19 +702,19 @@ struct CMUXInstalledExtensionSidebarHostView: View {
) -> some View {
VStack(alignment: .leading, spacing: 8) {
Text(String(localized: "sidebar.extensions.access.title", defaultValue: "Limited extension access"))
.font(.system(size: 12, weight: .semibold))
.cmuxFont(size: 12, weight: .semibold)
.foregroundStyle(.primary)
Text(String.localizedStringWithFormat(
String(localized: "sidebar.extensions.access.detail", defaultValue: "%@ will not receive workspace data or run actions until you grant its requested access."),
effectiveGrant.manifest.displayName
))
.font(.system(size: 11))
.cmuxFont(size: 11)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
VStack(alignment: .leading, spacing: 4) {
ForEach(pendingPermissionDescriptions(effectiveGrant: effectiveGrant), id: \.self) { description in
Label(description, systemImage: "circle")
.font(.system(size: 11))
.cmuxFont(size: 11)
.foregroundStyle(.secondary)
.labelStyle(.titleAndIcon)
}
@@ -760,22 +761,22 @@ struct CMUXInstalledExtensionSidebarHostView: View {
VStack(alignment: .leading, spacing: 14) {
HStack(spacing: 10) {
Image(systemName: "puzzlepiece.extension")
.font(.system(size: 22, weight: .medium))
.cmuxFont(size: 22, weight: .medium)
VStack(alignment: .leading, spacing: 2) {
Text(String.localizedStringWithFormat(
String(localized: "sidebar.extensions.access.review.title", defaultValue: "Review access for %@"),
effectiveGrant.manifest.displayName
))
.font(.system(size: 15, weight: .semibold))
.cmuxFont(size: 15, weight: .semibold)
Text(identity.bundleIdentifier)
.font(.system(size: 11))
.cmuxFont(size: 11)
.foregroundStyle(.secondary)
.textSelection(.enabled)
}
}
Text(String(localized: "sidebar.extensions.access.review.detail", defaultValue: "CMUX will only share the following data and actions if you allow this request."))
.font(.system(size: 12))
.cmuxFont(size: 12)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
+11 -1
View File
@@ -1,5 +1,6 @@
@_spi(CmuxHostTransport) import CmuxSidebar
import AppKit
import CmuxFoundation
import SwiftUI
@MainActor
@@ -116,6 +117,7 @@ private final class CMUXSidebarExtensionBrowserContainerViewController: NSViewCo
private var cardHorizontalSafetyConstraints: [NSLayoutConstraint] = []
private var cardBottomConstraint: NSLayoutConstraint?
private var browserConstraints: [NSLayoutConstraint] = []
private var fontMagnificationObserver: GlobalFontMagnificationChangeObserver?
init(
browserViewController: NSViewController,
@@ -162,13 +164,16 @@ private final class CMUXSidebarExtensionBrowserContainerViewController: NSViewCo
compactLabel.translatesAutoresizingMaskIntoConstraints = false
compactLabel.alignment = .center
compactLabel.font = .systemFont(ofSize: 13, weight: .medium)
applyFonts()
compactLabel.textColor = .secondaryLabelColor
compactLabel.maximumNumberOfLines = 0
compactLabel.lineBreakMode = .byWordWrapping
compactLabel.cell?.wraps = true
compactLabel.cell?.usesSingleLineMode = false
compactLabel.isHidden = true
fontMagnificationObserver = GlobalFontMagnificationChangeObserver { [weak self] in
self?.applyFonts()
}
rootView.addSubview(cardView)
cardView.addSubview(contentView)
@@ -204,6 +209,11 @@ private final class CMUXSidebarExtensionBrowserContainerViewController: NSViewCo
attachBrowserIfNeeded()
}
private func applyFonts() {
compactLabel.font = GlobalFontMagnification.systemFont(ofSize: 13, weight: .medium)
compactLabel.invalidateIntrinsicContentSize()
}
func attachBrowserIfNeeded() {
guard isViewLoaded else { return }
+2 -1
View File
@@ -1,5 +1,6 @@
import AppKit
import Bonsplit
import CmuxFoundation
struct CmuxConfigUIDefinition: Codable, Sendable, Hashable {
var newWorkspace: CmuxConfigButtonPlacement?
@@ -250,7 +251,7 @@ extension CmuxButtonIcon {
private static func contextMenuEmojiImage(_ value: String, scale: Double) -> NSImage? {
let clampedScale = min(max(scale, 0.25), 4)
let font = NSFont.systemFont(ofSize: CGFloat(16.0 * clampedScale))
let font = GlobalFontMagnification.systemFont(ofSize: CGFloat(16.0 * clampedScale))
let attributedString = NSAttributedString(string: value, attributes: [.font: font])
let measuredSize = attributedString.size()
let imageSize = NSSize(
@@ -1,3 +1,4 @@
import CmuxFoundation
import Observation
import SwiftUI
@@ -93,7 +94,7 @@ struct CommandPaletteCommandListRowsView: View {
if state.rows.isEmpty {
if state.shouldShowEmptyState {
Text(state.emptyStateText)
.font(.system(size: 13, weight: .regular))
.cmuxFont(size: 13, weight: .regular)
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 12)
+78 -60
View File
@@ -2110,7 +2110,7 @@ struct ContentView: View {
}
Text(titlebarText)
.font(.system(size: 13, weight: .bold))
.cmuxFont(size: 13, weight: .bold)
.foregroundColor(fakeTitlebarTextColor(appearance: appearance))
.lineLimit(1)
.allowsHitTesting(false)
@@ -3567,7 +3567,7 @@ struct ContentView: View {
case .singleLine(let accessibilityIdentifier, let focus, let onDeleteBackward):
TextField(placeholder, text: text)
.textFieldStyle(.plain)
.font(.system(size: 13, weight: .regular))
.cmuxFont(size: 13, weight: .regular)
.tint(Color(nsColor: sidebarActiveForegroundNSColor(opacity: 1.0)))
.focused(focus)
.accessibilityIdentifier(accessibilityIdentifier)
@@ -3616,7 +3616,7 @@ struct ContentView: View {
Divider()
Text(renameInputHintText(target: target))
.font(.system(size: 11))
.cmuxFont(size: 11)
.foregroundStyle(.secondary)
.lineLimit(1)
.frame(maxWidth: .infinity, alignment: .leading)
@@ -3648,7 +3648,7 @@ struct ContentView: View {
return VStack(spacing: 0) {
Text(nextName)
.font(.system(size: 13, weight: .regular))
.cmuxFont(size: 13, weight: .regular)
.lineLimit(1)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 9)
@@ -3657,7 +3657,7 @@ struct ContentView: View {
Divider()
Text(renameConfirmHintText(target: target))
.font(.system(size: 11))
.cmuxFont(size: 11)
.foregroundStyle(.secondary)
.lineLimit(1)
.frame(maxWidth: .infinity, alignment: .leading)
@@ -3706,7 +3706,7 @@ struct ContentView: View {
Divider()
Text(target.inputHint)
.font(.system(size: 11))
.cmuxFont(size: 11)
.foregroundStyle(.secondary)
.lineLimit(1)
.frame(maxWidth: .infinity, alignment: .leading)
@@ -3783,6 +3783,7 @@ struct ContentView: View {
let onEscape: () -> Void
let onMoveSelection: (Int) -> Void
let onUnhandledNavigationKey: (NSEvent) -> Bool
@Environment(\.cmuxGlobalFontMagnificationPercent) private var globalFontPercent
@MainActor final class Coordinator: NSObject, NSTextFieldDelegate {
var parent: CommandPaletteSearchFieldRepresentable
@@ -3906,7 +3907,7 @@ struct ContentView: View {
func makeNSView(context: Context) -> CommandPaletteNativeTextField {
let field = CommandPaletteNativeTextField(frame: .zero)
field.font = .systemFont(ofSize: 13)
field.font = GlobalFontMagnification.systemFont(ofSize: 13)
field.placeholderString = placeholder
field.setAccessibilityIdentifier("CommandPaletteSearchField")
field.delegate = context.coordinator
@@ -3925,6 +3926,7 @@ struct ContentView: View {
context.coordinator.parent = self
context.coordinator.parentField = nsView
nsView.placeholderString = placeholder
nsView.font = GlobalFontMagnification.systemFont(ofSize: 13)
if let editor = nsView.currentEditor() as? NSTextView {
context.coordinator.attachEditorTextDidChangeObserverIfNeeded(editor)
@@ -4106,16 +4108,19 @@ struct ContentView: View {
}
private final class CommandPaletteMultilineTextEditorView: NSView {
private static let font = NSFont.systemFont(ofSize: 13)
private static var font: NSFont {
GlobalFontMagnification.systemFont(ofSize: 13)
}
private static let textInset = NSSize(width: 0, height: 2)
static let defaultMinimumHeight: CGFloat = {
static var defaultMinimumHeight: CGFloat {
let lineHeight = ceil(font.ascender - font.descender + font.leading)
return lineHeight * 5 + textInset.height * 2
}()
}
private let scrollView = NSScrollView(frame: .zero)
let textView = CommandPaletteMultilineTextView(frame: .zero)
private let placeholderField = CommandPalettePassthroughLabel(labelWithString: "")
private var fontMagnificationObserver: GlobalFontMagnificationChangeObserver?
var onMeasuredHeightChange: ((CGFloat) -> Void)?
private var lastReportedHeight: CGFloat?
var maximumHeight: CGFloat = .greatestFiniteMagnitude {
@@ -4151,7 +4156,7 @@ struct ContentView: View {
textView.isVerticallyResizable = true
textView.backgroundColor = .clear
textView.drawsBackground = false
textView.font = Self.font
applyFonts()
textView.textColor = .labelColor
textView.insertionPointColor = .labelColor
textView.textContainerInset = Self.textInset
@@ -4166,7 +4171,6 @@ struct ContentView: View {
scrollView.documentView = textView
placeholderField.translatesAutoresizingMaskIntoConstraints = false
placeholderField.font = Self.font
placeholderField.textColor = .secondaryLabelColor
placeholderField.lineBreakMode = .byWordWrapping
placeholderField.maximumNumberOfLines = 0
@@ -4178,6 +4182,10 @@ struct ContentView: View {
name: NSText.didChangeNotification,
object: textView
)
fontMagnificationObserver = GlobalFontMagnificationChangeObserver { [weak self] in
self?.applyFonts()
self?.refreshMetrics()
}
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: topAnchor),
@@ -4201,6 +4209,13 @@ struct ContentView: View {
NotificationCenter.default.removeObserver(self)
}
private func applyFonts() {
let font = Self.font
textView.font = font
placeholderField.font = font
textView.minSize = NSSize(width: 0, height: Self.defaultMinimumHeight)
}
override func layout() {
super.layout()
updateTextViewLayout()
@@ -4311,7 +4326,9 @@ struct ContentView: View {
}
private struct CommandPaletteMultilineTextEditorRepresentable: NSViewRepresentable {
static let defaultMinimumHeight = CommandPaletteMultilineTextEditorView.defaultMinimumHeight
static var defaultMinimumHeight: CGFloat {
CommandPaletteMultilineTextEditorView.defaultMinimumHeight
}
let placeholder: String
let accessibilityLabel: String
@@ -4322,6 +4339,7 @@ struct ContentView: View {
let maxHeight: CGFloat
let onSubmit: (String) -> Void
let onEscape: () -> Void
@Environment(\.cmuxGlobalFontMagnificationPercent) private var globalFontPercent
final class Coordinator: NSObject, NSTextViewDelegate {
var parent: CommandPaletteMultilineTextEditorRepresentable
@@ -5192,7 +5210,7 @@ struct ContentView: View {
switch trailingLabel.style {
case .shortcut:
Text(trailingLabel.text)
.font(.system(size: 11, weight: .medium))
.cmuxFont(size: 11, weight: .medium)
.foregroundStyle(.secondary)
.padding(.horizontal, 4)
.padding(.vertical, 1)
@@ -5202,7 +5220,7 @@ struct ContentView: View {
)
case .kind:
Text(trailingLabel.text)
.font(.system(size: 11, weight: .regular))
.cmuxFont(size: 11, weight: .regular)
.foregroundStyle(.secondary)
.lineLimit(1)
}
@@ -5219,7 +5237,7 @@ struct ContentView: View {
title,
matchedIndices: matchedIndices
)
.font(.system(size: 13, weight: .regular))
.cmuxFont(size: 13, weight: .regular)
.lineLimit(1)
Spacer()
commandPaletteRenderTrailingLabelView(trailingLabel)
@@ -11341,10 +11359,10 @@ struct VerticalTabsSidebar: View {
Button(action: onNewTab) {
HStack(spacing: 9) {
Image(systemName: "plus")
.font(.system(size: 15, weight: .regular))
.cmuxFont(size: 15, weight: .regular)
.frame(width: 22, height: 22)
Text(String(localized: "sidebar.browserStack.newTab", defaultValue: "New Tab"))
.font(.system(size: 13, weight: .regular))
.cmuxFont(size: 13, weight: .regular)
Spacer(minLength: 0)
}
.foregroundColor(.secondary)
@@ -11378,14 +11396,14 @@ struct VerticalTabsSidebar: View {
VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 8) {
Image(systemName: "folder.fill")
.font(.system(size: 14, weight: .regular))
.cmuxFont(size: 14, weight: .regular)
.foregroundColor(.secondary)
Text(extensionSidebarTreeSectionTitle(section.treeSection))
.font(.system(size: 13, weight: .semibold))
.cmuxFont(size: 13, weight: .semibold)
.foregroundColor(.primary.opacity(0.86))
.lineLimit(1)
Image(systemName: "chevron.down")
.font(.system(size: 11, weight: .medium))
.cmuxFont(size: 11, weight: .medium)
.foregroundColor(.secondary)
Spacer(minLength: 0)
}
@@ -11502,14 +11520,14 @@ struct VerticalTabsSidebar: View {
HStack(spacing: 9) {
extensionBrowserStackIcon(row.leadingIcon, size: compact ? 22 : 24)
Text(row.title)
.font(.system(size: compact ? 12.5 : 13, weight: .medium))
.cmuxFont(size: compact ? 12.5 : 13, weight: .medium)
.foregroundColor(isSelected ? .primary : .primary.opacity(0.82))
.lineLimit(1)
.truncationMode(.tail)
Spacer(minLength: 0)
if let trailing = extensionSidebarRenderedText(row.trailingText, now: now) {
Text(trailing)
.font(.system(size: 11, weight: .regular))
.cmuxFont(size: 11, weight: .regular)
.foregroundColor(.secondary)
.lineLimit(1)
}
@@ -11678,11 +11696,11 @@ struct VerticalTabsSidebar: View {
}
if let systemImageName = icon?.systemImageName {
Image(systemName: systemImageName)
.font(.system(size: size * 0.58, weight: .semibold))
.cmuxFont(size: size * 0.58, weight: .semibold)
.foregroundColor(foreground)
} else {
Text(icon?.text ?? ".")
.font(.system(size: size * 0.58, weight: .bold))
.cmuxFont(size: size * 0.58, weight: .bold)
.foregroundColor(foreground)
}
}
@@ -11737,14 +11755,14 @@ struct VerticalTabsSidebar: View {
}
} label: {
Image(systemName: isCollapsed ? "folder" : "folder.fill")
.font(.system(size: 13, weight: .regular))
.cmuxFont(size: 13, weight: .regular)
.offset(y: -0.5)
}
.buttonStyle(.plain)
.safeHelp(String(localized: "sidebar.extension.toggleSection", defaultValue: "Toggle section"))
Text(extensionSidebarTreeSectionTitle(section.treeSection))
.font(.system(size: 12, weight: .regular))
.cmuxFont(size: 12, weight: .regular)
.foregroundColor(.secondary)
.lineLimit(1)
.truncationMode(.tail)
@@ -11756,7 +11774,7 @@ struct VerticalTabsSidebar: View {
createExtensionWorktreeWorkspace(for: section.treeSection)
} label: {
Image(systemName: extensionSidebarWorktreeCreationInFlightSectionIds.contains(section.id) ? "clock" : "plus")
.font(.system(size: 11, weight: .regular))
.cmuxFont(size: 11, weight: .regular)
.frame(width: 18, height: 18)
}
.buttonStyle(.plain)
@@ -12418,7 +12436,7 @@ private struct SidebarFooterButtons: View {
} label: {
Image(systemName: "puzzlepiece.extension")
.symbolRenderingMode(.monochrome)
.font(.system(size: 12, weight: .medium))
.cmuxFont(size: 12, weight: .medium)
.foregroundStyle(Color(nsColor: .secondaryLabelColor))
.frame(width: 22, height: 22, alignment: .center)
}
@@ -12588,7 +12606,7 @@ private struct SidebarHelpMenuButton: View {
} label: {
HStack(spacing: 8) {
Text(title)
.font(.system(size: 12))
.cmuxFont(size: 12)
Spacer(minLength: 0)
if let shortcutHint {
helpOptionShortcutHint(text: shortcutHint)
@@ -12612,7 +12630,7 @@ private struct SidebarHelpMenuButton: View {
Text(text)
.lineLimit(1)
.fixedSize(horizontal: true, vertical: false)
.font(.system(size: 10, weight: .regular, design: .rounded))
.cmuxFont(size: 10, weight: .regular, design: .rounded)
.monospacedDigit()
.foregroundStyle(Color(nsColor: .secondaryLabelColor))
}
@@ -12724,7 +12742,7 @@ private struct SidebarDevFooter: View {
SidebarFooterButtons(updateViewModel: updateViewModel, fileExplorerState: fileExplorerState, onSendFeedback: onSendFeedback)
if showSidebarDevBuildBanner {
Text(String(localized: "debug.devBuildBanner.title", defaultValue: "THIS IS A DEV BUILD"))
.font(.system(size: 11, weight: .semibold))
.cmuxFont(size: 11, weight: .semibold)
.foregroundColor(.red)
}
}
@@ -13248,7 +13266,7 @@ struct TabItemView: View, Equatable {
VStack(alignment: .leading, spacing: 2) {
HStack(spacing: 6) {
Text(remoteWorkspaceSidebarText)
.font(.system(size: scaledFontSize(10), design: .monospaced))
.cmuxFont(size: scaledFontSize(10), design: .monospaced)
.foregroundColor(activeSecondaryColor(0.8))
.lineLimit(1)
.truncationMode(.middle)
@@ -13256,7 +13274,7 @@ struct TabItemView: View, Equatable {
Spacer(minLength: 0)
Text(workspaceSnapshot.remoteConnectionStatusText)
.font(.system(size: scaledFontSize(9), weight: .medium))
.cmuxFont(size: scaledFontSize(9), weight: .medium)
.foregroundColor(activeSecondaryColor(0.58))
.lineLimit(1)
@@ -13269,7 +13287,7 @@ struct TabItemView: View, Equatable {
systemImage: "arrow.clockwise"
)
.labelStyle(.titleAndIcon)
.font(.system(size: scaledFontSize(9), weight: .semibold))
.cmuxFont(size: scaledFontSize(9), weight: .semibold)
}
.buttonStyle(.borderless)
.foregroundColor(activeSecondaryColor(0.9))
@@ -13372,7 +13390,7 @@ struct TabItemView: View, Equatable {
Circle()
.fill(activeUnreadBadgeFillColor)
Text("\(unreadCount)")
.font(.system(size: scaledFontSize(9), weight: .semibold))
.cmuxFont(size: scaledFontSize(9), weight: .semibold)
.foregroundColor(activeUnreadBadgeTextColor)
}
.frame(width: scaledUnreadBadgeSize, height: scaledUnreadBadgeSize)
@@ -13380,7 +13398,7 @@ struct TabItemView: View, Equatable {
if hasMemoryWarning {
Image(systemName: "exclamationmark.triangle.fill")
.font(.system(size: scaledFontSize(11), weight: .semibold))
.cmuxFont(size: scaledFontSize(11), weight: .semibold)
.foregroundColor(.orange)
.safeHelp(String(
localized: "sidebar.memoryWarning.tooltip",
@@ -13394,7 +13412,7 @@ struct TabItemView: View, Equatable {
if workspaceSnapshot.isPinned {
Image(systemName: "pin.fill")
.font(.system(size: scaledFontSize(9), weight: .semibold))
.cmuxFont(size: scaledFontSize(9), weight: .semibold)
.foregroundColor(activeSecondaryColor(0.8))
.safeHelp(protectedWorkspaceTooltip)
}
@@ -13428,7 +13446,7 @@ struct TabItemView: View, Equatable {
}
Text(displayedTitle)
.font(.system(size: scaledFontSize(12.5), weight: titleFontWeight))
.cmuxFont(size: scaledFontSize(12.5), weight: titleFontWeight)
.foregroundColor(activePrimaryTextColor)
.lineLimit(titleLineLimit)
.truncationMode(.tail)
@@ -13473,7 +13491,7 @@ struct TabItemView: View, Equatable {
if let subtitle = effectiveSubtitle {
Text(subtitle)
.font(.system(size: scaledFontSize(10)))
.cmuxFont(size: scaledFontSize(10))
.foregroundColor(activeSecondaryColor(0.8))
.lineLimit(2)
.truncationMode(.tail)
@@ -13514,10 +13532,10 @@ struct TabItemView: View, Equatable {
if detailVisibility.showsLog, let latestLog = workspaceSnapshot.latestLog {
HStack(spacing: 4) {
Image(systemName: logLevelIcon(latestLog.level))
.font(.system(size: scaledFontSize(8)))
.cmuxFont(size: scaledFontSize(8))
.foregroundColor(logLevelColor(latestLog.level, isActive: usesInvertedActiveForeground))
Text(latestLog.message)
.font(.system(size: scaledFontSize(10)))
.cmuxFont(size: scaledFontSize(10))
.foregroundColor(activeSecondaryColor(0.8))
.lineLimit(1)
.truncationMode(.tail)
@@ -13540,7 +13558,7 @@ struct TabItemView: View, Equatable {
if let label = progress.label {
Text(label)
.font(.system(size: scaledFontSize(9)))
.cmuxFont(size: scaledFontSize(9))
.foregroundColor(activeSecondaryColor(0.6))
.lineLimit(1)
}
@@ -13555,7 +13573,7 @@ struct TabItemView: View, Equatable {
HStack(alignment: .top, spacing: 3) {
if sidebarShowGitBranchIcon, workspaceSnapshot.branchLinesContainBranch {
Image(systemName: "arrow.triangle.branch")
.font(.system(size: scaledFontSize(9)))
.cmuxFont(size: scaledFontSize(9))
.foregroundColor(activeSecondaryColor(0.6))
}
VStack(alignment: .leading, spacing: 1) {
@@ -13563,7 +13581,7 @@ struct TabItemView: View, Equatable {
if sidebarStacksBranchAndDirectory {
if let branch = line.branch {
Text(branch)
.font(.system(size: scaledFontSize(10), design: .monospaced))
.cmuxFont(size: scaledFontSize(10), design: .monospaced)
.foregroundColor(activeSecondaryColor(0.75))
.lineLimit(1)
.truncationMode(.tail)
@@ -13579,14 +13597,14 @@ struct TabItemView: View, Equatable {
HStack(spacing: 3) {
if let branch = line.branch {
Text(branch)
.font(.system(size: scaledFontSize(10), design: .monospaced))
.cmuxFont(size: scaledFontSize(10), design: .monospaced)
.foregroundColor(activeSecondaryColor(0.75))
.lineLimit(1)
.truncationMode(.tail)
}
if line.branch != nil, !line.directoryCandidates.isEmpty {
Image(systemName: "circle.fill")
.font(.system(size: scaledFontSize(3)))
.cmuxFont(size: scaledFontSize(3))
.foregroundColor(activeSecondaryColor(0.6))
.padding(.horizontal, 1)
}
@@ -13609,13 +13627,13 @@ struct TabItemView: View, Equatable {
HStack(alignment: .top, spacing: 3) {
if sidebarShowGitBranchIcon, workspaceSnapshot.compactGitBranchSummaryText != nil {
Image(systemName: "arrow.triangle.branch")
.font(.system(size: scaledFontSize(9)))
.cmuxFont(size: scaledFontSize(9))
.foregroundColor(activeSecondaryColor(0.6))
}
VStack(alignment: .leading, spacing: 1) {
if let branchRow = workspaceSnapshot.compactGitBranchSummaryText {
Text(branchRow)
.font(.system(size: scaledFontSize(10), design: .monospaced))
.cmuxFont(size: scaledFontSize(10), design: .monospaced)
.foregroundColor(activeSecondaryColor(0.75))
.lineLimit(1)
.truncationMode(.tail)
@@ -13633,7 +13651,7 @@ struct TabItemView: View, Equatable {
HStack(spacing: 3) {
if sidebarShowGitBranchIcon, workspaceSnapshot.compactGitBranchSummaryText != nil {
Image(systemName: "arrow.triangle.branch")
.font(.system(size: scaledFontSize(9)))
.cmuxFont(size: scaledFontSize(9))
.foregroundColor(activeSecondaryColor(0.6))
}
SidebarDirectoryText(
@@ -13661,7 +13679,7 @@ struct TabItemView: View, Equatable {
Text(pullRequestStatusLabel(pullRequest.status)).lineLimit(1)
Spacer(minLength: 0)
}
.font(.system(size: scaledFontSize(10), weight: .semibold))
.cmuxFont(size: scaledFontSize(10), weight: .semibold)
.foregroundColor(pullRequestForegroundColor)
.opacity(pullRequest.isStale ? 0.5 : 1)
if settings.makesPullRequestsClickable {
@@ -13694,7 +13712,7 @@ struct TabItemView: View, Equatable {
}
Spacer(minLength: 0)
}
.font(.system(size: scaledFontSize(10), design: .monospaced))
.cmuxFont(size: scaledFontSize(10), design: .monospaced)
.foregroundColor(activeSecondaryColor(0.75))
.lineLimit(1)
}
@@ -14774,7 +14792,7 @@ struct TabItemView: View, Equatable {
.frame(width: customFrameSize, height: customFrameSize)
case .closed:
Image(systemName: "xmark.circle")
.font(.system(size: 7 * fontScale, weight: .regular))
.cmuxFont(size: 7 * fontScale, weight: .regular)
.foregroundColor(color)
.frame(width: closedFrameSize, height: closedFrameSize)
}
@@ -14954,7 +14972,7 @@ private struct SidebarWorkspaceDescriptionText: View {
Text(displayMarkdown)
}
}
.font(.system(size: 10.5 * fontScale))
.cmuxFont(size: 10.5 * fontScale)
.foregroundColor(foregroundColor)
.multilineTextAlignment(.leading)
.lineLimit(Self.maxDisplayedLines)
@@ -15062,7 +15080,7 @@ private struct SidebarMetadataRows: View {
}
}
.buttonStyle(.plain)
.font(.system(size: 10 * fontScale, weight: .semibold))
.cmuxFont(size: 10 * fontScale, weight: .semibold)
.foregroundColor(isActive ? activeSecondaryForegroundColor : .secondary.opacity(0.9))
.frame(maxWidth: .infinity, alignment: .leading)
}
@@ -15128,7 +15146,7 @@ private struct SidebarMetadataEntryRow: View {
.truncationMode(.tail)
Spacer(minLength: 0)
}
.font(.system(size: 10 * fontScale))
.cmuxFont(size: 10 * fontScale)
.frame(maxWidth: .infinity, alignment: .leading)
}
@@ -15152,12 +15170,12 @@ private struct SidebarMetadataEntryRow: View {
if iconRaw.hasPrefix("emoji:") {
let value = String(iconRaw.dropFirst("emoji:".count))
guard !value.isEmpty else { return nil }
return AnyView(Text(value).font(.system(size: 9 * fontScale)))
return AnyView(Text(value).cmuxFont(size: 9 * fontScale))
}
if iconRaw.hasPrefix("text:") {
let value = String(iconRaw.dropFirst("text:".count))
guard !value.isEmpty else { return nil }
return AnyView(Text(value).font(.system(size: 8 * fontScale, weight: .semibold)))
return AnyView(Text(value).cmuxFont(size: 8 * fontScale, weight: .semibold))
}
let symbolName: String
if iconRaw.hasPrefix("sf:") {
@@ -15218,7 +15236,7 @@ private struct SidebarMetadataMarkdownBlocks: View {
}
}
.buttonStyle(.plain)
.font(.system(size: 10 * fontScale, weight: .semibold))
.cmuxFont(size: 10 * fontScale, weight: .semibold)
.foregroundColor(isActive ? activeSecondaryForegroundColor : .secondary.opacity(0.9))
.frame(maxWidth: .infinity, alignment: .leading)
}
@@ -15262,7 +15280,7 @@ private struct SidebarMetadataMarkdownBlockRow: View {
.foregroundColor(foregroundColor)
}
}
.font(.system(size: 10 * fontScale))
.cmuxFont(size: 10 * fontScale)
.multilineTextAlignment(.leading)
.lineLimit(Self.maxDisplayedLines)
.truncationMode(.tail)
+4 -3
View File
@@ -1,3 +1,4 @@
import CmuxFoundation
import AppKit
import SwiftUI
@@ -52,12 +53,12 @@ private struct DevWindowDisplayDebugView: View {
var body: some View {
VStack(alignment: .leading, spacing: 14) {
Text(String(localized: "debug.devWindowDisplay.title", defaultValue: "Dev Window Display"))
.font(.headline)
.cmuxFont(.headline)
Text(String(
localized: "debug.devWindowDisplay.description",
defaultValue: "New DEBUG cmux windows open on the selected display. Shared across all tagged dev builds; applied at window creation."
))
.font(.subheadline)
.cmuxFont(.subheadline)
.foregroundStyle(.secondary)
GroupBox {
@@ -92,7 +93,7 @@ private struct DevWindowDisplayDebugView: View {
}
Text(currentLabel)
.font(.footnote)
.cmuxFont(.footnote)
.foregroundStyle(.secondary)
Spacer()
}
+6 -5
View File
@@ -1,3 +1,4 @@
import CmuxFoundation
import AppKit
import SwiftUI
@@ -7,15 +8,15 @@ struct DockEmptyView: View {
var body: some View {
VStack(spacing: 10) {
Image(systemName: "dock.rectangle")
.font(.system(size: 24))
.cmuxFont(size: 24)
.foregroundStyle(.secondary)
Text(String(localized: "dock.empty.title", defaultValue: "No Dock Controls"))
.font(.system(size: 13, weight: .semibold))
.cmuxFont(size: 13, weight: .semibold)
Text(String(
localized: "dock.empty.subtitle",
defaultValue: "Add controls to .cmux/dock.json."
))
.font(.system(size: 12))
.cmuxFont(size: 12)
.foregroundStyle(.secondary)
VStack(spacing: 8) {
HStack(spacing: 4) {
@@ -66,10 +67,10 @@ struct DockEmptyView: View {
private var agentPromptPopover: some View {
VStack(alignment: .leading, spacing: 10) {
Text(String(localized: "dock.empty.promptPopoverTitle", defaultValue: "Agent Prompt"))
.font(.system(size: 13, weight: .semibold))
.cmuxFont(size: 13, weight: .semibold)
ScrollView {
Text(agentPrompt)
.font(.system(size: 12))
.cmuxFont(size: 12)
.foregroundStyle(.secondary)
.textSelection(.enabled)
.frame(maxWidth: .infinity, alignment: .leading)
+16 -15
View File
@@ -1,3 +1,4 @@
import CmuxFoundation
import AppKit
import CmuxTerminal
import Bonsplit
@@ -584,7 +585,7 @@ struct DockPanelView: View {
private var toolbar: some View {
HStack(spacing: 6) {
Text(store.sourceLabel)
.font(.system(size: 11, weight: .medium))
.cmuxFont(size: 11, weight: .medium)
.foregroundStyle(.secondary)
.lineLimit(1)
.truncationMode(.middle)
@@ -593,7 +594,7 @@ struct DockPanelView: View {
store.openConfiguration()
} label: {
Image(systemName: "doc.text")
.font(.system(size: 11, weight: .medium))
.cmuxFont(size: 11, weight: .medium)
}
.buttonStyle(.plain)
.help(String(localized: "dock.action.openConfig", defaultValue: "Open Dock Config"))
@@ -603,7 +604,7 @@ struct DockPanelView: View {
store.reload(rootDirectory: rootDirectory, workspaceId: workspaceId)
} label: {
Image(systemName: "arrow.clockwise")
.font(.system(size: 11, weight: .medium))
.cmuxFont(size: 11, weight: .medium)
}
.buttonStyle(.plain)
.help(String(localized: "dock.action.reload", defaultValue: "Reload Dock"))
@@ -743,15 +744,15 @@ private struct DockControlSectionView<TerminalContent: View>: View {
private var header: some View {
HStack(spacing: 6) {
Text("\(ordinal)")
.font(.system(size: 10, weight: .semibold).monospacedDigit())
.cmuxFont(size: 10, weight: .semibold, monospacedDigit: true)
.foregroundStyle(.secondary)
.frame(width: 18, alignment: .center)
Text(snapshot.title)
.font(.system(size: 12, weight: .semibold))
.cmuxFont(size: 12, weight: .semibold)
.lineLimit(1)
.truncationMode(.tail)
Text(snapshot.command)
.font(.system(size: 10, weight: .regular, design: .monospaced))
.cmuxFont(size: 10, weight: .regular, design: .monospaced)
.foregroundStyle(.secondary)
.lineLimit(1)
.truncationMode(.middle)
@@ -760,7 +761,7 @@ private struct DockControlSectionView<TerminalContent: View>: View {
onFocus()
} label: {
Image(systemName: "keyboard")
.font(.system(size: 10, weight: .medium))
.cmuxFont(size: 10, weight: .medium)
}
.buttonStyle(.plain)
.help(String(localized: "dock.action.focusControl", defaultValue: "Focus Control"))
@@ -770,7 +771,7 @@ private struct DockControlSectionView<TerminalContent: View>: View {
onRestart()
} label: {
Image(systemName: "arrow.clockwise")
.font(.system(size: 10, weight: .medium))
.cmuxFont(size: 10, weight: .medium)
}
.buttonStyle(.plain)
.help(String(localized: "dock.action.restartControl", defaultValue: "Restart Control"))
@@ -816,19 +817,19 @@ private struct DockTrustView: View {
var body: some View {
VStack(spacing: 10) {
Image(systemName: "exclamationmark.shield")
.font(.system(size: 28))
.cmuxFont(size: 28)
.foregroundStyle(.orange)
Text(String(localized: "dock.trust.title", defaultValue: "Trust Project Dock?"))
.font(.system(size: 13, weight: .semibold))
.cmuxFont(size: 13, weight: .semibold)
Text(String(
localized: "dock.trust.message",
defaultValue: "This project wants to start commands from its Dock config."
))
.font(.system(size: 12))
.cmuxFont(size: 12)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
Text(request.configPath)
.font(.system(size: 10, weight: .regular, design: .monospaced))
.cmuxFont(size: 10, weight: .regular, design: .monospaced)
.foregroundStyle(.secondary)
.lineLimit(2)
.truncationMode(.middle)
@@ -849,12 +850,12 @@ private struct DockErrorView: View {
var body: some View {
VStack(spacing: 8) {
Image(systemName: "exclamationmark.triangle")
.font(.system(size: 24))
.cmuxFont(size: 24)
.foregroundStyle(.orange)
Text(String(localized: "dock.error.title", defaultValue: "Dock Config Error"))
.font(.system(size: 13, weight: .semibold))
.cmuxFont(size: 13, weight: .semibold)
Text(message)
.font(.system(size: 12))
.cmuxFont(size: 12)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
}
@@ -1,3 +1,4 @@
import CmuxFoundation
import AppKit
import CmuxSidebarProviderKit
import SwiftUI
@@ -36,14 +37,14 @@ struct CmuxExtensionSidebarWorkspaceRowView: View, Equatable {
HStack(spacing: isSuperCompact ? 5 : 7) {
VStack(alignment: .leading, spacing: isSuperCompact ? 0 : 2) {
Text(row.title)
.font(.system(size: primarySize, weight: .regular))
.cmuxFont(size: primarySize, weight: .regular)
.foregroundColor(isSelected ? .primary : .primary.opacity(0.86))
.lineLimit(1)
.truncationMode(.tail)
if !isSuperCompact, let subtitle = rendered(row.subtitle) {
Text(subtitle)
.font(.system(size: secondarySize, weight: .regular))
.cmuxFont(size: secondarySize, weight: .regular)
.foregroundColor(.secondary)
.lineLimit(1)
.truncationMode(.tail)
@@ -53,7 +54,7 @@ struct CmuxExtensionSidebarWorkspaceRowView: View, Equatable {
if !isSuperCompact, let trailing = rendered(row.trailingText) {
Text(trailing)
.font(.system(size: 10.5, weight: .regular))
.cmuxFont(size: 10.5, weight: .regular)
.foregroundColor(.secondary)
.lineLimit(1)
}
@@ -69,7 +70,7 @@ struct CmuxExtensionSidebarWorkspaceRowView: View, Equatable {
showsInspector = true
} label: {
Image(systemName: accessory.systemImageName)
.font(.system(size: isSuperCompact ? 10 : 12, weight: .regular))
.cmuxFont(size: isSuperCompact ? 10 : 12, weight: .regular)
.frame(width: isSuperCompact ? 14 : 18, height: isSuperCompact ? 14 : 18)
}
.buttonStyle(.plain)
@@ -205,7 +206,7 @@ struct CmuxExtensionWorkspaceInspectorView: View {
switch draft.selectedTab {
case .notes:
TextEditor(text: $draft.notes)
.font(.system(size: 13))
.cmuxFont(size: 13)
.scrollContentBackground(.hidden)
.padding(8)
.accessibilityIdentifier("ExtensionSidebarNotesEditor")
@@ -225,7 +226,7 @@ struct CmuxExtensionWorkspaceInspectorView: View {
draft.committedAddress = normalized
}
}
.font(.system(size: 12))
.cmuxFont(size: 12)
.padding(.horizontal, 9)
.padding(.vertical, 7)
.background(Color(nsColor: .controlBackgroundColor))
@@ -910,14 +910,14 @@ private struct FeedButtonStyleDebugView: View {
HStack(alignment: .firstTextBaseline, spacing: 12) {
VStack(alignment: .leading, spacing: 3) {
Text(String(localized: "feed.buttonDebug.title", defaultValue: "Feed Buttons"))
.font(.system(size: 17, weight: .semibold))
.cmuxFont(size: 17, weight: .semibold)
Text(
String(
localized: "feed.buttonDebug.subtitle",
defaultValue: "Tune every Feed button kind live."
)
)
.font(.system(size: 11))
.cmuxFont(size: 11)
.foregroundStyle(.secondary)
}
Spacer()
@@ -975,10 +975,10 @@ private struct FeedButtonStyleDebugView: View {
} label: {
HStack(spacing: 7) {
Image(systemName: palette == activePalette ? "checkmark.circle.fill" : "circle")
.font(.system(size: 11, weight: .medium))
.cmuxFont(size: 11, weight: .medium)
paletteSwatches(palette)
Text(palette.label)
.font(.system(size: 11, weight: .medium))
.cmuxFont(size: 11, weight: .medium)
.lineLimit(1)
.minimumScaleFactor(0.82)
}
@@ -1039,7 +1039,7 @@ private struct FeedButtonStyleDebugView: View {
) -> some View {
HStack(spacing: 8) {
Text(label)
.font(.system(size: 10, weight: .semibold))
.cmuxFont(size: 10, weight: .semibold)
.foregroundStyle(previewColorScheme == .dark ? Color.white.opacity(0.70) : Color.black.opacity(0.58))
.frame(width: 34, alignment: .leading)
ForEach([FeedButton.Kind.primary, .success, .warning, .destructive]) { kind in
@@ -1112,13 +1112,13 @@ private struct FeedButtonStyleDebugView: View {
.pickerStyle(.menu)
Text(String(localized: "feed.buttonDebug.variations", defaultValue: "Variations"))
.font(.system(size: 11, weight: .medium))
.cmuxFont(size: 11, weight: .medium)
.foregroundStyle(.secondary)
ForEach(FeedButtonDebugPresetSection.all) { section in
VStack(alignment: .leading, spacing: 6) {
Text(section.label)
.font(.system(size: 10, weight: .semibold))
.cmuxFont(size: 10, weight: .semibold)
.foregroundStyle(.secondary)
LazyVGrid(
columns: [
@@ -1193,9 +1193,9 @@ private struct FeedButtonStyleDebugView: View {
} label: {
HStack(spacing: 6) {
Image(systemName: preset == activePreset ? "checkmark.circle.fill" : "circle")
.font(.system(size: 11, weight: .medium))
.cmuxFont(size: 11, weight: .medium)
Text(preset.label)
.font(.system(size: 11, weight: .medium))
.cmuxFont(size: 11, weight: .medium)
.lineLimit(1)
.minimumScaleFactor(0.82)
}
@@ -1230,7 +1230,7 @@ private struct FeedButtonStyleDebugView: View {
.foregroundStyle(selectedKind == kind ? Color.accentColor : Color.secondary)
.frame(width: 15)
Text(kind.debugLabel)
.font(.system(size: 12, weight: .medium))
.cmuxFont(size: 12, weight: .medium)
Spacer()
FeedButton(label: kind.debugLabel, kind: kind, size: .compact) {
selectedKind = kind
@@ -1264,7 +1264,7 @@ private struct FeedButtonStyleDebugView: View {
)
HStack {
Text(String(localized: "feed.buttonDebug.preview", defaultValue: "Preview"))
.font(.system(size: 11, weight: .medium))
.cmuxFont(size: 11, weight: .medium)
.foregroundStyle(.secondary)
Spacer()
FeedButton(label: selectedKind.debugLabel, kind: selectedKind, size: .medium) {}
@@ -1340,11 +1340,11 @@ private struct FeedButtonStyleDebugView: View {
) -> some View {
HStack(spacing: 8) {
Text(title)
.font(.system(size: 11))
.cmuxFont(size: 11)
.frame(width: 150, alignment: .leading)
Slider(value: value, in: range)
Text(sliderValue(value.wrappedValue, suffix: suffix))
.font(.system(size: 10).monospacedDigit())
.cmuxFont(size: 10, monospacedDigit: true)
.foregroundStyle(.secondary)
.frame(width: 44, alignment: .trailing)
}
+65 -68
View File
@@ -1,3 +1,4 @@
import CmuxFoundation
import AppKit
import Bonsplit
import CMUXAgentLaunch
@@ -132,18 +133,14 @@ private struct FeedSecondaryFilterButton: View {
HStack(spacing: 3) {
Image(systemName: filter.symbolName)
.symbolRenderingMode(.monochrome)
.font(
.system(
size: RightSidebarChromeControlStyle.secondaryIconSize,
weight: RightSidebarChromeControlStyle.iconWeight
)
.cmuxFont(
size: RightSidebarChromeControlStyle.secondaryIconSize,
weight: RightSidebarChromeControlStyle.iconWeight
)
Text(filter.label)
.font(
.system(
size: RightSidebarChromeControlStyle.labelSize,
weight: RightSidebarChromeControlStyle.labelWeight
)
.cmuxFont(
size: RightSidebarChromeControlStyle.labelSize,
weight: RightSidebarChromeControlStyle.labelWeight
)
}
.rightSidebarChromePill(
@@ -587,14 +584,14 @@ private struct FeedListView: View {
defaultValue: "No pending decisions")
: String(localized: "feed.empty.activity.title",
defaultValue: "No activity yet"))
.font(.system(size: 12))
.cmuxFont(size: 12)
.foregroundColor(.secondary)
Text(filter == .actionable
? String(localized: "feed.empty.actionable.subtitle",
defaultValue: "Permission, plan, and question requests from AI agents will appear here.")
: String(localized: "feed.empty.activity.subtitle",
defaultValue: "Agent decisions and todo-list updates will appear here."))
.font(.system(size: 11))
.cmuxFont(size: 11)
.foregroundColor(.secondary.opacity(0.7))
.multilineTextAlignment(.center)
.padding(.horizontal, 16)
@@ -1024,7 +1021,7 @@ struct FeedItemRow: View, Equatable {
FeedContextBlock(context: context, source: snapshot.source)
} else if let echo = promptEcho, !echo.isEmpty {
Text(echo)
.font(.system(size: 11))
.cmuxFont(size: 11)
.foregroundColor(.secondary)
.lineLimit(2)
.truncationMode(.tail)
@@ -1084,11 +1081,11 @@ struct FeedItemRow: View, Equatable {
private var chipHeader: some View {
HStack(alignment: .center, spacing: 8) {
Image(systemName: snapshot.kind.symbolName)
.font(.system(size: 12, weight: .medium))
.cmuxFont(size: 12, weight: .medium)
.foregroundColor(kindTint)
.frame(width: 14, height: 14)
Text(headerTitle)
.font(.system(size: 12, weight: .medium))
.cmuxFont(size: 12, weight: .medium)
.foregroundColor(.primary.opacity(0.92))
.lineLimit(1)
.truncationMode(.middle)
@@ -1158,9 +1155,7 @@ struct FeedItemRow: View, Equatable {
private func chip(text: String, fg: Color, bg: Color, mono: Bool = false) -> some View {
Text(text)
.font(mono
? .system(size: 10, weight: .medium).monospacedDigit()
: .system(size: 10, weight: .medium))
.cmuxFont(size: 10, weight: .medium, monospacedDigit: mono)
.foregroundColor(fg)
.padding(.horizontal, 5)
.padding(.vertical, 2)
@@ -1323,7 +1318,7 @@ struct FeedItemRow: View, Equatable {
private func statusTag(_ text: String, color: Color) -> some View {
Text(text)
.font(.system(size: 10, weight: .medium))
.cmuxFont(size: 10, weight: .medium)
.foregroundColor(color)
.padding(.horizontal, 5)
.padding(.vertical, 1)
@@ -1408,7 +1403,7 @@ private struct FeedLabeledTextRow: View {
var body: some View {
HStack(alignment: .firstTextBaseline, spacing: 6) {
Text(label)
.font(.system(size: 10, weight: .semibold))
.cmuxFont(size: 10, weight: .semibold)
.foregroundColor(labelColor)
.frame(width: 48, alignment: .leading)
if rendersMarkdown {
@@ -1419,7 +1414,7 @@ private struct FeedLabeledTextRow: View {
)
} else {
Text(text)
.font(.system(size: 11))
.cmuxFont(size: 11)
.foregroundColor(textColor)
.fixedSize(horizontal: false, vertical: true)
}
@@ -1514,10 +1509,10 @@ private struct PermissionActionArea: View {
private var toolLabel: some View {
HStack(spacing: 5) {
Image(systemName: "exclamationmark.triangle.fill")
.font(.system(size: 10, weight: .medium))
.cmuxFont(size: 10, weight: .medium)
.foregroundColor(.orange)
Text(toolName)
.font(.system(size: 11, weight: .semibold))
.cmuxFont(size: 11, weight: .semibold)
.foregroundColor(.orange)
}
}
@@ -1532,11 +1527,11 @@ private struct PermissionActionArea: View {
HStack(alignment: .firstTextBaseline, spacing: 6) {
if let sigil = preview.sigil {
Text(sigil)
.font(.system(size: 11, weight: .medium, design: .monospaced))
.cmuxFont(size: 11, weight: .medium, design: .monospaced)
.foregroundColor(.orange)
}
Text(primary)
.font(.system(size: 11, design: .monospaced))
.cmuxFont(size: 11, design: .monospaced)
.foregroundColor(.primary.opacity(0.95))
.textSelection(.enabled)
.fixedSize(horizontal: false, vertical: true)
@@ -1544,7 +1539,7 @@ private struct PermissionActionArea: View {
}
if let secondary = preview.secondary, !secondary.isEmpty {
Text(secondary)
.font(.system(size: 10))
.cmuxFont(size: 10)
.foregroundColor(.secondary.opacity(0.85))
.fixedSize(horizontal: false, vertical: true)
}
@@ -1695,13 +1690,13 @@ struct FeedButton: View {
HStack(spacing: iconSpacing) {
if let leadingIcon {
Image(systemName: leadingIcon)
.font(.system(size: iconSize, weight: .semibold))
.cmuxFont(size: iconSize, weight: .semibold)
}
Text(label)
.font(.system(size: labelSize, weight: .semibold))
.cmuxFont(size: labelSize, weight: .semibold)
if let trailingIcon {
Image(systemName: trailingIcon)
.font(.system(size: iconSize, weight: .semibold))
.cmuxFont(size: iconSize, weight: .semibold)
}
}
}
@@ -1716,7 +1711,7 @@ struct FeedButton: View {
Image(systemName: trailingIcon)
}
}
.font(.system(size: labelSize, weight: .semibold))
.cmuxFont(size: labelSize, weight: .semibold)
}
private func performAction() {
@@ -2265,7 +2260,7 @@ private struct ExitPlanActionArea: View {
axis: .vertical
)
.textFieldStyle(.plain)
.font(.system(size: 12))
.cmuxFont(size: 12)
.tint(Color.primary.opacity(0.75))
.focused($feedbackFocused)
.lineLimit(2...5)
@@ -2369,10 +2364,10 @@ private struct ExitPlanAllowedPromptsView: View {
VStack(alignment: .leading, spacing: 6) {
HStack(spacing: 5) {
Image(systemName: "checklist")
.font(.system(size: 10, weight: .medium))
.cmuxFont(size: 10, weight: .medium)
.foregroundColor(Color.purple.opacity(0.85))
Text(String(localized: "feed.exitplan.allowedPrompts", defaultValue: "Allowed prompts"))
.font(.system(size: 11, weight: .semibold))
.cmuxFont(size: 11, weight: .semibold)
.foregroundColor(Color.purple.opacity(0.9))
}
VStack(alignment: .leading, spacing: 5) {
@@ -2380,7 +2375,7 @@ private struct ExitPlanAllowedPromptsView: View {
HStack(alignment: .firstTextBaseline, spacing: 6) {
if !prompt.tool.isEmpty {
Text(prompt.tool)
.font(.system(size: 10, weight: .semibold))
.cmuxFont(size: 10, weight: .semibold)
.foregroundColor(.purple)
.padding(.horizontal, 5)
.padding(.vertical, 1)
@@ -2390,7 +2385,7 @@ private struct ExitPlanAllowedPromptsView: View {
)
}
Text(prompt.prompt)
.font(.system(size: 11))
.cmuxFont(size: 11)
.foregroundColor(.primary.opacity(0.86))
.fixedSize(horizontal: false, vertical: true)
}
@@ -2412,13 +2407,13 @@ private struct ExitPlanPlanFileView: View {
var body: some View {
HStack(alignment: .firstTextBaseline, spacing: 6) {
Image(systemName: "doc.text")
.font(.system(size: 10, weight: .medium))
.cmuxFont(size: 10, weight: .medium)
.foregroundColor(.secondary)
Text(String(localized: "feed.exitplan.planFile", defaultValue: "Plan file"))
.font(.system(size: 10, weight: .semibold))
.cmuxFont(size: 10, weight: .semibold)
.foregroundColor(.secondary)
Text((path as NSString).lastPathComponent)
.font(.system(size: 10, design: .monospaced))
.cmuxFont(size: 10, design: .monospaced)
.foregroundColor(.secondary.opacity(0.85))
.lineLimit(1)
.truncationMode(.middle)
@@ -2452,10 +2447,8 @@ private struct FeedMarkdownInlineText: View {
interpretedSyntax: .inlineOnlyPreservingWhitespace
)
)) ?? AttributedString(text)
let font = weight.map { Font.system(size: fontSize, weight: $0) }
?? Font.system(size: fontSize)
Text(parsed)
.font(font)
.cmuxFont(size: fontSize, weight: weight ?? .regular)
.foregroundColor(foregroundColor)
.fixedSize(horizontal: false, vertical: true)
}
@@ -2487,7 +2480,7 @@ private struct PlanBodyView: View {
ForEach(Array(items.enumerated()), id: \.offset) { _, item in
HStack(alignment: .top, spacing: 5) {
Text("\(item.index).")
.font(.system(size: 11, weight: .medium).monospacedDigit())
.cmuxFont(size: 11, weight: .medium, monospacedDigit: true)
.foregroundColor(.secondary)
markdownText(item.text, color: .primary.opacity(0.85))
}
@@ -2526,7 +2519,7 @@ private struct PlanBodyView: View {
)
} else {
Text(text)
.font(.system(size: 11, weight: weight ?? .regular))
.cmuxFont(size: 11, weight: weight ?? .regular)
.foregroundColor(color)
.fixedSize(horizontal: false, vertical: true)
}
@@ -2655,8 +2648,10 @@ private struct QuestionActionArea: View {
@State private var freeTexts: [String: String] = [:]
@State private var customAnswerFocusKey: String?
@State private var customAnswerFocusRequest = 0
@Environment(\.cmuxGlobalFontMagnificationPercent) private var globalFontPercent
var body: some View {
let _ = globalFontPercent
VStack(alignment: .leading, spacing: 12) {
if shouldRenderLongForm, let q = questions.first {
longFormBlock(question: q)
@@ -2752,7 +2747,7 @@ private struct QuestionActionArea: View {
} label: {
HStack(alignment: .top, spacing: 10) {
Text("\(index)")
.font(.system(size: 11, weight: .bold).monospacedDigit())
.cmuxFont(size: 11, weight: .bold, monospacedDigit: true)
.foregroundColor(selected ? .white : .secondary)
.frame(width: 20, height: 20)
.background(
@@ -2761,18 +2756,18 @@ private struct QuestionActionArea: View {
)
VStack(alignment: .leading, spacing: 2) {
Text(option.label)
.font(.system(size: 12, weight: .semibold))
.cmuxFont(size: 12, weight: .semibold)
.foregroundColor(.primary)
if let description = option.description, !description.isEmpty {
Text(description)
.font(.system(size: 11))
.cmuxFont(size: 11)
.foregroundColor(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
}
Spacer()
Image(systemName: selected ? "checkmark.circle.fill" : "circle")
.font(.system(size: 12, weight: .medium))
.cmuxFont(size: 12, weight: .medium)
.foregroundColor(selected ? Color(red: 0.24, green: 0.48, blue: 0.88) : .secondary.opacity(0.45))
}
.padding(10)
@@ -2799,10 +2794,10 @@ private struct QuestionActionArea: View {
let customId = Self.customAnswerSelectionId
let selected = selections[questionId]?.contains(customId) == true
let focusKey = customAnswerFocusKey(questionId)
let font = NSFont.systemFont(ofSize: 12, weight: .semibold)
let font = GlobalFontMagnification.systemFont(ofSize: 12, weight: .semibold)
return HStack(alignment: .top, spacing: 10) {
Text("\(index)")
.font(.system(size: 11, weight: .bold).monospacedDigit())
.cmuxFont(size: 11, weight: .bold, monospacedDigit: true)
.foregroundColor(selected ? .white : .secondary)
.frame(width: 20, height: 20)
.background(
@@ -2820,7 +2815,7 @@ private struct QuestionActionArea: View {
onBlur: onBlurRow
)
Image(systemName: selected ? "checkmark.circle.fill" : "circle")
.font(.system(size: 12, weight: .medium))
.cmuxFont(size: 12, weight: .medium)
.foregroundColor(selected ? Color(red: 0.24, green: 0.48, blue: 0.88) : .secondary.opacity(0.45))
.padding(.leading, 8)
.padding(.top, 3)
@@ -2850,19 +2845,19 @@ private struct QuestionActionArea: View {
VStack(alignment: .leading, spacing: 5) {
HStack(alignment: .top, spacing: 5) {
Text("\(index).")
.font(.system(size: 11, weight: .semibold).monospacedDigit())
.cmuxFont(size: 11, weight: .semibold, monospacedDigit: true)
.foregroundColor(.blue)
Text(question.prompt)
.font(.system(size: 11, weight: .medium))
.cmuxFont(size: 11, weight: .medium)
.foregroundColor(.primary.opacity(0.95))
.fixedSize(horizontal: false, vertical: true)
}
if question.multiSelect {
HStack(spacing: 3) {
Image(systemName: "checklist")
.font(.system(size: 8, weight: .medium))
.cmuxFont(size: 8, weight: .medium)
Text(String(localized: "feed.question.multiSelect", defaultValue: "Multi-select"))
.font(.system(size: 9, weight: .semibold))
.cmuxFont(size: 9, weight: .semibold)
.tracking(0.3)
}
.foregroundColor(.orange)
@@ -2876,7 +2871,7 @@ private struct QuestionActionArea: View {
if question.options.isEmpty {
Text(String(localized: "feed.question.noOptions",
defaultValue: "Agent provided no options."))
.font(.system(size: 10))
.cmuxFont(size: 10)
.foregroundColor(.secondary)
} else {
WrapHStack(spacing: 6) {
@@ -2901,7 +2896,7 @@ private struct QuestionActionArea: View {
/// preset option selection for that question on submit.
private func freeFormField(questionId: String, multi: Bool) -> some View {
let focusKey = customAnswerFocusKey(questionId)
let font = NSFont.systemFont(ofSize: 11)
let font = GlobalFontMagnification.systemFont(ofSize: 11)
return customAnswerField(
text: customAnswerBinding(questionId: questionId, multi: multi),
focusRequest: focusRequest(forCustomAnswerKey: focusKey),
@@ -3228,7 +3223,7 @@ private final class FeedInlineTextEditorView: NSView {
let textView = FeedInlineNativeTextView(frame: .zero)
private let placeholderField = FeedInlinePassthroughLabel(labelWithString: "")
private var currentFont = NSFont.systemFont(ofSize: 11)
private var currentFont = GlobalFontMagnification.systemFont(ofSize: 11)
static func minimumHeight(for font: NSFont) -> CGFloat {
ceil(font.ascender - font.descender + font.leading) + textInset.height * 2
@@ -3669,7 +3664,8 @@ private struct StopActionArea: View {
draft.reply.trimmingCharacters(in: .whitespacesAndNewlines)
}
private var canSend: Bool { !trimmed.isEmpty }
private var replyFont: NSFont { NSFont.systemFont(ofSize: 12) }
@Environment(\.cmuxGlobalFontMagnificationPercent) private var globalFontPercent
private var replyFont: NSFont { GlobalFontMagnification.systemFont(ofSize: 12) }
private var replyBinding: Binding<String> {
Binding(
get: { draft.reply },
@@ -3678,13 +3674,14 @@ private struct StopActionArea: View {
}
var body: some View {
let _ = globalFontPercent
VStack(alignment: .leading, spacing: 8) {
HStack(spacing: 5) {
Image(systemName: "checkmark.circle")
.font(.system(size: 10))
.cmuxFont(size: 10)
.foregroundColor(.secondary)
Text(String(localized: "feed.stop.label", defaultValue: "Claude finished — reply to continue"))
.font(.system(size: 11, weight: .medium))
.cmuxFont(size: 11, weight: .medium)
.foregroundColor(.secondary)
}
FeedInlineTextField(
@@ -3761,7 +3758,7 @@ private struct TelemetryActionArea: View {
.truncationMode(.tail)
} else if !summary.isEmpty {
Text(summary)
.font(.system(size: 11, design: .monospaced))
.cmuxFont(size: 11, design: .monospaced)
.foregroundColor(.secondary.opacity(0.85))
.lineLimit(3)
.truncationMode(.tail)
@@ -3810,10 +3807,10 @@ private struct TodoListBody: View {
VStack(alignment: .leading, spacing: 5) {
HStack(spacing: 4) {
Text(String(localized: "feed.todos.title", defaultValue: "Tasks"))
.font(.system(size: 11, weight: .semibold))
.cmuxFont(size: 11, weight: .semibold)
.foregroundColor(.primary.opacity(0.9))
Text(summaryLabel)
.font(.system(size: 10))
.cmuxFont(size: 10)
.foregroundColor(.secondary)
}
VStack(alignment: .leading, spacing: 3) {
@@ -3828,7 +3825,7 @@ private struct TodoListBody: View {
localized: "feed.todos.moreCompleted",
defaultValue: "... +\(done.count - visibleDone.count) completed"
))
.font(.system(size: 11))
.cmuxFont(size: 11)
.foregroundColor(.secondary.opacity(0.8))
.padding(.leading, 22)
}
@@ -3837,7 +3834,7 @@ private struct TodoListBody: View {
if expanded && done.count > 2 {
Button { expanded = false } label: {
Text(String(localized: "feed.todos.collapse", defaultValue: "Collapse"))
.font(.system(size: 11))
.cmuxFont(size: 11)
.foregroundColor(.secondary.opacity(0.8))
.padding(.leading, 22)
}
@@ -3866,11 +3863,11 @@ private struct TodoListBody: View {
private func row(_ todo: WorkstreamTaskTodo) -> some View {
HStack(alignment: .top, spacing: 6) {
Image(systemName: symbol(for: todo.state))
.font(.system(size: 11, weight: .medium))
.cmuxFont(size: 11, weight: .medium)
.foregroundColor(color(for: todo.state))
.frame(width: 14, height: 14)
Text(todo.content)
.font(.system(size: 12))
.cmuxFont(size: 12)
.foregroundColor(todo.state == .completed
? .secondary.opacity(0.7)
: .primary.opacity(0.9))
@@ -3903,7 +3900,7 @@ private struct ResolvedDivider: View {
HStack(spacing: 8) {
line
Text(String(localized: "feed.divider.resolved", defaultValue: "Resolved"))
.font(.system(size: 10, weight: .medium))
.cmuxFont(size: 10, weight: .medium)
.tracking(0.5)
.foregroundColor(.secondary.opacity(0.7))
line
+2 -1
View File
@@ -1,3 +1,4 @@
import CmuxFoundation
import CMUXAgentLaunch
import Foundation
import Observation
@@ -67,7 +68,7 @@ struct FeedHistoryLoadMoreRow: View {
.frame(width: 12, height: 12)
}
Text(label)
.font(.system(size: 11, weight: .medium))
.cmuxFont(size: 11, weight: .medium)
.foregroundColor(.secondary)
}
.frame(maxWidth: .infinity)
@@ -1,3 +1,4 @@
import CmuxFoundation
#if DEBUG
import AppKit
import CMUXAgentLaunch
@@ -48,7 +49,7 @@ private struct FeedPreviewRootView: View {
private var toolbar: some View {
HStack(spacing: 12) {
Text("Feed Preview · all kinds + states")
.font(.system(size: 12, weight: .semibold))
.cmuxFont(size: 12, weight: .semibold)
.foregroundColor(.secondary)
Spacer()
Button("Inject all into Feed") {
@@ -63,7 +64,7 @@ private struct FeedPreviewRootView: View {
VStack(alignment: .leading, spacing: 8) {
HStack(spacing: 8) {
Text(kind.label.uppercased())
.font(.system(size: 11, weight: .heavy))
.cmuxFont(size: 11, weight: .heavy)
.tracking(0.8)
.foregroundColor(.primary.opacity(0.9))
Rectangle()
@@ -90,7 +91,7 @@ private struct FeedPreviewRootView: View {
}
}()
Text(label.uppercased())
.font(.system(size: 9, weight: .bold))
.cmuxFont(size: 9, weight: .bold)
.tracking(0.6)
.foregroundColor(color)
}
@@ -1,3 +1,4 @@
import CmuxFoundation
#if DEBUG
import AppKit
import SwiftUI
@@ -86,7 +87,7 @@ private struct FeedTextEditorDebugView: View {
header
Text(String(localized: "feed.textEditorDebug.section.swiftui", defaultValue: "SwiftUI"))
.font(.system(size: 12, weight: .semibold))
.cmuxFont(size: 12, weight: .semibold)
.foregroundStyle(.secondary)
LazyVGrid(columns: columns, alignment: .leading, spacing: 14) {
@@ -99,7 +100,7 @@ private struct FeedTextEditorDebugView: View {
}
Text(String(localized: "feed.textEditorDebug.section.appkit", defaultValue: "AppKit"))
.font(.system(size: 12, weight: .semibold))
.cmuxFont(size: 12, weight: .semibold)
.foregroundStyle(.secondary)
.padding(.top, 2)
@@ -126,12 +127,12 @@ private struct FeedTextEditorDebugView: View {
HStack(alignment: .firstTextBaseline) {
VStack(alignment: .leading, spacing: 4) {
Text(String(localized: "feed.textEditorDebug.title", defaultValue: "Feed Text Editors"))
.font(.system(size: 18, weight: .semibold))
.cmuxFont(size: 18, weight: .semibold)
Text(String(
localized: "feed.textEditorDebug.subtitle",
defaultValue: "Compare autosizing editors with identical input."
))
.font(.system(size: 12))
.cmuxFont(size: 12)
.foregroundStyle(.secondary)
}
Spacer()
@@ -145,7 +146,7 @@ private struct FeedTextEditorDebugView: View {
TextField(placeholder, text: $swiftUITextFieldText, axis: .vertical)
.textFieldStyle(.plain)
.lineLimit(1...10)
.font(.system(size: 13, weight: .semibold))
.cmuxFont(size: 13, weight: .semibold)
.padding(.horizontal, 9)
.padding(.vertical, 7)
.background(editorBackground)
@@ -154,7 +155,7 @@ private struct FeedTextEditorDebugView: View {
private var swiftUIMirrorTextEditor: some View {
ZStack(alignment: .topLeading) {
Text(swiftUIMirrorText.isEmpty ? " " : swiftUIMirrorText + " ")
.font(.system(size: 13, weight: .semibold))
.cmuxFont(size: 13, weight: .semibold)
.lineSpacing(0)
.padding(.horizontal, 9)
.padding(.top, 9)
@@ -167,7 +168,7 @@ private struct FeedTextEditorDebugView: View {
}
)
TextEditor(text: $swiftUIMirrorText)
.font(.system(size: 13, weight: .semibold))
.cmuxFont(size: 13, weight: .semibold)
.scrollContentBackground(.hidden)
.padding(.horizontal, 4)
.padding(.top, 5)
@@ -212,10 +213,10 @@ private struct FeedTextEditorDebugView: View {
VStack(alignment: .leading, spacing: 8) {
HStack(alignment: .firstTextBaseline) {
Text(variant.title)
.font(.system(size: 13, weight: .semibold))
.cmuxFont(size: 13, weight: .semibold)
Spacer()
Text(metrics(for: text))
.font(.system(size: 10).monospacedDigit())
.cmuxFont(size: 10, monospacedDigit: true)
.foregroundStyle(.secondary)
}
content()
@@ -406,7 +407,7 @@ private struct FeedTextEditorDebugAppKitEditor: NSViewRepresentable {
private func configure(_ host: FeedTextEditorDebugAppKitHost) {
host.apply(
mode: mode,
font: NSFont.systemFont(ofSize: 13, weight: .semibold),
font: GlobalFontMagnification.systemFont(ofSize: 13, weight: .semibold),
placeholder: placeholder
)
}
@@ -417,7 +418,7 @@ private final class FeedTextEditorDebugAppKitHost: NSView {
private let scrollView = NSScrollView(frame: .zero)
private let placeholderField = NSTextField(labelWithString: "")
private var mode = FeedTextEditorDebugAppKitMode.directSizeThatFits
private var currentFont = NSFont.systemFont(ofSize: 13, weight: .semibold)
private var currentFont = GlobalFontMagnification.systemFont(ofSize: 13, weight: .semibold)
private var lastMeasuredHeight: CGFloat = 0
var onMeasuredHeightChange: ((CGFloat) -> Void)?
+26 -4
View File
@@ -1,10 +1,12 @@
import AppKit
import CmuxFoundation
@MainActor
final class FileDropHintBadgeView: NSView {
private static let neutralBackgroundColor = NSColor.systemGray.withAlphaComponent(0.20)
private let effectView: NSView
private let label = NSTextField(labelWithString: "")
private var fontMagnificationObserver: GlobalFontMagnificationChangeObserver?
private var animationGeneration: UInt64 = 0
override var isOpaque: Bool { false }
@@ -21,7 +23,7 @@ final class FileDropHintBadgeView: NSView {
}
super.init(frame: frameRect)
frame.size = CGSize(width: 140, height: 26)
frame.size = CGSize(width: 140, height: Self.badgeHeight(for: label.font))
wantsLayer = true
layer?.backgroundColor = NSColor.clear.cgColor
@@ -31,13 +33,13 @@ final class FileDropHintBadgeView: NSView {
effectView.translatesAutoresizingMaskIntoConstraints = false
effectView.wantsLayer = true
effectView.layer?.backgroundColor = Self.neutralBackgroundColor.cgColor
effectView.layer?.cornerRadius = 13
effectView.layer?.cornerRadius = Self.badgeHeight(for: label.font) / 2
effectView.layer?.masksToBounds = true
configureNativeGlassIfNeeded(effectView)
addSubview(effectView)
label.translatesAutoresizingMaskIntoConstraints = false
label.font = .systemFont(ofSize: 12, weight: .medium)
applyFont()
label.textColor = .labelColor
label.alignment = .center
label.lineBreakMode = .byClipping
@@ -56,6 +58,10 @@ final class FileDropHintBadgeView: NSView {
label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 10),
label.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -10),
])
fontMagnificationObserver = GlobalFontMagnificationChangeObserver { [weak self] in
self?.applyFont()
}
}
@available(*, unavailable)
@@ -65,6 +71,22 @@ final class FileDropHintBadgeView: NSView {
deinit {}
private func applyFont() {
label.font = GlobalFontMagnification.systemFont(ofSize: 12, weight: .medium)
let height = Self.badgeHeight(for: label.font)
effectView.layer?.cornerRadius = height / 2
if !isHidden {
let fitting = label.intrinsicContentSize
frame.size = CGSize(width: max(frame.width, fitting.width + 20), height: height)
}
needsLayout = true
}
private static func badgeHeight(for font: NSFont?) -> CGFloat {
guard let font else { return 26 }
return max(26, ceil(font.ascender - font.descender + font.leading) + 10)
}
override func hitTest(_ point: NSPoint) -> NSView? {
nil
}
@@ -75,7 +97,7 @@ final class FileDropHintBadgeView: NSView {
let fitting = label.intrinsicContentSize
let maxWidth = max(80, min(bounds.width, targetBounds.width) - 16)
let width = min(max(140, fitting.width + 20), maxWidth)
let height: CGFloat = 26
let height = Self.badgeHeight(for: label.font)
let origin = CGPoint(
x: min(max(targetBounds.midX - width / 2, bounds.minX + 8), max(bounds.minX + 8, bounds.maxX - width - 8)),
y: min(max(targetBounds.midY - height / 2, bounds.minY + 8), max(bounds.minY + 8, bounds.maxY - height - 8))
+12 -2
View File
@@ -1,9 +1,11 @@
import AppKit
import CmuxFoundation
/// Pure AppKit header bar with folder icon, path label, and hidden files toggle.
final class FileExplorerHeaderView: NSView {
private let iconView = NSImageView()
private let pathLabel = NSTextField(labelWithString: "")
private var heightConstraint: NSLayoutConstraint?
private var displayPath = ""
private var quickSearchQuery: String?
@@ -21,7 +23,7 @@ final class FileExplorerHeaderView: NSView {
iconView.contentTintColor = .secondaryLabelColor
pathLabel.translatesAutoresizingMaskIntoConstraints = false
pathLabel.font = .systemFont(ofSize: 11, weight: .medium)
applyFonts()
pathLabel.textColor = .secondaryLabelColor
pathLabel.lineBreakMode = .byTruncatingMiddle
pathLabel.maximumNumberOfLines = 1
@@ -30,8 +32,11 @@ final class FileExplorerHeaderView: NSView {
addSubview(iconView)
addSubview(pathLabel)
let heightConstraint = heightAnchor.constraint(equalToConstant: RightSidebarChromeMetrics.secondaryBarHeight)
self.heightConstraint = heightConstraint
NSLayoutConstraint.activate([
heightAnchor.constraint(equalToConstant: RightSidebarChromeMetrics.secondaryBarHeight),
heightConstraint,
iconView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12),
iconView.centerYAnchor.constraint(equalTo: centerYAnchor),
@@ -45,6 +50,11 @@ final class FileExplorerHeaderView: NSView {
applyHeaderState()
}
func applyFonts() {
pathLabel.font = GlobalFontMagnification.systemFont(ofSize: 11, weight: .medium)
heightConstraint?.constant = RightSidebarChromeMetrics.secondaryBarHeight
}
func update(displayPath: String) {
guard self.displayPath != displayPath else { return }
self.displayPath = displayPath
+17 -2
View File
@@ -1,8 +1,19 @@
import AppKit
import CmuxFoundation
final class FileExplorerSearchResultCellView: NSTableCellView {
private let pathLabel = NSTextField(labelWithString: "")
private let previewLabel = NSTextField(labelWithString: "")
static var preferredRowHeight: CGFloat {
max(
46,
ceil(
13 +
lineHeight(for: GlobalFontMagnification.systemFont(ofSize: 12, weight: .semibold)) +
lineHeight(for: GlobalFontMagnification.monospacedSystemFont(ofSize: 11, weight: .regular))
)
)
}
init(identifier: NSUserInterfaceItemIdentifier) {
super.init(frame: .zero)
@@ -16,13 +27,11 @@ final class FileExplorerSearchResultCellView: NSTableCellView {
private func setupViews() {
pathLabel.translatesAutoresizingMaskIntoConstraints = false
pathLabel.font = .systemFont(ofSize: 12, weight: .semibold)
pathLabel.textColor = .labelColor
pathLabel.lineBreakMode = .byTruncatingMiddle
pathLabel.maximumNumberOfLines = 1
previewLabel.translatesAutoresizingMaskIntoConstraints = false
previewLabel.font = .monospacedSystemFont(ofSize: 11, weight: .regular)
previewLabel.textColor = .secondaryLabelColor
previewLabel.lineBreakMode = .byTruncatingTail
previewLabel.maximumNumberOfLines = 1
@@ -42,8 +51,14 @@ final class FileExplorerSearchResultCellView: NSTableCellView {
}
func configure(with result: FileSearchResult) {
pathLabel.font = GlobalFontMagnification.systemFont(ofSize: 12, weight: .semibold)
previewLabel.font = GlobalFontMagnification.monospacedSystemFont(ofSize: 11, weight: .regular)
pathLabel.stringValue = "\(result.relativePath):\(result.lineNumber)"
previewLabel.stringValue = result.preview.isEmpty ? " " : result.preview
toolTip = "\(result.path):\(result.lineNumber):\(result.columnNumber)"
}
private static func lineHeight(for font: NSFont) -> CGFloat {
ceil(font.ascender - font.descender + font.leading)
}
}
+12 -10
View File
@@ -25,13 +25,15 @@ enum FileExplorerStyle: Int, CaseIterable {
}
var rowHeight: CGFloat {
let baseHeight: CGFloat
switch self {
case .liquidGlass: return 28
case .highDensity: return 20
case .terminalStealth: return 24
case .proStudio: return 32
case .finder: return 26
case .liquidGlass: baseHeight = 28
case .highDensity: baseHeight = 20
case .terminalStealth: baseHeight = 24
case .proStudio: baseHeight = 32
case .finder: baseHeight = 26
}
return GlobalFontMagnification.scaledSize(baseHeight)
}
var indentation: CGFloat {
@@ -66,11 +68,11 @@ enum FileExplorerStyle: Int, CaseIterable {
var nameFont: NSFont {
switch self {
case .liquidGlass: return .systemFont(ofSize: 13, weight: .medium)
case .highDensity: return .systemFont(ofSize: 11, weight: .regular)
case .terminalStealth: return .monospacedSystemFont(ofSize: 12, weight: .regular)
case .proStudio: return .systemFont(ofSize: 14, weight: .semibold)
case .finder: return .systemFont(ofSize: 13, weight: .regular)
case .liquidGlass: return GlobalFontMagnification.systemFont(ofSize: 13, weight: .medium)
case .highDensity: return GlobalFontMagnification.systemFont(ofSize: 11, weight: .regular)
case .terminalStealth: return GlobalFontMagnification.monospacedSystemFont(ofSize: 12, weight: .regular)
case .proStudio: return GlobalFontMagnification.systemFont(ofSize: 14, weight: .semibold)
case .finder: return GlobalFontMagnification.systemFont(ofSize: 13, weight: .regular)
}
}
+30 -11
View File
@@ -1,6 +1,7 @@
import AppKit
import Bonsplit
import Combine
import CmuxFoundation
import CmuxWorkspaces
import CmuxSettings
import SwiftUI
@@ -362,6 +363,8 @@ struct FileExplorerPanelView: NSViewRepresentable {
}
}
@MainActor func openSelectedItem(in outlineView: NSOutlineView) { openNode(in: outlineView, at: outlineView.selectedRow) }
private func expandSelectedItemOrMoveToChild(in outlineView: NSOutlineView) {
guard let row = resolvedSelectionRow(in: outlineView),
let node = outlineView.item(atRow: row) as? FileExplorerNode,
@@ -641,6 +644,7 @@ final class FileExplorerContainerView: NSView {
private let loadingIndicator: NSProgressIndicator
private let searchController: any FileSearchControlling
private var searchBarHeightConstraint: NSLayoutConstraint!
private var searchFieldHeightConstraint: NSLayoutConstraint!
private(set) var searchSnapshot = FileSearchSnapshot.empty
private var currentRootPath = ""
private var currentProviderIsLocal = false
@@ -660,8 +664,10 @@ final class FileExplorerContainerView: NSView {
}
private var presentation: FileExplorerPanelPresentation
private let coordinator: FileExplorerPanelView.Coordinator
private var fontMagnificationObserver: GlobalFontMagnificationChangeObserver?
private let searchDebounceDelayMilliseconds = 200
private let searchBarVisibleHeight: CGFloat = 48
private var searchBarVisibleHeight: CGFloat { max(48, GlobalFontMagnification.scaled(48)) }
private var searchFieldVisibleHeight: CGFloat { max(24, GlobalFontMagnification.scaled(24)) }
#if DEBUG
private var debugLastSearchTextChangeUptime: TimeInterval = 0
@@ -706,7 +712,6 @@ final class FileExplorerContainerView: NSView {
searchField.translatesAutoresizingMaskIntoConstraints = false
searchField.setAccessibilityIdentifier("FileExplorerSearchField")
searchField.placeholderString = String(localized: "fileExplorer.search.placeholder", defaultValue: "Search files")
searchField.font = .systemFont(ofSize: 12, weight: .regular)
searchField.focusRingType = .none
searchField.cell?.usesSingleLineMode = true
searchField.cell?.isScrollable = true
@@ -732,7 +737,6 @@ final class FileExplorerContainerView: NSView {
searchBarView.addSubview(searchField)
searchStatusLabel.translatesAutoresizingMaskIntoConstraints = false
searchStatusLabel.font = .systemFont(ofSize: 11, weight: .medium)
searchStatusLabel.textColor = .secondaryLabelColor
searchStatusLabel.lineBreakMode = .byTruncatingTail
searchStatusLabel.maximumNumberOfLines = 1
@@ -743,7 +747,6 @@ final class FileExplorerContainerView: NSView {
// Empty state label
emptyLabel.translatesAutoresizingMaskIntoConstraints = false
emptyLabel.font = .systemFont(ofSize: 13)
emptyLabel.textColor = .secondaryLabelColor
emptyLabel.alignment = .center
emptyLabel.isHidden = true
@@ -755,6 +758,13 @@ final class FileExplorerContainerView: NSView {
loadingIndicator.controlSize = .small
loadingIndicator.isHidden = true
addSubview(loadingIndicator)
applyChromeFonts()
fontMagnificationObserver = GlobalFontMagnificationChangeObserver { [weak self] in
self?.applyChromeFonts()
self?.outlineView.reloadData()
self?.searchResultsView.rowHeight = FileExplorerSearchResultCellView.preferredRowHeight
self?.searchResultsView.reloadData()
}
// Outline view setup
outlineView.headerView = nil
@@ -806,7 +816,7 @@ final class FileExplorerContainerView: NSView {
searchResultsView.style = .plain
searchResultsView.selectionHighlightStyle = .regular
searchResultsView.backgroundColor = .clear
searchResultsView.rowHeight = 46
searchResultsView.rowHeight = FileExplorerSearchResultCellView.preferredRowHeight
searchResultsView.allowsMultipleSelection = true
searchResultsView.intercellSpacing = NSSize(width: 0, height: 0)
searchResultsView.onCancel = { [weak self] in
@@ -854,6 +864,7 @@ final class FileExplorerContainerView: NSView {
}
searchBarHeightConstraint = searchBarView.heightAnchor.constraint(equalToConstant: 0)
searchFieldHeightConstraint = searchField.heightAnchor.constraint(equalToConstant: searchFieldVisibleHeight)
NSLayoutConstraint.activate([
headerView.topAnchor.constraint(equalTo: topAnchor),
headerView.leadingAnchor.constraint(equalTo: leadingAnchor),
@@ -867,7 +878,7 @@ final class FileExplorerContainerView: NSView {
searchField.leadingAnchor.constraint(equalTo: searchBarView.leadingAnchor, constant: 8),
searchField.trailingAnchor.constraint(equalTo: searchBarView.trailingAnchor, constant: -8),
searchField.topAnchor.constraint(equalTo: searchBarView.topAnchor, constant: 4),
searchField.heightAnchor.constraint(equalToConstant: 24),
searchFieldHeightConstraint,
searchField.widthAnchor.constraint(greaterThanOrEqualToConstant: 120),
searchStatusLabel.leadingAnchor.constraint(equalTo: searchField.leadingAnchor, constant: 4),
@@ -892,6 +903,17 @@ final class FileExplorerContainerView: NSView {
])
}
private func applyChromeFonts() {
searchField.font = GlobalFontMagnification.systemFont(ofSize: 12, weight: .regular)
searchStatusLabel.font = GlobalFontMagnification.systemFont(ofSize: 11, weight: .medium)
emptyLabel.font = GlobalFontMagnification.systemFont(ofSize: 13)
searchFieldHeightConstraint?.constant = searchFieldVisibleHeight
if isSearchVisible {
searchBarHeightConstraint?.constant = searchBarVisibleHeight
}
headerView.applyFonts()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@@ -1518,6 +1540,7 @@ extension FileExplorerContainerView: NSSearchFieldDelegate, NSTableViewDataSourc
if let event = NSApp.currentEvent, searchField.handleOpenSelectionShortcut(event) { return true }
switch commandSelector {
case #selector(NSResponder.insertNewline(_:)):
guard !textView.hasMarkedText() else { return false }
openSelectedSearchResult()
return true
case #selector(NSResponder.cancelOperation(_:)):
@@ -1538,10 +1561,6 @@ extension FileExplorerContainerView: NSSearchFieldDelegate, NSTableViewDataSourc
searchSnapshot.results.count
}
func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat {
46
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
guard row >= 0, row < searchSnapshot.results.count else { return nil }
let identifier = NSUserInterfaceItemIdentifier("FileSearchResultCell")
@@ -1634,4 +1653,4 @@ extension FileExplorerContainerView: NSSearchFieldDelegate, NSTableViewDataSourc
copyRelativePathItem.representedObject = NSNumber(value: row)
menu.addItem(copyRelativePathItem)
}
}
}
+6 -3
View File
@@ -1,3 +1,4 @@
import CmuxFoundation
import AppKit
import Bonsplit
import SwiftUI
@@ -48,13 +49,13 @@ struct BrowserSearchOverlay: View {
if let selected = searchState.selected {
let totalText = searchState.total.map { String($0) } ?? "?"
Text("\(selected + 1)/\(totalText)")
.font(.caption)
.cmuxFont(.caption)
.foregroundColor(.secondary)
.monospacedDigit()
.padding(.trailing, 8)
} else if let total = searchState.total {
Text(total == 0 ? "0/0" : "-/\(total)")
.font(.caption)
.cmuxFont(.caption)
.foregroundColor(.secondary)
.monospacedDigit()
.padding(.trailing, 8)
@@ -206,6 +207,7 @@ private struct BrowserSearchTextFieldRepresentable: NSViewRepresentable {
let onFieldDidFocus: () -> Void
let onEscape: () -> Void
let onReturn: (_ isShift: Bool) -> Void
@Environment(\.cmuxGlobalFontMagnificationPercent) private var globalFontPercent
final class Coordinator: NSObject, NSTextFieldDelegate {
var parent: BrowserSearchTextFieldRepresentable
@@ -316,7 +318,7 @@ private struct BrowserSearchTextFieldRepresentable: NSViewRepresentable {
func makeNSView(context: Context) -> BrowserSearchNativeTextField {
let field = BrowserSearchNativeTextField(frame: .zero)
field.font = .systemFont(ofSize: NSFont.systemFontSize)
field.font = GlobalFontMagnification.systemFont(ofSize: NSFont.systemFontSize)
field.placeholderString = String(localized: "search.placeholder", defaultValue: "Search")
field.setAccessibilityIdentifier("BrowserFindSearchTextField")
field.delegate = context.coordinator
@@ -353,6 +355,7 @@ private struct BrowserSearchTextFieldRepresentable: NSViewRepresentable {
nsView.delegate = context.coordinator
nsView.cmuxSelectionOwner = selectionOwner
nsView.cmuxOnEscape = { [weak coordinator = context.coordinator] textView in coordinator?.handleEscape(from: textView) ?? false }
nsView.font = GlobalFontMagnification.systemFont(ofSize: NSFont.systemFontSize)
if let editor = nsView.currentEditor() as? NSTextView {
if editor.string != text, !editor.hasMarkedText() {
+6 -3
View File
@@ -1,3 +1,4 @@
import CmuxFoundation
import AppKit
import Bonsplit
import SwiftUI
@@ -65,13 +66,13 @@ struct SurfaceSearchOverlay: View {
if let selected = searchState.selected {
let totalText = searchState.total.map { String($0) } ?? "?"
Text("\(selected + 1)/\(totalText)")
.font(.caption)
.cmuxFont(.caption)
.foregroundColor(.secondary)
.monospacedDigit()
.padding(.trailing, 8)
} else if let total = searchState.total {
Text("-/\(total)")
.font(.caption)
.cmuxFont(.caption)
.foregroundColor(.secondary)
.monospacedDigit()
.padding(.trailing, 8)
@@ -232,6 +233,7 @@ private struct SearchTextFieldRepresentable: NSViewRepresentable {
let onFieldDidFocus: () -> Void
let onEscape: () -> Void
let onReturn: (_ isShift: Bool) -> Void
@Environment(\.cmuxGlobalFontMagnificationPercent) private var globalFontPercent
final class Coordinator: NSObject, NSTextFieldDelegate {
var parent: SearchTextFieldRepresentable
@@ -350,7 +352,7 @@ private struct SearchTextFieldRepresentable: NSViewRepresentable {
func makeNSView(context: Context) -> SearchNativeTextField {
let field = SearchNativeTextField(frame: .zero)
field.font = .systemFont(ofSize: NSFont.systemFontSize)
field.font = GlobalFontMagnification.systemFont(ofSize: NSFont.systemFontSize)
field.placeholderString = String(localized: "search.placeholder", defaultValue: "Search")
field.setAccessibilityIdentifier("TerminalFindSearchTextField")
field.delegate = context.coordinator
@@ -401,6 +403,7 @@ private struct SearchTextFieldRepresentable: NSViewRepresentable {
nsView.delegate = context.coordinator
nsView.cmuxSelectionOwner = selectionOwner
nsView.cmuxOnEscape = { [weak coordinator = context.coordinator] textView in coordinator?.handleEscape(from: textView) ?? false }
nsView.font = GlobalFontMagnification.systemFont(ofSize: NSFont.systemFontSize)
// Sync text from binding to field (skip during active IME composition)
if let editor = nsView.currentEditor() as? NSTextView {
+36 -10
View File
@@ -9,7 +9,6 @@ import CmuxWorkspaces
import CmuxTestSupport
import SwiftUI
import AppKit
import CmuxFoundation
import Metal
import QuartzCore
import Combine
@@ -407,7 +406,7 @@ class GhosttyApp {
runtimeFilesystem: .live(),
sessionPortBase: GhosttyApp.terminalSessionPortBase,
sessionPortRangeSize: GhosttyApp.terminalSessionPortRangeSize,
scrollbackReplayEnvironmentKey: SessionScrollbackReplayStore.environmentKey
scrollbackReplayEnvironmentKey: SessionScrollbackReplayStore.environmentKey, globalFontMagnificationPercent: { GlobalFontMagnification.storedPercent }
)
private static let releaseBundleIdentifier = "com.cmuxterm.app"
@@ -993,6 +992,7 @@ class GhosttyApp {
logLabel: "shell integration override (fallback)"
)
loadCmuxManagedTerminalSettingsConfig(fallbackConfig)
loadGlobalFontMagnificationConfig(fallbackConfig)
loadCmuxOwnedGhosttyKeybindOverrides(fallbackConfig)
loadNoActiveDisplayVsyncFallbackIfNeeded(fallbackConfig)
let fallbackRenderingModeChanged = setUsesHostLayerBackground(
@@ -1253,13 +1253,26 @@ class GhosttyApp {
logLabel: "shell integration override"
)
loadCmuxManagedTerminalSettingsConfig(config)
loadGlobalFontMagnificationConfig(config)
loadCmuxOwnedGhosttyKeybindOverrides(config)
loadNoActiveDisplayVsyncFallbackIfNeeded(config)
ghostty_config_finalize(config)
return renderingModeChanged
}
func loadGlobalFontMagnificationConfig(_ config: ghostty_config_t) {
guard !GlobalFontMagnification.isDefault else { return }
var fontSize: Float32 = 0
let key = "font-size"
guard ghostty_config_get(config, &fontSize, key, UInt(key.lengthOfBytes(using: .utf8))),
fontSize.isFinite, fontSize > 0 else { return }
let scaledFontSize = max(1, CGFloat(fontSize) * GlobalFontMagnification.scale)
loadInlineGhosttyConfig(
"font-size = \(Double(scaledFontSize))", into: config,
prefix: "cmux-global-font-magnification",
logLabel: "global font magnification"
)
}
private func loadNoActiveDisplayVsyncFallbackIfNeeded(_ config: ghostty_config_t) {
var displayCount: UInt32 = 0
let error = CGGetActiveDisplayList(0, nil, &displayCount)
@@ -2111,7 +2124,7 @@ class GhosttyApp {
)
return
}
let resolved = GhosttyConfig.load(preferredColorScheme: preferredColorScheme, useCache: false)
let resolved = GhosttyConfig.load(preferredColorScheme: preferredColorScheme, useCache: false, globalFontMagnificationPercent: GlobalFontMagnification.storedPercent)
let fallbackForUnspecified = Self.shouldIgnoreNativeLegacyBaselineForUnparsedAppearance()
? defaultBackgroundValues(from: nil)
: baseline
@@ -7939,11 +7952,6 @@ final class GhosttySurfaceScrollView: NSView {
keyboardCopyModeBadgeView.layer?.borderColor = NSColor.white.withAlphaComponent(0.12).cgColor
keyboardCopyModeBadgeView.alphaValue = 0.97
keyboardCopyModeBadgeIconView.translatesAutoresizingMaskIntoConstraints = false
keyboardCopyModeBadgeIconView.symbolConfiguration = NSImage.SymbolConfiguration(
pointSize: 13,
weight: .regular,
scale: .medium
)
keyboardCopyModeBadgeIconView.image = NSImage(
systemSymbolName: "keyboard.badge.ellipsis",
accessibilityDescription: terminalKeyTableIndicatorAccessibilityLabel
@@ -7951,7 +7959,7 @@ final class GhosttySurfaceScrollView: NSView {
keyboardCopyModeBadgeIconView.contentTintColor = NSColor.secondaryLabelColor
keyboardCopyModeBadgeLabel.translatesAutoresizingMaskIntoConstraints = false
keyboardCopyModeBadgeLabel.textColor = NSColor.labelColor
keyboardCopyModeBadgeLabel.font = NSFont.systemFont(ofSize: 13, weight: .semibold)
applyKeyboardCopyModeBadgeFonts()
keyboardCopyModeBadgeLabel.lineBreakMode = .byTruncatingTail
keyboardCopyModeBadgeLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
keyboardCopyModeBadgeLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
@@ -7975,6 +7983,15 @@ final class GhosttySurfaceScrollView: NSView {
])
keyboardCopyModeBadgeContainerView.isHidden = true
addSubview(keyboardCopyModeBadgeContainerView)
observers.append(
NotificationCenter.default.addObserver(
forName: GlobalFontMagnification.didChangeNotification,
object: nil,
queue: .main
) { [weak self] _ in
self?.applyKeyboardCopyModeBadgeFonts()
}
)
NSLayoutConstraint.activate([
keyboardCopyModeBadgeContainerView.topAnchor.constraint(equalTo: topAnchor, constant: 8),
keyboardCopyModeBadgeContainerView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8),
@@ -8160,6 +8177,15 @@ final class GhosttySurfaceScrollView: NSView {
}
private func applyKeyboardCopyModeBadgeFonts() {
keyboardCopyModeBadgeIconView.symbolConfiguration = NSImage.SymbolConfiguration(
pointSize: GlobalFontMagnification.scaledSize(13),
weight: .regular,
scale: .medium
)
keyboardCopyModeBadgeLabel.font = GlobalFontMagnification.systemFont(ofSize: 13, weight: .semibold)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) not implemented")
}
+1
View File
@@ -335,6 +335,7 @@ final class HostSettingsActions: SettingsHostActions {
GhosttyApp.shared.reloadConfiguration(source: reloadSource)
return true
}
}
/// Wraps the opaque observer returned by `NotificationCenter.addObserver` so the
+6 -5
View File
@@ -1,3 +1,4 @@
import CmuxFoundation
import AppKit
import SwiftUI
@@ -30,7 +31,7 @@ struct KeyboardShortcutRecorder: View {
Text(label)
if let subtitle {
Text(subtitle)
.font(.caption)
.cmuxFont(.caption)
.foregroundStyle(.secondary)
}
}
@@ -85,24 +86,24 @@ struct KeyboardShortcutRecorder: View {
if let validationMessage {
HStack(alignment: .firstTextBaseline, spacing: 8) {
Image(systemName: "exclamationmark.triangle.fill")
.font(.caption)
.cmuxFont(.caption)
.foregroundStyle(.red)
Text(validationMessage)
.font(.caption)
.cmuxFont(.caption)
.foregroundStyle(.red)
.fixedSize(horizontal: false, vertical: true)
if let validationButtonTitle, let onValidationButtonPressed {
Button(validationButtonTitle, action: onValidationButtonPressed)
.buttonStyle(.link)
.font(.caption)
.cmuxFont(.caption)
}
if let undoButtonTitle, let onUndoButtonPressed {
Button(undoButtonTitle, action: onUndoButtonPressed)
.buttonStyle(.link)
.font(.caption)
.cmuxFont(.caption)
}
}
.padding(.horizontal, 8)
@@ -436,6 +436,16 @@ final class CmuxSettingsFileStore {
}
snapshot.managedUserDefaults[SettingCatalog().app.newWorkspacePlacement.userDefaultsKey] = .string(placement.rawValue)
}
if let value = jsonInt(section["globalFontMagnification"]) {
let clamped = GlobalFontMagnification.clamp(value)
guard clamped == value else {
logInvalid("app.globalFontMagnification", sourcePath: sourcePath)
return
}
snapshot.managedUserDefaults[GlobalFontMagnification.percentKey] = .int(clamped)
} else if section.keys.contains("globalFontMagnification") {
logInvalid("app.globalFontMagnification", sourcePath: sourcePath)
}
if let raw = jsonString(section["forkConversationDefaultDestination"]) {
if let destination = AgentConversationForkDestination(rawValue: raw) {
snapshot.managedUserDefaults[AgentConversationForkDefaultSettings.key] = .string(destination.rawValue)
@@ -1592,6 +1602,8 @@ final class CmuxSettingsFileStore {
)
} else if change.defaultsKey == AppIconSettings.modeKey {
AppIconSettings.applyIcon(AppIconSettings.resolvedMode())
} else if change.defaultsKey == GlobalFontMagnification.percentKey {
notificationCenter.post(name: GlobalFontMagnification.didChangeNotification, object: nil)
}
}
@@ -1,3 +1,4 @@
import CmuxFoundation
import AppKit
import CMUXMobileCore
import SwiftUI
@@ -35,7 +36,7 @@ struct MobilePairingQRImageView: View {
.aspectRatio(1, contentMode: .fit)
.overlay(
Image(systemName: "qrcode")
.font(.system(size: 48))
.cmuxFont(size: 48)
.foregroundStyle(.secondary)
)
.accessibilityLabel(
+19 -18
View File
@@ -1,3 +1,4 @@
import CmuxFoundation
import AppKit
import CMUXMobileCore
import CmuxAuthRuntime
@@ -53,9 +54,9 @@ struct MobilePairingView: View {
private var header: some View {
VStack(alignment: .leading, spacing: 2) {
Text(String(localized: "mobile.pairing.window.heading", defaultValue: "Pair your iPhone"))
.font(.title2.weight(.semibold))
.cmuxFont(.title2, weight: .semibold)
Text(String(localized: "mobile.pairing.window.subheading", defaultValue: "Scan this code with the cmux app on your iPhone to sync your terminal workspaces."))
.font(.callout)
.cmuxFont(.callout)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
@@ -91,7 +92,7 @@ struct MobilePairingView: View {
String(localized: "mobile.pairing.req.tailscale.get", defaultValue: "Get Tailscale"),
destination: Self.tailscaleDownloadURL
)
.font(.callout)
.cmuxFont(.callout)
}
}
}
@@ -124,9 +125,9 @@ struct MobilePairingView: View {
) -> some View {
HStack(alignment: .firstTextBaseline, spacing: 10) {
VStack(alignment: .leading, spacing: 2) {
Text(title).font(.callout.weight(.medium))
Text(title).cmuxFont(.callout, weight: .medium)
Text(subtitle)
.font(.caption)
.cmuxFont(.caption)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
@@ -164,7 +165,7 @@ struct MobilePairingView: View {
private var needsTailscaleContent: some View {
VStack(spacing: 12) {
Image(systemName: "network.slash")
.font(.system(size: 28))
.cmuxFont(size: 28)
.foregroundStyle(.orange)
Text(String(localized: "mobile.pairing.needsTailscale.body", defaultValue: "This Mac has no Tailscale address, so your iPhone can't reach it. Install Tailscale on this Mac and your iPhone (same account), then refresh."))
.multilineTextAlignment(.center)
@@ -187,7 +188,7 @@ struct MobilePairingView: View {
private var signedOut: some View {
VStack(spacing: 12) {
Image(systemName: "person.crop.circle.badge.plus")
.font(.system(size: 28))
.cmuxFont(size: 28)
.foregroundStyle(.tint)
Text(String(localized: "mobile.pairing.signIn.prompt", defaultValue: "Sign in with your cmux account to pair your iPhone."))
.multilineTextAlignment(.center)
@@ -259,7 +260,7 @@ struct MobilePairingView: View {
private func failure(message: String) -> some View {
VStack(spacing: 12) {
Image(systemName: "exclamationmark.triangle")
.font(.system(size: 28))
.cmuxFont(size: 28)
.foregroundStyle(.orange)
Text(message)
.multilineTextAlignment(.center)
@@ -297,7 +298,7 @@ struct MobilePairingView: View {
HStack(spacing: 6) {
ProgressView().controlSize(.small)
Text(String(localized: "mobile.pairing.waiting", defaultValue: "Waiting for your iPhone…"))
.font(.callout)
.cmuxFont(.callout)
.foregroundStyle(.secondary)
}
}
@@ -319,10 +320,10 @@ struct MobilePairingView: View {
private func connectedContent(_ ready: MobilePairingModel.Ready) -> some View {
VStack(spacing: 12) {
Image(systemName: "checkmark.circle.fill")
.font(.system(size: 36))
.cmuxFont(size: 36)
.foregroundStyle(.green)
Text(String(localized: "mobile.pairing.connected.title", defaultValue: "iPhone connected"))
.font(.title3.weight(.semibold))
.cmuxFont(.title3, weight: .semibold)
Text(String(localized: "mobile.pairing.connected.subtitle", defaultValue: "Your terminal workspaces are now syncing to your iPhone. You can close this window."))
.multilineTextAlignment(.center)
.foregroundStyle(.secondary)
@@ -337,13 +338,13 @@ struct MobilePairingView: View {
HStack(spacing: 4) {
Spacer(minLength: 30)
Text(String(localized: "mobile.pairing.getApp.prompt", defaultValue: "Don't have it yet?"))
.font(.caption)
.cmuxFont(.caption)
.foregroundStyle(.secondary)
Link(
String(localized: "mobile.pairing.getApp.link", defaultValue: "Get cmux for iPhone"),
destination: Self.iphoneAppURL
)
.font(.caption)
.cmuxFont(.caption)
Spacer(minLength: 0)
}
step(2, String(localized: "mobile.pairing.step.signIn", defaultValue: "Sign in with the same account you use on this Mac."))
@@ -355,12 +356,12 @@ struct MobilePairingView: View {
private func step(_ number: Int, _ text: String) -> some View {
HStack(alignment: .firstTextBaseline, spacing: 10) {
Text("\(number)")
.font(.caption.weight(.bold))
.cmuxFont(.caption, weight: .bold)
.foregroundStyle(.white)
.frame(width: 20, height: 20)
.background(Color.accentColor, in: Circle())
Text(text)
.font(.callout)
.cmuxFont(.callout)
.fixedSize(horizontal: false, vertical: true)
Spacer(minLength: 0)
}
@@ -370,11 +371,11 @@ struct MobilePairingView: View {
private func manualFallback(_ ready: MobilePairingModel.Ready) -> some View {
VStack(alignment: .leading, spacing: 6) {
Text(String(localized: "mobile.pairing.manual.title", defaultValue: "Can't scan? Add this Mac manually:"))
.font(.caption.weight(.semibold))
.cmuxFont(.caption, weight: .semibold)
.foregroundStyle(.secondary)
ForEach(ready.tailscaleLines, id: \.self) { line in
Text(line)
.font(.system(.caption, design: .monospaced))
.cmuxFont(.caption, design: .monospaced)
.textSelection(.enabled)
.foregroundStyle(.secondary)
}
@@ -414,7 +415,7 @@ struct MobilePairingView: View {
: label
)
}
.font(.caption)
.cmuxFont(.caption)
}
.buttonStyle(.bordered)
.controlSize(.small)
+16 -15
View File
@@ -1,3 +1,4 @@
import CmuxFoundation
import Bonsplit
import SwiftUI
@@ -74,7 +75,7 @@ struct NotificationsPage: View {
private var header: some View {
HStack {
Text(String(localized: "notifications.title", defaultValue: "Notifications"))
.font(.title2)
.cmuxFont(.title2)
.fontWeight(.semibold)
Spacer()
@@ -98,7 +99,7 @@ struct NotificationsPage: View {
VStack(alignment: .leading, spacing: 2) {
Text(String(localized: "notifications.forwardToPhone.title", defaultValue: "Forward notifications to my iPhone"))
Text(String(localized: "notifications.forwardToPhone.subtitle", defaultValue: "Send agent notifications to the cmux iPhone app. Off by default; nothing is uploaded unless this is on."))
.font(.caption)
.cmuxFont(.caption)
.foregroundColor(.secondary)
}
}
@@ -115,17 +116,17 @@ struct NotificationsPage: View {
}
.pickerStyle(.menu)
.fixedSize()
.font(.caption)
.cmuxFont(.caption)
if forwardToPhoneMode == PhoneForwardingMode.onlyWhenAway.rawValue {
Text(awayModeExplanation)
.font(.caption)
.cmuxFont(.caption)
.foregroundColor(.secondary)
}
}
.padding(.leading, 20)
Toggle(isOn: $hidePhoneNotificationContent) {
Text(String(localized: "notifications.forwardToPhone.hideContent", defaultValue: "Hide content (send a generic message instead of the terminal text)"))
.font(.caption)
.cmuxFont(.caption)
}
.padding(.leading, 20)
}
@@ -145,12 +146,12 @@ struct NotificationsPage: View {
private var emptyState: some View {
VStack(spacing: 8) {
Image(systemName: "bell.slash")
.font(.system(size: 32))
.cmuxFont(size: 32)
.foregroundColor(.secondary)
Text(String(localized: "notifications.empty.title", defaultValue: "No notifications yet"))
.font(.headline)
.cmuxFont(.headline)
Text(String(localized: "notifications.empty.description", defaultValue: "Desktop notifications will appear here for quick review."))
.font(.subheadline)
.cmuxFont(.subheadline)
.foregroundColor(.secondary)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
@@ -159,10 +160,10 @@ struct NotificationsPage: View {
private var workspaceUnreadIndicatorState: some View {
VStack(spacing: 8) {
Image(systemName: "bell.badge")
.font(.system(size: 32))
.cmuxFont(size: 32)
.foregroundColor(.secondary)
Text(notificationStore.notificationMenuSnapshot.stateHintTitle)
.font(.headline)
.cmuxFont(.headline)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
@@ -226,7 +227,7 @@ struct ShortcutAnnotation: View {
private var badge: some View {
Text(text)
.font(.system(size: 10, weight: .semibold, design: .rounded))
.cmuxFont(size: 10, weight: .semibold, design: .rounded)
.foregroundStyle(.primary)
.padding(.horizontal, 6)
.padding(.vertical, 2)
@@ -260,24 +261,24 @@ private struct NotificationRow: View {
VStack(alignment: .leading, spacing: 6) {
HStack {
Text(notification.title)
.font(.headline)
.cmuxFont(.headline)
.foregroundColor(.primary)
Spacer()
Text(notification.createdAt.formatted(date: .omitted, time: .shortened))
.font(.caption)
.cmuxFont(.caption)
.foregroundColor(.secondary)
}
if !notification.body.isEmpty {
Text(notification.body)
.font(.subheadline)
.cmuxFont(.subheadline)
.foregroundColor(.secondary)
.lineLimit(3)
}
if let tabTitle {
Text(tabTitle)
.font(.caption)
.cmuxFont(.caption)
.foregroundColor(.secondary)
}
}
+5 -4
View File
@@ -1,3 +1,4 @@
import CmuxFoundation
import SwiftUI
/// Top-of-content dismissible banner shown when a pane's process tree crosses
@@ -44,7 +45,7 @@ struct PaneMemoryGuardrailBanner: View {
let memoryText = Self.byteFormatter.string(fromByteCount: warning.memoryBytes)
HStack(alignment: .top, spacing: 10) {
Image(systemName: "exclamationmark.triangle.fill")
.font(.system(size: 15, weight: .semibold))
.cmuxFont(size: 15, weight: .semibold)
.foregroundStyle(.orange)
.padding(.top, 1)
@@ -53,10 +54,10 @@ struct PaneMemoryGuardrailBanner: View {
localized: "paneMemoryGuardrail.banner.title",
defaultValue: "A pane is using a lot of memory"
))
.font(.system(size: 12.5, weight: .semibold))
.cmuxFont(size: 12.5, weight: .semibold)
Text(detailText(for: warning, memoryText: memoryText))
.font(.system(size: 11.5))
.cmuxFont(size: 11.5)
.foregroundStyle(.secondary)
.lineLimit(3)
.fixedSize(horizontal: false, vertical: true)
@@ -111,7 +112,7 @@ struct PaneMemoryGuardrailBanner: View {
guardrail.dismissActiveBanner()
} label: {
Image(systemName: "xmark")
.font(.system(size: 11, weight: .semibold))
.cmuxFont(size: 11, weight: .semibold)
.foregroundStyle(.secondary)
.frame(width: 20, height: 20)
.contentShape(Rectangle())

Some files were not shown because too many files have changed in this diff Show More