diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings index b7c73485..54aa5c85 100644 --- a/Resources/Localizable.xcstrings +++ b/Resources/Localizable.xcstrings @@ -41436,6 +41436,57 @@ } } }, + "settings.app.hideAllSidebarDetails": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Hide All Sidebar Details" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "サイドバーの詳細をすべて隠す" + } + } + } + }, + "settings.app.hideAllSidebarDetails.subtitleOff": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Show secondary workspace details as controlled by the toggles below." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "下のトグル設定に従って、ワークスペースの補助情報を表示します。" + } + } + } + }, + "settings.app.hideAllSidebarDetails.subtitleOn": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Show only the workspace title row. Overrides the detail toggles below." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ワークスペースのタイトル行だけを表示します。下の詳細トグルより優先されます。" + } + } + } + }, "settings.app.renameSelectsName": { "extractionState": "manual", "localizations": { @@ -42679,6 +42730,40 @@ } } }, + "settings.app.showNotificationMessage": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Show Notification Message in Sidebar" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "サイドバーに通知メッセージを表示" + } + } + } + }, + "settings.app.showNotificationMessage.subtitle": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Display the latest notification message below the workspace title." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ワークスペース名の下に最新の通知メッセージを表示します。" + } + } + } + }, "settings.app.showPorts": { "extractionState": "manual", "localizations": { diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index 4f3c0725..d72a1e5d 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -7162,11 +7162,22 @@ struct VerticalTabsSidebar: View { @StateObject private var dragFailsafeMonitor = SidebarDragFailsafeMonitor() @State private var draggedTabId: UUID? @State private var dropIndicator: SidebarDropIndicator? + @AppStorage(SidebarWorkspaceDetailSettings.hideAllDetailsKey) + private var sidebarHideAllDetails = SidebarWorkspaceDetailSettings.defaultHideAllDetails + @AppStorage(SidebarWorkspaceDetailSettings.showNotificationMessageKey) + private var sidebarShowNotificationMessage = SidebarWorkspaceDetailSettings.defaultShowNotificationMessage /// Space at top of sidebar for traffic light buttons private let trafficLightPadding: CGFloat = 28 private let tabRowSpacing: CGFloat = 2 + private var showsSidebarNotificationMessage: Bool { + SidebarWorkspaceDetailSettings.resolvedNotificationMessageVisibility( + showNotificationMessage: sidebarShowNotificationMessage, + hideAllDetails: sidebarHideAllDetails + ) + } + var body: some View { VStack(spacing: 0) { GeometryReader { proxy in @@ -7187,7 +7198,10 @@ struct VerticalTabsSidebar: View { tabCount: tabManager.tabs.count, unreadCount: notificationStore.unreadCount(forTabId: tab.id), latestNotificationText: { - guard let notification = notificationStore.latestNotification(forTabId: tab.id) else { return nil } + guard showsSidebarNotificationMessage, + let notification = notificationStore.latestNotification(forTabId: tab.id) else { + return nil + } let text = notification.body.isEmpty ? notification.title : notification.body let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines) return trimmed.isEmpty ? nil : trimmed @@ -9493,6 +9507,8 @@ private struct TabItemView: View, Equatable { @AppStorage("sidebarShowLog") private var sidebarShowLog = true @AppStorage("sidebarShowProgress") private var sidebarShowProgress = true @AppStorage("sidebarShowStatusPills") private var sidebarShowMetadata = true + @AppStorage(SidebarWorkspaceDetailSettings.hideAllDetailsKey) + private var sidebarHideAllDetails = SidebarWorkspaceDetailSettings.defaultHideAllDetails @AppStorage(SidebarActiveTabIndicatorSettings.styleKey) private var activeTabIndicatorStyleRaw = SidebarActiveTabIndicatorSettings.defaultStyle.rawValue @@ -9591,17 +9607,30 @@ private struct TabItemView: View, Equatable { ) } + private var visibleAuxiliaryDetails: SidebarWorkspaceAuxiliaryDetailVisibility { + SidebarWorkspaceAuxiliaryDetailVisibility.resolved( + showMetadata: sidebarShowMetadata, + showLog: sidebarShowLog, + showProgress: sidebarShowProgress, + showBranchDirectory: sidebarShowBranchDirectory, + showPullRequests: sidebarShowPullRequest, + showPorts: sidebarShowPorts, + hideAllDetails: sidebarHideAllDetails + ) + } + var body: some View { let closeWorkspaceTooltip = String(localized: "sidebar.closeWorkspace.tooltip", defaultValue: "Close Workspace") let accessibilityHintText = String(localized: "sidebar.workspace.accessibilityHint", defaultValue: "Activate to focus this workspace. Drag to reorder, or use Move Up and Move Down actions.") let moveUpActionText = String(localized: "sidebar.workspace.moveUpAction", defaultValue: "Move Up") let moveDownActionText = String(localized: "sidebar.workspace.moveDownAction", defaultValue: "Move Down") let latestNotificationSubtitle = latestNotificationText - let orderedPanelIds: [UUID]? = (sidebarShowBranchDirectory || sidebarShowPullRequest) + let detailVisibility = visibleAuxiliaryDetails + let orderedPanelIds: [UUID]? = (detailVisibility.showsBranchDirectory || detailVisibility.showsPullRequests) ? tab.sidebarOrderedPanelIds() : nil let compactGitBranchSummaryText: String? = { - guard sidebarShowBranchDirectory, + guard detailVisibility.showsBranchDirectory, !sidebarBranchVerticalLayout, sidebarShowGitBranch, let orderedPanelIds else { @@ -9610,7 +9639,7 @@ private struct TabItemView: View, Equatable { return gitBranchSummaryText(orderedPanelIds: orderedPanelIds) }() let compactDirectorySummaryText: String? = { - guard sidebarShowBranchDirectory, + guard detailVisibility.showsBranchDirectory, !sidebarBranchVerticalLayout, let orderedPanelIds else { return nil @@ -9622,7 +9651,7 @@ private struct TabItemView: View, Equatable { directorySummary: compactDirectorySummaryText ) let branchDirectoryLines: [VerticalBranchDirectoryLine] = { - guard sidebarShowBranchDirectory, + guard detailVisibility.showsBranchDirectory, sidebarBranchVerticalLayout, let orderedPanelIds else { return [] @@ -9631,7 +9660,7 @@ private struct TabItemView: View, Equatable { }() let branchLinesContainBranch = sidebarShowGitBranch && branchDirectoryLines.contains { $0.branch != nil } let pullRequestRows: [PullRequestDisplay] = { - guard sidebarShowPullRequest, let orderedPanelIds else { return [] } + guard detailVisibility.showsPullRequests, let orderedPanelIds else { return [] } return pullRequestDisplays(orderedPanelIds: orderedPanelIds) }() @@ -9709,7 +9738,7 @@ private struct TabItemView: View, Equatable { .multilineTextAlignment(.leading) } - if sidebarShowMetadata { + if detailVisibility.showsMetadata { let metadataEntries = tab.sidebarStatusEntriesInDisplayOrder() let metadataBlocks = tab.sidebarMetadataBlocksInDisplayOrder() if !metadataEntries.isEmpty { @@ -9731,7 +9760,7 @@ private struct TabItemView: View, Equatable { } // Latest log entry - if sidebarShowLog, let latestLog = tab.logEntries.last { + if detailVisibility.showsLog, let latestLog = tab.logEntries.last { HStack(spacing: 4) { Image(systemName: logLevelIcon(latestLog.level)) .font(.system(size: 8)) @@ -9746,7 +9775,7 @@ private struct TabItemView: View, Equatable { } // Progress bar - if sidebarShowProgress, let progress = tab.progress { + if detailVisibility.showsProgress, let progress = tab.progress { VStack(alignment: .leading, spacing: 2) { GeometryReader { geo in ZStack(alignment: .leading) { @@ -9770,7 +9799,7 @@ private struct TabItemView: View, Equatable { } // Branch + directory row - if sidebarShowBranchDirectory { + if detailVisibility.showsBranchDirectory { if sidebarBranchVerticalLayout { if !branchDirectoryLines.isEmpty { HStack(alignment: .top, spacing: 3) { @@ -9824,7 +9853,7 @@ private struct TabItemView: View, Equatable { } // Pull request rows - if sidebarShowPullRequest, !pullRequestRows.isEmpty { + if detailVisibility.showsPullRequests, !pullRequestRows.isEmpty { VStack(alignment: .leading, spacing: 1) { ForEach(pullRequestRows) { pullRequest in Button(action: { @@ -9853,7 +9882,7 @@ private struct TabItemView: View, Equatable { } // Ports row - if sidebarShowPorts, !tab.listeningPorts.isEmpty { + if detailVisibility.showsPorts, !tab.listeningPorts.isEmpty { Text(tab.listeningPorts.map { ":\($0)" }.joined(separator: ", ")) .font(.system(size: 10, design: .monospaced)) .foregroundColor(activeSecondaryColor(0.75)) diff --git a/Sources/TabManager.swift b/Sources/TabManager.swift index 764b15ce..ea1cda35 100644 --- a/Sources/TabManager.swift +++ b/Sources/TabManager.swift @@ -63,6 +63,72 @@ enum SidebarBranchLayoutSettings { } } +enum SidebarWorkspaceDetailSettings { + static let hideAllDetailsKey = "sidebarHideAllDetails" + static let showNotificationMessageKey = "sidebarShowNotificationMessage" + static let defaultHideAllDetails = false + static let defaultShowNotificationMessage = true + + static func hidesAllDetails(defaults: UserDefaults = .standard) -> Bool { + if defaults.object(forKey: hideAllDetailsKey) == nil { + return defaultHideAllDetails + } + return defaults.bool(forKey: hideAllDetailsKey) + } + + static func showsNotificationMessage(defaults: UserDefaults = .standard) -> Bool { + if defaults.object(forKey: showNotificationMessageKey) == nil { + return defaultShowNotificationMessage + } + return defaults.bool(forKey: showNotificationMessageKey) + } + + static func resolvedNotificationMessageVisibility( + showNotificationMessage: Bool, + hideAllDetails: Bool + ) -> Bool { + showNotificationMessage && !hideAllDetails + } +} + +struct SidebarWorkspaceAuxiliaryDetailVisibility: Equatable { + let showsMetadata: Bool + let showsLog: Bool + let showsProgress: Bool + let showsBranchDirectory: Bool + let showsPullRequests: Bool + let showsPorts: Bool + + static let hidden = Self( + showsMetadata: false, + showsLog: false, + showsProgress: false, + showsBranchDirectory: false, + showsPullRequests: false, + showsPorts: false + ) + + static func resolved( + showMetadata: Bool, + showLog: Bool, + showProgress: Bool, + showBranchDirectory: Bool, + showPullRequests: Bool, + showPorts: Bool, + hideAllDetails: Bool + ) -> Self { + guard !hideAllDetails else { return .hidden } + return Self( + showsMetadata: showMetadata, + showsLog: showLog, + showsProgress: showProgress, + showsBranchDirectory: showBranchDirectory, + showsPullRequests: showPullRequests, + showsPorts: showPorts + ) + } +} + enum SidebarActiveTabIndicatorStyle: String, CaseIterable, Identifiable { case leftRail case solidFill diff --git a/Sources/cmuxApp.swift b/Sources/cmuxApp.swift index e2c65a34..96703df9 100644 --- a/Sources/cmuxApp.swift +++ b/Sources/cmuxApp.swift @@ -3077,6 +3077,10 @@ struct SettingsView: View { private var alwaysShowShortcutHints = ShortcutHintDebugSettings.defaultAlwaysShowHints @AppStorage(WorkspacePlacementSettings.placementKey) private var newWorkspacePlacement = WorkspacePlacementSettings.defaultPlacement.rawValue @AppStorage(WorkspaceAutoReorderSettings.key) private var workspaceAutoReorder = WorkspaceAutoReorderSettings.defaultValue + @AppStorage(SidebarWorkspaceDetailSettings.hideAllDetailsKey) + private var sidebarHideAllDetails = SidebarWorkspaceDetailSettings.defaultHideAllDetails + @AppStorage(SidebarWorkspaceDetailSettings.showNotificationMessageKey) + private var sidebarShowNotificationMessage = SidebarWorkspaceDetailSettings.defaultShowNotificationMessage @AppStorage(SidebarBranchLayoutSettings.key) private var sidebarBranchVerticalLayout = SidebarBranchLayoutSettings.defaultVerticalLayout @AppStorage(SidebarActiveTabIndicatorSettings.styleKey) private var sidebarActiveTabIndicatorStyle = SidebarActiveTabIndicatorSettings.defaultStyle.rawValue @@ -3648,6 +3652,19 @@ struct SettingsView: View { SettingsCardDivider() + SettingsCardRow( + String(localized: "settings.app.hideAllSidebarDetails", defaultValue: "Hide All Sidebar Details"), + subtitle: sidebarHideAllDetails + ? String(localized: "settings.app.hideAllSidebarDetails.subtitleOn", defaultValue: "Show only the workspace title row. Overrides the detail toggles below.") + : String(localized: "settings.app.hideAllSidebarDetails.subtitleOff", defaultValue: "Show secondary workspace details as controlled by the toggles below.") + ) { + Toggle("", isOn: $sidebarHideAllDetails) + .labelsHidden() + .controlSize(.small) + } + + SettingsCardDivider() + SettingsPickerRow( String(localized: "settings.app.sidebarBranchLayout", defaultValue: "Sidebar Branch Layout"), subtitle: sidebarBranchVerticalLayout @@ -3659,6 +3676,19 @@ struct SettingsView: View { Text(String(localized: "settings.app.sidebarBranchLayout.vertical", defaultValue: "Vertical")).tag(true) Text(String(localized: "settings.app.sidebarBranchLayout.inline", defaultValue: "Inline")).tag(false) } + .disabled(sidebarHideAllDetails) + + SettingsCardDivider() + + SettingsCardRow( + String(localized: "settings.app.showNotificationMessage", defaultValue: "Show Notification Message in Sidebar"), + subtitle: String(localized: "settings.app.showNotificationMessage.subtitle", defaultValue: "Display the latest notification message below the workspace title.") + ) { + Toggle("", isOn: $sidebarShowNotificationMessage) + .labelsHidden() + .controlSize(.small) + } + .disabled(sidebarHideAllDetails) SettingsCardDivider() @@ -3670,6 +3700,7 @@ struct SettingsView: View { .labelsHidden() .controlSize(.small) } + .disabled(sidebarHideAllDetails) SettingsCardDivider() @@ -3681,6 +3712,7 @@ struct SettingsView: View { .labelsHidden() .controlSize(.small) } + .disabled(sidebarHideAllDetails) SettingsCardDivider() @@ -3694,6 +3726,7 @@ struct SettingsView: View { .labelsHidden() .controlSize(.small) } + .disabled(sidebarHideAllDetails) SettingsCardDivider() @@ -3705,6 +3738,7 @@ struct SettingsView: View { .labelsHidden() .controlSize(.small) } + .disabled(sidebarHideAllDetails) SettingsCardDivider() @@ -3716,6 +3750,7 @@ struct SettingsView: View { .labelsHidden() .controlSize(.small) } + .disabled(sidebarHideAllDetails) SettingsCardDivider() @@ -3727,6 +3762,7 @@ struct SettingsView: View { .labelsHidden() .controlSize(.small) } + .disabled(sidebarHideAllDetails) SettingsCardDivider() @@ -3738,6 +3774,7 @@ struct SettingsView: View { .labelsHidden() .controlSize(.small) } + .disabled(sidebarHideAllDetails) } SettingsSectionHeader(title: String(localized: "settings.section.workspaceColors", defaultValue: "Workspace Colors")) @@ -4376,6 +4413,8 @@ struct SettingsView: View { alwaysShowShortcutHints = ShortcutHintDebugSettings.defaultAlwaysShowHints newWorkspacePlacement = WorkspacePlacementSettings.defaultPlacement.rawValue workspaceAutoReorder = WorkspaceAutoReorderSettings.defaultValue + sidebarHideAllDetails = SidebarWorkspaceDetailSettings.defaultHideAllDetails + sidebarShowNotificationMessage = SidebarWorkspaceDetailSettings.defaultShowNotificationMessage sidebarBranchVerticalLayout = SidebarBranchLayoutSettings.defaultVerticalLayout sidebarActiveTabIndicatorStyle = SidebarActiveTabIndicatorSettings.defaultStyle.rawValue sidebarShowBranchDirectory = true diff --git a/cmuxTests/CmuxWebViewKeyEquivalentTests.swift b/cmuxTests/CmuxWebViewKeyEquivalentTests.swift index 580466bd..4fbe260e 100644 --- a/cmuxTests/CmuxWebViewKeyEquivalentTests.swift +++ b/cmuxTests/CmuxWebViewKeyEquivalentTests.swift @@ -4664,6 +4664,92 @@ final class SidebarBranchLayoutSettingsTests: XCTestCase { } } +final class SidebarWorkspaceDetailSettingsTests: XCTestCase { + func testDefaultPreferencesWhenUnset() { + let suiteName = "SidebarWorkspaceDetailSettingsTests.Default.\(UUID().uuidString)" + guard let defaults = UserDefaults(suiteName: suiteName) else { + XCTFail("Failed to create isolated UserDefaults suite") + return + } + defer { defaults.removePersistentDomain(forName: suiteName) } + + XCTAssertFalse(SidebarWorkspaceDetailSettings.hidesAllDetails(defaults: defaults)) + XCTAssertTrue(SidebarWorkspaceDetailSettings.showsNotificationMessage(defaults: defaults)) + XCTAssertTrue( + SidebarWorkspaceDetailSettings.resolvedNotificationMessageVisibility( + showNotificationMessage: SidebarWorkspaceDetailSettings.showsNotificationMessage(defaults: defaults), + hideAllDetails: SidebarWorkspaceDetailSettings.hidesAllDetails(defaults: defaults) + ) + ) + } + + func testStoredPreferencesOverrideDefaults() { + let suiteName = "SidebarWorkspaceDetailSettingsTests.Stored.\(UUID().uuidString)" + guard let defaults = UserDefaults(suiteName: suiteName) else { + XCTFail("Failed to create isolated UserDefaults suite") + return + } + defer { defaults.removePersistentDomain(forName: suiteName) } + + defaults.set(true, forKey: SidebarWorkspaceDetailSettings.hideAllDetailsKey) + defaults.set(false, forKey: SidebarWorkspaceDetailSettings.showNotificationMessageKey) + + XCTAssertTrue(SidebarWorkspaceDetailSettings.hidesAllDetails(defaults: defaults)) + XCTAssertFalse(SidebarWorkspaceDetailSettings.showsNotificationMessage(defaults: defaults)) + XCTAssertFalse( + SidebarWorkspaceDetailSettings.resolvedNotificationMessageVisibility( + showNotificationMessage: SidebarWorkspaceDetailSettings.showsNotificationMessage(defaults: defaults), + hideAllDetails: false + ) + ) + XCTAssertFalse( + SidebarWorkspaceDetailSettings.resolvedNotificationMessageVisibility( + showNotificationMessage: true, + hideAllDetails: SidebarWorkspaceDetailSettings.hidesAllDetails(defaults: defaults) + ) + ) + } +} + +final class SidebarWorkspaceAuxiliaryDetailVisibilityTests: XCTestCase { + func testResolvedVisibilityPreservesPerRowTogglesWhenDetailsAreShown() { + XCTAssertEqual( + SidebarWorkspaceAuxiliaryDetailVisibility.resolved( + showMetadata: true, + showLog: false, + showProgress: true, + showBranchDirectory: false, + showPullRequests: true, + showPorts: false, + hideAllDetails: false + ), + SidebarWorkspaceAuxiliaryDetailVisibility( + showsMetadata: true, + showsLog: false, + showsProgress: true, + showsBranchDirectory: false, + showsPullRequests: true, + showsPorts: false + ) + ) + } + + func testResolvedVisibilityHidesAllAuxiliaryRowsWhenDetailsAreHidden() { + XCTAssertEqual( + SidebarWorkspaceAuxiliaryDetailVisibility.resolved( + showMetadata: true, + showLog: true, + showProgress: true, + showBranchDirectory: true, + showPullRequests: true, + showPorts: true, + hideAllDetails: true + ), + .hidden + ) + } +} + final class SidebarActiveTabIndicatorSettingsTests: XCTestCase { func testDefaultStyleWhenUnset() { let suiteName = "SidebarActiveTabIndicatorSettingsTests.Default.\(UUID().uuidString)"