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:
co-authored by
Gigi Sayfan
parent
cd09255f41
commit
a6255a1d5f
@@ -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
|
||||
|
||||
|
+4
-3
@@ -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"),
|
||||
|
||||
+19
-5
@@ -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
|
||||
|
||||
+12
-11
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
+10
@@ -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)
|
||||
}
|
||||
}
|
||||
+6
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
@@ -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()
|
||||
}
|
||||
}
|
||||
+37
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-6
@@ -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"),
|
||||
]
|
||||
),
|
||||
|
||||
+4
-3
@@ -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)
|
||||
|
||||
+21
-1
@@ -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)
|
||||
}
|
||||
|
||||
+2
-1
@@ -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)
|
||||
|
||||
+1
@@ -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"),
|
||||
|
||||
+2
-1
@@ -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)
|
||||
|
||||
+2
-1
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+12
-1
@@ -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)
|
||||
}
|
||||
|
||||
+2
-1
@@ -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)
|
||||
|
||||
+4
-3
@@ -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)
|
||||
|
||||
+3
-2
@@ -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)
|
||||
}
|
||||
|
||||
+14
-13
@@ -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)
|
||||
}
|
||||
|
||||
+50
@@ -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)
|
||||
}
|
||||
}
|
||||
+7
-6
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -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)
|
||||
}
|
||||
|
||||
+3
-2
@@ -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 {
|
||||
|
||||
+23
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -39,6 +39,7 @@ struct SettingsRowAnchorResolutionTests {
|
||||
"app.commandPaletteSearchesAllSurfaces",
|
||||
"app.confirmQuit",
|
||||
"app.focusPaneOnFirstClick",
|
||||
"app.globalFontMagnification",
|
||||
"app.hideTabCloseButton",
|
||||
"app.iMessageMode",
|
||||
"app.keepWorkspaceOpenWhenClosingLastSurface",
|
||||
|
||||
+5
-4
@@ -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
|
||||
}
|
||||
+18
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+21
-12
@@ -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
|
||||
|
||||
+15
-14
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+4
-3
@@ -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)
|
||||
|
||||
+7
-7
@@ -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] = [
|
||||
|
||||
+6
-1
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+7
-7
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+8
-3
@@ -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.
|
||||
|
||||
+31
-28
@@ -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(
|
||||
|
||||
+57
-5
@@ -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)))
|
||||
}
|
||||
}
|
||||
|
||||
+23
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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,
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)?
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user