diff --git a/.claude/scheduled_tasks.lock b/.claude/scheduled_tasks.lock index 20959ecc454a..b972110ca382 100644 --- a/.claude/scheduled_tasks.lock +++ b/.claude/scheduled_tasks.lock @@ -1 +1 @@ -{"sessionId":"5c2a62d0-99c2-42c0-9452-f67b6fbaacda","pid":75304,"procStart":"Thu Jun 4 23:34:54 2026","acquiredAt":1780616564932} \ No newline at end of file +{"sessionId":"4311956b-e43f-405e-8b49-c3990850966d","pid":43365,"procStart":"Sun Jun 14 21:55:11 2026","acquiredAt":1781485012958} \ No newline at end of file diff --git a/.github/swift-file-length-budget.tsv b/.github/swift-file-length-budget.tsv index a11a55be6676..bc4cac065ea9 100644 --- a/.github/swift-file-length-budget.tsv +++ b/.github/swift-file-length-budget.tsv @@ -3,7 +3,7 @@ # Reduce counts as files shrink. CI fails if tracked files exceed this budget. 34202 CLI/cmux.swift 17772 Sources/AppDelegate.swift -16698 Sources/ContentView.swift +16814 Sources/ContentView.swift 14785 Sources/TerminalController.swift 13358 Sources/Panels/BrowserPanel.swift 12292 Sources/Workspace.swift @@ -13,8 +13,8 @@ 7912 Sources/Panels/BrowserPanelView.swift 7355 cmuxTests/WorkspaceUnitTests.swift 7221 cmuxTests/WorkspaceRemoteConnectionTests.swift -6363 cmuxTests/GhosttyConfigTests.swift 6317 cmuxTests/SessionPersistenceTests.swift +6299 cmuxTests/GhosttyConfigTests.swift 6153 CLI/cmux_open.swift 6096 Sources/TabManager.swift 6074 Sources/TextBoxInput.swift @@ -47,8 +47,8 @@ 2117 cmuxTests/CmuxConfigTests.swift 2092 cmuxTests/ShortcutAndCommandPaletteTests.swift 2070 Sources/SessionPersistence.swift -2065 Sources/KeyboardShortcutSettingsFileStore.swift 1949 Sources/Panels/BrowserWebAuthnSupport.swift +1936 Sources/KeyboardShortcutSettingsFileStore.swift 1860 cmuxTests/NotificationAndMenuBarTests.swift 1794 Sources/SessionIndexStore.swift 1777 Sources/RestorableAgentSession.swift @@ -152,8 +152,8 @@ 614 cmuxTests/SessionIndexViewTests.swift 611 Sources/TerminalController+ControlPaneContext.swift 608 Packages/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceGroupCoordinator.swift +605 Sources/SettingsNavigation.swift 604 Packages/CmuxCommandPalette/Tests/CmuxCommandPaletteTests/CommandPaletteNucleoFFITests.swift -603 Sources/SettingsNavigation.swift 599 Packages/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentLaunchSanitizerPrimaryPolicies.swift 596 cmuxTests/CmuxEventBusTests.swift 594 Sources/SessionIndexModels.swift diff --git a/Packages/CmuxSettings/Sources/CmuxSettings/Keys/SidebarCatalogSection.swift b/Packages/CmuxSettings/Sources/CmuxSettings/Keys/SidebarCatalogSection.swift index e3155946a48f..203d87b7c898 100644 --- a/Packages/CmuxSettings/Sources/CmuxSettings/Keys/SidebarCatalogSection.swift +++ b/Packages/CmuxSettings/Sources/CmuxSettings/Keys/SidebarCatalogSection.swift @@ -116,6 +116,18 @@ public struct SidebarCatalogSection: SettingCatalogSection { userDefaultsKey: "sidebarShowStatusPills" ) + public let rightMaxWidth = DefaultsKey( + id: "sidebar.rightMaxWidth", + defaultValue: RightSidebarWidthSettings.noOverrideValue, + userDefaultsKey: RightSidebarWidthSettings.maxWidthKey + ) + + public let rememberedRightMaxWidth = DefaultsKey( + id: "sidebar.rightMaxWidth.remembered", + defaultValue: RightSidebarWidthSettings.defaultConfiguredMaximumWidth, + userDefaultsKey: RightSidebarWidthSettings.rememberedMaxWidthKey + ) + public let activeTabIndicatorStyle = DefaultsKey( id: "sidebar.activeTabIndicatorStyle", defaultValue: "leftRail", diff --git a/Packages/CmuxSettings/Sources/CmuxSettings/Policies/RightSidebarWidthSettings.swift b/Packages/CmuxSettings/Sources/CmuxSettings/Policies/RightSidebarWidthSettings.swift new file mode 100644 index 000000000000..28b3e6a9e29e --- /dev/null +++ b/Packages/CmuxSettings/Sources/CmuxSettings/Policies/RightSidebarWidthSettings.swift @@ -0,0 +1,98 @@ +import Foundation + +/// Policy values and normalization helpers for the right sidebar width override. +public struct RightSidebarWidthSettings: Sendable { + /// Creates a stateless right sidebar width policy value. + public init() {} + + /// The `cmux.json` key under `sidebar` that stores the right sidebar maximum width. + public static let jsonKey = "rightMaxWidth" + + /// The dotted settings path for the right sidebar maximum width override. + public static let settingsPath = "sidebar.rightMaxWidth" + + /// The `UserDefaults` key that stores the active right sidebar maximum width override. + public static let maxWidthKey = "rightSidebarMaxWidth" + + /// The `UserDefaults` key that remembers the previous right sidebar maximum width override. + public static let rememberedMaxWidthKey = "rightSidebarRememberedMaxWidth" + + /// The stored sentinel value that means the built-in dynamic width cap is active. + public static let noOverrideValue = -1.0 + + /// The smallest allowed right sidebar width, in points. + public static let minimumWidth = 276.0 + + /// The built-in right sidebar maximum width, in points, used when no override is active. + public static let builtInMaximumWidth = 1200.0 + + /// The width restored when enabling the override without a remembered value. + public static let defaultConfiguredMaximumWidth = builtInMaximumWidth + + /// The largest width accepted by settings editors and imported `cmux.json` values. + public static let settingsEditorMaximumWidth = 4096.0 + + /// Returns the effective configured maximum width for a stored value. + /// + /// - Parameter storedValue: The persisted width override value. + /// - Returns: A clamped width when the stored value enables the override, or `nil`. + public func configuredMaximumWidth(from storedValue: Double) -> Double? { + guard storedValue.isFinite, storedValue > 0 else { + return nil + } + return clampedSettingsEditorMaximumWidth(storedValue) + } + + /// Clamps a settings-editor width to the supported range. + /// + /// - Parameter value: The requested width in points. + /// - Returns: A finite rounded width within the settings editor bounds. + public func clampedSettingsEditorMaximumWidth(_ value: Double) -> Double { + guard value.isFinite else { + return Self.defaultConfiguredMaximumWidth + } + return min(Self.settingsEditorMaximumWidth, max(Self.minimumWidth, value.rounded())) + } + + /// Returns the remembered width to restore for a persisted value. + /// + /// - Parameter storedValue: The stored remembered width. + /// - Returns: A clamped remembered width, or the default configured width. + public func rememberedMaximumWidth(from storedValue: Double) -> Double { + guard let configuredMaximumWidth = configuredMaximumWidth(from: storedValue) else { + return Self.defaultConfiguredMaximumWidth + } + return clampedSettingsEditorMaximumWidth(configuredMaximumWidth) + } + + /// Returns the width displayed in settings for the active and remembered values. + /// + /// - Parameters: + /// - activeStoredValue: The currently active stored override value. + /// - rememberedStoredValue: The remembered override value used when inactive. + /// - Returns: The clamped editor width to show. + public func editorMaximumWidth(activeStoredValue: Double, rememberedStoredValue: Double) -> Double { + if let configuredMaximumWidth = configuredMaximumWidth(from: activeStoredValue) { + return clampedSettingsEditorMaximumWidth(configuredMaximumWidth) + } + return rememberedMaximumWidth(from: rememberedStoredValue) + } + + /// Returns the stored value to write when enabling the override. + /// + /// - Parameter rememberedStoredValue: The remembered override value. + /// - Returns: The clamped override width to store as active. + public func storedMaximumWidthWhenEnabling(rememberedStoredValue: Double) -> Double { + rememberedMaximumWidth(from: rememberedStoredValue) + } + + /// Returns the remembered value to preserve before disabling the override. + /// + /// - Parameters: + /// - activeStoredValue: The currently active stored override value. + /// - rememberedStoredValue: The remembered override value used as fallback. + /// - Returns: The clamped width to store as remembered. + public func storedRememberedMaximumWidth(activeStoredValue: Double, rememberedStoredValue: Double) -> Double { + editorMaximumWidth(activeStoredValue: activeStoredValue, rememberedStoredValue: rememberedStoredValue) + } +} diff --git a/Packages/CmuxSettings/Tests/CmuxSettingsTests/RightSidebarWidthSettingsTests.swift b/Packages/CmuxSettings/Tests/CmuxSettingsTests/RightSidebarWidthSettingsTests.swift new file mode 100644 index 000000000000..19dd7b335dd7 --- /dev/null +++ b/Packages/CmuxSettings/Tests/CmuxSettingsTests/RightSidebarWidthSettingsTests.swift @@ -0,0 +1,48 @@ +import Testing +@testable import CmuxSettings + +@Suite("RightSidebarWidthSettings") +struct RightSidebarWidthSettingsTests { + private let settings = RightSidebarWidthSettings() + + @Test func disabledOverrideRestoresRememberedCustomMaximumWhenEnabledAgain() { + let restored = settings.storedMaximumWidthWhenEnabling( + rememberedStoredValue: 1_234 + ) + + #expect(restored == 1_234) + } + + @Test func invalidRememberedMaximumFallsBackToDefaultWhenEnabled() { + let restored = settings.storedMaximumWidthWhenEnabling( + rememberedStoredValue: RightSidebarWidthSettings.noOverrideValue + ) + + #expect(restored == RightSidebarWidthSettings.defaultConfiguredMaximumWidth) + } + + @Test func activeCustomMaximumWinsOverRememberedValueForEditor() { + let editorValue = settings.editorMaximumWidth( + activeStoredValue: 1_500, + rememberedStoredValue: 900 + ) + + #expect(editorValue == 1_500) + } + + @Test func configuredMaximumWidthIsClampedToEditorRange() throws { + let configured = try #require( + settings.configuredMaximumWidth(from: 10_000) + ) + + #expect(configured == RightSidebarWidthSettings.settingsEditorMaximumWidth) + } + + @Test func rememberedCustomMaximumIsClampedToEditorRange() { + let restored = settings.storedMaximumWidthWhenEnabling( + rememberedStoredValue: 10_000 + ) + + #expect(restored == RightSidebarWidthSettings.settingsEditorMaximumWidth) + } +} diff --git a/Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry+Default.swift b/Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry+Default.swift index be12511c55fa..b14330e9fee2 100644 --- a/Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry+Default.swift +++ b/Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry+Default.swift @@ -98,6 +98,7 @@ extension Array where Element == CuratedSettingEntry { .init(section: .sidebarAppearance, id: "show-log", title: "Show Latest Log in Sidebar", synonyms: "sidebar.showLog log status latest message imperative"), .init(section: .sidebarAppearance, id: "show-progress", title: "Show Progress in Sidebar", synonyms: "sidebar.showProgress progress bar percent status set_progress"), .init(section: .sidebarAppearance, id: "show-metadata", title: "Show Custom Metadata in Sidebar", synonyms: "sidebar.showCustomMetadata metadata meta report_meta status custom block"), + .init(section: .sidebarAppearance, id: "right-max-width", title: "Dock Max Width", synonyms: "sidebar.rightMaxWidth dock right sidebar max width terminal reservation cap logs lazygit"), // Mobile .init(section: .mobile, id: "pairDevice", title: "Pair a Device", synonyms: "pair pairing add device qr qr code scan iphone ipad ios mobile tailscale connect onboarding sign in"), diff --git a/Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/SidebarSection.swift b/Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/SidebarSection.swift index 2dde705522bd..a6975d152380 100644 --- a/Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/SidebarSection.swift +++ b/Packages/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/SidebarSection.swift @@ -10,6 +10,7 @@ import SwiftUI public struct SidebarSection: View { private let catalog: SettingCatalog private let hostActions: SettingsHostActions + private let rightSidebarWidthSettings = RightSidebarWidthSettings() @State private var sidebarFont: SettingsFontSize @State private var fontSaveFailed = false @@ -33,6 +34,8 @@ public struct SidebarSection: View { @State private var showLog: DefaultsValueModel @State private var showProgress: DefaultsValueModel @State private var showMetadata: DefaultsValueModel + @State private var rightMaxWidth: DefaultsValueModel + @State private var rememberedRightMaxWidth: DefaultsValueModel public init(defaultsStore: UserDefaultsSettingsStore, catalog: SettingCatalog, hostActions: SettingsHostActions) { self.catalog = catalog @@ -57,6 +60,8 @@ public struct SidebarSection: View { _showLog = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.sidebar.showLog)) _showProgress = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.sidebar.showProgress)) _showMetadata = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.sidebar.showCustomMetadata)) + _rightMaxWidth = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.sidebar.rightMaxWidth)) + _rememberedRightMaxWidth = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.sidebar.rememberedRightMaxWidth)) } public var body: some View { @@ -77,6 +82,62 @@ public struct SidebarSection: View { } } + private var rightMaxWidthOverrideEnabled: Bool { + rightMaxWidth.current.isFinite && rightMaxWidth.current > 0 + } + + private var rightMaxWidthOverrideBinding: Binding { + Binding( + get: { rightMaxWidthOverrideEnabled }, + set: { enabled in + if enabled { + let restored = rightSidebarWidthSettings.storedMaximumWidthWhenEnabling( + rememberedStoredValue: rememberedRightMaxWidth.current + ) + rememberedRightMaxWidth.set(restored) + rightMaxWidth.set(restored) + } else { + rememberedRightMaxWidth.set( + rightSidebarWidthSettings.storedRememberedMaximumWidth( + activeStoredValue: rightMaxWidth.current, + rememberedStoredValue: rememberedRightMaxWidth.current + ) + ) + rightMaxWidth.set(RightSidebarWidthSettings.noOverrideValue) + } + } + ) + } + + private var rightMaxWidthEditorBinding: Binding { + Binding( + get: { + rightSidebarWidthSettings.editorMaximumWidth( + activeStoredValue: rightMaxWidth.current, + rememberedStoredValue: rememberedRightMaxWidth.current + ) + }, + set: { + let clamped = clampedRightMaxWidth($0) + rememberedRightMaxWidth.set(clamped) + if rightMaxWidthOverrideEnabled { + rightMaxWidth.set(clamped) + } + } + ) + } + + private var rightMaxWidthSubtitle: String { + if rightMaxWidthOverrideEnabled { + return String(localized: "settings.sidebar.rightMaxWidth.subtitleOn", defaultValue: "The Dock can grow past the built-in width cap while preserving terminal space.") + } + return String(localized: "settings.sidebar.rightMaxWidth.subtitleOff", defaultValue: "Use the built-in dynamic cap that keeps extra terminal space reserved.") + } + + private func clampedRightMaxWidth(_ value: Double) -> Double { + rightSidebarWidthSettings.clampedSettingsEditorMaximumWidth(value) + } + @ViewBuilder private var mainCard: some View { SettingsCard { @@ -135,6 +196,31 @@ public struct SidebarSection: View { } SettingsCardDivider() + SettingsCardRow( + configurationReview: .json("sidebar.rightMaxWidth"), + String(localized: "settings.sidebar.rightMaxWidth", defaultValue: "Dock Max Width"), + subtitle: rightMaxWidthSubtitle, + controlWidth: 250 + ) { + HStack(spacing: 8) { + Toggle("", isOn: rightMaxWidthOverrideBinding) + .labelsHidden() + .toggleStyle(.switch) + .controlSize(.small) + .accessibilityLabel(String(localized: "settings.sidebar.rightMaxWidth.toggle", defaultValue: "Use custom Dock max width")) + + TextField("", value: rightMaxWidthEditorBinding, format: .number) + .textFieldStyle(.roundedBorder) + .frame(width: 72) + .disabled(!rightMaxWidthOverrideEnabled) + .accessibilityLabel(String(localized: "settings.sidebar.rightMaxWidth", defaultValue: "Dock Max Width")) + + Text(String(localized: "settings.sidebar.rightMaxWidth.unit", defaultValue: "pt")) + .foregroundStyle(.secondary) + } + } + SettingsCardDivider() + SettingsCardRow( configurationReview: .json("sidebar.hideAllDetails"), String(localized: "settings.app.hideAllSidebarDetails", defaultValue: "Hide All Sidebar Details"), diff --git a/Packages/CmuxSettingsUI/Tests/CmuxSettingsUITests/SettingsRowAnchorResolutionTests.swift b/Packages/CmuxSettingsUI/Tests/CmuxSettingsUITests/SettingsRowAnchorResolutionTests.swift index d290e6dc8974..daf3a96b0c6e 100644 --- a/Packages/CmuxSettingsUI/Tests/CmuxSettingsUITests/SettingsRowAnchorResolutionTests.swift +++ b/Packages/CmuxSettingsUI/Tests/CmuxSettingsUITests/SettingsRowAnchorResolutionTests.swift @@ -88,6 +88,7 @@ struct SettingsRowAnchorResolutionTests { "sidebar.openPortLinksInCmuxBrowser", "sidebar.openPullRequestLinksInCmuxBrowser", "sidebar.pathLastSegmentOnly", + "sidebar.rightMaxWidth", "sidebar.showBranchDirectory", "sidebar.showCustomMetadata", "sidebar.showLog", diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings index 0c49ee0fa3bf..c509411c4a1c 100644 --- a/Resources/Localizable.xcstrings +++ b/Resources/Localizable.xcstrings @@ -195011,6 +195011,756 @@ } } }, + "settings.search.alias.setting.sidebarAppearance.right-max-width": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "sidebar.rightMaxWidth dock right sidebar max width terminal reservation cap logs lazygit" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "sidebar.rightMaxWidth dock right sidebar max width terminal reservation cap logs lazygit Dock 右サイドバー 最大幅 端末 予約幅 上限 ログ lazygit" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "sidebar.rightMaxWidth dock right sidebar max width terminal reservation cap logs lazygit أقصى عرض اللوحة الجانبية اليمنى الطرفية سجلات" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "sidebar.rightMaxWidth dock right sidebar max width terminal reservation cap logs lazygit maksimalna širina desna bočna traka terminal zapisnici" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "sidebar.rightMaxWidth dock right sidebar max width terminal reservation cap logs lazygit maksimal bredde højre sidepanel terminal logge" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "sidebar.rightMaxWidth dock right sidebar max width terminal reservation cap logs lazygit maximale breite rechte seitenleiste terminal protokolle" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "sidebar.rightMaxWidth dock right sidebar max width terminal reservation cap logs lazygit ancho máximo barra lateral derecha terminal registros" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "sidebar.rightMaxWidth dock right sidebar max width terminal reservation cap logs lazygit largeur maximale barre latérale droite terminal journaux" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "sidebar.rightMaxWidth dock right sidebar max width terminal reservation cap logs lazygit larghezza massima barra laterale destra terminale log" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "sidebar.rightMaxWidth dock right sidebar max width terminal reservation cap logs lazygit Dock 오른쪽 사이드바 최대 너비 터미널 로그" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "sidebar.rightMaxWidth dock right sidebar max width terminal reservation cap logs lazygit ទទឹងអតិបរមា របារចំហៀងស្តាំ terminal កំណត់ហេតុ lazygit" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "sidebar.rightMaxWidth dock right sidebar max width terminal reservation cap logs lazygit maks bredde høyre sidepanel terminal logger" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "sidebar.rightMaxWidth dock right sidebar max width terminal reservation cap logs lazygit maksymalna szerokość prawy pasek boczny terminal logi" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "sidebar.rightMaxWidth dock right sidebar max width terminal reservation cap logs lazygit largura máxima barra lateral direita terminal logs" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "sidebar.rightMaxWidth dock right sidebar max width terminal reservation cap logs lazygit максимальная ширина правая боковая панель терминал журналы" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "sidebar.rightMaxWidth dock right sidebar max width terminal reservation cap logs lazygit ความกว้างสูงสุด แถบด้านขวา เทอร์มินัล บันทึก" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "sidebar.rightMaxWidth dock right sidebar max width terminal reservation cap logs lazygit en fazla genişlik sağ kenar çubuğu terminal günlükler" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "sidebar.rightMaxWidth dock right sidebar max width terminal reservation cap logs lazygit максимальна ширина права бічна панель термінал журнали" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "sidebar.rightMaxWidth dock right sidebar max width terminal reservation cap logs lazygit Dock 右侧边栏 最大宽度 终端 日志" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "sidebar.rightMaxWidth dock right sidebar max width terminal reservation cap logs lazygit Dock 右側邊欄 最大寬度 終端 日誌" + } + } + } + }, + "settings.sidebar.rightMaxWidth": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Dock Max Width" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "Dock の最大幅" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "أقصى عرض للـ Dock" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Maksimalna širina Docka" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Maks. bredde for Dock" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Maximale Dock-Breite" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ancho máximo del Dock" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Largeur maximale du Dock" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Larghezza massima del Dock" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "Dock 최대 너비" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "ទទឹងអតិបរមា Dock" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Maks bredde for Dock" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Maksymalna szerokość Docka" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Largura máxima do Dock" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Максимальная ширина Dock" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ความกว้างสูงสุดของ Dock" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Dock en fazla genişliği" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Максимальна ширина Dock" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "Dock 最大宽度" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "Dock 最大寬度" + } + } + } + }, + "settings.sidebar.rightMaxWidth.subtitleOff": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Use the built-in dynamic cap that keeps extra terminal space reserved." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "端末用の余白を確保する組み込みの動的上限を使います。" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "استخدم الحد الديناميكي المضمن الذي يحافظ على مساحة إضافية للطرفية." + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Koristi ugrađeno dinamičko ograničenje koje zadržava dodatni prostor za terminal." + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Brug den indbyggede dynamiske grænse, der reserverer ekstra plads til terminalen." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die integrierte dynamische Begrenzung verwenden, die zusätzlichen Terminalplatz freihält." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Usa el límite dinámico integrado que reserva espacio adicional para la terminal." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Utiliser la limite dynamique intégrée qui réserve de l’espace supplémentaire au terminal." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Usa il limite dinamico integrato che riserva spazio extra al terminale." + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "터미널 공간을 추가로 남겨 두는 내장 동적 제한을 사용합니다." + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "ប្រើដែនកំណត់ថាមវន្តដែលមានស្រាប់ ដែលរក្សាទុកកន្លែងបន្ថែមសម្រាប់ terminal។" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Bruk den innebygde dynamiske grensen som holder av ekstra plass til terminalen." + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Użyj wbudowanego limitu dynamicznego, który rezerwuje dodatkowe miejsce dla terminala." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Use o limite dinâmico integrado que reserva espaço extra para o terminal." + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Использовать встроенное динамическое ограничение, которое оставляет дополнительное место для терминала." + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ใช้ขีดจำกัดแบบไดนามิกในตัวที่กันพื้นที่เพิ่มเติมไว้สำหรับเทอร์มินัล" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Terminal için ek alan ayıran yerleşik dinamik sınırı kullan." + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Використовувати вбудоване динамічне обмеження, яке залишає додаткове місце для термінала." + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "使用内置动态上限,为终端保留额外空间。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "使用內建動態上限,為終端保留額外空間。" + } + } + } + }, + "settings.sidebar.rightMaxWidth.subtitleOn": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The Dock can grow past the built-in width cap while preserving terminal space." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "Dock は端末用の空間を確保したまま、組み込みの幅上限を超えて広げられます。" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "يمكن للـ Dock أن يتجاوز حد العرض المضمن مع الحفاظ على مساحة للطرفية." + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Dock se može proširiti preko ugrađenog ograničenja širine uz očuvanje prostora za terminal." + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Docken kan vokse forbi den indbyggede breddegrænse, mens der stadig reserveres plads til terminalen." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Das Dock kann über die integrierte Breitenbegrenzung hinaus wachsen, während Platz für das Terminal erhalten bleibt." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "El Dock puede superar el límite de ancho integrado sin dejar de reservar espacio para la terminal." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le Dock peut dépasser la limite de largeur intégrée tout en réservant de l’espace au terminal." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il Dock può superare il limite di larghezza integrato mantenendo spazio per il terminale." + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "Dock은 터미널 공간을 유지하면서 내장 너비 상한을 넘어 커질 수 있습니다." + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "Dock អាចពង្រីកលើសដែនកំណត់ទទឹងដែលមានស្រាប់ ខណៈរក្សាកន្លែងសម្រាប់ terminal។" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Docken kan vokse forbi den innebygde breddegrensen samtidig som terminalplass beholdes." + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Dock może przekroczyć wbudowany limit szerokości, zachowując miejsce dla terminala." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O Dock pode crescer além do limite de largura integrado enquanto preserva espaço para o terminal." + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Dock может превышать встроенное ограничение ширины, сохраняя место для терминала." + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "Dock สามารถขยายเกินขีดจำกัดความกว้างในตัว โดยยังคงเว้นพื้นที่ให้เทอร์มินัล" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Dock, terminal alanını korurken yerleşik genişlik sınırını aşarak büyüyebilir." + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Dock може перевищувати вбудоване обмеження ширини, зберігаючи місце для термінала." + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "Dock 可以在保留终端空间的同时超过内置宽度上限。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "Dock 可以在保留終端空間的同時超過內建寬度上限。" + } + } + } + }, + "settings.sidebar.rightMaxWidth.toggle": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Use custom Dock max width" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "Dock の最大幅をカスタム設定" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "استخدام عرض Dock أقصى مخصص" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Koristi prilagođenu maksimalnu širinu Docka" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Brug brugerdefineret maks. Dock-bredde" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Eigene maximale Dock-Breite verwenden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Usar ancho máximo personalizado del Dock" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Utiliser une largeur maximale personnalisée du Dock" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Usa larghezza massima personalizzata del Dock" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "사용자 지정 Dock 최대 너비 사용" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "ប្រើទទឹងអតិបរមា Dock ផ្ទាល់ខ្លួន" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Bruk egendefinert maks bredde for Dock" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Użyj niestandardowej maksymalnej szerokości Docka" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Usar largura máxima personalizada do Dock" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Использовать пользовательскую максимальную ширину Dock" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ใช้ความกว้างสูงสุดของ Dock แบบกำหนดเอง" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Özel Dock en fazla genişliğini kullan" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Використовувати власну максимальну ширину Dock" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "使用自定义 Dock 最大宽度" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "使用自訂 Dock 最大寬度" + } + } + } + }, + "settings.sidebar.rightMaxWidth.unit": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "pt" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "pt" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "pt" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "pt" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "pt" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "pt" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "pt" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "pt" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "pt" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "pt" + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "pt" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "pt" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "pt" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "pt" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "pt" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "pt" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "pt" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "pt" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "pt" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "pt" + } + } + } + }, "terminal.shortcut.name.ctrlL": { "extractionState": "manual", "localizations": { diff --git a/Sources/CmuxSettingsJSONPathSupport.swift b/Sources/CmuxSettingsJSONPathSupport.swift index f1c154019e1d..bc81ca6dbc68 100644 --- a/Sources/CmuxSettingsJSONPathSupport.swift +++ b/Sources/CmuxSettingsJSONPathSupport.swift @@ -1,5 +1,8 @@ +import CmuxSettings import Foundation +typealias RightSidebarWidthSettings = CmuxSettings.RightSidebarWidthSettings + enum SidebarWorkspaceDetailDefaults { static let showBranchDirectoryKey = "sidebarShowBranchDirectory" static let showPullRequestsKey = "sidebarShowPullRequest" @@ -61,6 +64,257 @@ enum AutomationSettings { static let defaultPortRange = 10 } +struct SettingsFileBooleanMapping { + let jsonKey: String + let defaultsKey: String + let invalidPath: String? + + init(jsonKey: String, defaultsKey: String, invalidPath: String? = nil) { + self.jsonKey = jsonKey + self.defaultsKey = defaultsKey + self.invalidPath = invalidPath + } +} + +struct SettingsFileStringMapping { + let jsonKey: String + let defaultsKey: String +} + +struct SettingsFileStringArrayMapping { + let jsonKey: String + let defaultsKey: String + let invalidPath: String +} + +enum AppSettingsFileMapping { + private static let app = AppCatalogSection() + + static let booleanSettings: [SettingsFileBooleanMapping] = [ + .init( + jsonKey: "workspaceInheritWorkingDirectory", + defaultsKey: app.workspaceInheritWorkingDirectory.userDefaultsKey, + invalidPath: "app.workspaceInheritWorkingDirectory" + ), + .init(jsonKey: "focusPaneOnFirstClick", defaultsKey: PaneFirstClickFocusSettings.enabledKey), + .init( + jsonKey: "openSupportedFilesInCmux", + defaultsKey: app.openSupportedFilesInCmux.userDefaultsKey + ), + .init( + jsonKey: "openMarkdownInCmuxViewer", + defaultsKey: app.openMarkdownInCmuxViewer.userDefaultsKey + ), + .init(jsonKey: "reorderOnNotification", defaultsKey: app.reorderOnNotification.userDefaultsKey), + .init(jsonKey: "iMessageMode", defaultsKey: IMessageModeSettings.key), + .init( + jsonKey: "sendAnonymousTelemetry", + defaultsKey: app.sendAnonymousTelemetry.userDefaultsKey + ), + .init( + jsonKey: "warnBeforeClosingTab", + defaultsKey: app.warnBeforeClosingTab.userDefaultsKey + ), + .init( + jsonKey: "warnBeforeClosingTabXButton", + defaultsKey: app.warnBeforeClosingTabXButton.userDefaultsKey + ), + .init( + jsonKey: "hideTabCloseButton", + defaultsKey: app.hideTabCloseButton.userDefaultsKey + ), + .init( + jsonKey: "renameSelectsExistingName", + defaultsKey: app.renameSelectsExistingName.userDefaultsKey + ), + .init( + jsonKey: "commandPaletteSearchesAllSurfaces", + defaultsKey: app.commandPaletteSearchesAllSurfaces.userDefaultsKey + ), + ] + + static let stringSettings: [SettingsFileStringMapping] = [ + .init(jsonKey: "preferredEditor", defaultsKey: app.preferredEditor.userDefaultsKey), + ] +} + +enum NotificationSettingsFileMapping { + static let booleanSettings: [SettingsFileBooleanMapping] = [ + .init(jsonKey: "dockBadge", defaultsKey: NotificationBadgeSettings.dockBadgeEnabledKey), + .init(jsonKey: "showInMenuBar", defaultsKey: MenuBarExtraSettings.showInMenuBarKey), + .init(jsonKey: "unreadPaneRing", defaultsKey: NotificationPaneRingSettings.enabledKey), + .init(jsonKey: "paneFlash", defaultsKey: NotificationPaneFlashSettings.enabledKey), + ] + + static let stringSettings: [SettingsFileStringMapping] = [ + .init(jsonKey: "customSoundFilePath", defaultsKey: NotificationSoundSettings.customFilePathKey), + .init(jsonKey: "command", defaultsKey: NotificationSoundSettings.customCommandKey), + ] +} + +enum TerminalSettingsFileMapping { + static let booleanSettings: [SettingsFileBooleanMapping] = [ + .init( + jsonKey: "showScrollBar", + defaultsKey: TerminalScrollBarSettings.showScrollBarKey, + invalidPath: "terminal.showScrollBar" + ), + .init( + jsonKey: "copyOnSelect", + defaultsKey: TerminalCopyOnSelectSettings.copyOnSelectKey, + invalidPath: "terminal.copyOnSelect" + ), + .init( + jsonKey: "autoResumeAgentSessions", + defaultsKey: AgentSessionAutoResumeSettings.autoResumeAgentSessionsKey, + invalidPath: "terminal.autoResumeAgentSessions" + ), + ] +} + +enum SidebarSettingsFileMapping { + private static let sidebar = SidebarCatalogSection() + + struct BooleanSetting { + let jsonKey: String + let defaultsKey: String + } + + static let booleanSettings: [BooleanSetting] = [ + .init( + jsonKey: "hideAllDetails", + defaultsKey: sidebar.hideAllDetails.userDefaultsKey + ), + .init( + jsonKey: "wrapWorkspaceTitles", + defaultsKey: SidebarWorkspaceTitleWrapSettings.key + ), + .init( + jsonKey: "showWorkspaceDescription", + defaultsKey: sidebar.showWorkspaceDescription.userDefaultsKey + ), + .init( + jsonKey: "stackBranchDirectory", + defaultsKey: sidebar.stackBranchDirectory.userDefaultsKey + ), + .init( + jsonKey: "pathLastSegmentOnly", + defaultsKey: sidebar.pathLastSegmentOnly.userDefaultsKey + ), + .init( + jsonKey: "showNotificationMessage", + defaultsKey: sidebar.showNotificationMessage.userDefaultsKey + ), + .init( + jsonKey: "showBranchDirectory", + defaultsKey: SidebarWorkspaceDetailDefaults.showBranchDirectoryKey + ), + .init( + jsonKey: "showPullRequests", + defaultsKey: SidebarWorkspaceDetailDefaults.showPullRequestsKey + ), + .init( + jsonKey: "watchGitStatus", + defaultsKey: SidebarWorkspaceDetailDefaults.watchGitStatusKey + ), + .init( + jsonKey: "makePullRequestsClickable", + defaultsKey: sidebar.makePullRequestsClickable.userDefaultsKey + ), + .init( + jsonKey: "openPullRequestLinksInCmuxBrowser", + defaultsKey: BrowserLinkOpenSettings.openSidebarPullRequestLinksInCmuxBrowserKey + ), + .init( + jsonKey: "openPortLinksInCmuxBrowser", + defaultsKey: BrowserLinkOpenSettings.openSidebarPortLinksInCmuxBrowserKey + ), + .init(jsonKey: "showSSH", defaultsKey: SidebarWorkspaceDetailDefaults.showSSHKey), + .init(jsonKey: "showPorts", defaultsKey: SidebarWorkspaceDetailDefaults.showPortsKey), + .init(jsonKey: "showLog", defaultsKey: SidebarWorkspaceDetailDefaults.showLogKey), + .init( + jsonKey: "showProgress", + defaultsKey: SidebarWorkspaceDetailDefaults.showProgressKey + ), + .init( + jsonKey: "showCustomMetadata", + defaultsKey: SidebarWorkspaceDetailDefaults.showCustomMetadataKey + ), + ] + + static func branchLayoutStoredValue(_ rawValue: String) -> Bool? { + switch rawValue { + case "vertical": + return true + case "inline": + return false + default: + return nil + } + } +} + +enum AutomationSettingsFileMapping { + private static let automation = AutomationCatalogSection() + + static let booleanSettings: [SettingsFileBooleanMapping] = [ + .init(jsonKey: "claudeCodeIntegration", defaultsKey: automation.claudeCodeIntegration.userDefaultsKey), + .init( + jsonKey: "suppressSubagentNotifications", + defaultsKey: automation.suppressSubagentNotifications.userDefaultsKey + ), + .init(jsonKey: "ampIntegration", defaultsKey: automation.ampIntegration.userDefaultsKey), + .init(jsonKey: "cursorIntegration", defaultsKey: automation.cursorIntegration.userDefaultsKey), + .init(jsonKey: "geminiIntegration", defaultsKey: automation.geminiIntegration.userDefaultsKey), + .init(jsonKey: "kiroIntegration", defaultsKey: automation.kiroIntegration.userDefaultsKey), + .init(jsonKey: "workspaceAutoNaming", defaultsKey: automation.workspaceAutoNaming.userDefaultsKey), + ] + + static let stringSettings: [SettingsFileStringMapping] = [ + .init(jsonKey: "claudeBinaryPath", defaultsKey: automation.claudeBinaryPath.userDefaultsKey), + .init(jsonKey: "ripgrepBinaryPath", defaultsKey: automation.ripgrepBinaryPath.userDefaultsKey), + .init(jsonKey: "autoNamingAgent", defaultsKey: automation.autoNamingAgent.userDefaultsKey), + ] +} + +enum BrowserSettingsFileMapping { + static let booleanSettings: [SettingsFileBooleanMapping] = [ + .init(jsonKey: "showSearchSuggestions", defaultsKey: BrowserSearchSettingsStore.searchSuggestionsEnabledKey), + .init(jsonKey: "discardHiddenWebViews", defaultsKey: BrowserHiddenWebViewDiscardPolicy.enabledKey), + .init( + jsonKey: "openTerminalLinksInCmuxBrowser", + defaultsKey: BrowserLinkOpenSettings.openTerminalLinksInCmuxBrowserKey + ), + .init( + jsonKey: "interceptTerminalOpenCommandInCmuxBrowser", + defaultsKey: BrowserLinkOpenSettings.interceptTerminalOpenCommandInCmuxBrowserKey + ), + .init(jsonKey: "showImportHintOnBlankTabs", defaultsKey: BrowserImportHintSettings.showOnBlankTabsKey), + ] + + static let stringSettings: [SettingsFileStringMapping] = [ + .init(jsonKey: "reactGrabVersion", defaultsKey: ReactGrabSettings.versionKey), + ] + + static let stringArraySettings: [SettingsFileStringArrayMapping] = [ + .init( + jsonKey: "hostsToOpenInEmbeddedBrowser", + defaultsKey: BrowserLinkOpenSettings.browserHostWhitelistKey, + invalidPath: "browser.hostsToOpenInEmbeddedBrowser" + ), + .init( + jsonKey: "urlsToAlwaysOpenExternally", + defaultsKey: BrowserLinkOpenSettings.browserExternalOpenPatternsKey, + invalidPath: "browser.urlsToAlwaysOpenExternally" + ), + .init( + jsonKey: "insecureHttpHostsAllowedInEmbeddedBrowser", + defaultsKey: BrowserInsecureHTTPSettings.allowlistKey, + invalidPath: "browser.insecureHttpHostsAllowedInEmbeddedBrowser" + ), + ] +} + extension CmuxSettingsFileStore { // Keep this in sync with the parser below and the web schema/docs. Settings UI rows // validate against this set so new persisted settings need an explicit cmux.json review. @@ -129,6 +383,7 @@ extension CmuxSettingsFileStore { "sidebar.showLog", "sidebar.showProgress", "sidebar.showCustomMetadata", + RightSidebarWidthSettings.settingsPath, "workspaceColors.indicatorStyle", "workspaceColors.selectionColor", "workspaceColors.notificationBadgeColor", diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index 0f6174a909b9..6553bd707b5c 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -991,6 +991,7 @@ struct ContentView: View { @EnvironmentObject var fileExplorerState: FileExplorerState @Environment(\.colorScheme) private var colorScheme @AppStorage("titlebarControlsStyle") private var titlebarControlsStyleRawValue = TitlebarControlsStyle.classic.rawValue + @AppStorage(RightSidebarWidthSettings.maxWidthKey) private var rightSidebarMaxWidthSetting = RightSidebarWidthSettings.noOverrideValue @AppStorage(SessionPersistencePolicy.sidebarMinimumWidthKey) private var sidebarMinimumWidthSetting = SessionPersistencePolicy.defaultMinimumSidebarWidth @AppStorage(MinimalModeTitlebarDebugSettings.leftControlsLeadingInsetKey) private var titlebarLeftControlsLeadingInset = MinimalModeTitlebarDebugSettings.defaultLeftControlsLeadingInset @AppStorage(MinimalModeTitlebarDebugSettings.leftControlsTopInsetKey) private var titlebarLeftControlsTopInset = MinimalModeTitlebarDebugSettings.defaultLeftControlsTopInset @@ -1252,8 +1253,8 @@ struct ContentView: View { private static let commandPaletteVisiblePreviewResultLimit = 48 private static let commandPaletteVisiblePreviewCandidateLimit = 128 private static let maximumSidebarWidthRatio: CGFloat = 1.0 / 3.0 - private static let minimumRightSidebarWidth: CGFloat = 276 - private static let maximumRightSidebarWidth: CGFloat = 1200 + private static let minimumRightSidebarWidth: CGFloat = CGFloat(RightSidebarWidthSettings.minimumWidth) + private static let maximumRightSidebarWidth: CGFloat = CGFloat(RightSidebarWidthSettings.builtInMaximumWidth) private static let minimumTerminalWidthWithRightSidebar: CGFloat = 360 private var minimumSidebarWidth: CGFloat { @@ -1298,7 +1299,8 @@ struct ContentView: View { let startWidth = fileExplorerDragStartWidth ?? fileExplorerWidth let nextWidth = Self.clampedRightSidebarWidth( startWidth - translation, - availableWidth: availableWidth + availableWidth: availableWidth, + configuredMaximumWidth: rightSidebarConfiguredMaximumWidth ) withTransaction(Transaction(animation: nil)) { fileExplorerWidth = nextWidth @@ -1343,15 +1345,25 @@ struct ContentView: View { return max(minimumWidth, min(sanitizedMaximumWidth, candidate)) } - static func clampedRightSidebarWidth(_ candidate: CGFloat, availableWidth: CGFloat) -> CGFloat { + static func clampedRightSidebarWidth( + _ candidate: CGFloat, + availableWidth: CGFloat, + configuredMaximumWidth: CGFloat? = nil + ) -> CGFloat { let minimumWidth = Self.minimumRightSidebarWidth let sanitizedCandidate = candidate.isFinite ? candidate : 220 let sanitizedAvailableWidth = availableWidth.isFinite && availableWidth > 0 ? availableWidth : 1920 - let availableWidthCap = sanitizedAvailableWidth - Self.minimumTerminalWidthWithRightSidebar - let maximumWidth = min( - Self.maximumRightSidebarWidth, - max(minimumWidth, availableWidthCap) + let availableWidthCap = max( + minimumWidth, + sanitizedAvailableWidth - Self.minimumTerminalWidthWithRightSidebar ) + let configuredOrDefaultCap: CGFloat + if let configuredMaximumWidth, configuredMaximumWidth.isFinite { + configuredOrDefaultCap = max(minimumWidth, configuredMaximumWidth) + } else { + configuredOrDefaultCap = Self.maximumRightSidebarWidth + } + let maximumWidth = min(configuredOrDefaultCap, availableWidthCap) return max(minimumWidth, min(maximumWidth, sanitizedCandidate)) } @@ -1400,10 +1412,18 @@ struct ContentView: View { return 1920 } + private var rightSidebarConfiguredMaximumWidth: CGFloat? { + guard let width = RightSidebarWidthSettings().configuredMaximumWidth(from: rightSidebarMaxWidthSetting) else { + return nil + } + return CGFloat(width) + } + private func normalizedRightSidebarWidth(_ candidate: CGFloat, availableWidth: CGFloat? = nil) -> CGFloat { Self.clampedRightSidebarWidth( candidate, - availableWidth: resolvedRightSidebarAvailableWidth(availableWidth) + availableWidth: resolvedRightSidebarAvailableWidth(availableWidth), + configuredMaximumWidth: rightSidebarConfiguredMaximumWidth ) } @@ -2981,6 +3001,14 @@ struct ContentView: View { updateSidebarResizerBandState() }) + view = AnyView(view.onChange(of: rightSidebarMaxWidthSetting) { _, _ in + clampRightSidebarWidthIfNeeded() + if rightSidebarVisible { + schedulePortalGeometrySynchronize() + } + updateSidebarResizerBandState() + }) + view = AnyView(view.onChange(of: sidebarWidth) { _ in let sanitized = normalizedSidebarWidth(sidebarWidth) if abs(sidebarWidth - sanitized) > 0.5 { @@ -12239,7 +12267,7 @@ struct VerticalTabsSidebar: View { dragState.beginDragging(tabId: tabId) return SidebarTabDragPayload.provider(for: tabId) } - let tabDropDelegateFactory: (CGFloat?) -> SidebarTabDropDelegate = { [ + let tabDropDelegateFactory: (CGFloat) -> SidebarTabDropDelegate = { [ tabId = tab.id, selectedTabIds = $selectedTabIds, lastSidebarSelectionIndex = $lastSidebarSelectionIndex @@ -13267,10 +13295,10 @@ struct TabItemView: View, Equatable { let isBeingDragged: Bool let topDropIndicatorVisible: Bool let onDragStart: () -> NSItemProvider - /// Factory invoked from `body` with a stable drop-hit height. Closure + /// Factory invoked from `body` with the row's measured `rowHeight`. Closure /// captures the parent's `dragState`, so TabItemView itself never holds an - /// `@Observable` store reference or layout-driven state (snapshot-boundary rule). - let tabDropDelegateFactory: (CGFloat?) -> SidebarTabDropDelegate + /// `@Observable` store reference (snapshot-boundary rule). + let tabDropDelegateFactory: (CGFloat) -> SidebarTabDropDelegate let contextMenuWorkspaceIds: [UUID] let remoteContextMenuWorkspaceIds: [UUID] let allRemoteContextMenuTargetsConnecting: Bool @@ -13287,9 +13315,11 @@ struct TabItemView: View, Equatable { @State private var workspaceSnapshotStorage: SidebarWorkspaceSnapshotBuilder.Snapshot? @StateObject private var contextMenuState = SidebarTabItemContextMenuState() @State private var rowInteractionState = SidebarWorkspaceRowInteractionState() - @State var workspaceFinderDirectoryOpenRequest: WorkspaceFinderDirectoryOpenRequest? - @State var metadataRowsExpanded = false - @State var metadataBlocksExpanded = false + @State private var rowHeight: CGFloat = 1 + @State private var workspaceFinderDirectoryOpenRequest: WorkspaceFinderDirectoryOpenRequest? + + private static let maxWrappedTitleLines = 8 + private static let maxDisplayedTitleCharacters = 2048 var isMultiSelected: Bool { selectedTabIds.contains(tab.id) @@ -13517,6 +13547,18 @@ struct TabItemView: View, Equatable { } } + private var rowHeightProbe: some View { + GeometryReader { proxy in + Color.clear + .onAppear { + rowHeight = max(proxy.size.height, 1) + } + .onChange(of: proxy.size.height) { newHeight in + rowHeight = max(newHeight, 1) + } + } + } + @ViewBuilder private var remoteWorkspaceSection: some View { let workspaceSnapshot = self.workspaceSnapshot @@ -13608,12 +13650,10 @@ struct TabItemView: View, Equatable { : nil let effectiveSubtitle = latestNotificationSubtitle ?? conversationMessageSubtitle let detailVisibility = visibleAuxiliaryDetails - let titleLineLimit = settings.wrapsWorkspaceTitles - ? SidebarWorkspaceRowDropMetrics.maxWrappedTitleLines - : 1 - let dropTargetHeight = workspaceDropTargetHeight( - snapshot: workspaceSnapshot, - effectiveSubtitle: effectiveSubtitle + let titleLineLimit = settings.wrapsWorkspaceTitles ? Self.maxWrappedTitleLines : 1 + let displayedTitle = workspaceSnapshot.title.sidebarBoundedDisplayString( + maxDisplayedLines: titleLineLimit, + maxDisplayedCharacters: Self.maxDisplayedTitleCharacters ) let scaledUnreadBadgeSize = 16 * fontScale let scaledCloseButtonHitSize = max(16, 16 * fontScale) @@ -13642,7 +13682,7 @@ struct TabItemView: View, Equatable { .safeHelp(protectedWorkspaceTooltip) } - Text(workspaceSnapshot.title) + Text(displayedTitle) .font(.system(size: scaledFontSize(12.5), weight: titleFontWeight)) .foregroundColor(activePrimaryTextColor) .lineLimit(titleLineLimit) @@ -13658,7 +13698,10 @@ struct TabItemView: View, Equatable { // row. (Matches the group-header plus-button pattern.) if canCloseWorkspace { Button(action: { - closeWorkspace(method: "button") + #if DEBUG + cmuxDebugLog("sidebar.close workspace=\(tab.id.uuidString.prefix(5)) method=button") + #endif + tabManager.closeWorkspaceWithConfirmation(tab) }) { Image(systemName: "xmark") .font(.system(size: scaledFontSize(9), weight: .medium)) @@ -13700,7 +13743,6 @@ struct TabItemView: View, Equatable { if !metadataEntries.isEmpty { SidebarMetadataRows( entries: metadataEntries, - isExpanded: $metadataRowsExpanded, isActive: usesInvertedActiveForeground, activeForegroundColor: activeSecondaryColor(0.95), activeSecondaryForegroundColor: activeSecondaryColor(0.65), @@ -13712,7 +13754,6 @@ struct TabItemView: View, Equatable { if !metadataBlocks.isEmpty { SidebarMetadataMarkdownBlocks( blocks: metadataBlocks, - isExpanded: $metadataBlocksExpanded, isActive: usesInvertedActiveForeground, activeForegroundColor: activeSecondaryColor(0.8), activeSecondaryForegroundColor: activeSecondaryColor(0.65), @@ -13946,6 +13987,7 @@ struct TabItemView: View, Equatable { ) .shortcutHintVisibilityAnimation(value: showsWorkspaceShortcutHint) .padding(.horizontal, 6) + .background { rowHeightProbe } .contentShape(Rectangle()) .opacity(isBeingDragged ? 0.6 : 1) .overlay { @@ -13953,7 +13995,10 @@ struct TabItemView: View, Equatable { } .overlay { MiddleClickCapture { - closeWorkspace(method: "middleClick") + #if DEBUG + cmuxDebugLog("sidebar.close workspace=\(tab.id.uuidString.prefix(5)) method=middleClick") + #endif + tabManager.closeWorkspaceWithConfirmation(tab) } } .overlay(alignment: .top) { @@ -13967,13 +14012,26 @@ struct TabItemView: View, Equatable { refreshWorkspaceSnapshot(force: true) } .task(id: workspaceFinderDirectoryOpenRequest) { - await openPendingFinderDirectoryRequest() + guard let request = workspaceFinderDirectoryOpenRequest else { return } + await WorkspaceFinderDirectoryOpener.openInFinder(request.directoryURL) + guard !Task.isCancelled, workspaceFinderDirectoryOpenRequest == request else { return } + workspaceFinderDirectoryOpenRequest = nil } .onReceive( tab.sidebarImmediateObservationPublisher .receive(on: RunLoop.main) ) { _ in - refreshWorkspaceSnapshotAfterObservation(source: "immediate") +#if DEBUG + let description = tab.customDescription ?? "" + cmuxDebugLog( + "sidebar.row.invalidate workspace=\(tab.id.uuidString.prefix(8)) " + + "source=immediate " + + "title=\"\(debugCommandPaletteTextPreview(tab.title))\" " + + "descLen=\((description as NSString).length) " + + "desc=\"\(debugCommandPaletteTextPreview(description))\"" + ) +#endif + refreshWorkspaceSnapshot() } .onReceive( tab.sidebarObservationPublisher @@ -13983,14 +14041,24 @@ struct TabItemView: View, Equatable { // row redraws once with the settled state instead of blinking. .debounce(for: Self.workspaceObservationCoalesceInterval, scheduler: RunLoop.main) ) { _ in - refreshWorkspaceSnapshotAfterObservation(source: "debounced") +#if DEBUG + let description = tab.customDescription ?? "" + cmuxDebugLog( + "sidebar.row.invalidate workspace=\(tab.id.uuidString.prefix(8)) " + + "source=debounced " + + "title=\"\(debugCommandPaletteTextPreview(tab.title))\" " + + "descLen=\((description as NSString).length) " + + "desc=\"\(debugCommandPaletteTextPreview(description))\"" + ) +#endif + refreshWorkspaceSnapshot() } .onChange(of: settings) { _ in refreshWorkspaceSnapshot(force: true) } .onDrag(onDragStart) .internalOnlyTabDrag() - .onDrop(of: SidebarTabDragPayload.dropContentTypes, delegate: tabDropDelegateFactory(dropTargetHeight)) + .onDrop(of: SidebarTabDragPayload.dropContentTypes, delegate: tabDropDelegateFactory(rowHeight)) .onDrop(of: BonsplitTabDragPayload.dropContentTypes, delegate: SidebarBonsplitTabDropDelegate( targetWorkspaceId: tab.id, tabManager: tabManager, @@ -14026,7 +14094,7 @@ struct TabItemView: View, Equatable { } } - func refreshWorkspaceSnapshot(force: Bool = false) { + private func refreshWorkspaceSnapshot(force: Bool = false) { let nextSnapshot = makeWorkspaceSnapshot() let decision = SidebarWorkspaceSnapshotRefreshPolicy.decision( current: workspaceSnapshotStorage, @@ -15113,24 +15181,31 @@ private struct SidebarWorkspaceDescriptionText: View { let isActive: Bool let activeForegroundColor: Color let fontScale: CGFloat + private static let maxDisplayedLines = 12 + private static let maxDisplayedCharacters = 4096 var body: some View { - let renderedMarkdown = SidebarMarkdownRenderer(markdown: markdown).workspaceDescription + let displayMarkdown = markdown.sidebarBoundedDisplayString( + maxDisplayedLines: Self.maxDisplayedLines, + maxDisplayedCharacters: Self.maxDisplayedCharacters + ) + let renderedMarkdown = SidebarMarkdownRenderer(markdown: displayMarkdown).workspaceDescription Group { if let renderedMarkdown { Text(renderedMarkdown) } else { - Text(markdown) + Text(displayMarkdown) } } .font(.system(size: 10.5 * fontScale)) .foregroundColor(foregroundColor) .multilineTextAlignment(.leading) - .lineLimit(SidebarWorkspaceRowDropMetrics.maxDescriptionLines) + .lineLimit(Self.maxDisplayedLines) + .truncationMode(.tail) .fixedSize(horizontal: false, vertical: true) .frame(maxWidth: .infinity, alignment: .leading) .accessibilityIdentifier("SidebarWorkspaceDescriptionText") - .accessibilityLabel(accessibilityText(renderedMarkdown: renderedMarkdown)) + .accessibilityLabel(accessibilityText(renderedMarkdown: renderedMarkdown, displayMarkdown: displayMarkdown)) .onAppear { #if DEBUG let newlineCount = markdown.reduce(into: 0) { count, character in @@ -15163,23 +15238,55 @@ private struct SidebarWorkspaceDescriptionText: View { isActive ? activeForegroundColor : .secondary.opacity(0.95) } - private func accessibilityText(renderedMarkdown: AttributedString?) -> String { + private func accessibilityText(renderedMarkdown: AttributedString?, displayMarkdown: String) -> String { if let renderedMarkdown { return String(renderedMarkdown.characters) } - return markdown + return displayMarkdown + } +} + +private extension String { + func sidebarBoundedDisplayString(maxDisplayedLines: Int, maxDisplayedCharacters: Int) -> String { + var result = "" + result.reserveCapacity(maxDisplayedCharacters) + var lineCount = 1 + var characterCount = 0 + var truncated = false + + for character in self { + if characterCount >= maxDisplayedCharacters { + truncated = true + break + } + if character == "\n" { + if lineCount >= maxDisplayedLines { + truncated = true + break + } + lineCount += 1 + } + result.append(character) + characterCount += 1 + } + + guard truncated else { return self } + let trimmed = result.trimmingCharacters(in: .whitespacesAndNewlines) + return trimmed.isEmpty ? "..." : trimmed + "..." } } private struct SidebarMetadataRows: View { let entries: [SidebarStatusEntry] - @Binding var isExpanded: Bool let isActive: Bool let activeForegroundColor: Color let activeSecondaryForegroundColor: Color let fontScale: CGFloat let onFocus: () -> Void + @State private var isExpanded: Bool = false + private let collapsedEntryLimit = 3 + var body: some View { VStack(alignment: .leading, spacing: 2) { ForEach(visibleEntries, id: \.key) { entry in @@ -15209,10 +15316,8 @@ private struct SidebarMetadataRows: View { } private var visibleEntries: [SidebarStatusEntry] { - guard !isExpanded, entries.count > SidebarWorkspaceRowDropMetrics.collapsedMetadataEntryLimit else { - return entries - } - return Array(entries.prefix(SidebarWorkspaceRowDropMetrics.collapsedMetadataEntryLimit)) + guard !isExpanded, entries.count > collapsedEntryLimit else { return entries } + return Array(entries.prefix(collapsedEntryLimit)) } private var helpText: String { @@ -15224,7 +15329,7 @@ private struct SidebarMetadataRows: View { } private var shouldShowToggle: Bool { - entries.count > SidebarWorkspaceRowDropMetrics.collapsedMetadataEntryLimit + entries.count > collapsedEntryLimit } } @@ -15329,13 +15434,15 @@ private struct SidebarMetadataEntryRow: View { private struct SidebarMetadataMarkdownBlocks: View { let blocks: [SidebarMetadataBlock] - @Binding var isExpanded: Bool let isActive: Bool let activeForegroundColor: Color let activeSecondaryForegroundColor: Color let fontScale: CGFloat let onFocus: () -> Void + @State private var isExpanded: Bool = false + private let collapsedBlockLimit = 1 + var body: some View { VStack(alignment: .leading, spacing: 3) { ForEach(visibleBlocks, id: \.key) { block in @@ -15364,14 +15471,12 @@ private struct SidebarMetadataMarkdownBlocks: View { } private var visibleBlocks: [SidebarMetadataBlock] { - guard !isExpanded, blocks.count > SidebarWorkspaceRowDropMetrics.collapsedMetadataBlockLimit else { - return blocks - } - return Array(blocks.prefix(SidebarWorkspaceRowDropMetrics.collapsedMetadataBlockLimit)) + guard !isExpanded, blocks.count > collapsedBlockLimit else { return blocks } + return Array(blocks.prefix(collapsedBlockLimit)) } private var shouldShowToggle: Bool { - blocks.count > SidebarWorkspaceRowDropMetrics.collapsedMetadataBlockLimit + blocks.count > collapsedBlockLimit } } @@ -15381,25 +15486,29 @@ private struct SidebarMetadataMarkdownBlockRow: View { let activeForegroundColor: Color let fontScale: CGFloat let onFocus: () -> Void + private static let maxDisplayedLines = 12 + private static let maxDisplayedCharacters = 4096 var body: some View { // Render inline (memoized) so the FIRST render is already attributed. // Parsing in onAppear into @State performed a guaranteed nil -> // attributed swap on every first appearance, changing the row's height // mid-scroll and re-feeding the sidebar-wide layout cycle (#5764). - let renderedMarkdown = SidebarMetadataMarkdownRenderer.rendered(block.markdown) + let displayMarkdown = Self.displayMarkdown(from: block.markdown) + let renderedMarkdown = SidebarMetadataMarkdownRenderer.rendered(displayMarkdown) Group { if let renderedMarkdown { Text(renderedMarkdown) .foregroundColor(foregroundColor) } else { - Text(block.markdown) + Text(displayMarkdown) .foregroundColor(foregroundColor) } } .font(.system(size: 10 * fontScale)) .multilineTextAlignment(.leading) - .lineLimit(SidebarWorkspaceRowDropMetrics.maxMetadataBlockLines) + .lineLimit(Self.maxDisplayedLines) + .truncationMode(.tail) .fixedSize(horizontal: false, vertical: true) .contentShape(Rectangle()) .onTapGesture { onFocus() } @@ -15408,6 +15517,13 @@ private struct SidebarMetadataMarkdownBlockRow: View { private var foregroundColor: Color { isActive ? activeForegroundColor : .secondary } + + private static func displayMarkdown(from markdown: String) -> String { + markdown.sidebarBoundedDisplayString( + maxDisplayedLines: maxDisplayedLines, + maxDisplayedCharacters: maxDisplayedCharacters + ) + } } /// Immutable, equatable snapshot of the group list a row's "Move to Group" diff --git a/Sources/KeyboardShortcutSettingsFileStore.swift b/Sources/KeyboardShortcutSettingsFileStore.swift index 4c2ef8a88676..51e505c926ae 100644 --- a/Sources/KeyboardShortcutSettingsFileStore.swift +++ b/Sources/KeyboardShortcutSettingsFileStore.swift @@ -444,11 +444,8 @@ final class CmuxSettingsFileStore { logInvalid("app.forkConversationDefaultDestination", sourcePath: sourcePath) } } - if let value = jsonBool(section["workspaceInheritWorkingDirectory"]) { - snapshot.managedUserDefaults[SettingCatalog().app.workspaceInheritWorkingDirectory.userDefaultsKey] = .bool(value) - } else if section.keys.contains("workspaceInheritWorkingDirectory") { - logInvalid("app.workspaceInheritWorkingDirectory", sourcePath: sourcePath) - } + applyBooleanSettings(AppSettingsFileMapping.booleanSettings, from: section, sourcePath: sourcePath, snapshot: &snapshot) + applyStringSettings(AppSettingsFileMapping.stringSettings, from: section, snapshot: &snapshot) if let value = jsonBool(section["minimalMode"]) { let mode = value ? WorkspacePresentationModeSettings.Mode.minimal : .standard snapshot.managedUserDefaults[WorkspacePresentationModeSettings.modeKey] = .string(mode.rawValue) @@ -456,27 +453,6 @@ final class CmuxSettingsFileStore { if let value = jsonBool(section["keepWorkspaceOpenWhenClosingLastSurface"]) { snapshot.managedUserDefaults[SettingCatalog().app.keepWorkspaceOpenWhenClosingLastSurface.userDefaultsKey] = .bool(!value) } - if let value = jsonBool(section["focusPaneOnFirstClick"]) { - snapshot.managedUserDefaults[PaneFirstClickFocusSettings.enabledKey] = .bool(value) - } - if let value = jsonString(section["preferredEditor"]) { - snapshot.managedUserDefaults[AppCatalogSection().preferredEditor.userDefaultsKey] = .string(value) - } - if let value = jsonBool(section["openSupportedFilesInCmux"]) { - snapshot.managedUserDefaults[AppCatalogSection().openSupportedFilesInCmux.userDefaultsKey] = .bool(value) - } - if let value = jsonBool(section["openMarkdownInCmuxViewer"]) { - snapshot.managedUserDefaults[AppCatalogSection().openMarkdownInCmuxViewer.userDefaultsKey] = .bool(value) - } - if let value = jsonBool(section["reorderOnNotification"]) { - snapshot.managedUserDefaults[SettingCatalog().app.reorderOnNotification.userDefaultsKey] = .bool(value) - } - if let value = jsonBool(section["iMessageMode"]) { - snapshot.managedUserDefaults[IMessageModeSettings.key] = .bool(value) - } - if let value = jsonBool(section["sendAnonymousTelemetry"]) { - snapshot.managedUserDefaults[AppCatalogSection().sendAnonymousTelemetry.userDefaultsKey] = .bool(value) - } var parsedConfirmQuitMode: ConfirmQuitMode? let confirmQuitKey = AppCatalogSection().confirmQuitMode.userDefaultsKey let warnBeforeQuitKey = AppCatalogSection().warnBeforeQuit.userDefaultsKey @@ -496,21 +472,6 @@ final class CmuxSettingsFileStore { snapshot.legacyDerivedManagedUserDefaultKeys.insert(confirmQuitKey) } } - if let value = jsonBool(section["warnBeforeClosingTab"]) { - snapshot.managedUserDefaults[AppCatalogSection().warnBeforeClosingTab.userDefaultsKey] = .bool(value) - } - if let value = jsonBool(section["warnBeforeClosingTabXButton"]) { - snapshot.managedUserDefaults[AppCatalogSection().warnBeforeClosingTabXButton.userDefaultsKey] = .bool(value) - } - if let value = jsonBool(section["hideTabCloseButton"]) { - snapshot.managedUserDefaults[AppCatalogSection().hideTabCloseButton.userDefaultsKey] = .bool(value) - } - if let value = jsonBool(section["renameSelectsExistingName"]) { - snapshot.managedUserDefaults[AppCatalogSection().renameSelectsExistingName.userDefaultsKey] = .bool(value) - } - if let value = jsonBool(section["commandPaletteSearchesAllSurfaces"]) { - snapshot.managedUserDefaults[AppCatalogSection().commandPaletteSearchesAllSurfaces.userDefaultsKey] = .bool(value) - } } private func parseNotificationsSection( @@ -518,18 +479,7 @@ final class CmuxSettingsFileStore { sourcePath: String, snapshot: inout ResolvedSettingsSnapshot ) { - if let value = jsonBool(section["dockBadge"]) { - snapshot.managedUserDefaults[NotificationBadgeSettings.dockBadgeEnabledKey] = .bool(value) - } - if let value = jsonBool(section["showInMenuBar"]) { - snapshot.managedUserDefaults[MenuBarExtraSettings.showInMenuBarKey] = .bool(value) - } - if let value = jsonBool(section["unreadPaneRing"]) { - snapshot.managedUserDefaults[NotificationPaneRingSettings.enabledKey] = .bool(value) - } - if let value = jsonBool(section["paneFlash"]) { - snapshot.managedUserDefaults[NotificationPaneFlashSettings.enabledKey] = .bool(value) - } + applyBooleanSettings(NotificationSettingsFileMapping.booleanSettings, from: section, sourcePath: sourcePath, snapshot: &snapshot) if let raw = jsonString(section["sound"]) { let allowed = Set(NotificationSoundSettings.systemSounds.map(\.value)) guard allowed.contains(raw) else { @@ -538,12 +488,7 @@ final class CmuxSettingsFileStore { } snapshot.managedUserDefaults[NotificationSoundSettings.key] = .string(raw) } - if let raw = jsonString(section["customSoundFilePath"]) { - snapshot.managedUserDefaults[NotificationSoundSettings.customFilePathKey] = .string(raw) - } - if let raw = jsonString(section["command"]) { - snapshot.managedUserDefaults[NotificationSoundSettings.customCommandKey] = .string(raw) - } + applyStringSettings(NotificationSettingsFileMapping.stringSettings, from: section, snapshot: &snapshot) } private func parseTerminalSection( @@ -551,23 +496,7 @@ final class CmuxSettingsFileStore { sourcePath: String, snapshot: inout ResolvedSettingsSnapshot ) { - if let value = jsonBool(section["showScrollBar"]) { - snapshot.managedUserDefaults[TerminalScrollBarSettings.showScrollBarKey] = .bool(value) - } else if section.keys.contains("showScrollBar") { - logInvalid("terminal.showScrollBar", sourcePath: sourcePath) - } - - if let value = jsonBool(section["copyOnSelect"]) { - snapshot.managedUserDefaults[TerminalCopyOnSelectSettings.copyOnSelectKey] = .bool(value) - } else if section.keys.contains("copyOnSelect") { - logInvalid("terminal.copyOnSelect", sourcePath: sourcePath) - } - - if let value = jsonBool(section["autoResumeAgentSessions"]) { - snapshot.managedUserDefaults[AgentSessionAutoResumeSettings.autoResumeAgentSessionsKey] = .bool(value) - } else if section.keys.contains("autoResumeAgentSessions") { - logInvalid("terminal.autoResumeAgentSessions", sourcePath: sourcePath) - } + applyBooleanSettings(TerminalSettingsFileMapping.booleanSettings, from: section, sourcePath: sourcePath, snapshot: &snapshot) if let value = jsonBool(section["showTextBoxOnNewTerminals"]) { snapshot.managedUserDefaults[TerminalTextBoxInputSettings.showOnNewTerminalsKey] = .bool(value) @@ -712,58 +641,28 @@ final class CmuxSettingsFileStore { sourcePath: String, snapshot: inout ResolvedSettingsSnapshot ) { - if let value = jsonBool(section["hideAllDetails"]) { - snapshot.managedUserDefaults[SettingCatalog().sidebar.hideAllDetails.userDefaultsKey] = .bool(value) - } - if let value = jsonBool(section["wrapWorkspaceTitles"]) { - snapshot.managedUserDefaults[SidebarWorkspaceTitleWrapSettings.key] = .bool(value) - } - if let value = jsonBool(section["showWorkspaceDescription"]) { - snapshot.managedUserDefaults[SettingCatalog().sidebar.showWorkspaceDescription.userDefaultsKey] = .bool(value) + for setting in SidebarSettingsFileMapping.booleanSettings { + if let value = jsonBool(section[setting.jsonKey]) { + snapshot.managedUserDefaults[setting.defaultsKey] = .bool(value) + } } + if let raw = jsonString(section["branchLayout"]) { - switch raw { - case "vertical": - snapshot.managedUserDefaults[SettingCatalog().sidebar.branchVerticalLayout.userDefaultsKey] = .bool(true) - case "inline": - snapshot.managedUserDefaults[SettingCatalog().sidebar.branchVerticalLayout.userDefaultsKey] = .bool(false) - default: + if let value = SidebarSettingsFileMapping.branchLayoutStoredValue(raw) { + snapshot.managedUserDefaults[ + SidebarCatalogSection().branchVerticalLayout.userDefaultsKey + ] = .bool(value) + } else { logInvalid("sidebar.branchLayout", sourcePath: sourcePath) } } - if let value = jsonBool(section["stackBranchDirectory"]) { - snapshot.managedUserDefaults[SettingCatalog().sidebar.stackBranchDirectory.userDefaultsKey] = .bool(value) - } - if let value = jsonBool(section["pathLastSegmentOnly"]) { - snapshot.managedUserDefaults[SettingCatalog().sidebar.pathLastSegmentOnly.userDefaultsKey] = .bool(value) - } - if let value = jsonBool(section["showNotificationMessage"]) { - snapshot.managedUserDefaults[SettingCatalog().sidebar.showNotificationMessage.userDefaultsKey] = .bool(value) - } - if let value = jsonBool(section["showBranchDirectory"]) { snapshot.managedUserDefaults["sidebarShowBranchDirectory"] = .bool(value) } - if let value = jsonBool(section["showPullRequests"]) { snapshot.managedUserDefaults["sidebarShowPullRequest"] = .bool(value) } - if let value = jsonBool(section["watchGitStatus"]) { snapshot.managedUserDefaults[SidebarWorkspaceDetailDefaults.watchGitStatusKey] = .bool(value) } - if let value = jsonBool(section["makePullRequestsClickable"]) { snapshot.managedUserDefaults[SettingCatalog().sidebar.makePullRequestsClickable.userDefaultsKey] = .bool(value) } - if let value = jsonBool(section["openPullRequestLinksInCmuxBrowser"]) { - snapshot.managedUserDefaults[BrowserLinkOpenSettings.openSidebarPullRequestLinksInCmuxBrowserKey] = .bool(value) - } - if let value = jsonBool(section["openPortLinksInCmuxBrowser"]) { - snapshot.managedUserDefaults[BrowserLinkOpenSettings.openSidebarPortLinksInCmuxBrowserKey] = .bool(value) - } - if let value = jsonBool(section["showSSH"]) { - snapshot.managedUserDefaults["sidebarShowSSH"] = .bool(value) - } - if let value = jsonBool(section["showPorts"]) { - snapshot.managedUserDefaults["sidebarShowPorts"] = .bool(value) - } - if let value = jsonBool(section["showLog"]) { - snapshot.managedUserDefaults["sidebarShowLog"] = .bool(value) - } - if let value = jsonBool(section["showProgress"]) { - snapshot.managedUserDefaults["sidebarShowProgress"] = .bool(value) - } - if let value = jsonBool(section["showCustomMetadata"]) { - snapshot.managedUserDefaults["sidebarShowStatusPills"] = .bool(value) + + if let value = jsonDouble(section[RightSidebarWidthSettings.jsonKey]), value > 0 { + snapshot.managedUserDefaults[RightSidebarWidthSettings.maxWidthKey] = .double( + RightSidebarWidthSettings().clampedSettingsEditorMaximumWidth(value) + ) + } else if section.keys.contains(RightSidebarWidthSettings.jsonKey) { + logInvalid(RightSidebarWidthSettings.settingsPath, sourcePath: sourcePath) } } @@ -934,38 +833,8 @@ final class CmuxSettingsFileStore { return } } - if let value = jsonBool(section["claudeCodeIntegration"]) { - snapshot.managedUserDefaults[IntegrationsCatalogSection().claudeCodeHooksEnabled.userDefaultsKey] = .bool(value) - } - if let raw = jsonString(section["claudeBinaryPath"]) { - snapshot.managedUserDefaults[IntegrationsCatalogSection().claudeCodeCustomClaudePath.userDefaultsKey] = .string(raw) - } - if let value = jsonBool(section["workspaceAutoNaming"]) { - snapshot.managedUserDefaults[AutomationCatalogSection().workspaceAutoNaming.userDefaultsKey] = .bool(value) - } - if let raw = jsonString(section["autoNamingAgent"]) { - // Open string (any agent slug, or "auto"); the catalog/CLI resolve - // unknown values by falling back to each session's own agent. - snapshot.managedUserDefaults[AutomationCatalogSection().autoNamingAgent.userDefaultsKey] = .string(raw) - } - if let raw = jsonString(section["ripgrepBinaryPath"]) { - snapshot.managedUserDefaults[RipgrepIntegrationSettings.customRipgrepPathKey] = .string(raw) - } - if let value = jsonBool(section["suppressSubagentNotifications"]) { - snapshot.managedUserDefaults[IntegrationsCatalogSection().suppressSubagentNotifications.userDefaultsKey] = .bool(value) - } - if let value = jsonBool(section["ampIntegration"]) { - snapshot.managedUserDefaults[IntegrationsCatalogSection().ampHooksEnabled.userDefaultsKey] = .bool(value) - } - if let value = jsonBool(section["cursorIntegration"]) { - snapshot.managedUserDefaults[IntegrationsCatalogSection().cursorHooksEnabled.userDefaultsKey] = .bool(value) - } - if let value = jsonBool(section["geminiIntegration"]) { - snapshot.managedUserDefaults[IntegrationsCatalogSection().geminiHooksEnabled.userDefaultsKey] = .bool(value) - } - if let value = jsonBool(section["kiroIntegration"]) { - snapshot.managedUserDefaults[IntegrationsCatalogSection().kiroHooksEnabled.userDefaultsKey] = .bool(value) - } + applyBooleanSettings(AutomationSettingsFileMapping.booleanSettings, from: section, sourcePath: sourcePath, snapshot: &snapshot) + applyStringSettings(AutomationSettingsFileMapping.stringSettings, from: section, snapshot: &snapshot) if let raw = jsonString(section["kiroNotificationLevel"]) { if KiroNotificationLevel(rawValue: raw) != nil { snapshot.managedUserDefaults[IntegrationsCatalogSection().kiroNotificationLevel.userDefaultsKey] = .string(raw) @@ -1016,9 +885,8 @@ final class CmuxSettingsFileStore { logInvalid("browser.customSearchEngineURLTemplate", sourcePath: sourcePath) } } - if let value = jsonBool(section["showSearchSuggestions"]) { - snapshot.managedUserDefaults[BrowserSearchSettingsStore.searchSuggestionsEnabledKey] = .bool(value) - } + applyBooleanSettings(BrowserSettingsFileMapping.booleanSettings, from: section, sourcePath: sourcePath, snapshot: &snapshot) + applyStringSettings(BrowserSettingsFileMapping.stringSettings, from: section, snapshot: &snapshot) if let raw = jsonString(section["theme"]) { guard let mode = BrowserThemeMode(rawValue: raw) else { logInvalid("browser.theme", sourcePath: sourcePath) @@ -1026,9 +894,6 @@ final class CmuxSettingsFileStore { } snapshot.managedUserDefaults[BrowserThemeSettings.modeKey] = .string(mode.rawValue) } - if let value = jsonBool(section["discardHiddenWebViews"]) { - snapshot.managedUserDefaults[BrowserHiddenWebViewDiscardPolicy.enabledKey] = .bool(value) - } if let value = jsonDouble(section["hiddenWebViewDiscardDelaySeconds"]) { guard let delay = BrowserHiddenWebViewDiscardPolicy.resolvedHiddenDelay(value) else { logInvalid("browser.hiddenWebViewDiscardDelaySeconds", sourcePath: sourcePath) @@ -1036,46 +901,7 @@ final class CmuxSettingsFileStore { } snapshot.managedUserDefaults[BrowserHiddenWebViewDiscardPolicy.hiddenDelayKey] = .double(delay) } - if let value = jsonBool(section["openTerminalLinksInCmuxBrowser"]) { - snapshot.managedUserDefaults[BrowserLinkOpenSettings.openTerminalLinksInCmuxBrowserKey] = .bool(value) - } - if let value = jsonBool(section["interceptTerminalOpenCommandInCmuxBrowser"]) { - snapshot.managedUserDefaults[BrowserLinkOpenSettings.interceptTerminalOpenCommandInCmuxBrowserKey] = .bool(value) - } - if let values = jsonStringArray(section["hostsToOpenInEmbeddedBrowser"]) { - let normalized = values - .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } - .filter { !$0.isEmpty } - snapshot.managedUserDefaults[BrowserLinkOpenSettings.browserHostWhitelistKey] = .string(normalized.joined(separator: "\n")) - } else if section.keys.contains("hostsToOpenInEmbeddedBrowser") { - logInvalid("browser.hostsToOpenInEmbeddedBrowser", sourcePath: sourcePath) - } - if let values = jsonStringArray(section["urlsToAlwaysOpenExternally"]) { - let normalized = values - .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } - .filter { !$0.isEmpty } - snapshot.managedUserDefaults[BrowserLinkOpenSettings.browserExternalOpenPatternsKey] = .string( - normalized.joined(separator: "\n") - ) - } else if section.keys.contains("urlsToAlwaysOpenExternally") { - logInvalid("browser.urlsToAlwaysOpenExternally", sourcePath: sourcePath) - } - if let values = jsonStringArray(section["insecureHttpHostsAllowedInEmbeddedBrowser"]) { - let normalized = values - .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } - .filter { !$0.isEmpty } - snapshot.managedUserDefaults[BrowserInsecureHTTPSettings.allowlistKey] = .string( - normalized.joined(separator: "\n") - ) - } else if section.keys.contains("insecureHttpHostsAllowedInEmbeddedBrowser") { - logInvalid("browser.insecureHttpHostsAllowedInEmbeddedBrowser", sourcePath: sourcePath) - } - if let value = jsonBool(section["showImportHintOnBlankTabs"]) { - snapshot.managedUserDefaults[BrowserImportHintSettings.showOnBlankTabsKey] = .bool(value) - } - if let raw = jsonString(section["reactGrabVersion"]) { - snapshot.managedUserDefaults[ReactGrabSettings.versionKey] = .string(raw) - } + applyNormalizedStringArraySettings(BrowserSettingsFileMapping.stringArraySettings, from: section, sourcePath: sourcePath, snapshot: &snapshot) } private func parseWorkspaceGroupsSection( @@ -1837,6 +1663,51 @@ final class CmuxSettingsFileStore { defaults.set(data, forKey: Self.backupsDefaultsKey) } + private func applyBooleanSettings( + _ settings: [SettingsFileBooleanMapping], + from section: [String: Any], + sourcePath: String, + snapshot: inout ResolvedSettingsSnapshot + ) { + for setting in settings { + if let value = jsonBool(section[setting.jsonKey]) { + snapshot.managedUserDefaults[setting.defaultsKey] = .bool(value) + } else if let invalidPath = setting.invalidPath, section.keys.contains(setting.jsonKey) { + logInvalid(invalidPath, sourcePath: sourcePath) + } + } + } + + private func applyStringSettings( + _ settings: [SettingsFileStringMapping], + from section: [String: Any], + snapshot: inout ResolvedSettingsSnapshot + ) { + for setting in settings { + if let raw = jsonString(section[setting.jsonKey]) { + snapshot.managedUserDefaults[setting.defaultsKey] = .string(raw) + } + } + } + + private func applyNormalizedStringArraySettings( + _ settings: [SettingsFileStringArrayMapping], + from section: [String: Any], + sourcePath: String, + snapshot: inout ResolvedSettingsSnapshot + ) { + for setting in settings { + if let values = jsonStringArray(section[setting.jsonKey]) { + let normalized = values + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + .filter { !$0.isEmpty } + snapshot.managedUserDefaults[setting.defaultsKey] = .string(normalized.joined(separator: "\n")) + } else if section.keys.contains(setting.jsonKey) { + logInvalid(setting.invalidPath, sourcePath: sourcePath) + } + } + } + private func logInvalid(_ path: String, sourcePath: String) { cmuxSettingsFileStoreLogger.warning("ignoring invalid setting '\(path, privacy: .private(mask: .hash))' in \(sourcePath, privacy: .private(mask: .hash))") } diff --git a/Sources/SettingsNavigation.swift b/Sources/SettingsNavigation.swift index 383d3a5bcc0c..9d81cb5c61b4 100644 --- a/Sources/SettingsNavigation.swift +++ b/Sources/SettingsNavigation.swift @@ -387,6 +387,7 @@ enum SettingsSearchIndex { setting(.sidebarAppearance, "show-log", String(localized: "settings.app.showLog", defaultValue: "Show Latest Log in Sidebar"), "status message"), setting(.sidebarAppearance, "show-progress", String(localized: "settings.app.showProgress", defaultValue: "Show Progress in Sidebar"), "progress bar"), setting(.sidebarAppearance, "show-metadata", String(localized: "settings.app.showMetadata", defaultValue: "Show Custom Metadata in Sidebar"), "report meta status block"), + setting(.sidebarAppearance, "right-max-width", String(localized: "settings.sidebar.rightMaxWidth", defaultValue: "Dock Max Width"), "dock right sidebar max width terminal reservation cap logs lazygit"), setting(.customSidebars, "enabled", String(localized: "settings.customSidebars.enabled", defaultValue: "Show Custom Sidebars"), "custom sidebars enable show vibe swift json interpreted picker"), setting(.customSidebars, "renderer", String(localized: "settings.customSidebars.renderer", defaultValue: "Renderer"), "renderer in-process in app remote worker isolated process hover focus typing input"), setting(.betaFeatures, "feed", String(localized: "settings.betaFeatures.feed", defaultValue: "Feed"), "feed right sidebar agent decisions permissions questions"), @@ -497,6 +498,7 @@ enum SettingsSearchIndex { "sidebar.showLog": settingID(for: .sidebarAppearance, idSuffix: "show-log"), "sidebar.showProgress": settingID(for: .sidebarAppearance, idSuffix: "show-progress"), "sidebar.showCustomMetadata": settingID(for: .sidebarAppearance, idSuffix: "show-metadata"), + "sidebar.rightMaxWidth": settingID(for: .sidebarAppearance, idSuffix: "right-max-width"), "sidebar-font-size": settingID(for: .sidebarAppearance, idSuffix: "font-size"), "surface-tab-bar-font-size": settingID(for: .terminal, idSuffix: "tab-bar-font-size"), "terminal.showScrollBar": settingID(for: .terminal, idSuffix: "scrollbar"), diff --git a/Sources/SettingsSearchAliases.swift b/Sources/SettingsSearchAliases.swift index 28a812bc7919..bea5591c4008 100644 --- a/Sources/SettingsSearchAliases.swift +++ b/Sources/SettingsSearchAliases.swift @@ -115,6 +115,7 @@ enum SettingsSearchAliasIndex { "sidebarAppearance:show-log": localized("settings.search.alias.setting.app.show-log", defaultValue: "sidebar.showLog log status latest message imperative"), "sidebarAppearance:show-progress": localized("settings.search.alias.setting.app.show-progress", defaultValue: "sidebar.showProgress progress bar percent status set_progress"), "sidebarAppearance:show-metadata": localized("settings.search.alias.setting.app.show-metadata", defaultValue: "sidebar.showCustomMetadata metadata meta report_meta status custom block"), + "sidebarAppearance:right-max-width": localized("settings.search.alias.setting.sidebarAppearance.right-max-width", defaultValue: "sidebar.rightMaxWidth dock right sidebar max width terminal reservation cap logs lazygit"), "betaFeatures:feed": localized("settings.search.alias.setting.betaFeatures.feed", defaultValue: "feed right sidebar agent decisions permissions questions approval beta unstable"), "betaFeatures:dock": localized("settings.search.alias.setting.betaFeatures.dock", defaultValue: "dock right sidebar terminal controls tui beta unstable"), "mobile:iOSPairingHost": localized("settings.search.alias.setting.mobile.iOSPairingHost", defaultValue: "ios iphone ipad mobile pairing local network permission sync"), diff --git a/Sources/SidebarWorkspaceGroupHeaderMetrics.swift b/Sources/SidebarWorkspaceGroupHeaderMetrics.swift index 6ef20d892109..ca746eb87e87 100644 --- a/Sources/SidebarWorkspaceGroupHeaderMetrics.swift +++ b/Sources/SidebarWorkspaceGroupHeaderMetrics.swift @@ -73,9 +73,4 @@ struct SidebarWorkspaceGroupHeaderMetrics: Equatable { var plusFontSize: CGFloat { Self.basePlusFontSize * fontScale } /// Scaled plus-button frame edge. var plusFrame: CGFloat { Self.basePlusFrame * fontScale } - /// Stable drop-hit height for the group header, without reading SwiftUI layout. - var dropTargetHeight: CGFloat { - let contentHeight = max(chevronFrame, iconFrame, plusFrame, nameFontSize + 4) - return max(24 * fontScale, contentHeight + 10) - } } diff --git a/Sources/SidebarWorkspaceGroupHeaderView.swift b/Sources/SidebarWorkspaceGroupHeaderView.swift index 742511575efe..6bd30c1b7187 100644 --- a/Sources/SidebarWorkspaceGroupHeaderView.swift +++ b/Sources/SidebarWorkspaceGroupHeaderView.swift @@ -57,9 +57,6 @@ struct SidebarWorkspaceGroupHeaderView: View, Equatable { let isBeingDragged: Bool let topDropIndicatorVisible: Bool let onDragStart: () -> NSItemProvider - /// Factory invoked from `body` with a stable drop-hit height. Closure - /// captures the parent's drag state while this LazyVStack row stays free of - /// layout-driven state writes. let tabDropDelegateFactory: (CGFloat) -> SidebarWorkspaceGroupHeaderDropDelegate let onToggleCollapsed: () -> Void let onFocusAnchor: () -> Void @@ -73,6 +70,7 @@ struct SidebarWorkspaceGroupHeaderView: View, Equatable { let onOpenDocs: () -> Void @State private var isHovered = false + @State private var rowHeight: CGFloat = 1 private var metrics: SidebarWorkspaceGroupHeaderMetrics { SidebarWorkspaceGroupHeaderMetrics(fontScale: fontScale) @@ -96,6 +94,18 @@ struct SidebarWorkspaceGroupHeaderView: View, Equatable { return "\(shortcutModifierSymbol)\(shortcutDigit)" } + private var rowHeightProbe: some View { + GeometryReader { proxy in + Color.clear + .onAppear { + rowHeight = max(proxy.size.height, 1) + } + .onChange(of: proxy.size.height) { _, newHeight in + rowHeight = max(newHeight, 1) + } + } + } + var body: some View { HStack(spacing: 4) { Image(systemName: isCollapsed ? "chevron.right" : "chevron.down") @@ -217,6 +227,7 @@ struct SidebarWorkspaceGroupHeaderView: View, Equatable { offsetY: shortcutHintYOffset ) .padding(.horizontal, 6) + .background { rowHeightProbe } .shortcutHintVisibilityAnimation(value: showsShortcutHint) .opacity(isBeingDragged ? 0.6 : 1) .overlay(alignment: .top) { @@ -228,7 +239,7 @@ struct SidebarWorkspaceGroupHeaderView: View, Equatable { } .onDrag(onDragStart) .internalOnlyTabDrag() - .onDrop(of: SidebarTabDragPayload.dropContentTypes, delegate: tabDropDelegateFactory(metrics.dropTargetHeight)) + .onDrop(of: SidebarTabDragPayload.dropContentTypes, delegate: tabDropDelegateFactory(rowHeight)) .onHover { hovering in isHovered = hovering } diff --git a/Sources/SidebarWorkspaceRowDropMetrics.swift b/Sources/SidebarWorkspaceRowDropMetrics.swift deleted file mode 100644 index 7d864978e1fb..000000000000 --- a/Sources/SidebarWorkspaceRowDropMetrics.swift +++ /dev/null @@ -1,208 +0,0 @@ -import CoreGraphics -import CmuxSidebar - -struct SidebarWorkspaceRowDropMetrics { - static let collapsedMetadataEntryLimit = 3 - static let collapsedMetadataBlockLimit = 1 - static let maxWrappedTitleLines = 8 - static let maxDescriptionLines = 12 - static let maxMetadataBlockLines = 12 - private static let estimatedCharactersPerLine = 42 - - static func targetHeight( - fontScale: CGFloat, - titleLineCount: Int, - descriptionLineCount: Int, - hasSubtitle: Bool, - hasRemoteStatus: Bool, - metadataEntryCount: Int, - metadataEntryIsExpanded: Bool, - metadataBlockLineCounts: [Int], - hasMetadataBlockToggle: Bool, - hasLog: Bool, - hasProgress: Bool, - branchDirectoryRowCount: Int, - pullRequestRowCount: Int, - hasPorts: Bool - ) -> CGFloat { - let scale = max(fontScale, 0.5) - var height = 16 + CGFloat(max(titleLineCount, 1)) * 16 * scale - if descriptionLineCount > 0 { - height += (CGFloat(descriptionLineCount) * 13 + 2) * scale - } - if hasSubtitle { - height += 24 * scale - } - if hasRemoteStatus { - height += 18 * scale - } - height += metadataEntriesHeight( - entryCount: metadataEntryCount, - isExpanded: metadataEntryIsExpanded, - scale: scale - ) - height += metadataBlocksHeight( - lineCounts: metadataBlockLineCounts, - hasToggle: hasMetadataBlockToggle, - scale: scale - ) - if hasLog { - height += 16 * scale - } - if hasProgress { - height += 16 * scale - } - if branchDirectoryRowCount > 0 { - height += (CGFloat(branchDirectoryRowCount) * 13 + CGFloat(max(branchDirectoryRowCount - 1, 0))) * scale - } - if pullRequestRowCount > 0 { - height += (CGFloat(pullRequestRowCount) * 14 + CGFloat(max(pullRequestRowCount - 1, 0))) * scale - } - if hasPorts { - height += 16 * scale - } - return max(34, height.rounded(.up)) - } - - static func estimatedMetadataBlockLineCounts( - _ blocks: [SidebarMetadataBlock], - isExpanded: Bool - ) -> [Int] { - let visibleBlocks = isExpanded ? blocks : Array(blocks.prefix(collapsedMetadataBlockLimit)) - return visibleBlocks.map { estimatedLineCount($0.markdown) } - } - - static func estimatedTitleLineCount(_ title: String, wraps: Bool) -> Int { - wraps ? estimatedLineCount(title, maxLines: maxWrappedTitleLines) : 1 - } - - static func estimatedDescriptionLineCount(_ description: String?) -> Int { - guard let description else { return 0 } - return estimatedLineCount(description, maxLines: maxDescriptionLines) - } - - static func shouldUsePointerEdgeHeight( - wrapsWorkspaceTitles: Bool, - hasDescription: Bool, - hasMetadataBlocks: Bool - ) -> Bool { - !wrapsWorkspaceTitles && !hasDescription && !hasMetadataBlocks - } - - private static func metadataEntriesHeight( - entryCount: Int, - isExpanded: Bool, - scale: CGFloat - ) -> CGFloat { - guard entryCount > 0 else { return 0 } - let visibleCount = isExpanded ? entryCount : min(entryCount, collapsedMetadataEntryLimit) - let toggleCount = entryCount > collapsedMetadataEntryLimit ? 1 : 0 - let rowCount = visibleCount + toggleCount - return (CGFloat(rowCount) * 13 + CGFloat(max(rowCount - 1, 0)) * 2) * scale - } - - private static func metadataBlocksHeight( - lineCounts: [Int], - hasToggle: Bool, - scale: CGFloat - ) -> CGFloat { - guard !lineCounts.isEmpty || hasToggle else { return 0 } - let visibleLineCount = lineCounts.reduce(0) { $0 + max($1, 1) } - let toggleCount = hasToggle ? 1 : 0 - let blockSpacingCount = max(lineCounts.count + toggleCount - 1, 0) - return (CGFloat(visibleLineCount) * 13 + CGFloat(toggleCount) * 13 + CGFloat(blockSpacingCount) * 3) * scale - } - - private static func estimatedLineCount(_ text: String, maxLines: Int = maxMetadataBlockLines) -> Int { - let boundedText = String(text.prefix(estimatedCharactersPerLine * maxLines)) - let lines = boundedText.components(separatedBy: .newlines) - let lineCount = lines.reduce(0) { count, line in - let characterCount = line.trimmingCharacters(in: .whitespacesAndNewlines).count - return count + max(1, Int(ceil(Double(characterCount) / Double(estimatedCharactersPerLine)))) - } - return min(max(lineCount, 1), maxLines) - } - - private static func branchDirectoryRowCount( - snapshot: SidebarWorkspaceSnapshotBuilder.Snapshot, - settings: SidebarTabItemSettingsSnapshot - ) -> Int { - guard settings.visibleAuxiliaryDetails.showsBranchDirectory else { return 0 } - if settings.usesVerticalBranchLayout { - return snapshot.branchDirectoryLines.reduce(0) { count, line in - if settings.stacksBranchAndDirectory { - let branchCount = line.branch == nil ? 0 : 1 - let directoryCount = line.directoryCandidates.isEmpty ? 0 : 1 - return count + max(branchCount + directoryCount, 1) - } - return count + 1 - } - } - if settings.stacksBranchAndDirectory, - (snapshot.compactGitBranchSummaryText != nil || !snapshot.compactDirectoryCandidates.isEmpty) { - let branchCount = snapshot.compactGitBranchSummaryText == nil ? 0 : 1 - let directoryCount = snapshot.compactDirectoryCandidates.isEmpty ? 0 : 1 - return max(branchCount + directoryCount, 1) - } - if !snapshot.compactBranchDirectoryCandidates.isEmpty { - return 1 - } - return 0 - } - - static func targetHeight( - snapshot: SidebarWorkspaceSnapshotBuilder.Snapshot, - settings: SidebarTabItemSettingsSnapshot, - effectiveSubtitle: String?, - metadataEntryIsExpanded: Bool, - metadataBlocksAreExpanded: Bool - ) -> CGFloat { - let visibleDetails = settings.visibleAuxiliaryDetails - let metadataEntryCount = visibleDetails.showsMetadata ? snapshot.metadataEntries.count : 0 - let metadataBlockLineCounts = visibleDetails.showsMetadata - ? estimatedMetadataBlockLineCounts(snapshot.metadataBlocks, isExpanded: metadataBlocksAreExpanded) - : [] - let hasMetadataBlockToggle = visibleDetails.showsMetadata && - snapshot.metadataBlocks.count > collapsedMetadataBlockLimit - return targetHeight( - fontScale: settings.sidebarFontScale, - titleLineCount: estimatedTitleLineCount(snapshot.title, wraps: settings.wrapsWorkspaceTitles), - descriptionLineCount: estimatedDescriptionLineCount(snapshot.customDescription), - hasSubtitle: effectiveSubtitle != nil, - hasRemoteStatus: !settings.hidesAllDetails && settings.showsSSH && snapshot.remoteWorkspaceSidebarText != nil, - metadataEntryCount: metadataEntryCount, - metadataEntryIsExpanded: metadataEntryIsExpanded, - metadataBlockLineCounts: metadataBlockLineCounts, - hasMetadataBlockToggle: hasMetadataBlockToggle, - hasLog: visibleDetails.showsLog && snapshot.latestLog != nil, - hasProgress: visibleDetails.showsProgress && snapshot.progress != nil, - branchDirectoryRowCount: branchDirectoryRowCount(snapshot: snapshot, settings: settings), - pullRequestRowCount: visibleDetails.showsPullRequests ? snapshot.pullRequestRows.count : 0, - hasPorts: visibleDetails.showsPorts && !snapshot.listeningPorts.isEmpty - ) - } - - static func dropTargetHeight( - snapshot: SidebarWorkspaceSnapshotBuilder.Snapshot, - settings: SidebarTabItemSettingsSnapshot, - effectiveSubtitle: String?, - metadataEntryIsExpanded: Bool, - metadataBlocksAreExpanded: Bool - ) -> CGFloat? { - let visibleDetails = settings.visibleAuxiliaryDetails - guard shouldUsePointerEdgeHeight( - wrapsWorkspaceTitles: settings.wrapsWorkspaceTitles, - hasDescription: snapshot.customDescription != nil, - hasMetadataBlocks: visibleDetails.showsMetadata && !snapshot.metadataBlocks.isEmpty - ) else { - return nil - } - return targetHeight( - snapshot: snapshot, - settings: settings, - effectiveSubtitle: effectiveSubtitle, - metadataEntryIsExpanded: metadataEntryIsExpanded, - metadataBlocksAreExpanded: metadataBlocksAreExpanded - ) - } -} diff --git a/Sources/TabItemViewDropSupport.swift b/Sources/TabItemViewDropSupport.swift deleted file mode 100644 index 73f19c743461..000000000000 --- a/Sources/TabItemViewDropSupport.swift +++ /dev/null @@ -1,67 +0,0 @@ -import CoreGraphics -import Foundation - -extension TabItemView { - func workspaceDropTargetHeight( - snapshot: SidebarWorkspaceSnapshotBuilder.Snapshot, - effectiveSubtitle: String? - ) -> CGFloat? { - SidebarWorkspaceRowDropMetrics.dropTargetHeight( - snapshot: snapshot, - settings: settings, - effectiveSubtitle: effectiveSubtitle, - metadataEntryIsExpanded: metadataRowsExpanded, - metadataBlocksAreExpanded: metadataBlocksExpanded - ) - } - - func closeWorkspace(method: StaticString) { - #if DEBUG - let workspaceDebugID = tab.id.uuidString.prefix(5) - cmuxDebugLog("sidebar.close workspace=\(workspaceDebugID) method=\(method)") - #endif - tabManager.closeWorkspaceWithConfirmation(tab) - } - - func refreshWorkspaceSnapshotAfterObservation(source: StaticString) { - logWorkspaceObservationInvalidation(source: source) - refreshWorkspaceSnapshot() - } - - func openPendingFinderDirectoryRequest() async { - guard let request = workspaceFinderDirectoryOpenRequest else { return } - await WorkspaceFinderDirectoryOpener.openInFinder(request.directoryURL) - guard !Task.isCancelled, workspaceFinderDirectoryOpenRequest == request else { return } - workspaceFinderDirectoryOpenRequest = nil - } - - private func logWorkspaceObservationInvalidation(source: StaticString) { - #if DEBUG - let description = tab.customDescription ?? "" - let workspaceDebugID = tab.id.uuidString.prefix(8) - let titlePreview = debugSidebarTextPreview(tab.title) - let descriptionLength = (description as NSString).length - let descriptionPreview = debugSidebarTextPreview(description) - cmuxDebugLog( - "sidebar.row.invalidate workspace=\(workspaceDebugID) " + - "source=\(source) " + - "title=\"\(titlePreview)\" " + - "descLen=\(descriptionLength) " + - "desc=\"\(descriptionPreview)\"" - ) - #endif - } - - private func debugSidebarTextPreview(_ text: String, limit: Int = 120) -> String { - let escaped = text - .replacingOccurrences(of: "\\", with: "\\\\") - .replacingOccurrences(of: "\n", with: "\\n") - .replacingOccurrences(of: "\r", with: "\\r") - .replacingOccurrences(of: "\t", with: "\\t") - if escaped.count <= limit { - return escaped - } - let prefix = escaped.prefix(limit) - return "\(prefix)..." - } -} diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj index 17985a70c0ee..ee1d02ce69bf 100644 --- a/cmux.xcodeproj/project.pbxproj +++ b/cmux.xcodeproj/project.pbxproj @@ -676,7 +676,6 @@ F57072635F25EBCA741E125D /* SidebarState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1614EAD3CCF70A177A51BD1 /* SidebarState.swift */; }; D7AB34300000000000000105 /* SidebarTabDropIndicatorPredicateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7AB34300000000000000106 /* SidebarTabDropIndicatorPredicateTests.swift */; }; CA39C0304FE351A21C372429 /* SidebarWidthPolicyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE0171AF1F49F7547191CEE5 /* SidebarWidthPolicyTests.swift */; }; - C9A57403C9A57403C9A57403 /* SidebarWorkspaceDropMetricsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9A57404C9A57404C9A57404 /* SidebarWorkspaceDropMetricsTests.swift */; }; D7AB34300000000000000005 /* SidebarWorkspaceDropPlannerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7AB34300000000000000006 /* SidebarWorkspaceDropPlannerTests.swift */; }; C9A57103C9A57103C9A57103 /* SidebarWorkspaceGroupConfigOpener.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9A57104C9A57104C9A57104 /* SidebarWorkspaceGroupConfigOpener.swift */; }; C135190000000000000000B1 /* SidebarWorkspaceGroupConfigOpenerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C135190000000000000000B2 /* SidebarWorkspaceGroupConfigOpenerTests.swift */; }; @@ -686,10 +685,9 @@ C9A57303C9A57303C9A57303 /* SidebarWorkspaceGroupHeaderMetricsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9A57304C9A57304C9A57304 /* SidebarWorkspaceGroupHeaderMetricsTests.swift */; }; C9A57101C9A57101C9A57101 /* SidebarWorkspaceGroupHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9A57102C9A57102C9A57102 /* SidebarWorkspaceGroupHeaderView.swift */; }; C9A57109C9A57109C9A57109 /* SidebarWorkspaceGroupingMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9A5710AC9A5710AC9A5710A /* SidebarWorkspaceGroupingMetrics.swift */; }; - C9A57201C9A57201C9A57201 /* SidebarWorkspaceRenderItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9A57202C9A57202C9A57202 /* SidebarWorkspaceRenderItem.swift */; }; - C9A57401C9A57401C9A57401 /* SidebarWorkspaceRowDropMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9A57402C9A57402C9A57402 /* SidebarWorkspaceRowDropMetrics.swift */; }; - D35450010000000000000001 /* SidebarWorkspaceRowHoverTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = D35450020000000000000002 /* SidebarWorkspaceRowHoverTracker.swift */; }; - C9A57505C9A57505C9A57505 /* SidebarWorkspaceScrollLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9A57506C9A57506C9A57506 /* SidebarWorkspaceScrollLayoutTests.swift */; }; + C9A57201C9A57201C9A57201 /* SidebarWorkspaceRenderItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9A57202C9A57202C9A57202 /* SidebarWorkspaceRenderItem.swift */; }; + D35450010000000000000001 /* SidebarWorkspaceRowHoverTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = D35450020000000000000002 /* SidebarWorkspaceRowHoverTracker.swift */; }; + C9A57505C9A57505C9A57505 /* SidebarWorkspaceScrollLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9A57506C9A57506C9A57506 /* SidebarWorkspaceScrollLayoutTests.swift */; }; C0DE56010000000000000001 /* SidebarWorkspaceSelectionAnchorPolicyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE56010000000000000002 /* SidebarWorkspaceSelectionAnchorPolicyTests.swift */; }; 988C6A036BA56EA5759A95A0 /* SidebarWorkspaceSnapshotRefreshPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = D50D9BCE46701FBA91C2EFB6 /* SidebarWorkspaceSnapshotRefreshPolicy.swift */; }; 62270F3DCECB4787D789CCE3 /* SidebarWorkspaceSnapshotRefreshPolicyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F016B5C09357B3226FA2E014 /* SidebarWorkspaceSnapshotRefreshPolicyTests.swift */; }; @@ -704,7 +702,6 @@ D7AB00000000000000B041 /* SupersededPhoneDismissBuffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7AB00000000000000B040 /* SupersededPhoneDismissBuffer.swift */; }; A5001303 /* SurfaceSearchOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001301 /* SurfaceSearchOverlay.swift */; }; C9A5720BC9A5720BC9A5720B /* TabItemView+WorkspaceGroups.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9A5720CC9A5720CC9A5720C /* TabItemView+WorkspaceGroups.swift */; }; - C9A57405C9A57405C9A57405 /* TabItemViewDropSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9A57406C9A57406C9A57406 /* TabItemViewDropSupport.swift */; }; D7AB00000000000000000013 /* TabManager+DetachedWorkspace.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7AB00000000000000000014 /* TabManager+DetachedWorkspace.swift */; }; E3309A03 /* TabManager+EqualizeSplits.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3309A04 /* TabManager+EqualizeSplits.swift */; }; E3B7A400000000000000000B /* TabManager+FocusHistoryHosting.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3B7A400000000000000000C /* TabManager+FocusHistoryHosting.swift */; }; @@ -1543,7 +1540,6 @@ D1614EAD3CCF70A177A51BD1 /* SidebarState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sidebar/SidebarState.swift; sourceTree = ""; }; D7AB34300000000000000106 /* SidebarTabDropIndicatorPredicateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarTabDropIndicatorPredicateTests.swift; sourceTree = ""; }; EE0171AF1F49F7547191CEE5 /* SidebarWidthPolicyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarWidthPolicyTests.swift; sourceTree = ""; }; - C9A57404C9A57404C9A57404 /* SidebarWorkspaceDropMetricsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarWorkspaceDropMetricsTests.swift; sourceTree = ""; }; D7AB34300000000000000006 /* SidebarWorkspaceDropPlannerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarWorkspaceDropPlannerTests.swift; sourceTree = ""; }; C9A57104C9A57104C9A57104 /* SidebarWorkspaceGroupConfigOpener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarWorkspaceGroupConfigOpener.swift; sourceTree = ""; }; C135190000000000000000B2 /* SidebarWorkspaceGroupConfigOpenerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarWorkspaceGroupConfigOpenerTests.swift; sourceTree = ""; }; @@ -1553,10 +1549,9 @@ C9A57304C9A57304C9A57304 /* SidebarWorkspaceGroupHeaderMetricsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarWorkspaceGroupHeaderMetricsTests.swift; sourceTree = ""; }; C9A57102C9A57102C9A57102 /* SidebarWorkspaceGroupHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarWorkspaceGroupHeaderView.swift; sourceTree = ""; }; C9A5710AC9A5710AC9A5710A /* SidebarWorkspaceGroupingMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarWorkspaceGroupingMetrics.swift; sourceTree = ""; }; - C9A57202C9A57202C9A57202 /* SidebarWorkspaceRenderItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarWorkspaceRenderItem.swift; sourceTree = ""; }; - C9A57402C9A57402C9A57402 /* SidebarWorkspaceRowDropMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarWorkspaceRowDropMetrics.swift; sourceTree = ""; }; - D35450020000000000000002 /* SidebarWorkspaceRowHoverTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sidebar/SidebarWorkspaceRowHoverTracker.swift; sourceTree = ""; }; - C9A57506C9A57506C9A57506 /* SidebarWorkspaceScrollLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarWorkspaceScrollLayoutTests.swift; sourceTree = ""; }; + C9A57202C9A57202C9A57202 /* SidebarWorkspaceRenderItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarWorkspaceRenderItem.swift; sourceTree = ""; }; + D35450020000000000000002 /* SidebarWorkspaceRowHoverTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sidebar/SidebarWorkspaceRowHoverTracker.swift; sourceTree = ""; }; + C9A57506C9A57506C9A57506 /* SidebarWorkspaceScrollLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarWorkspaceScrollLayoutTests.swift; sourceTree = ""; }; C0DE56010000000000000002 /* SidebarWorkspaceSelectionAnchorPolicyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarWorkspaceSelectionAnchorPolicyTests.swift; sourceTree = ""; }; D50D9BCE46701FBA91C2EFB6 /* SidebarWorkspaceSnapshotRefreshPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sidebar/SidebarWorkspaceSnapshotRefreshPolicy.swift; sourceTree = ""; }; F016B5C09357B3226FA2E014 /* SidebarWorkspaceSnapshotRefreshPolicyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarWorkspaceSnapshotRefreshPolicyTests.swift; sourceTree = ""; }; @@ -1569,7 +1564,6 @@ D7AB00000000000000B040 /* SupersededPhoneDismissBuffer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupersededPhoneDismissBuffer.swift; sourceTree = ""; }; A5001301 /* SurfaceSearchOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Find/SurfaceSearchOverlay.swift; sourceTree = ""; }; C9A5720CC9A5720CC9A5720C /* TabItemView+WorkspaceGroups.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TabItemView+WorkspaceGroups.swift"; sourceTree = ""; }; - C9A57406C9A57406C9A57406 /* TabItemViewDropSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabItemViewDropSupport.swift; sourceTree = ""; }; D7AB00000000000000000014 /* TabManager+DetachedWorkspace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TabManager+DetachedWorkspace.swift"; sourceTree = ""; }; E3309A04 /* TabManager+EqualizeSplits.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TabManager+EqualizeSplits.swift"; sourceTree = ""; }; E3B7A400000000000000000C /* TabManager+FocusHistoryHosting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TabManager+FocusHistoryHosting.swift"; sourceTree = ""; }; @@ -2078,13 +2072,11 @@ C9A57302C9A57302C9A57302 /* SidebarWorkspaceGroupHeaderMetrics.swift */, C9A5710AC9A5710AC9A5710A /* SidebarWorkspaceGroupingMetrics.swift */, D5037010000000000000002 /* RenderableSystemSymbol.swift */, - C9A57202C9A57202C9A57202 /* SidebarWorkspaceRenderItem.swift */, - C9A57402C9A57402C9A57402 /* SidebarWorkspaceRowDropMetrics.swift */, - C0DE5F210000000000000002 /* SidebarMetadataMarkdownRenderer.swift */, - C9A57502C9A57502C9A57502 /* SidebarRowsFillLayout.swift */, - C9A57406C9A57406C9A57406 /* TabItemViewDropSupport.swift */, - C9A5720CC9A5720CC9A5720C /* TabItemView+WorkspaceGroups.swift */, - C9A5720AC9A5720AC9A5720A /* VerticalTabsSidebar+WorkspaceGroups.swift */, + C9A57202C9A57202C9A57202 /* SidebarWorkspaceRenderItem.swift */, + C0DE5F210000000000000002 /* SidebarMetadataMarkdownRenderer.swift */, + C9A57502C9A57502C9A57502 /* SidebarRowsFillLayout.swift */, + C9A5720CC9A5720CC9A5720C /* TabItemView+WorkspaceGroups.swift */, + C9A5720AC9A5720AC9A5720A /* VerticalTabsSidebar+WorkspaceGroups.swift */, C9A57204C9A57204C9A57204 /* WorkspaceGroupMenuSnapshot.swift */, C0DEF0A30000000000000002 /* SidebarPortDisplayText.swift */, 610008EA5E3744DDBE05C8E2 /* InternalTabDragConfiguration.swift */, @@ -2713,7 +2705,6 @@ C7A509000000000000000001 /* CmuxTopSnapshotScopeTests.swift */, C7A5090000000000000005A1 /* CmuxTopSnapshotScopeCacheTests.swift */, C7A50C000000000000000001 /* CmuxTopProcessCPUTests.swift */, - C9A57404C9A57404C9A57404 /* SidebarWorkspaceDropMetricsTests.swift */, D7AB34300000000000000006 /* SidebarWorkspaceDropPlannerTests.swift */, D7AB34300000000000000106 /* SidebarTabDropIndicatorPredicateTests.swift */, A47E00010000000000000002 /* AuthEnvironmentTests.swift */, @@ -3563,18 +3554,16 @@ C9A57107C9A57107C9A57107 /* SidebarWorkspaceGroupDialogs.swift in Sources */, C9A57301C9A57301C9A57301 /* SidebarWorkspaceGroupHeaderMetrics.swift in Sources */, C9A57101C9A57101C9A57101 /* SidebarWorkspaceGroupHeaderView.swift in Sources */, - C9A57109C9A57109C9A57109 /* SidebarWorkspaceGroupingMetrics.swift in Sources */, - C9A57201C9A57201C9A57201 /* SidebarWorkspaceRenderItem.swift in Sources */, - C9A57401C9A57401C9A57401 /* SidebarWorkspaceRowDropMetrics.swift in Sources */, - D35450010000000000000001 /* SidebarWorkspaceRowHoverTracker.swift in Sources */, - 988C6A036BA56EA5759A95A0 /* SidebarWorkspaceSnapshotRefreshPolicy.swift in Sources */, + C9A57109C9A57109C9A57109 /* SidebarWorkspaceGroupingMetrics.swift in Sources */, + C9A57201C9A57201C9A57201 /* SidebarWorkspaceRenderItem.swift in Sources */, + D35450010000000000000001 /* SidebarWorkspaceRowHoverTracker.swift in Sources */, + 988C6A036BA56EA5759A95A0 /* SidebarWorkspaceSnapshotRefreshPolicy.swift in Sources */, A5001226 /* SocketControlMode+Display.swift in Sources */, E30780000000000000000012 /* SSHPTYAttachStartupCommandBuilder.swift in Sources */, D35B71010000000000000001 /* StartupBreadcrumbLog.swift in Sources */, D7AB00000000000000B041 /* SupersededPhoneDismissBuffer.swift in Sources */, A5001303 /* SurfaceSearchOverlay.swift in Sources */, C9A5720BC9A5720BC9A5720B /* TabItemView+WorkspaceGroups.swift in Sources */, - C9A57405C9A57405C9A57405 /* TabItemViewDropSupport.swift in Sources */, D7AB00000000000000000013 /* TabManager+DetachedWorkspace.swift in Sources */, E3309A03 /* TabManager+EqualizeSplits.swift in Sources */, E3B7A400000000000000000B /* TabManager+FocusHistoryHosting.swift in Sources */, @@ -3973,7 +3962,6 @@ C9A57513C9A57513C9A57513 /* SidebarScrollViewConfiguratorTests.swift in Sources */, D7AB34300000000000000105 /* SidebarTabDropIndicatorPredicateTests.swift in Sources */, CA39C0304FE351A21C372429 /* SidebarWidthPolicyTests.swift in Sources */, - C9A57403C9A57403C9A57403 /* SidebarWorkspaceDropMetricsTests.swift in Sources */, D7AB34300000000000000005 /* SidebarWorkspaceDropPlannerTests.swift in Sources */, C135190000000000000000B1 /* SidebarWorkspaceGroupConfigOpenerTests.swift in Sources */, C9A57303C9A57303C9A57303 /* SidebarWorkspaceGroupHeaderMetricsTests.swift in Sources */, diff --git a/cmuxTests/GhosttyConfigTests.swift b/cmuxTests/GhosttyConfigTests.swift index 4a1ca10f5557..52e35d44ab3d 100644 --- a/cmuxTests/GhosttyConfigTests.swift +++ b/cmuxTests/GhosttyConfigTests.swift @@ -721,70 +721,6 @@ final class GhosttyConfigTests: XCTestCase { XCTAssertEqual(rgb255(darkConfig.backgroundColor), RGB(red: 0, green: 43, blue: 54)) } - func testLoadHonorsPairedThemeAndTopLevelForegroundOverrideByColorScheme() throws { - let fileManager = FileManager.default - let root = fileManager.temporaryDirectory - .appendingPathComponent("cmux-ghostty-theme-pair-foreground-\(UUID().uuidString)") - let themesDir = root.appendingPathComponent("themes", isDirectory: true) - try fileManager.createDirectory(at: themesDir, withIntermediateDirectories: true) - defer { try? fileManager.removeItem(at: root) } - - let originalFixedHome = getenv("CFFIXED_USER_HOME").map { String(cString: $0) } - let originalResourcesDir = getenv("GHOSTTY_RESOURCES_DIR").map { String(cString: $0) } - setenv("CFFIXED_USER_HOME", root.path, 1) - setenv("GHOSTTY_RESOURCES_DIR", root.path, 1) - defer { - if let originalFixedHome { - setenv("CFFIXED_USER_HOME", originalFixedHome, 1) - } else { - unsetenv("CFFIXED_USER_HOME") - } - if let originalResourcesDir { - setenv("GHOSTTY_RESOURCES_DIR", originalResourcesDir, 1) - } else { - unsetenv("GHOSTTY_RESOURCES_DIR") - } - GhosttyConfig.invalidateLoadCache() - } - - try """ - background = #fffcf0 - foreground = #100f0f - """.write( - to: themesDir.appendingPathComponent("Flexoki Light", isDirectory: false), - atomically: true, - encoding: .utf8 - ) - try """ - background = #100f0f - foreground = #cecdc3 - """.write( - to: themesDir.appendingPathComponent("Flexoki Dark", isDirectory: false), - atomically: true, - encoding: .utf8 - ) - - let ghosttyConfigDir = root.appendingPathComponent(".config/ghostty", isDirectory: true) - try fileManager.createDirectory(at: ghosttyConfigDir, withIntermediateDirectories: true) - try """ - window-theme = auto - theme = light:Flexoki Light,dark:Flexoki Dark - foreground = #ff0000 - """.write( - to: ghosttyConfigDir.appendingPathComponent("config", isDirectory: false), - atomically: true, - encoding: .utf8 - ) - - let lightConfig = GhosttyConfig.load(preferredColorScheme: .light, useCache: false) - let darkConfig = GhosttyConfig.load(preferredColorScheme: .dark, useCache: false) - - XCTAssertEqual(lightConfig.backgroundColor.hexString(), "#FFFCF0") - XCTAssertEqual(lightConfig.foregroundColor.hexString(), "#FF0000") - XCTAssertEqual(darkConfig.backgroundColor.hexString(), "#100F0F") - XCTAssertEqual(darkConfig.foregroundColor.hexString(), "#FF0000") - } - func testParseBackgroundOpacityReadsConfigValue() { var config = GhosttyConfig() config.parse("background-opacity = 0.42") diff --git a/cmuxTests/SidebarWidthPolicyTests.swift b/cmuxTests/SidebarWidthPolicyTests.swift index 3ce57f3b800b..efe610ce98a0 100644 --- a/cmuxTests/SidebarWidthPolicyTests.swift +++ b/cmuxTests/SidebarWidthPolicyTests.swift @@ -10,6 +10,9 @@ import XCTest #endif final class SidebarWidthPolicyTests: XCTestCase { + private let settingsFileBackupsDefaultsKey = "cmux.settingsFile.backups.v1" + private let importedManagedDefaultsKey = "cmux.settingsFile.importedManagedDefaults.v1" + func testDefaultMinimumSidebarWidthIsPersistedProductDefault() { let suiteName = "SidebarWidthPolicyTests.defaultMinimum.\(UUID().uuidString)" let defaults = UserDefaults(suiteName: suiteName)! @@ -74,7 +77,15 @@ final class SidebarWidthPolicyTests: XCTestCase { ) } - func testRightSidebarClampLeavesTerminalWidth() { + func testRightSidebarFirstCustomMaximumMatchesBuiltInCap() { + XCTAssertEqual( + ContentView.clampedRightSidebarWidth(10_000, availableWidth: 10_000), + CGFloat(RightSidebarWidthSettings.defaultConfiguredMaximumWidth), + accuracy: 0.001 + ) + } + + func testRightSidebarClampLeavesTerminalWidthWhenMaxWidthSettingIsMissing() { XCTAssertEqual( ContentView.clampedRightSidebarWidth(10_000, availableWidth: 1000), 640, @@ -82,6 +93,42 @@ final class SidebarWidthPolicyTests: XCTestCase { ) } + func testRightSidebarConfiguredMaxCanExceedBuiltInDefaultOnWideWindows() { + XCTAssertEqual( + ContentView.clampedRightSidebarWidth( + 10_000, + availableWidth: 2400, + configuredMaximumWidth: 1_500 + ), + 1_500, + accuracy: 0.001 + ) + } + + func testRightSidebarConfiguredMaxStillLeavesTerminalWidth() { + XCTAssertEqual( + ContentView.clampedRightSidebarWidth( + 10_000, + availableWidth: 1000, + configuredMaximumWidth: 1_400 + ), + 640, + accuracy: 0.001 + ) + } + + func testRightSidebarConfiguredMaxBelowMinimumClampsToMinimumWidth() { + XCTAssertEqual( + ContentView.clampedRightSidebarWidth( + 10_000, + availableWidth: 1000, + configuredMaximumWidth: 120 + ), + 276, + accuracy: 0.001 + ) + } + func testRightSidebarClampKeepsMinimumWidth() { XCTAssertEqual( ContentView.clampedRightSidebarWidth(20, availableWidth: 1000), @@ -90,6 +137,122 @@ final class SidebarWidthPolicyTests: XCTestCase { ) } + func testSettingsFileStoreAppliesRightSidebarMaxWidthSetting() throws { + let defaults = UserDefaults.standard + let managedKey = RightSidebarWidthSettings.maxWidthKey + let previousValues = [ + managedKey, + settingsFileBackupsDefaultsKey, + importedManagedDefaultsKey, + ].reduce(into: [String: Any]()) { values, key in + values[key] = defaults.object(forKey: key) + } + defer { + for key in [managedKey, settingsFileBackupsDefaultsKey, importedManagedDefaultsKey] { + if let value = previousValues[key] { + defaults.set(value, forKey: key) + } else { + defaults.removeObject(forKey: key) + } + } + } + + defaults.removeObject(forKey: managedKey) + defaults.removeObject(forKey: settingsFileBackupsDefaultsKey) + defaults.removeObject(forKey: importedManagedDefaultsKey) + + let directoryURL = FileManager.default.temporaryDirectory.appendingPathComponent( + "right-sidebar-width-settings-\(UUID().uuidString)", + isDirectory: true + ) + try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: directoryURL) } + + let settingsFileURL = directoryURL.appendingPathComponent("cmux.json", isDirectory: false) + try """ + { + "sidebar": { + "rightMaxWidth": 900 + } + } + """.write(to: settingsFileURL, atomically: true, encoding: .utf8) + + _ = KeyboardShortcutSettingsFileStore( + primaryPath: settingsFileURL.path, + fallbackPath: nil, + additionalFallbackPaths: [], + startWatching: false + ) + + XCTAssertEqual(defaults.double(forKey: managedKey), 900, accuracy: 0.001) + let configuredMaximumWidth = try XCTUnwrap( + RightSidebarWidthSettings().configuredMaximumWidth(from: defaults.double(forKey: managedKey)) + ) + XCTAssertEqual(configuredMaximumWidth, 900, accuracy: 0.001) + } + + func testSettingsFileStoreClampsRightSidebarMaxWidthSetting() throws { + let defaults = UserDefaults.standard + let managedKey = RightSidebarWidthSettings.maxWidthKey + let previousValues = [ + managedKey, + settingsFileBackupsDefaultsKey, + importedManagedDefaultsKey, + ].reduce(into: [String: Any]()) { values, key in + values[key] = defaults.object(forKey: key) + } + defer { + for key in [managedKey, settingsFileBackupsDefaultsKey, importedManagedDefaultsKey] { + if let value = previousValues[key] { + defaults.set(value, forKey: key) + } else { + defaults.removeObject(forKey: key) + } + } + } + + defaults.removeObject(forKey: managedKey) + defaults.removeObject(forKey: settingsFileBackupsDefaultsKey) + defaults.removeObject(forKey: importedManagedDefaultsKey) + + let directoryURL = FileManager.default.temporaryDirectory.appendingPathComponent( + "right-sidebar-width-settings-clamped-\(UUID().uuidString)", + isDirectory: true + ) + try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: directoryURL) } + + let settingsFileURL = directoryURL.appendingPathComponent("cmux.json", isDirectory: false) + try """ + { + "sidebar": { + "rightMaxWidth": 10000 + } + } + """.write(to: settingsFileURL, atomically: true, encoding: .utf8) + + _ = KeyboardShortcutSettingsFileStore( + primaryPath: settingsFileURL.path, + fallbackPath: nil, + additionalFallbackPaths: [], + startWatching: false + ) + + XCTAssertEqual( + defaults.double(forKey: managedKey), + RightSidebarWidthSettings.settingsEditorMaximumWidth, + accuracy: 0.001 + ) + let configuredMaximumWidth = try XCTUnwrap( + RightSidebarWidthSettings().configuredMaximumWidth(from: defaults.double(forKey: managedKey)) + ) + XCTAssertEqual( + configuredMaximumWidth, + RightSidebarWidthSettings.settingsEditorMaximumWidth, + accuracy: 0.001 + ) + } + func testLeadingSidebarResizeRangeFavorsSidebarSide() { let range = SidebarResizeInteraction.Edge.leading.hitRange(dividerX: 200) diff --git a/cmuxTests/SidebarWorkspaceDropMetricsTests.swift b/cmuxTests/SidebarWorkspaceDropMetricsTests.swift deleted file mode 100644 index c9331f30c15f..000000000000 --- a/cmuxTests/SidebarWorkspaceDropMetricsTests.swift +++ /dev/null @@ -1,147 +0,0 @@ -import CoreGraphics -import CmuxSidebar -import Foundation -import Testing - -#if canImport(cmux_DEV) -@testable import cmux_DEV -#elseif canImport(cmux) -@testable import cmux -#endif - -struct SidebarWorkspaceDropMetricsTests { - @Test - func testWorkspaceGroupHeaderDropTargetHeightScalesWithoutLayoutMeasurement() { - #expect(SidebarWorkspaceGroupHeaderMetrics(fontScale: 1).dropTargetHeight == 28) - #expect(SidebarWorkspaceGroupHeaderMetrics(fontScale: 2).dropTargetHeight == 48) - } - - @Test - func testWorkspaceRowDropTargetHeightScalesWithContentWithoutLayoutMeasurement() { - let base = workspaceRowHeight() - let rich = workspaceRowHeight( - titleLineCount: 2, - descriptionLineCount: 2, - hasSubtitle: true, - hasRemoteStatus: true, - metadataEntryCount: 4, - metadataEntryIsExpanded: true, - metadataBlockLineCounts: [2, 3], - hasMetadataBlockToggle: true, - hasLog: true, - hasProgress: true, - branchDirectoryRowCount: 2, - pullRequestRowCount: 2, - hasPorts: true - ) - let scaled = workspaceRowHeight(fontScale: 1.5) - - #expect(base == 34) - #expect(rich > base) - #expect(scaled > base) - } - - @Test - func testWorkspaceRowDropTargetHeightTracksExpandedMetadataRows() { - let collapsed = workspaceRowHeight(metadataEntryCount: 6, metadataEntryIsExpanded: false) - let expanded = workspaceRowHeight(metadataEntryCount: 6, metadataEntryIsExpanded: true) - - #expect(expanded > collapsed) - } - - @Test - func testWorkspaceRowDropTargetHeightTracksExpandedMetadataBlocks() { - let collapsed = workspaceRowHeight( - metadataBlockLineCounts: [1], - hasMetadataBlockToggle: true - ) - let expanded = workspaceRowHeight( - metadataBlockLineCounts: [1, 4, 2], - hasMetadataBlockToggle: true - ) - - #expect(expanded > collapsed) - } - - @Test - func testMetadataBlockLineEstimationOnlyScansVisibleCollapsedBlock() { - let blocks = [ - SidebarMetadataBlock(key: "visible", markdown: "one", priority: 0, timestamp: Date()), - SidebarMetadataBlock( - key: "hidden", - markdown: String(repeating: "hidden\n", count: 100), - priority: 0, - timestamp: Date() - ), - ] - - #expect(SidebarWorkspaceRowDropMetrics.estimatedMetadataBlockLineCounts(blocks, isExpanded: false) == [1]) - #expect(SidebarWorkspaceRowDropMetrics.estimatedMetadataBlockLineCounts(blocks, isExpanded: true) == [1, 12]) - } - - @Test - func testWorkspaceRowDropTargetHeightUsesLineAwareTitleAndDescriptionEstimates() { - let short = workspaceRowHeight(titleLineCount: 1, descriptionLineCount: 1) - let tall = workspaceRowHeight(titleLineCount: 4, descriptionLineCount: 5) - - #expect(tall > short) - } - - @Test - func testPointerEdgeHeightIsOnlyUsedForWidthIndependentRows() { - #expect(SidebarWorkspaceRowDropMetrics.shouldUsePointerEdgeHeight( - wrapsWorkspaceTitles: false, - hasDescription: false, - hasMetadataBlocks: false - )) - #expect(!SidebarWorkspaceRowDropMetrics.shouldUsePointerEdgeHeight( - wrapsWorkspaceTitles: true, - hasDescription: false, - hasMetadataBlocks: false - )) - #expect(!SidebarWorkspaceRowDropMetrics.shouldUsePointerEdgeHeight( - wrapsWorkspaceTitles: false, - hasDescription: true, - hasMetadataBlocks: false - )) - #expect(!SidebarWorkspaceRowDropMetrics.shouldUsePointerEdgeHeight( - wrapsWorkspaceTitles: false, - hasDescription: false, - hasMetadataBlocks: true - )) - } - - private func workspaceRowHeight( - fontScale: CGFloat = 1, - titleLineCount: Int = 1, - descriptionLineCount: Int = 0, - hasSubtitle: Bool = false, - hasRemoteStatus: Bool = false, - metadataEntryCount: Int = 0, - metadataEntryIsExpanded: Bool = false, - metadataBlockLineCounts: [Int] = [], - hasMetadataBlockToggle: Bool = false, - hasLog: Bool = false, - hasProgress: Bool = false, - branchDirectoryRowCount: Int = 0, - pullRequestRowCount: Int = 0, - hasPorts: Bool = false - ) -> CGFloat { - SidebarWorkspaceRowDropMetrics.targetHeight( - fontScale: fontScale, - titleLineCount: titleLineCount, - descriptionLineCount: descriptionLineCount, - hasSubtitle: hasSubtitle, - hasRemoteStatus: hasRemoteStatus, - metadataEntryCount: metadataEntryCount, - metadataEntryIsExpanded: metadataEntryIsExpanded, - metadataBlockLineCounts: metadataBlockLineCounts, - hasMetadataBlockToggle: hasMetadataBlockToggle, - hasLog: hasLog, - hasProgress: hasProgress, - branchDirectoryRowCount: branchDirectoryRowCount, - pullRequestRowCount: pullRequestRowCount, - hasPorts: hasPorts - ) - } -} diff --git a/web/data/cmux.schema.json b/web/data/cmux.schema.json index 0632c84aa2a6..f3fef071371c 100644 --- a/web/data/cmux.schema.json +++ b/web/data/cmux.schema.json @@ -733,6 +733,12 @@ "type": "boolean", "default": true, "description": "Show custom metadata pills." + }, + "rightMaxWidth": { + "type": "number", + "exclusiveMinimum": 0, + "descriptionKey": "schemaDescriptions.sidebar.rightMaxWidth", + "description": "Maximum width in points for the right sidebar. When omitted, the built-in dynamic cap applies." } } }, diff --git a/web/messages/ar.json b/web/messages/ar.json index c8a8ca24cbc0..b912d3685885 100644 --- a/web/messages/ar.json +++ b/web/messages/ar.json @@ -793,6 +793,9 @@ "fontFamily": "Default body font family for newly opened markdown viewers. Leave empty for the system markdown font stack.", "maxWidth": "Default maximum reading column width, in CSS pixels, for newly opened markdown viewers." }, + "sidebar": { + "rightMaxWidth": "أقصى عرض بالنقاط للشريط الجانبي الأيمن. عند تركه فارغًا، يُطبّق الحد الديناميكي المضمن." + }, "fileExplorer": { "description": "إعدادات مستكشف الملفات (شجرة الملفات) في الشريط الجانبي الأيمن.", "doubleClickAction": "ما يفعله النقر المزدوج (أو الضغط على Return في نتيجة بحث) على ملف في مستكشف الملفات. يفتح `preview` معاينة ملفات cmux المضمنة (السلوك الافتراضي والتاريخي). يفتح `defaultEditor` الملف باستخدام تطبيق macOS الافتراضي لنوع الملف. يفتح `preferredEditor` باستخدام أمر `app.preferredEditor`، مع الرجوع إلى التطبيق الافتراضي عند عدم ضبطه. ينطبق ذلك على الملفات فقط؛ فالمجلدات تُوسَّع أو تُطوى دائمًا، ومستكشفات الملفات غير المحلية (البعيدة) تفتح دائمًا معاينة cmux." diff --git a/web/messages/bs.json b/web/messages/bs.json index 6f3dd610634c..c98e27e36a03 100644 --- a/web/messages/bs.json +++ b/web/messages/bs.json @@ -793,6 +793,9 @@ "fontFamily": "Default body font family for newly opened markdown viewers. Leave empty for the system markdown font stack.", "maxWidth": "Default maximum reading column width, in CSS pixels, for newly opened markdown viewers." }, + "sidebar": { + "rightMaxWidth": "Maksimalna širina desne bočne trake u tačkama. Ako se izostavi, primjenjuje se ugrađeno dinamičko ograničenje." + }, "fileExplorer": { "description": "Postavke istraživača datoteka (stabla datoteka) u desnoj bočnoj traci.", "doubleClickAction": "Šta se dešava kada dvaput kliknete (ili pritisnete Return na rezultatu pretrage) datoteku u istraživaču datoteka. `preview` otvara ugrađeni cmux pregled datoteke (zadano i dosadašnje ponašanje). `defaultEditor` otvara datoteku macOS zadanom aplikacijom za tu vrstu datoteke. `preferredEditor` otvara pomoću komande `app.preferredEditor`, uz povratak na zadanu aplikaciju kada nije postavljena. Primjenjuje se samo na datoteke; direktoriji se uvijek proširuju/skupljaju, a nelokalni (udaljeni) istraživači datoteka uvijek otvaraju cmux pregled." diff --git a/web/messages/da.json b/web/messages/da.json index cf273c61f97a..f7273550a4ce 100644 --- a/web/messages/da.json +++ b/web/messages/da.json @@ -793,6 +793,9 @@ "fontFamily": "Default body font family for newly opened markdown viewers. Leave empty for the system markdown font stack.", "maxWidth": "Default maximum reading column width, in CSS pixels, for newly opened markdown viewers." }, + "sidebar": { + "rightMaxWidth": "Maksimal bredde i punkter for højre sidepanel. Hvis den udelades, bruges den indbyggede dynamiske grænse." + }, "fileExplorer": { "description": "Indstillinger for filoversigten (filtræet) i højre sidepanel.", "doubleClickAction": "Bestemmer, hvad der sker, når du dobbeltklikker (eller trykker Return på et søgeresultat for) en fil i filoversigten. `preview` åbner den indbyggede cmux-filvisning (standard og historisk adfærd). `defaultEditor` åbner med macOS-standardappen for filtypen. `preferredEditor` åbner med kommandoen `app.preferredEditor` og falder tilbage til standardappen, når den ikke er sat. Gælder kun filer; mapper udvides/skjules altid, og ikke-lokale (eksterne) filoversigter åbner altid cmux-forhåndsvisningen." diff --git a/web/messages/de.json b/web/messages/de.json index a36b30a36397..c276ff089474 100644 --- a/web/messages/de.json +++ b/web/messages/de.json @@ -793,6 +793,9 @@ "fontFamily": "Default body font family for newly opened markdown viewers. Leave empty for the system markdown font stack.", "maxWidth": "Default maximum reading column width, in CSS pixels, for newly opened markdown viewers." }, + "sidebar": { + "rightMaxWidth": "Maximale Breite der rechten Seitenleiste in Punkten. Wenn weggelassen, gilt die integrierte dynamische Obergrenze." + }, "fileExplorer": { "description": "Einstellungen für den Datei-Explorer (Dateibaum) in der rechten Seitenleiste.", "doubleClickAction": "Legt fest, was ein Doppelklick auf eine Datei im Dateiexplorer bewirkt (oder Return bei einem Suchergebnis). `preview` öffnet die integrierte cmux-Dateivorschau (Standard und bisheriges Verhalten). `defaultEditor` öffnet die Datei mit der macOS-Standard-App für diesen Dateityp. `preferredEditor` öffnet mit dem Befehl `app.preferredEditor` und fällt auf die Standard-App zurück, wenn keiner gesetzt ist. Gilt nur für Dateien; Verzeichnisse werden immer ein-/ausgeklappt, und nicht-lokale (Remote-)Dateiexplorer öffnen immer die cmux-Vorschau." diff --git a/web/messages/en.json b/web/messages/en.json index febd2db9d82d..1acf8ab77b5b 100644 --- a/web/messages/en.json +++ b/web/messages/en.json @@ -901,6 +901,9 @@ "fileEditor": { "wordWrap": "Wrap long lines at the editor's right edge instead of scrolling horizontally." }, + "sidebar": { + "rightMaxWidth": "Maximum width in points for the right sidebar. When omitted, the built-in dynamic cap applies." + }, "fileExplorer": { "description": "Right-sidebar file explorer (file tree) settings.", "doubleClickAction": "What double-clicking (or pressing Return on a search result for) a file in the file explorer does. `preview` opens the built-in cmux file preview (the default and historical behavior). `defaultEditor` opens with the macOS default app for the file type. `preferredEditor` opens with the `app.preferredEditor` command, falling back to the default app when none is set. Only applies to files; directories always expand/collapse, and non-local (remote) file explorers always open the cmux preview." diff --git a/web/messages/es.json b/web/messages/es.json index 2a99024addc5..bd8f41c17149 100644 --- a/web/messages/es.json +++ b/web/messages/es.json @@ -793,6 +793,9 @@ "fontFamily": "Default body font family for newly opened markdown viewers. Leave empty for the system markdown font stack.", "maxWidth": "Default maximum reading column width, in CSS pixels, for newly opened markdown viewers." }, + "sidebar": { + "rightMaxWidth": "Ancho máximo en puntos para la barra lateral derecha. Si se omite, se aplica el límite dinámico integrado." + }, "fileExplorer": { "description": "Ajustes del explorador de archivos (árbol de archivos) de la barra lateral derecha.", "doubleClickAction": "Qué hace al hacer doble clic (o presionar Return en un resultado de búsqueda) sobre un archivo en el explorador de archivos. `preview` abre la vista previa integrada de archivos de cmux (comportamiento predeterminado e histórico). `defaultEditor` abre con la app predeterminada de macOS para ese tipo de archivo. `preferredEditor` abre con el comando `app.preferredEditor`, con fallback a la app predeterminada cuando no está configurado. Solo aplica a archivos; los directorios siempre se expanden/contraen, y los exploradores de archivos no locales (remotos) siempre abren la vista previa de cmux." diff --git a/web/messages/fr.json b/web/messages/fr.json index 5936b68d4102..dc41c186886a 100644 --- a/web/messages/fr.json +++ b/web/messages/fr.json @@ -793,6 +793,9 @@ "fontFamily": "Default body font family for newly opened markdown viewers. Leave empty for the system markdown font stack.", "maxWidth": "Default maximum reading column width, in CSS pixels, for newly opened markdown viewers." }, + "sidebar": { + "rightMaxWidth": "Largeur maximale en points de la barre latérale droite. Si elle est omise, la limite dynamique intégrée s’applique." + }, "fileExplorer": { "description": "Paramètres de l’explorateur de fichiers (arborescence) dans la barre latérale droite.", "doubleClickAction": "Ce que fait un double-clic (ou l’appui sur Return dans un résultat de recherche) sur un fichier dans l’explorateur de fichiers. `preview` ouvre l’aperçu de fichier intégré à cmux (comportement par défaut et historique). `defaultEditor` ouvre le fichier avec l’app macOS par défaut pour ce type de fichier. `preferredEditor` ouvre avec la commande `app.preferredEditor`, avec repli vers l’app par défaut lorsqu’aucune commande n’est définie. S’applique uniquement aux fichiers ; les dossiers se développent/se replient toujours, et les explorateurs de fichiers non locaux (distants) ouvrent toujours l’aperçu cmux." diff --git a/web/messages/it.json b/web/messages/it.json index 361ef2fc0952..1ada3c4bf23f 100644 --- a/web/messages/it.json +++ b/web/messages/it.json @@ -793,6 +793,9 @@ "fontFamily": "Default body font family for newly opened markdown viewers. Leave empty for the system markdown font stack.", "maxWidth": "Default maximum reading column width, in CSS pixels, for newly opened markdown viewers." }, + "sidebar": { + "rightMaxWidth": "Larghezza massima in punti della barra laterale destra. Se omessa, si applica il limite dinamico integrato." + }, "fileExplorer": { "description": "Impostazioni dell’esplora file (albero dei file) nella barra laterale destra.", "doubleClickAction": "Definisce cosa succede quando fai doppio clic (o premi Return su un risultato di ricerca) su un file nell’esplora file. `preview` apre l’anteprima file integrata di cmux (comportamento predefinito e storico). `defaultEditor` apre il file con l’app macOS predefinita per quel tipo di file. `preferredEditor` apre con il comando `app.preferredEditor`, ricadendo sull’app predefinita quando non è impostato. Si applica solo ai file; le cartelle si espandono/chiudono sempre e gli esplora file non locali (remoti) aprono sempre l’anteprima di cmux." diff --git a/web/messages/ja.json b/web/messages/ja.json index 7a4bb8ecab23..4488e0f4d838 100644 --- a/web/messages/ja.json +++ b/web/messages/ja.json @@ -860,6 +860,9 @@ "fileEditor": { "wordWrap": "横スクロールする代わりに、エディタの右端で長い行を折り返します。" }, + "sidebar": { + "rightMaxWidth": "右サイドバーの最大幅(ポイント)。省略すると、組み込みの動的上限が適用されます。" + }, "fileExplorer": { "description": "右サイドバーのファイルエクスプローラー(ファイルツリー)設定。", "doubleClickAction": "ファイルエクスプローラーでファイルをダブルクリックした(または検索結果で Return を押した)ときの動作です。`preview` は組み込みの cmux ファイルプレビューを開きます(既定かつ従来の動作)。`defaultEditor` はそのファイル形式の macOS 既定アプリで開きます。`preferredEditor` は `app.preferredEditor` コマンドで開き、未設定の場合は既定アプリにフォールバックします。ファイルにのみ適用され、ディレクトリは常に展開・折りたたみされ、非ローカル(リモート)のファイルエクスプローラーは常に cmux プレビューを開きます。" diff --git a/web/messages/km.json b/web/messages/km.json index d58ed7ba6795..d6f01d7d727f 100644 --- a/web/messages/km.json +++ b/web/messages/km.json @@ -793,6 +793,9 @@ "fontFamily": "Default body font family for newly opened markdown viewers. Leave empty for the system markdown font stack.", "maxWidth": "Default maximum reading column width, in CSS pixels, for newly opened markdown viewers." }, + "sidebar": { + "rightMaxWidth": "ទទឹងអតិបរមាជាពិន្ទុសម្រាប់របារចំហៀងខាងស្តាំ។ បើមិនបញ្ជាក់ នឹងប្រើដែនកំណត់ថាមវន្តដែលមានស្រាប់។" + }, "fileExplorer": { "description": "ការកំណត់កម្មវិធីរុករកឯកសារ (មែកធាងឯកសារ) នៅក្នុងរបារចំហៀងស្តាំ។", "doubleClickAction": "កំណត់អ្វីដែលកើតឡើងពេលចុចពីរដង (ឬចុច Return លើលទ្ធផលស្វែងរក) លើឯកសារនៅក្នុងកម្មវិធីរុករកឯកសារ។ `preview` បើកការមើលជាមុនឯកសារ cmux ដែលភ្ជាប់មកជាមួយ (ជាលំនាំដើម និងជាឥរិយាបថចាស់)។ `defaultEditor` បើកដោយប្រើកម្មវិធីលំនាំដើមរបស់ macOS សម្រាប់ប្រភេទឯកសារនោះ។ `preferredEditor` បើកដោយប្រើពាក្យបញ្ជា `app.preferredEditor` ហើយត្រឡប់ទៅកម្មវិធីលំនាំដើម ប្រសិនបើមិនបានកំណត់។ អនុវត្តតែលើឯកសារ; ថតតែងតែពង្រីក/បង្រួម ហើយកម្មវិធីរុករកឯកសារដែលមិនមែនក្នុងមូលដ្ឋាន (ពីចម្ងាយ) តែងតែបើកការមើលជាមុនរបស់ cmux។" diff --git a/web/messages/ko.json b/web/messages/ko.json index 932d0b3231ad..a113dd2277e1 100644 --- a/web/messages/ko.json +++ b/web/messages/ko.json @@ -793,6 +793,9 @@ "fontFamily": "Default body font family for newly opened markdown viewers. Leave empty for the system markdown font stack.", "maxWidth": "Default maximum reading column width, in CSS pixels, for newly opened markdown viewers." }, + "sidebar": { + "rightMaxWidth": "오른쪽 사이드바의 최대 너비(포인트). 생략하면 내장 동적 상한이 적용됩니다." + }, "fileExplorer": { "description": "오른쪽 사이드바 파일 탐색기(파일 트리) 설정입니다.", "doubleClickAction": "파일 탐색기에서 파일을 더블클릭(또는 검색 결과에서 Return 키 입력)했을 때의 동작을 설정합니다. `preview`는 cmux 내장 파일 미리보기(기본값이자 기존 동작)를 엽니다. `defaultEditor`는 파일 형식의 macOS 기본 앱으로 엽니다. `preferredEditor`는 `app.preferredEditor` 명령으로 열고, 설정되지 않은 경우 기본 앱으로 대체합니다. 이 설정은 파일에만 적용되며, 디렉터리는 항상 펼치기/접기 동작을 하고, 로컬이 아닌(원격) 파일 탐색기는 항상 cmux 미리보기를 엽니다." diff --git a/web/messages/no.json b/web/messages/no.json index ed4d17609a28..ad3be5a37204 100644 --- a/web/messages/no.json +++ b/web/messages/no.json @@ -793,6 +793,9 @@ "fontFamily": "Default body font family for newly opened markdown viewers. Leave empty for the system markdown font stack.", "maxWidth": "Default maximum reading column width, in CSS pixels, for newly opened markdown viewers." }, + "sidebar": { + "rightMaxWidth": "Maksimal bredde i punkter for høyre sidepanel. Hvis den utelates, brukes den innebygde dynamiske grensen." + }, "fileExplorer": { "description": "Innstillinger for filutforskeren (filtreet) i høyre sidepanel.", "doubleClickAction": "Bestemmer hva som skjer når du dobbeltklikker (eller trykker Return på et søkeresultat for) en fil i filutforskeren. `preview` åpner den innebygde cmux-filforhåndsvisningen (standard og historisk oppførsel). `defaultEditor` åpner med macOS-standardappen for filtypen. `preferredEditor` åpner med kommandoen `app.preferredEditor`, med fallback til standardappen når ingen er satt. Gjelder bare filer; mapper utvides/skjules alltid, og ikke-lokale (eksterne) filutforskere åpner alltid cmux-forhåndsvisningen." diff --git a/web/messages/pl.json b/web/messages/pl.json index 433fdc35da97..0f8c8dcef1ca 100644 --- a/web/messages/pl.json +++ b/web/messages/pl.json @@ -793,6 +793,9 @@ "fontFamily": "Default body font family for newly opened markdown viewers. Leave empty for the system markdown font stack.", "maxWidth": "Default maximum reading column width, in CSS pixels, for newly opened markdown viewers." }, + "sidebar": { + "rightMaxWidth": "Maksymalna szerokość prawego paska bocznego w punktach. Jeśli pominięta, obowiązuje wbudowany limit dynamiczny." + }, "fileExplorer": { "description": "Ustawienia eksploratora plików (drzewa plików) w prawym pasku bocznym.", "doubleClickAction": "Określa, co robi dwukrotne kliknięcie pliku w eksploratorze plików (albo naciśnięcie Return na wyniku wyszukiwania). `preview` otwiera wbudowany podgląd pliku cmux (domyślne i dotychczasowe zachowanie). `defaultEditor` otwiera plik w domyślnej aplikacji macOS dla tego typu pliku. `preferredEditor` otwiera za pomocą polecenia `app.preferredEditor`, a gdy nie jest ono ustawione, wraca do domyślnej aplikacji. Dotyczy tylko plików; katalogi zawsze się rozwijają/zwijają, a nielokalne (zdalne) eksploratory plików zawsze otwierają podgląd cmux." diff --git a/web/messages/pt-BR.json b/web/messages/pt-BR.json index 8de9074c7f64..daf0dc8ed7be 100644 --- a/web/messages/pt-BR.json +++ b/web/messages/pt-BR.json @@ -793,6 +793,9 @@ "fontFamily": "Default body font family for newly opened markdown viewers. Leave empty for the system markdown font stack.", "maxWidth": "Default maximum reading column width, in CSS pixels, for newly opened markdown viewers." }, + "sidebar": { + "rightMaxWidth": "Largura máxima em pontos para a barra lateral direita. Quando omitida, o limite dinâmico integrado se aplica." + }, "fileExplorer": { "description": "Configurações do explorador de arquivos (árvore de arquivos) na barra lateral direita.", "doubleClickAction": "Define o que acontece ao clicar duas vezes (ou pressionar Return em um resultado de busca) em um arquivo no explorador de arquivos. `preview` abre a visualização de arquivo integrada do cmux (comportamento padrão e histórico). `defaultEditor` abre com o app padrão do macOS para o tipo de arquivo. `preferredEditor` abre com o comando `app.preferredEditor`, voltando para o app padrão quando nenhum estiver definido. Aplica-se apenas a arquivos; diretórios sempre expandem/recolhem, e exploradores de arquivos não locais (remotos) sempre abrem a visualização do cmux." diff --git a/web/messages/ru.json b/web/messages/ru.json index 0328347d61a9..5425c93d2fa2 100644 --- a/web/messages/ru.json +++ b/web/messages/ru.json @@ -793,6 +793,9 @@ "fontFamily": "Default body font family for newly opened markdown viewers. Leave empty for the system markdown font stack.", "maxWidth": "Default maximum reading column width, in CSS pixels, for newly opened markdown viewers." }, + "sidebar": { + "rightMaxWidth": "Максимальная ширина правой боковой панели в пунктах. Если не указано, применяется встроенное динамическое ограничение." + }, "fileExplorer": { "description": "Настройки проводника файлов (дерева файлов) в правой боковой панели.", "doubleClickAction": "Что происходит при двойном щелчке по файлу в проводнике файлов (или при нажатии Return на результате поиска). `preview` открывает встроенный предпросмотр файлов cmux (поведение по умолчанию и историческое поведение). `defaultEditor` открывает файл в приложении macOS по умолчанию для этого типа файла. `preferredEditor` открывает через команду `app.preferredEditor`, а если она не задана, использует приложение по умолчанию. Применяется только к файлам; каталоги всегда разворачиваются/сворачиваются, а нелокальные (удалённые) проводники файлов всегда открывают предпросмотр cmux." diff --git a/web/messages/th.json b/web/messages/th.json index 957c80676106..33bab0813de1 100644 --- a/web/messages/th.json +++ b/web/messages/th.json @@ -793,6 +793,9 @@ "fontFamily": "Default body font family for newly opened markdown viewers. Leave empty for the system markdown font stack.", "maxWidth": "Default maximum reading column width, in CSS pixels, for newly opened markdown viewers." }, + "sidebar": { + "rightMaxWidth": "ความกว้างสูงสุดเป็นพอยต์สำหรับแถบด้านขวา หากเว้นไว้ จะใช้ขีดจำกัดแบบไดนามิกในตัว" + }, "fileExplorer": { "description": "การตั้งค่าตัวสำรวจไฟล์ (แผนผังไฟล์) ในแถบด้านขวา", "doubleClickAction": "กำหนดว่าการดับเบิลคลิกไฟล์ในตัวสำรวจไฟล์ (หรือกด Return บนผลการค้นหา) จะทำอะไร `preview` เปิดตัวอย่างไฟล์ในตัวของ cmux (ค่าเริ่มต้นและพฤติกรรมเดิม) `defaultEditor` เปิดด้วยแอปเริ่มต้นของ macOS สำหรับชนิดไฟล์นั้น `preferredEditor` เปิดด้วยคำสั่ง `app.preferredEditor` และถ้าไม่ได้ตั้งค่าไว้จะกลับไปใช้แอปเริ่มต้น มีผลกับไฟล์เท่านั้น; ไดเรกทอรีจะขยาย/ยุบเสมอ และตัวสำรวจไฟล์ที่ไม่ใช่ในเครื่อง (รีโมต) จะเปิดตัวอย่างของ cmux เสมอ" diff --git a/web/messages/tr.json b/web/messages/tr.json index 7da692a8d72b..e98e24389d19 100644 --- a/web/messages/tr.json +++ b/web/messages/tr.json @@ -793,6 +793,9 @@ "fontFamily": "Default body font family for newly opened markdown viewers. Leave empty for the system markdown font stack.", "maxWidth": "Default maximum reading column width, in CSS pixels, for newly opened markdown viewers." }, + "sidebar": { + "rightMaxWidth": "Sağ kenar çubuğu için puan cinsinden en fazla genişlik. Atlanırsa yerleşik dinamik sınır uygulanır." + }, "fileExplorer": { "description": "Sağ kenar çubuğundaki dosya gezgini (dosya ağacı) ayarları.", "doubleClickAction": "Dosya gezgininde bir dosyaya çift tıklamanın (veya bir arama sonucunda Return tuşuna basmanın) ne yapacağını belirler. `preview`, yerleşik cmux dosya önizlemesini açar (varsayılan ve geçmiş davranış). `defaultEditor`, dosya türü için macOS varsayılan uygulamasıyla açar. `preferredEditor`, `app.preferredEditor` komutuyla açar; ayarlı değilse varsayılan uygulamaya geri döner. Yalnızca dosyalar için geçerlidir; dizinler her zaman genişletilir/daraltılır ve yerel olmayan (uzak) dosya gezginleri her zaman cmux önizlemesini açar." diff --git a/web/messages/uk.json b/web/messages/uk.json index 6b0a6e35b575..8c37cf824852 100644 --- a/web/messages/uk.json +++ b/web/messages/uk.json @@ -793,6 +793,9 @@ "fontFamily": "Default body font family for newly opened markdown viewers. Leave empty for the system markdown font stack.", "maxWidth": "Default maximum reading column width, in CSS pixels, for newly opened markdown viewers." }, + "sidebar": { + "rightMaxWidth": "Максимальна ширина правої бічної панелі в пунктах. Якщо пропущено, застосовується вбудоване динамічне обмеження." + }, "fileExplorer": { "description": "Налаштування провідника файлів (дерева файлів) у правій бічній панелі.", "doubleClickAction": "Визначає, що відбувається під час подвійного клацання файлу в провіднику файлів (або натискання Return на результаті пошуку). `preview` відкриває вбудований перегляд файлів cmux (типова й історична поведінка). `defaultEditor` відкриває файл у типовій програмі macOS для цього типу файлів. `preferredEditor` відкриває через команду `app.preferredEditor`, а якщо її не задано, повертається до типової програми. Застосовується лише до файлів; каталоги завжди розгортаються/згортаються, а нелокальні (віддалені) провідники файлів завжди відкривають перегляд cmux." diff --git a/web/messages/zh-CN.json b/web/messages/zh-CN.json index b1e0fb10d32f..e4bb6b05bbab 100644 --- a/web/messages/zh-CN.json +++ b/web/messages/zh-CN.json @@ -793,6 +793,9 @@ "fontFamily": "Default body font family for newly opened markdown viewers. Leave empty for the system markdown font stack.", "maxWidth": "Default maximum reading column width, in CSS pixels, for newly opened markdown viewers." }, + "sidebar": { + "rightMaxWidth": "右侧边栏的最大宽度(点)。省略时使用内置动态上限。" + }, "fileExplorer": { "description": "右侧边栏文件浏览器(文件树)设置。", "doubleClickAction": "设置在文件浏览器中双击文件(或在搜索结果上按 Return)时执行的操作。`preview` 会打开 cmux 内置文件预览(默认和历史行为)。`defaultEditor` 会使用该文件类型的 macOS 默认应用打开。`preferredEditor` 会使用 `app.preferredEditor` 命令打开;未设置时回退到默认应用。仅适用于文件;目录始终展开/折叠,非本地(远程)文件浏览器始终打开 cmux 预览。" diff --git a/web/messages/zh-TW.json b/web/messages/zh-TW.json index e3d6933a6e1d..4c2f2de884b7 100644 --- a/web/messages/zh-TW.json +++ b/web/messages/zh-TW.json @@ -793,6 +793,9 @@ "fontFamily": "Default body font family for newly opened markdown viewers. Leave empty for the system markdown font stack.", "maxWidth": "Default maximum reading column width, in CSS pixels, for newly opened markdown viewers." }, + "sidebar": { + "rightMaxWidth": "右側邊欄的最大寬度(點)。省略時使用內建動態上限。" + }, "fileExplorer": { "description": "右側邊欄檔案瀏覽器(檔案樹)設定。", "doubleClickAction": "設定在檔案瀏覽器中按兩下檔案(或在搜尋結果上按 Return)時要執行的動作。`preview` 會開啟 cmux 內建檔案預覽(預設與既有行為)。`defaultEditor` 會使用該檔案類型的 macOS 預設 App 開啟。`preferredEditor` 會使用 `app.preferredEditor` 命令開啟;未設定時會退回預設 App。僅適用於檔案;目錄一律展開/收合,非本機(遠端)檔案瀏覽器一律開啟 cmux 預覽。"