diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index 08110cba..2f2dc137 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -8492,6 +8492,132 @@ private struct SidebarResizerAccessibilityModifier: ViewModifier { } } +private struct SidebarTabItemSettingsSnapshot: Equatable { + let sidebarShortcutHintXOffset: Double + let sidebarShortcutHintYOffset: Double + let alwaysShowShortcutHints: Bool + let showsGitBranch: Bool + let usesVerticalBranchLayout: Bool + let showsGitBranchIcon: Bool + let showsSSH: Bool + let openPullRequestLinksInCmuxBrowser: Bool + let openPortLinksInCmuxBrowser: Bool + let showsNotificationMessage: Bool + let activeTabIndicatorStyle: SidebarActiveTabIndicatorStyle + let selectionColorHex: String? + let notificationBadgeColorHex: String? + let visibleAuxiliaryDetails: SidebarWorkspaceAuxiliaryDetailVisibility + + init(defaults: UserDefaults = .standard) { + sidebarShortcutHintXOffset = Self.double( + defaults: defaults, + key: ShortcutHintDebugSettings.sidebarHintXKey, + defaultValue: ShortcutHintDebugSettings.defaultSidebarHintX + ) + sidebarShortcutHintYOffset = Self.double( + defaults: defaults, + key: ShortcutHintDebugSettings.sidebarHintYKey, + defaultValue: ShortcutHintDebugSettings.defaultSidebarHintY + ) + alwaysShowShortcutHints = Self.bool( + defaults: defaults, + key: ShortcutHintDebugSettings.alwaysShowHintsKey, + defaultValue: ShortcutHintDebugSettings.defaultAlwaysShowHints + ) + showsGitBranch = Self.bool(defaults: defaults, key: "sidebarShowGitBranch", defaultValue: true) + usesVerticalBranchLayout = SidebarBranchLayoutSettings.usesVerticalLayout(defaults: defaults) + showsGitBranchIcon = Self.bool(defaults: defaults, key: "sidebarShowGitBranchIcon", defaultValue: false) + showsSSH = Self.bool(defaults: defaults, key: "sidebarShowSSH", defaultValue: true) + openPullRequestLinksInCmuxBrowser = BrowserLinkOpenSettings.openSidebarPullRequestLinksInCmuxBrowser( + defaults: defaults + ) + openPortLinksInCmuxBrowser = BrowserLinkOpenSettings.openSidebarPortLinksInCmuxBrowser( + defaults: defaults + ) + + let hidesAllDetails = SidebarWorkspaceDetailSettings.hidesAllDetails(defaults: defaults) + let showsNotificationMessageSetting = SidebarWorkspaceDetailSettings.showsNotificationMessage( + defaults: defaults + ) + showsNotificationMessage = SidebarWorkspaceDetailSettings.resolvedNotificationMessageVisibility( + showNotificationMessage: showsNotificationMessageSetting, + hideAllDetails: hidesAllDetails + ) + + let showsMetadata = Self.bool(defaults: defaults, key: "sidebarShowStatusPills", defaultValue: true) + let showsLog = Self.bool(defaults: defaults, key: "sidebarShowLog", defaultValue: true) + let showsProgress = Self.bool(defaults: defaults, key: "sidebarShowProgress", defaultValue: true) + let showsBranchDirectory = Self.bool(defaults: defaults, key: "sidebarShowBranchDirectory", defaultValue: true) + let showsPullRequests = Self.bool(defaults: defaults, key: "sidebarShowPullRequest", defaultValue: true) + let showsPorts = Self.bool(defaults: defaults, key: "sidebarShowPorts", defaultValue: true) + visibleAuxiliaryDetails = SidebarWorkspaceAuxiliaryDetailVisibility.resolved( + showMetadata: showsMetadata, + showLog: showsLog, + showProgress: showsProgress, + showBranchDirectory: showsBranchDirectory, + showPullRequests: showsPullRequests, + showPorts: showsPorts, + hideAllDetails: hidesAllDetails + ) + + activeTabIndicatorStyle = SidebarActiveTabIndicatorSettings.current(defaults: defaults) + selectionColorHex = defaults.string(forKey: "sidebarSelectionColorHex") + notificationBadgeColorHex = defaults.string(forKey: "sidebarNotificationBadgeColorHex") + } + + private static func bool( + defaults: UserDefaults, + key: String, + defaultValue: Bool + ) -> Bool { + guard defaults.object(forKey: key) != nil else { return defaultValue } + return defaults.bool(forKey: key) + } + + private static func double( + defaults: UserDefaults, + key: String, + defaultValue: Double + ) -> Double { + guard let value = defaults.object(forKey: key) as? NSNumber else { return defaultValue } + return value.doubleValue + } +} + +@MainActor +private final class SidebarTabItemSettingsStore: ObservableObject { + @Published private(set) var snapshot: SidebarTabItemSettingsSnapshot + + private let defaults: UserDefaults + private var defaultsObserver: NSObjectProtocol? + + init(defaults: UserDefaults = .standard) { + self.defaults = defaults + self.snapshot = SidebarTabItemSettingsSnapshot(defaults: defaults) + defaultsObserver = NotificationCenter.default.addObserver( + forName: UserDefaults.didChangeNotification, + object: nil, + queue: .main + ) { [weak self] _ in + Task { @MainActor [weak self] in + self?.refreshSnapshot() + } + } + } + + deinit { + if let defaultsObserver { + NotificationCenter.default.removeObserver(defaultsObserver) + } + } + + private func refreshSnapshot() { + let nextSnapshot = SidebarTabItemSettingsSnapshot(defaults: defaults) + guard nextSnapshot != snapshot else { return } + snapshot = nextSnapshot + } +} + struct VerticalTabsSidebar: View { @ObservedObject var updateViewModel: UpdateViewModel let onSendFeedback: () -> Void @@ -8503,12 +8629,9 @@ struct VerticalTabsSidebar: View { @StateObject private var modifierKeyMonitor = SidebarShortcutHintModifierMonitor() @StateObject private var dragAutoScrollController = SidebarDragAutoScrollController() @StateObject private var dragFailsafeMonitor = SidebarDragFailsafeMonitor() + @StateObject private var tabItemSettingsStore = SidebarTabItemSettingsStore() @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 @AppStorage(WorkspacePresentationModeSettings.modeKey) private var workspacePresentationMode = WorkspacePresentationModeSettings.defaultMode.rawValue @AppStorage(KeyboardShortcutSettings.Action.selectWorkspaceByNumber.defaultsKey) @@ -8524,10 +8647,7 @@ struct VerticalTabsSidebar: View { } private var showsSidebarNotificationMessage: Bool { - SidebarWorkspaceDetailSettings.resolvedNotificationMessageVisibility( - showNotificationMessage: sidebarShowNotificationMessage, - hideAllDetails: sidebarHideAllDetails - ) + tabItemSettingsStore.snapshot.showsNotificationMessage } private var workspaceNumberShortcut: StoredShortcut { @@ -8546,9 +8666,22 @@ struct VerticalTabsSidebar: View { } var body: some View { - let workspaceCount = tabManager.tabs.count + let tabs = tabManager.tabs + let workspaceCount = tabs.count let canCloseWorkspace = workspaceCount > 1 let workspaceNumberShortcut = self.workspaceNumberShortcut + let tabItemSettings = tabItemSettingsStore.snapshot + let tabIndexById = Dictionary(uniqueKeysWithValues: tabs.enumerated().map { + ($0.element.id, $0.offset) + }) + let orderedSelectedTabs = tabs.filter { selectedTabIds.contains($0.id) } + let selectedContextTargetIds = orderedSelectedTabs.map(\.id) + let selectedRemoteContextMenuTargets = orderedSelectedTabs.filter { $0.isRemoteWorkspace } + let selectedRemoteContextMenuWorkspaceIds = selectedRemoteContextMenuTargets.map(\.id) + let allSelectedRemoteContextMenuTargetsConnecting = !selectedRemoteContextMenuTargets.isEmpty && + selectedRemoteContextMenuTargets.allSatisfy { $0.remoteConnectionState == .connecting } + let allSelectedRemoteContextMenuTargetsDisconnected = !selectedRemoteContextMenuTargets.isEmpty && + selectedRemoteContextMenuTargets.allSatisfy { $0.remoteConnectionState == .disconnected } VStack(spacing: 0) { GeometryReader { proxy in @@ -8559,14 +8692,21 @@ struct VerticalTabsSidebar: View { .frame(height: trafficLightPadding) LazyVStack(spacing: tabRowSpacing) { - ForEach(Array(tabManager.tabs.enumerated()), id: \.element.id) { index, tab in - let selectedContextIds: Set = selectedTabIds.contains(tab.id) ? selectedTabIds : [tab.id] - let contextTargetIds = tabManager.tabs.compactMap { workspace in - selectedContextIds.contains(workspace.id) ? workspace.id : nil - } - let remoteContextMenuTargets = tabManager.tabs.filter { workspace in - contextTargetIds.contains(workspace.id) && workspace.isRemoteWorkspace - } + ForEach(tabs, id: \.id) { tab in + let index = tabIndexById[tab.id] ?? 0 + let usesSelectedContextMenuTargets = selectedTabIds.contains(tab.id) + let contextMenuWorkspaceIds = usesSelectedContextMenuTargets + ? selectedContextTargetIds + : [tab.id] + let remoteContextMenuWorkspaceIds = usesSelectedContextMenuTargets + ? selectedRemoteContextMenuWorkspaceIds + : (tab.isRemoteWorkspace ? [tab.id] : []) + let allRemoteContextMenuTargetsConnecting = usesSelectedContextMenuTargets + ? allSelectedRemoteContextMenuTargetsConnecting + : (tab.isRemoteWorkspace && tab.remoteConnectionState == .connecting) + let allRemoteContextMenuTargetsDisconnected = usesSelectedContextMenuTargets + ? allSelectedRemoteContextMenuTargetsDisconnected + : (tab.isRemoteWorkspace && tab.remoteConnectionState == .disconnected) TabItemView( tabManager: tabManager, notificationStore: notificationStore, @@ -8598,9 +8738,11 @@ struct VerticalTabsSidebar: View { dragAutoScrollController: dragAutoScrollController, draggedTabId: $draggedTabId, dropIndicator: $dropIndicator, - remoteContextMenuWorkspaceIds: remoteContextMenuTargets.map(\.id), - allRemoteContextMenuTargetsConnecting: !remoteContextMenuTargets.isEmpty && remoteContextMenuTargets.allSatisfy { $0.remoteConnectionState == .connecting }, - allRemoteContextMenuTargetsDisconnected: !remoteContextMenuTargets.isEmpty && remoteContextMenuTargets.allSatisfy { $0.remoteConnectionState == .disconnected } + contextMenuWorkspaceIds: contextMenuWorkspaceIds, + remoteContextMenuWorkspaceIds: remoteContextMenuWorkspaceIds, + allRemoteContextMenuTargetsConnecting: allRemoteContextMenuTargetsConnecting, + allRemoteContextMenuTargetsDisconnected: allRemoteContextMenuTargetsDisconnected, + settings: tabItemSettings ) .equatable() } @@ -10998,9 +11140,11 @@ private struct TabItemView: View, Equatable { lhs.latestNotificationText == rhs.latestNotificationText && lhs.rowSpacing == rhs.rowSpacing && lhs.showsModifierShortcutHints == rhs.showsModifierShortcutHints && + lhs.contextMenuWorkspaceIds == rhs.contextMenuWorkspaceIds && lhs.remoteContextMenuWorkspaceIds == rhs.remoteContextMenuWorkspaceIds && lhs.allRemoteContextMenuTargetsConnecting == rhs.allRemoteContextMenuTargetsConnecting && - lhs.allRemoteContextMenuTargetsDisconnected == rhs.allRemoteContextMenuTargetsDisconnected + lhs.allRemoteContextMenuTargetsDisconnected == rhs.allRemoteContextMenuTargetsDisconnected && + lhs.settings == rhs.settings } // Use plain references instead of @EnvironmentObject to avoid subscribing @@ -11026,35 +11170,14 @@ private struct TabItemView: View, Equatable { let dragAutoScrollController: SidebarDragAutoScrollController @Binding var draggedTabId: UUID? @Binding var dropIndicator: SidebarDropIndicator? + let contextMenuWorkspaceIds: [UUID] let remoteContextMenuWorkspaceIds: [UUID] let allRemoteContextMenuTargetsConnecting: Bool let allRemoteContextMenuTargetsDisconnected: Bool + let settings: SidebarTabItemSettingsSnapshot @State private var workspaceObservationGeneration: UInt64 = 0 @State private var isHovering = false @State private var rowHeight: CGFloat = 1 - @AppStorage(ShortcutHintDebugSettings.sidebarHintXKey) private var sidebarShortcutHintXOffset = ShortcutHintDebugSettings.defaultSidebarHintX - @AppStorage(ShortcutHintDebugSettings.sidebarHintYKey) private var sidebarShortcutHintYOffset = ShortcutHintDebugSettings.defaultSidebarHintY - @AppStorage(ShortcutHintDebugSettings.alwaysShowHintsKey) private var alwaysShowShortcutHints = ShortcutHintDebugSettings.defaultAlwaysShowHints - @AppStorage("sidebarShowGitBranch") private var sidebarShowGitBranch = true - @AppStorage(SidebarBranchLayoutSettings.key) private var sidebarBranchVerticalLayout = SidebarBranchLayoutSettings.defaultVerticalLayout - @AppStorage("sidebarShowBranchDirectory") private var sidebarShowBranchDirectory = true - @AppStorage("sidebarShowGitBranchIcon") private var sidebarShowGitBranchIcon = false - @AppStorage("sidebarShowPullRequest") private var sidebarShowPullRequest = true - @AppStorage(BrowserLinkOpenSettings.openSidebarPullRequestLinksInCmuxBrowserKey) - private var openSidebarPullRequestLinksInCmuxBrowser = BrowserLinkOpenSettings.defaultOpenSidebarPullRequestLinksInCmuxBrowser - @AppStorage(BrowserLinkOpenSettings.openSidebarPortLinksInCmuxBrowserKey) - private var openSidebarPortLinksInCmuxBrowser = BrowserLinkOpenSettings.defaultOpenSidebarPortLinksInCmuxBrowser - @AppStorage("sidebarShowSSH") private var sidebarShowSSH = true - @AppStorage("sidebarShowPorts") private var sidebarShowPorts = true - @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 - @AppStorage("sidebarSelectionColorHex") private var sidebarSelectionColorHex: String? - @AppStorage("sidebarNotificationBadgeColorHex") private var sidebarNotificationBadgeColorHex: String? var isMultiSelected: Bool { selectedTabIds.contains(tab.id) @@ -11064,8 +11187,52 @@ private struct TabItemView: View, Equatable { draggedTabId == tab.id } + private var sidebarShortcutHintXOffset: Double { + settings.sidebarShortcutHintXOffset + } + + private var sidebarShortcutHintYOffset: Double { + settings.sidebarShortcutHintYOffset + } + + private var alwaysShowShortcutHints: Bool { + settings.alwaysShowShortcutHints + } + + private var sidebarShowGitBranch: Bool { + settings.showsGitBranch + } + + private var sidebarBranchVerticalLayout: Bool { + settings.usesVerticalBranchLayout + } + + private var sidebarShowGitBranchIcon: Bool { + settings.showsGitBranchIcon + } + + private var sidebarShowSSH: Bool { + settings.showsSSH + } + private var activeTabIndicatorStyle: SidebarActiveTabIndicatorStyle { - SidebarActiveTabIndicatorSettings.resolvedStyle(rawValue: activeTabIndicatorStyleRaw) + settings.activeTabIndicatorStyle + } + + private var sidebarSelectionColorHex: String? { + settings.selectionColorHex + } + + private var sidebarNotificationBadgeColorHex: String? { + settings.notificationBadgeColorHex + } + + private var openSidebarPullRequestLinksInCmuxBrowser: Bool { + settings.openPullRequestLinksInCmuxBrowser + } + + private var openSidebarPortLinksInCmuxBrowser: Bool { + settings.openPortLinksInCmuxBrowser } private var titleFontWeight: Font.Weight { @@ -11232,15 +11399,7 @@ 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 - ) + settings.visibleAuxiliaryDetails } var body: some View { @@ -11658,7 +11817,7 @@ private struct TabItemView: View, Equatable { @ViewBuilder private var workspaceContextMenu: some View { - let targetIds = contextTargetIds() + let targetIds = contextMenuWorkspaceIds let isMulti = targetIds.count > 1 let tabColorPalette = WorkspaceTabColorSettings.palette() let shouldPin = !tab.isPinned @@ -11987,11 +12146,6 @@ private struct TabItemView: View, Equatable { setSelectionToTabs() } - private func contextTargetIds() -> [UUID] { - let baseIds: Set = selectedTabIds.contains(tab.id) ? selectedTabIds : [tab.id] - return tabManager.tabs.compactMap { baseIds.contains($0.id) ? $0.id : nil } - } - private func closeTabs(_ targetIds: [UUID], allowPinned: Bool) { tabManager.closeWorkspacesWithConfirmation(targetIds, allowPinned: allowPinned) syncSelectionAfterMutation()