native macOS codings agent orchestrator
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

Dim unfocused split panes

Adds a Ghostty-style dim overlay on inactive split panes so the focused
one stands out, with a Settings toggle (Appearance > Splits) defaulting
to on. Light mode uses a lighter tint than dark to avoid washing the
pane in grey, and the split divider switches to NSColor.separatorColor
for a more neutral look.

+102 -15
+9 -3
supacode/Features/Canvas/Views/CanvasCardView.swift
··· 16 16 /// PNG/SVG filenames against the per-repo icons directory. 17 17 var repositoryRootURL: URL? 18 18 let tree: SplitTree<GhosttySurfaceView> 19 + let focusedSurfaceID: UUID? 19 20 let isFocused: Bool 20 21 let isSelected: Bool 21 22 let hasUnseenNotification: Bool ··· 225 226 } 226 227 227 228 private var terminalContent: some View { 228 - TerminalSplitTreeView(tree: tree, pinnedSize: cardSize, action: onSplitOperation) 229 - .frame(width: cardSize.width, height: cardSize.height) 230 - .allowsHitTesting(isFocused && !showsSelectionShield) 229 + TerminalSplitTreeView( 230 + tree: tree, 231 + pinnedSize: cardSize, 232 + focusedSurfaceID: focusedSurfaceID, 233 + action: onSplitOperation 234 + ) 235 + .frame(width: cardSize.width, height: cardSize.height) 236 + .allowsHitTesting(isFocused && !showsSelectionShield) 231 237 } 232 238 233 239 private var selectionShield: some View {
+1
supacode/Features/Canvas/Views/CanvasView.swift
··· 98 98 repositoryColor: repositoryAppearance.color?.color, 99 99 repositoryRootURL: state.repositoryRootURL, 100 100 tree: tree, 101 + focusedSurfaceID: state.focusedSurfaceId(in: tab.id), 101 102 isFocused: selectionState.primaryTabID == tab.id, 102 103 isSelected: selectionState.selectedTabIDs.contains(tab.id), 103 104 hasUnseenNotification: state.hasUnseenNotification(for: tab.id),
+11 -2
supacode/Features/Settings/Models/GlobalSettings.swift
··· 27 27 var archivedAutoDeletePeriod: AutoDeletePeriod? 28 28 var keybindingUserOverrides: KeybindingUserOverrideStore 29 29 var defaultViewMode: DefaultViewMode 30 + var dimUnfocusedSplits: Bool 30 31 31 32 static let `default` = GlobalSettings( 32 33 appearanceMode: .dark, ··· 56 57 archivedAutoDeletePeriod: nil, 57 58 terminalFontSize: nil, 58 59 keybindingUserOverrides: .empty, 59 - defaultViewMode: .normal 60 + defaultViewMode: .normal, 61 + dimUnfocusedSplits: true 60 62 ) 61 63 62 64 init( ··· 87 89 archivedAutoDeletePeriod: AutoDeletePeriod? = nil, 88 90 terminalFontSize: Float32? = nil, 89 91 keybindingUserOverrides: KeybindingUserOverrideStore = .empty, 90 - defaultViewMode: DefaultViewMode = .normal 92 + defaultViewMode: DefaultViewMode = .normal, 93 + dimUnfocusedSplits: Bool = true 91 94 ) { 92 95 self.appearanceMode = appearanceMode 93 96 self.defaultEditorID = defaultEditorID ··· 117 120 self.terminalFontSize = terminalFontSize 118 121 self.keybindingUserOverrides = keybindingUserOverrides 119 122 self.defaultViewMode = defaultViewMode 123 + self.dimUnfocusedSplits = dimUnfocusedSplits 120 124 } 121 125 122 126 func encode(to encoder: any Encoder) throws { ··· 149 153 try container.encodeIfPresent(terminalFontSize, forKey: .terminalFontSize) 150 154 try container.encode(keybindingUserOverrides, forKey: .keybindingUserOverrides) 151 155 try container.encode(defaultViewMode, forKey: .defaultViewMode) 156 + try container.encode(dimUnfocusedSplits, forKey: .dimUnfocusedSplits) 152 157 } 153 158 154 159 private enum CodingKeys: String, CodingKey { ··· 180 185 case terminalFontSize 181 186 case keybindingUserOverrides 182 187 case defaultViewMode 188 + case dimUnfocusedSplits 183 189 // Legacy key for migration 184 190 case automaticallyArchiveMergedWorktrees 185 191 } ··· 272 278 defaultViewMode = 273 279 try container.decodeIfPresent(DefaultViewMode.self, forKey: .defaultViewMode) 274 280 ?? Self.default.defaultViewMode 281 + dimUnfocusedSplits = 282 + try container.decodeIfPresent(Bool.self, forKey: .dimUnfocusedSplits) 283 + ?? Self.default.dimUnfocusedSplits 275 284 } 276 285 }
+5 -1
supacode/Features/Settings/Reducer/SettingsFeature.swift
··· 33 33 var terminalFontSize: Float32? 34 34 var keybindingUserOverrides: KeybindingUserOverrideStore 35 35 var defaultViewMode: DefaultViewMode 36 + var dimUnfocusedSplits: Bool 36 37 var cliInstallStatus: CLIInstallStatus = .notInstalled 37 38 var cliInstallShowAlert: Bool = true 38 39 var selection: SettingsSection? = .general ··· 70 71 terminalFontSize = settings.terminalFontSize 71 72 keybindingUserOverrides = settings.keybindingUserOverrides 72 73 defaultViewMode = settings.defaultViewMode 74 + dimUnfocusedSplits = settings.dimUnfocusedSplits 73 75 } 74 76 75 77 var globalSettings: GlobalSettings { ··· 103 105 archivedAutoDeletePeriod: archivedAutoDeletePeriod, 104 106 terminalFontSize: terminalFontSize, 105 107 keybindingUserOverrides: keybindingUserOverrides, 106 - defaultViewMode: defaultViewMode 108 + defaultViewMode: defaultViewMode, 109 + dimUnfocusedSplits: dimUnfocusedSplits 107 110 ) 108 111 } 109 112 } ··· 204 207 state.terminalFontSize = normalizedSettings.terminalFontSize 205 208 state.keybindingUserOverrides = normalizedSettings.keybindingUserOverrides 206 209 state.defaultViewMode = normalizedSettings.defaultViewMode 210 + state.dimUnfocusedSplits = normalizedSettings.dimUnfocusedSplits 207 211 state.syncGlobalDefaults(from: normalizedSettings) 208 212 return .send(.delegate(.settingsChanged(normalizedSettings))) 209 213
+7
supacode/Features/Settings/Views/AppearanceSettingsView.swift
··· 46 46 .font(.footnote) 47 47 .foregroundStyle(.secondary) 48 48 } 49 + Section("Splits") { 50 + Toggle( 51 + "Dim unfocused split panes", 52 + isOn: $store.dimUnfocusedSplits 53 + ) 54 + .help("Fade split panes that aren't focused so the active one stands out.") 55 + } 49 56 Section("Default View") { 50 57 Picker("Launch in", selection: $store.defaultViewMode) { 51 58 ForEach(DefaultViewMode.allCases) { mode in
+4 -1
supacode/Features/Shelf/Views/ShelfOpenBookView.swift
··· 25 25 Group { 26 26 if let selectedId = state.tabManager.selectedTabId { 27 27 TerminalTabContentStack(tabs: state.tabManager.tabs, selectedTabId: selectedId) { tabId in 28 - TerminalSplitTreeAXContainer(tree: state.splitTree(for: tabId)) { operation in 28 + TerminalSplitTreeAXContainer( 29 + tree: state.splitTree(for: tabId), 30 + focusedSurfaceID: state.focusedSurfaceId(in: tabId) 31 + ) { operation in 29 32 state.performSplitOperation(operation, in: tabId) 30 33 } 31 34 }
+61 -7
supacode/Features/Terminal/Views/TerminalSplitTreeView.swift
··· 1 1 import AppKit 2 + import Sharing 2 3 import SwiftUI 3 4 import UniformTypeIdentifiers 4 5 5 6 struct TerminalSplitTreeView: View { 6 7 let tree: SplitTree<GhosttySurfaceView> 7 8 var pinnedSize: CGSize? 9 + var focusedSurfaceID: UUID? 8 10 let action: (Operation) -> Void 9 11 10 12 private static let dragType = UTType(exportedAs: "com.onevcat.prowl.ghosttySurfaceId") ··· 23 25 24 26 var body: some View { 25 27 if let node = tree.visibleNode { 26 - SubtreeView(node: node, isRoot: node == tree.root, pinnedSize: pinnedSize, action: action) 27 - .id(node.structuralIdentity) 28 + SubtreeView( 29 + node: node, 30 + isRoot: node == tree.root, 31 + pinnedSize: pinnedSize, 32 + focusedSurfaceID: focusedSurfaceID, 33 + action: action 34 + ) 35 + .id(node.structuralIdentity) 28 36 } 29 37 } 30 38 ··· 38 46 let node: SplitTree<GhosttySurfaceView>.Node 39 47 var isRoot: Bool = false 40 48 var pinnedSize: CGSize? 49 + var focusedSurfaceID: UUID? 41 50 let action: (Operation) -> Void 42 51 43 52 var body: some View { 44 53 switch node { 45 54 case .leaf(let leafView): 46 - LeafView(surfaceView: leafView, isSplit: !isRoot, pinnedSize: pinnedSize, action: action) 55 + LeafView( 56 + surfaceView: leafView, 57 + isSplit: !isRoot, 58 + isFocused: leafView.id == focusedSurfaceID, 59 + pinnedSize: pinnedSize, 60 + action: action 61 + ) 47 62 case .split(let split): 48 63 let splitViewDirection: SplitView<SubtreeView, SubtreeView>.Direction = 49 64 switch split.direction { ··· 63 78 set: { 64 79 action(.resize(node: node, ratio: Double($0))) 65 80 }), 66 - dividerColor: .secondary, 81 + dividerColor: Color(nsColor: .separatorColor), 67 82 resizeIncrements: .init(width: 1, height: 1), 68 83 left: { 69 - SubtreeView(node: split.left, pinnedSize: leftPinned, action: action) 84 + SubtreeView( 85 + node: split.left, 86 + pinnedSize: leftPinned, 87 + focusedSurfaceID: focusedSurfaceID, 88 + action: action 89 + ) 70 90 }, 71 91 right: { 72 - SubtreeView(node: split.right, pinnedSize: rightPinned, action: action) 92 + SubtreeView( 93 + node: split.right, 94 + pinnedSize: rightPinned, 95 + focusedSurfaceID: focusedSurfaceID, 96 + action: action 97 + ) 73 98 }, 74 99 onEqualize: { 75 100 action(.equalize) ··· 93 118 struct LeafView: View { 94 119 let surfaceView: GhosttySurfaceView 95 120 let isSplit: Bool 121 + var isFocused: Bool = true 96 122 var pinnedSize: CGSize? 97 123 let action: (Operation) -> Void 98 124 99 125 @State private var dropState: DropState = .idle 126 + @Shared(.settingsFile) private var settingsFile: SettingsFile 127 + @Environment(\.colorScheme) private var colorScheme 128 + 129 + private var shouldDim: Bool { 130 + isSplit && !isFocused && settingsFile.global.dimUnfocusedSplits 131 + } 132 + 133 + /// Lighter tint in light mode so the inactive pane reads as faded 134 + /// rather than washed in grey; dark mode's deeper tint matches the 135 + /// effect users see in Ghostty.app. 136 + private var dimOpacity: Double { 137 + colorScheme == .dark ? 0.3 : 0.12 138 + } 100 139 101 140 var body: some View { 102 141 GeometryReader { geometry in 103 142 GhosttyTerminalView(surfaceView: surfaceView, pinnedSize: pinnedSize) 104 143 .frame(maxWidth: .infinity, maxHeight: .infinity) 144 + .overlay { 145 + // Mirrors Ghostty's `unfocused-split-fill`/`unfocused-split-opacity`: 146 + // a translucent black tint fades the inactive pane. 147 + Color.black 148 + .opacity(shouldDim ? dimOpacity : 0) 149 + .allowsHitTesting(false) 150 + .animation(.easeOut(duration: 0.12), value: shouldDim) 151 + } 105 152 .overlay(alignment: .top) { 106 153 GhosttySurfaceProgressOverlay(state: surfaceView.bridge.state) 107 154 } ··· 299 346 /// list of terminal panes to assistive technologies. 300 347 struct TerminalSplitTreeAXContainer: NSViewRepresentable { 301 348 let tree: SplitTree<GhosttySurfaceView> 349 + var focusedSurfaceID: UUID? 302 350 let action: (TerminalSplitTreeView.Operation) -> Void 303 351 304 352 func makeNSView(context: Context) -> TerminalSplitAXContainerView { ··· 307 355 308 356 func updateNSView(_ nsView: TerminalSplitAXContainerView, context: Context) { 309 357 nsView.update( 310 - rootView: AnyView(TerminalSplitTreeView(tree: tree, action: action)), 358 + rootView: AnyView( 359 + TerminalSplitTreeView( 360 + tree: tree, 361 + focusedSurfaceID: focusedSurfaceID, 362 + action: action 363 + ) 364 + ), 311 365 panes: tree.visibleLeaves() 312 366 ) 313 367 }
+4 -1
supacode/Features/Terminal/Views/WorktreeTerminalTabsView.swift
··· 43 43 ) 44 44 if let selectedId = state.tabManager.selectedTabId { 45 45 TerminalTabContentStack(tabs: state.tabManager.tabs, selectedTabId: selectedId) { tabId in 46 - TerminalSplitTreeAXContainer(tree: state.splitTree(for: tabId)) { operation in 46 + TerminalSplitTreeAXContainer( 47 + tree: state.splitTree(for: tabId), 48 + focusedSurfaceID: state.focusedSurfaceId(in: tabId) 49 + ) { operation in 47 50 state.performSplitOperation(operation, in: tabId) 48 51 } 49 52 }