native macOS codings agent orchestrator
6
fork

Configure Feed

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

Merge pull request #77 from onevcat/feat/persist-terminal-font-size

Persist terminal font-size override in Prowl settings

authored by

Wei Wang and committed by
GitHub
7469598d f9991cae

+352 -14
+5 -3
supacode/App/supacodeApp.swift
··· 124 124 _ghostty = State(initialValue: runtime) 125 125 let shortcuts = GhosttyShortcutManager(runtime: runtime) 126 126 _ghosttyShortcuts = State(initialValue: shortcuts) 127 - let terminalManager = WorktreeTerminalManager(runtime: runtime) 127 + let terminalManager = WorktreeTerminalManager( 128 + runtime: runtime, 129 + preferredFontSize: initialSettings.terminalFontSize 130 + ) 128 131 _terminalManager = State(initialValue: terminalManager) 129 132 let worktreeInfoWatcher = WorktreeInfoWatcherManager() 130 133 _worktreeInfoWatcher = State(initialValue: worktreeInfoWatcher) ··· 199 202 NSApp.activate(ignoringOtherApps: true) 200 203 } 201 204 } 202 - .keyboardShortcut("0") 203 - .help("Show main window (⌘0)") 205 + .help("Show main window") 204 206 Divider() 205 207 Button("Minimize") { 206 208 NSApp.keyWindow?.miniaturize(nil)
+1
supacode/Clients/Terminal/TerminalClient.swift
··· 38 38 case runScriptStatusChanged(worktreeID: Worktree.ID, isRunning: Bool) 39 39 case commandPaletteToggleRequested(worktreeID: Worktree.ID) 40 40 case setupScriptConsumed(worktreeID: Worktree.ID) 41 + case fontSizeChanged(Float32?) 41 42 } 42 43 } 43 44
+63 -1
supacode/Commands/TerminalCommands.swift
··· 5 5 @FocusedValue(\.newTerminalAction) private var newTerminalAction 6 6 @FocusedValue(\.closeSurfaceAction) private var closeSurfaceAction 7 7 @FocusedValue(\.closeTabAction) private var closeTabAction 8 + @FocusedValue(\.resetFontSizeAction) private var resetFontSizeAction 9 + @FocusedValue(\.increaseFontSizeAction) private var increaseFontSizeAction 10 + @FocusedValue(\.decreaseFontSizeAction) private var decreaseFontSizeAction 8 11 @FocusedValue(\.startSearchAction) private var startSearchAction 9 12 @FocusedValue(\.searchSelectionAction) private var searchSelectionAction 10 13 @FocusedValue(\.navigateSearchNextAction) private var navigateSearchNextAction ··· 34 37 KeyboardShortcutModifier(shortcut: ghosttyShortcuts.keyboardShortcut(for: "close_tab")) 35 38 ) 36 39 .disabled(closeTabAction == nil) 40 + } 41 + CommandGroup(after: .toolbar) { 42 + Divider() 43 + Button("Reset Font Size", systemImage: "textformat.size") { 44 + resetFontSizeAction?() 45 + } 46 + .modifier( 47 + KeyboardShortcutModifier(shortcut: ghosttyShortcuts.keyboardShortcut(for: "reset_font_size")) 48 + ) 49 + .disabled(resetFontSizeAction == nil) 50 + 51 + Button("Increase Font Size", systemImage: "textformat.size.larger") { 52 + increaseFontSizeAction?() 53 + } 54 + .modifier( 55 + KeyboardShortcutModifier(shortcut: ghosttyShortcuts.keyboardShortcut(for: "increase_font_size:1")) 56 + ) 57 + .disabled(increaseFontSizeAction == nil) 58 + 59 + Button("Decrease Font Size", systemImage: "textformat.size.smaller") { 60 + decreaseFontSizeAction?() 61 + } 62 + .modifier( 63 + KeyboardShortcutModifier(shortcut: ghosttyShortcuts.keyboardShortcut(for: "decrease_font_size:1")) 64 + ) 65 + .disabled(decreaseFontSizeAction == nil) 37 66 } 38 67 CommandGroup(after: .textEditing) { 39 68 Button("Find...") { ··· 116 145 } 117 146 } 118 147 148 + private struct ResetFontSizeActionKey: FocusedValueKey { 149 + typealias Value = () -> Void 150 + } 151 + 152 + extension FocusedValues { 153 + var resetFontSizeAction: (() -> Void)? { 154 + get { self[ResetFontSizeActionKey.self] } 155 + set { self[ResetFontSizeActionKey.self] = newValue } 156 + } 157 + } 158 + 159 + private struct IncreaseFontSizeActionKey: FocusedValueKey { 160 + typealias Value = () -> Void 161 + } 162 + 163 + extension FocusedValues { 164 + var increaseFontSizeAction: (() -> Void)? { 165 + get { self[IncreaseFontSizeActionKey.self] } 166 + set { self[IncreaseFontSizeActionKey.self] = newValue } 167 + } 168 + } 169 + 170 + private struct DecreaseFontSizeActionKey: FocusedValueKey { 171 + typealias Value = () -> Void 172 + } 173 + 174 + extension FocusedValues { 175 + var decreaseFontSizeAction: (() -> Void)? { 176 + get { self[DecreaseFontSizeActionKey.self] } 177 + set { self[DecreaseFontSizeActionKey.self] = newValue } 178 + } 179 + } 180 + 119 181 private struct StartSearchActionKey: FocusedValueKey { 120 182 typealias Value = () -> Void 121 183 } ··· 169 231 get { self[EndSearchActionKey.self] } 170 232 set { self[EndSearchActionKey.self] = newValue } 171 233 } 172 - } 234 + }
+6
supacode/Features/App/Reducer/AppFeature.swift
··· 355 355 } 356 356 ) 357 357 358 + case .settings(.delegate(.terminalFontSizeChanged)): 359 + return .none 360 + 358 361 case .openActionSelectionChanged(let action): 359 362 state.openActionSelection = action 360 363 guard let worktree = state.repositories.selectedTerminalWorktree else { ··· 786 789 return .send(.commandPalette(.setPresented(true))) 787 790 case .terminalEvent(.setupScriptConsumed(let worktreeID)): 788 791 return .send(.repositories(.consumeSetupScript(worktreeID))) 792 + 793 + case .terminalEvent(.fontSizeChanged(let fontSize)): 794 + return .send(.settings(.setTerminalFontSize(fontSize))) 789 795 790 796 case .terminalEvent: 791 797 return .none
+20
supacode/Features/Repositories/Views/WorktreeDetailView.swift
··· 236 236 .focusedSceneValue(\.newTerminalAction, actions.newTerminal) 237 237 .focusedValue(\.closeTabAction, actions.closeTab) 238 238 .focusedValue(\.closeSurfaceAction, actions.closeSurface) 239 + .focusedSceneValue(\.resetFontSizeAction, actions.resetFontSize) 240 + .focusedSceneValue(\.increaseFontSizeAction, actions.increaseFontSize) 241 + .focusedSceneValue(\.decreaseFontSizeAction, actions.decreaseFontSize) 239 242 .focusedSceneValue(\.startSearchAction, actions.startSearch) 240 243 .focusedSceneValue(\.searchSelectionAction, actions.searchSelection) 241 244 .focusedSceneValue(\.navigateSearchNextAction, actions.navigateSearchNext) ··· 267 270 } 268 271 } 269 272 273 + func fontSizeAction(_ bindingAction: String) -> (() -> Void)? { 274 + if let action = canvasAction({ $0.performBindingActionOnFocusedSurface(bindingAction) }) { 275 + return action 276 + } 277 + guard hasActiveWorktree, let selectedWorktree = repositories.selectedTerminalWorktree else { return nil } 278 + return { 279 + guard let state = terminalManager.stateIfExists(for: selectedWorktree.id) else { return } 280 + _ = state.performBindingActionOnFocusedSurface(bindingAction) 281 + } 282 + } 283 + 270 284 return FocusedActions( 271 285 openSelectedWorktree: action(.openSelectedWorktree), 272 286 newTerminal: action(.newTerminal), 273 287 closeTab: canvasAction { $0.closeFocusedTab() } ?? action(.closeTab), 274 288 closeSurface: canvasAction { $0.closeFocusedSurface() } ?? action(.closeSurface), 289 + resetFontSize: fontSizeAction("reset_font_size"), 290 + increaseFontSize: fontSizeAction("increase_font_size:1"), 291 + decreaseFontSize: fontSizeAction("decrease_font_size:1"), 275 292 startSearch: action(.startSearch), 276 293 searchSelection: action(.searchSelection), 277 294 navigateSearchNext: action(.navigateSearchNext), ··· 305 322 let newTerminal: (() -> Void)? 306 323 let closeTab: (() -> Void)? 307 324 let closeSurface: (() -> Void)? 325 + let resetFontSize: (() -> Void)? 326 + let increaseFontSize: (() -> Void)? 327 + let decreaseFontSize: (() -> Void)? 308 328 let startSearch: (() -> Void)? 309 329 let searchSelection: (() -> Void)? 310 330 let navigateSearchNext: (() -> Void)?
+9 -2
supacode/Features/Settings/Models/GlobalSettings.swift
··· 18 18 var automaticallyArchiveMergedWorktrees: Bool 19 19 var promptForWorktreeCreation: Bool 20 20 var defaultWorktreeBaseDirectoryPath: String? 21 + var terminalFontSize: Float32? 21 22 22 23 static let `default` = GlobalSettings( 23 24 appearanceMode: .dark, ··· 38 39 deleteBranchOnDeleteWorktree: true, 39 40 automaticallyArchiveMergedWorktrees: false, 40 41 promptForWorktreeCreation: true, 41 - defaultWorktreeBaseDirectoryPath: nil 42 + defaultWorktreeBaseDirectoryPath: nil, 43 + terminalFontSize: nil 42 44 ) 43 45 44 46 init( ··· 60 62 deleteBranchOnDeleteWorktree: Bool, 61 63 automaticallyArchiveMergedWorktrees: Bool, 62 64 promptForWorktreeCreation: Bool, 63 - defaultWorktreeBaseDirectoryPath: String? = nil 65 + defaultWorktreeBaseDirectoryPath: String? = nil, 66 + terminalFontSize: Float32? = nil 64 67 ) { 65 68 self.appearanceMode = appearanceMode 66 69 self.defaultEditorID = defaultEditorID ··· 81 84 self.automaticallyArchiveMergedWorktrees = automaticallyArchiveMergedWorktrees 82 85 self.promptForWorktreeCreation = promptForWorktreeCreation 83 86 self.defaultWorktreeBaseDirectoryPath = defaultWorktreeBaseDirectoryPath 87 + self.terminalFontSize = terminalFontSize 84 88 } 85 89 86 90 init(from decoder: any Decoder) throws { ··· 136 140 defaultWorktreeBaseDirectoryPath = 137 141 try container.decodeIfPresent(String.self, forKey: .defaultWorktreeBaseDirectoryPath) 138 142 ?? Self.default.defaultWorktreeBaseDirectoryPath 143 + terminalFontSize = 144 + try container.decodeIfPresent(Float32.self, forKey: .terminalFontSize) 145 + ?? Self.default.terminalFontSize 139 146 } 140 147 }
+25 -4
supacode/Features/Settings/Reducer/SettingsFeature.swift
··· 24 24 var automaticallyArchiveMergedWorktrees: Bool 25 25 var promptForWorktreeCreation: Bool 26 26 var defaultWorktreeBaseDirectoryPath: String 27 + var terminalFontSize: Float32? 27 28 var selection: SettingsSection? = .general 28 29 var repositorySettings: RepositorySettingsFeature.State? 29 30 @Presents var alert: AlertState<Alert>? ··· 50 51 promptForWorktreeCreation = settings.promptForWorktreeCreation 51 52 defaultWorktreeBaseDirectoryPath = 52 53 SupacodePaths.normalizedWorktreeBaseDirectoryPath(settings.defaultWorktreeBaseDirectoryPath) ?? "" 54 + terminalFontSize = settings.terminalFontSize 53 55 } 54 56 55 57 var globalSettings: GlobalSettings { ··· 74 76 promptForWorktreeCreation: promptForWorktreeCreation, 75 77 defaultWorktreeBaseDirectoryPath: SupacodePaths.normalizedWorktreeBaseDirectoryPath( 76 78 defaultWorktreeBaseDirectoryPath 77 - ) 79 + ), 80 + terminalFontSize: terminalFontSize 78 81 ) 79 82 } 80 83 } ··· 85 88 case setSelection(SettingsSection?) 86 89 case setSystemNotificationsEnabled(Bool) 87 90 case setCommandFinishedNotificationThreshold(String) 91 + case setTerminalFontSize(Float32?) 88 92 case showNotificationPermissionAlert(errorMessage: String?) 89 93 case repositorySettings(RepositorySettingsFeature.Action) 90 94 case alert(PresentationAction<Alert>) ··· 100 104 @CasePathable 101 105 enum Delegate: Equatable { 102 106 case settingsChanged(GlobalSettings) 107 + case terminalFontSizeChanged(Float32?) 103 108 } 104 109 105 110 @Dependency(AnalyticsClient.self) private var analyticsClient ··· 149 154 state.automaticallyArchiveMergedWorktrees = normalizedSettings.automaticallyArchiveMergedWorktrees 150 155 state.promptForWorktreeCreation = normalizedSettings.promptForWorktreeCreation 151 156 state.defaultWorktreeBaseDirectoryPath = normalizedSettings.defaultWorktreeBaseDirectoryPath ?? "" 157 + state.terminalFontSize = normalizedSettings.terminalFontSize 152 158 state.repositorySettings?.globalDefaultWorktreeBaseDirectoryPath = 153 159 normalizedSettings.defaultWorktreeBaseDirectoryPath 154 160 return .send(.delegate(.settingsChanged(normalizedSettings))) ··· 175 181 defaultWorktreeBaseDirectoryPath 176 182 return persist(state) 177 183 184 + case .setTerminalFontSize(let fontSize): 185 + guard state.terminalFontSize != fontSize else { return .none } 186 + state.terminalFontSize = fontSize 187 + return .merge( 188 + persist(state, captureAnalytics: false, emitSettingsChanged: false), 189 + .send(.delegate(.terminalFontSizeChanged(fontSize))) 190 + ) 191 + 178 192 case .showNotificationPermissionAlert(let errorMessage): 179 193 let message: String 180 194 if let errorMessage, !errorMessage.isEmpty { ··· 227 241 } 228 242 } 229 243 230 - private func persist(_ state: State) -> Effect<Action> { 244 + private func persist( 245 + _ state: State, 246 + captureAnalytics: Bool = true, 247 + emitSettingsChanged: Bool = true 248 + ) -> Effect<Action> { 231 249 let settings = state.globalSettings 232 250 @Shared(.settingsFile) var settingsFile 233 251 $settingsFile.withLock { $0.global = settings } 234 - if settings.analyticsEnabled { 252 + if captureAnalytics, settings.analyticsEnabled { 235 253 analyticsClient.capture("settings_changed", nil) 236 254 } 237 - return .send(.delegate(.settingsChanged(settings))) 255 + if emitSettingsChanged { 256 + return .send(.delegate(.settingsChanged(settings))) 257 + } 258 + return .none 238 259 } 239 260 }
+30 -2
supacode/Features/Terminal/BusinessLogic/WorktreeTerminalManager.swift
··· 11 11 private var notificationsEnabled = true 12 12 private var commandFinishedNotificationEnabled = true 13 13 private var commandFinishedNotificationThreshold = 10 14 + private var preferredFontSize: Float32? 15 + private let baselineFontSize: Float32 14 16 private var lastNotificationIndicatorCount: Int? 15 17 private var eventContinuation: AsyncStream<TerminalClient.Event>.Continuation? 16 18 private var pendingEvents: [TerminalClient.Event] = [] ··· 19 21 /// Used by toggleCanvas to know which worktree to return to. 20 22 var canvasFocusedWorktreeID: Worktree.ID? 21 23 22 - init(runtime: GhosttyRuntime) { 24 + init(runtime: GhosttyRuntime, preferredFontSize: Float32? = nil) { 23 25 self.runtime = runtime 26 + self.preferredFontSize = preferredFontSize 27 + baselineFontSize = runtime.defaultFontSize() 24 28 } 25 29 26 30 func handleCommand(_ command: TerminalClient.Command) { ··· 152 156 runSetupScriptIfNew: () -> Bool = { false } 153 157 ) -> WorktreeTerminalState { 154 158 if let existing = states[worktree.id] { 159 + existing.setDefaultFontSize(preferredFontSize) 155 160 if runSetupScriptIfNew() { 156 161 existing.enableSetupScriptIfNeeded() 157 162 } ··· 161 166 let state = WorktreeTerminalState( 162 167 runtime: runtime, 163 168 worktree: worktree, 164 - runSetupScript: runSetupScript 169 + runSetupScript: runSetupScript, 170 + defaultFontSize: preferredFontSize 165 171 ) 166 172 state.setNotificationsEnabled(notificationsEnabled) 167 173 state.setCommandFinishedNotification( ··· 197 203 } 198 204 state.onSetupScriptConsumed = { [weak self] in 199 205 self?.emit(.setupScriptConsumed(worktreeID: worktree.id)) 206 + } 207 + state.onFontSizeChanged = { [weak self] fontSize in 208 + self?.applyFontSize(fontSize) 200 209 } 201 210 states[worktree.id] = state 202 211 terminalLogger.info("Created terminal state for worktree \(worktree.id)") ··· 323 332 324 333 func surfaceBackgroundOpacity() -> Double { 325 334 runtime.backgroundOpacity() 335 + } 336 + 337 + private func applyFontSize(_ fontSize: Float32?) { 338 + let normalized = normalizedFontSize(fontSize) 339 + guard preferredFontSize != normalized else { return } 340 + preferredFontSize = normalized 341 + for state in states.values { 342 + state.setDefaultFontSize(normalized) 343 + } 344 + emit(.fontSizeChanged(normalized)) 345 + } 346 + 347 + private func normalizedFontSize(_ fontSize: Float32?) -> Float32? { 348 + guard let fontSize else { return nil } 349 + let epsilon: Float32 = 0.01 350 + if abs(fontSize - baselineFontSize) <= epsilon { 351 + return nil 352 + } 353 + return fontSize 326 354 } 327 355 328 356 private func emit(_ event: TerminalClient.Event) {
+39 -2
supacode/Features/Terminal/Models/WorktreeTerminalState.swift
··· 24 24 var tabIsRunningById: [TerminalTabID: Bool] = [:] 25 25 private var runScriptTabId: TerminalTabID? 26 26 private var pendingSetupScript: Bool 27 + private var defaultFontSize: Float32? 28 + private var hasInitializedCellSizeSurfaceIDs: Set<UUID> = [] 27 29 private var isEnsuringInitialTab = false 28 30 private var lastReportedTaskStatus: WorktreeTaskStatus? 29 31 private var lastEmittedFocusSurfaceId: UUID? ··· 50 52 var onRunScriptStatusChanged: ((Bool) -> Void)? 51 53 var onCommandPaletteToggle: (() -> Void)? 52 54 var onSetupScriptConsumed: (() -> Void)? 55 + var onFontSizeChanged: ((Float32?) -> Void)? 53 56 54 - init(runtime: GhosttyRuntime, worktree: Worktree, runSetupScript: Bool = false) { 57 + init( 58 + runtime: GhosttyRuntime, 59 + worktree: Worktree, 60 + runSetupScript: Bool = false, 61 + defaultFontSize: Float32? = nil 62 + ) { 55 63 self.runtime = runtime 56 64 self.worktree = worktree 57 65 self.pendingSetupScript = runSetupScript 66 + self.defaultFontSize = defaultFontSize 58 67 self.tabManager = TerminalTabManager() 59 68 _repositorySettings = SharedReader( 60 69 wrappedValue: RepositorySettings.default, ··· 99 108 100 109 var isRunScriptRunning: Bool { 101 110 runScriptTabId != nil 111 + } 112 + 113 + func setDefaultFontSize(_ fontSize: Float32?) { 114 + defaultFontSize = fontSize 102 115 } 103 116 104 117 func ensureInitialTab(focusing: Bool) { ··· 441 454 } catch { 442 455 newSurface.closeSurface() 443 456 surfaces.removeValue(forKey: newSurface.id) 457 + hasInitializedCellSizeSurfaceIDs.remove(newSurface.id) 444 458 return false 445 459 } 446 460 ··· 533 547 surface.closeSurface() 534 548 } 535 549 surfaces.removeAll() 550 + hasInitializedCellSizeSurfaceIDs.removeAll() 536 551 trees.removeAll() 537 552 focusedSurfaceIdByTab.removeAll() 538 553 tabIsRunningById.removeAll() ··· 640 655 runtime: runtime, 641 656 workingDirectory: inherited.workingDirectory ?? worktree.workingDirectory, 642 657 initialInput: initialInput, 643 - fontSize: inherited.fontSize, 658 + fontSize: inherited.fontSize ?? defaultFontSize, 644 659 context: context 645 660 ) 646 661 view.bridge.onTitleChange = { [weak self, weak view] title in ··· 675 690 guard let self else { return } 676 691 self.updateRunningState(for: tabId) 677 692 } 693 + view.bridge.onCellSizeChange = { [weak self, weak view] in 694 + guard let self, let view else { return } 695 + self.handleCellSizeChange(forSurfaceID: view.id) 696 + } 678 697 view.bridge.onDesktopNotification = { [weak self, weak view] title, body in 679 698 guard let self, let view else { return } 680 699 self.appendNotification(title: title, body: body, surfaceId: view.id) ··· 741 760 return focusedSurfaceIdByTab[selectedTabId] 742 761 } 743 762 763 + private func handleCellSizeChange(forSurfaceID surfaceID: UUID) { 764 + handleCellSizeChange(forSurfaceID: surfaceID, fontSize: fontSize(forSurfaceID: surfaceID)) 765 + } 766 + 767 + func handleCellSizeChange(forSurfaceID surfaceID: UUID, fontSize: Float32?) { 768 + let inserted = hasInitializedCellSizeSurfaceIDs.insert(surfaceID).inserted 769 + guard !inserted else { return } 770 + onFontSizeChanged?(fontSize) 771 + } 772 + 773 + private func fontSize(forSurfaceID surfaceID: UUID) -> Float32? { 774 + inheritedSurfaceConfig(fromSurfaceId: surfaceID, context: GHOSTTY_SURFACE_CONTEXT_TAB).fontSize 775 + } 776 + 744 777 private func handlePromptTitle( 745 778 _ promptType: ghostty_action_prompt_title_e, 746 779 tabId: TerminalTabID ··· 891 924 for surface in tree.leaves() { 892 925 surface.closeSurface() 893 926 surfaces.removeValue(forKey: surface.id) 927 + hasInitializedCellSizeSurfaceIDs.remove(surface.id) 894 928 } 895 929 focusedSurfaceIdByTab.removeValue(forKey: tabId) 896 930 tabIsRunningById.removeValue(forKey: tabId) ··· 1006 1040 guard let tabId = tabId(containing: view.id), let tree = trees[tabId] else { 1007 1041 view.closeSurface() 1008 1042 surfaces.removeValue(forKey: view.id) 1043 + hasInitializedCellSizeSurfaceIDs.remove(view.id) 1009 1044 return 1010 1045 } 1011 1046 guard let node = tree.find(id: view.id) else { 1012 1047 view.closeSurface() 1013 1048 surfaces.removeValue(forKey: view.id) 1049 + hasInitializedCellSizeSurfaceIDs.remove(view.id) 1014 1050 return 1015 1051 } 1016 1052 let nextSurface = ··· 1020 1056 let newTree = tree.removing(node) 1021 1057 view.closeSurface() 1022 1058 surfaces.removeValue(forKey: view.id) 1059 + hasInitializedCellSizeSurfaceIDs.remove(view.id) 1023 1060 if newTree.isEmpty { 1024 1061 trees.removeValue(forKey: tabId) 1025 1062 focusedSurfaceIdByTab.removeValue(forKey: tabId)
+10
supacode/Infrastructure/Ghostty/GhosttyRuntime.swift
··· 492 492 return Self.keyboardShortcut(for: trigger) 493 493 } 494 494 495 + func defaultFontSize() -> Float32 { 496 + guard let config else { return 0 } 497 + var value: Double = 0 498 + let key = "font-size" 499 + guard ghostty_config_get(config, &value, key, UInt(key.lengthOfBytes(using: .utf8))) else { 500 + return 0 501 + } 502 + return Float32(value) 503 + } 504 + 495 505 func commandPaletteEntries() -> [GhosttyCommand] { 496 506 guard let config else { return [] } 497 507 var value = ghostty_config_command_list_s()
+2
supacode/Infrastructure/Ghostty/GhosttySurfaceBridge.swift
··· 16 16 var onMoveTab: ((ghostty_action_move_tab_s) -> Bool)? 17 17 var onCommandPaletteToggle: (() -> Bool)? 18 18 var onProgressReport: ((ghostty_action_progress_report_state_e) -> Void)? 19 + var onCellSizeChange: (() -> Void)? 19 20 var onDesktopNotification: ((String, String) -> Void)? 20 21 var onCommandFinished: ((Int?, UInt64) -> Void)? 21 22 var onPromptTitle: ((ghostty_action_prompt_title_e) -> Void)? ··· 370 371 case GHOSTTY_ACTION_CELL_SIZE: 371 372 let cell = action.action.cell_size 372 373 surfaceView?.updateCellSize(width: cell.width, height: cell.height) 374 + onCellSizeChange?() 373 375 return true 374 376 375 377 case GHOSTTY_ACTION_RESET_WINDOW_SIZE:
+25
supacodeTests/AppFeatureSettingsChangedTests.swift
··· 30 30 } 31 31 await store.finish() 32 32 } 33 + 34 + @Test(.dependencies) func terminalFontSizeEventDoesNotFanOutGlobalSettingsEffects() async { 35 + let sentTerminalCommands = LockIsolated<[TerminalClient.Command]>([]) 36 + let watcherCommands = LockIsolated<[WorktreeInfoWatcherClient.Command]>([]) 37 + let store = TestStore(initialState: AppFeature.State()) { 38 + AppFeature() 39 + } withDependencies: { 40 + $0.terminalClient.send = { command in 41 + sentTerminalCommands.withValue { $0.append(command) } 42 + } 43 + $0.worktreeInfoWatcher.send = { command in 44 + watcherCommands.withValue { $0.append(command) } 45 + } 46 + } 47 + 48 + await store.send(.terminalEvent(.fontSizeChanged(18))) 49 + await store.receive(\.settings.setTerminalFontSize) { 50 + $0.settings.terminalFontSize = 18 51 + } 52 + await store.receive(\.settings.delegate.terminalFontSizeChanged) 53 + await store.finish() 54 + 55 + #expect(sentTerminalCommands.value.isEmpty) 56 + #expect(watcherCommands.value.isEmpty) 57 + } 33 58 }
+49
supacodeTests/SettingsFeatureTests.swift
··· 253 253 #expect(store.state.repositorySettings?.globalDefaultWorktreeBaseDirectoryPath == expectedPath) 254 254 #expect(settingsFile.global.defaultWorktreeBaseDirectoryPath == expectedPath) 255 255 } 256 + 257 + @Test(.dependencies) func setTerminalFontSizePersistsWithoutAnalyticsOrGlobalFanout() async { 258 + var initialSettings = GlobalSettings.default 259 + initialSettings.analyticsEnabled = true 260 + initialSettings.terminalFontSize = nil 261 + @Shared(.settingsFile) var settingsFile 262 + $settingsFile.withLock { $0.global = initialSettings } 263 + let capturedEvents = LockIsolated<[String]>([]) 264 + 265 + let store = TestStore(initialState: SettingsFeature.State(settings: initialSettings)) { 266 + SettingsFeature() 267 + } withDependencies: { 268 + $0.analyticsClient.capture = { event, _ in 269 + capturedEvents.withValue { $0.append(event) } 270 + } 271 + } 272 + 273 + await store.send(.setTerminalFontSize(18)) { 274 + $0.terminalFontSize = 18 275 + } 276 + await store.receive(\.delegate.terminalFontSizeChanged) 277 + await store.finish() 278 + 279 + #expect(settingsFile.global.terminalFontSize == 18) 280 + #expect(capturedEvents.value.isEmpty) 281 + } 282 + 283 + @Test(.dependencies) func setTerminalFontSizeIgnoresDuplicateValue() async { 284 + var initialSettings = GlobalSettings.default 285 + initialSettings.analyticsEnabled = true 286 + initialSettings.terminalFontSize = 18 287 + @Shared(.settingsFile) var settingsFile 288 + $settingsFile.withLock { $0.global = initialSettings } 289 + let capturedEvents = LockIsolated<[String]>([]) 290 + 291 + let store = TestStore(initialState: SettingsFeature.State(settings: initialSettings)) { 292 + SettingsFeature() 293 + } withDependencies: { 294 + $0.analyticsClient.capture = { event, _ in 295 + capturedEvents.withValue { $0.append(event) } 296 + } 297 + } 298 + 299 + await store.send(.setTerminalFontSize(18)) 300 + await store.finish() 301 + 302 + #expect(settingsFile.global.terminalFontSize == 18) 303 + #expect(capturedEvents.value.isEmpty) 304 + } 256 305 }
+68
supacodeTests/WorktreeTerminalManagerTests.swift
··· 44 44 #expect(event == .setupScriptConsumed(worktreeID: worktree.id)) 45 45 } 46 46 47 + @Test func fontSizeResetToBaselineEmitsNilOverride() async { 48 + let runtime = GhosttyRuntime() 49 + let baseline = runtime.defaultFontSize() 50 + let manager = WorktreeTerminalManager(runtime: runtime, preferredFontSize: baseline + 1) 51 + let worktree = makeWorktree() 52 + let state = manager.state(for: worktree) 53 + let stream = manager.eventStream() 54 + var iterator = stream.makeAsyncIterator() 55 + 56 + state.onFontSizeChanged?(baseline) 57 + 58 + var event: TerminalClient.Event? 59 + while let next = await iterator.next() { 60 + if case .fontSizeChanged = next { 61 + event = next 62 + break 63 + } 64 + } 65 + 66 + #expect(event == .fontSizeChanged(nil)) 67 + } 68 + 69 + @Test func duplicateFontSizeChangeIsDeduplicated() async { 70 + let runtime = GhosttyRuntime() 71 + let baseline = runtime.defaultFontSize() 72 + let firstSize = baseline + 2 73 + let secondSize = baseline + 3 74 + let manager = WorktreeTerminalManager(runtime: runtime) 75 + let worktree = makeWorktree() 76 + let state = manager.state(for: worktree) 77 + let stream = manager.eventStream() 78 + var iterator = stream.makeAsyncIterator() 79 + 80 + state.onFontSizeChanged?(firstSize) 81 + state.onFontSizeChanged?(firstSize) 82 + state.onFontSizeChanged?(secondSize) 83 + 84 + var fontSizeEvents: [TerminalClient.Event] = [] 85 + while let next = await iterator.next() { 86 + if case .fontSizeChanged = next { 87 + fontSizeEvents.append(next) 88 + } 89 + if fontSizeEvents.count == 2 { 90 + break 91 + } 92 + } 93 + 94 + #expect(fontSizeEvents == [.fontSizeChanged(firstSize), .fontSizeChanged(secondSize)]) 95 + } 96 + 97 + @Test func cellSizeChangeSkipsFirstEventPerSurface() { 98 + let state = WorktreeTerminalState(runtime: GhosttyRuntime(), worktree: makeWorktree()) 99 + var captured: [Float32?] = [] 100 + let firstSurface = UUID() 101 + let secondSurface = UUID() 102 + 103 + state.onFontSizeChanged = { fontSize in 104 + captured.append(fontSize) 105 + } 106 + 107 + state.handleCellSizeChange(forSurfaceID: firstSurface, fontSize: 13) 108 + state.handleCellSizeChange(forSurfaceID: firstSurface, fontSize: 14) 109 + state.handleCellSizeChange(forSurfaceID: secondSurface, fontSize: 15) 110 + state.handleCellSizeChange(forSurfaceID: secondSurface, fontSize: 16) 111 + 112 + #expect(captured == [14, 16]) 113 + } 114 + 47 115 @Test func notificationIndicatorUsesCurrentCountOnStreamStart() async { 48 116 let manager = WorktreeTerminalManager(runtime: GhosttyRuntime()) 49 117 let worktree = makeWorktree()