native macOS codings agent orchestrator
6
fork

Configure Feed

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

Merge pull request #81 from onevcat/fix/reset-font-size-persistence

Fix font size reset not propagating to new tabs/session

authored by

Wei Wang and committed by
GitHub
504141ff 740a7862

+187 -5
+6
supacode/Features/Repositories/Views/WorktreeDetailView.swift
··· 271 271 } 272 272 273 273 func fontSizeAction(_ bindingAction: String) -> (() -> Void)? { 274 + if bindingAction == "reset_font_size" { 275 + guard hasActiveWorktree else { return nil } 276 + return { 277 + terminalManager.resetFontSizeAcrossStates() 278 + } 279 + } 274 280 if let action = canvasAction({ $0.performBindingActionOnFocusedSurface(bindingAction) }) { 275 281 return action 276 282 }
+12
supacode/Features/Terminal/BusinessLogic/WorktreeTerminalManager.swift
··· 334 334 runtime.backgroundOpacity() 335 335 } 336 336 337 + func resetFontSizeAcrossStates() { 338 + let shouldEmit = preferredFontSize != nil 339 + preferredFontSize = nil 340 + for state in states.values { 341 + state.setDefaultFontSize(nil) 342 + _ = state.performBindingActionOnFocusedSurface("reset_font_size") 343 + } 344 + if shouldEmit { 345 + emit(.fontSizeChanged(nil)) 346 + } 347 + } 348 + 337 349 private func applyFontSize(_ fontSize: Float32?) { 338 350 let normalized = normalizedFontSize(fontSize) 339 351 guard preferredFontSize != normalized else { return }
+36 -5
supacode/Features/Terminal/Models/WorktreeTerminalState.swift
··· 497 497 } 498 498 } 499 499 500 - func performSplitOperation(_ operation: TerminalSplitTreeView.Operation, in tabId: TerminalTabID) 501 - { 500 + func performSplitOperation(_ operation: TerminalSplitTreeView.Operation, in tabId: TerminalTabID) { 502 501 guard var tree = trees[tabId] else { return } 503 502 504 503 switch operation { ··· 651 650 context: ghostty_surface_context_e 652 651 ) -> GhosttySurfaceView { 653 652 let inherited = inheritedSurfaceConfig(fromSurfaceId: inheritingFromSurfaceId, context: context) 653 + let resolvedFontSize = Self.resolvedFontSizeForNewSurface( 654 + defaultFontSize: defaultFontSize, 655 + inheritedFontSize: inherited.fontSize, 656 + context: context 657 + ) 654 658 let view = GhosttySurfaceView( 655 659 runtime: runtime, 656 660 workingDirectory: inherited.workingDirectory ?? worktree.workingDirectory, 657 661 initialInput: initialInput, 658 - fontSize: inherited.fontSize ?? defaultFontSize, 662 + fontSize: resolvedFontSize, 659 663 context: context 660 664 ) 665 + configureBridgeCallbacks(for: view, tabId: tabId) 666 + configureSurfaceCallbacks(for: view, tabId: tabId) 667 + surfaces[view.id] = view 668 + return view 669 + } 670 + 671 + private func configureBridgeCallbacks(for view: GhosttySurfaceView, tabId: TerminalTabID) { 661 672 view.bridge.onTitleChange = { [weak self, weak view] title in 662 673 guard let self, let view else { return } 663 674 if self.focusedSurfaceIdByTab[tabId] == view.id { ··· 694 705 guard let self, let view else { return } 695 706 self.handleCellSizeChange(forSurfaceID: view.id) 696 707 } 708 + view.bridge.onConfigChange = { [weak self, weak view] in 709 + guard let self, let view else { return } 710 + self.handleCellSizeChange(forSurfaceID: view.id) 711 + } 697 712 view.bridge.onDesktopNotification = { [weak self, weak view] title, body in 698 713 guard let self, let view else { return } 699 714 self.appendNotification(title: title, body: body, surfaceId: view.id) ··· 710 725 guard let self else { return } 711 726 self.handlePromptTitle(promptType, tabId: tabId) 712 727 } 728 + } 729 + 730 + private func configureSurfaceCallbacks(for view: GhosttySurfaceView, tabId: TerminalTabID) { 713 731 view.onFocusChange = { [weak self, weak view] focused in 714 732 guard let self, let view, focused else { return } 715 733 self.focusedSurfaceIdByTab[tabId] = view.id ··· 723 741 self.recordKeyInput(forSurfaceID: view.id) 724 742 self.markNotificationsRead(forSurfaceID: view.id) 725 743 } 726 - surfaces[view.id] = view 727 - return view 744 + view.onResetFontSizeShortcut = { [weak self] in 745 + guard let self else { return } 746 + self.onFontSizeChanged?(nil) 747 + } 748 + } 749 + 750 + static func resolvedFontSizeForNewSurface( 751 + defaultFontSize: Float32?, 752 + inheritedFontSize: Float32?, 753 + context: ghostty_surface_context_e 754 + ) -> Float32? { 755 + if context == GHOSTTY_SURFACE_CONTEXT_SPLIT { 756 + return inheritedFontSize ?? defaultFontSize 757 + } 758 + return defaultFontSize 728 759 } 729 760 730 761 private struct InheritedSurfaceConfig: Equatable {
+2
supacode/Infrastructure/Ghostty/GhosttySurfaceBridge.swift
··· 17 17 var onCommandPaletteToggle: (() -> Bool)? 18 18 var onProgressReport: ((ghostty_action_progress_report_state_e) -> Void)? 19 19 var onCellSizeChange: (() -> Void)? 20 + var onConfigChange: (() -> Void)? 20 21 var onDesktopNotification: ((String, String) -> Void)? 21 22 var onCommandFinished: ((Int?, UInt64) -> Void)? 22 23 var onPromptTitle: ((ghostty_action_prompt_title_e) -> Void)? ··· 436 437 437 438 case GHOSTTY_ACTION_CONFIG_CHANGE: 438 439 state.configChangeCount += 1 440 + onConfigChange?() 439 441 return true 440 442 441 443 case GHOSTTY_ACTION_OPEN_CONFIG:
+27
supacode/Infrastructure/Ghostty/GhosttySurfaceView.swift
··· 3 3 import CoreText 4 4 import GhosttyKit 5 5 import QuartzCore 6 + import SwiftUI 6 7 7 8 final class GhosttySurfaceView: NSView, Identifiable { 8 9 struct OcclusionState { ··· 121 122 var onKeyInput: (() -> Void)? 122 123 var onCommittedText: ((String) -> Void)? 123 124 var onMirroredKey: ((MirroredTerminalKey) -> Void)? 125 + var onResetFontSizeShortcut: (() -> Void)? 124 126 125 127 private var accessibilityPaneIndexHelp: String? 126 128 ··· 981 983 982 984 override func performKeyEquivalent(with event: NSEvent) -> Bool { 983 985 guard event.type == .keyDown else { return false } 986 + let isResetFontSizeShortcut = matchesBindingShortcut(event: event, action: "reset_font_size") 984 987 guard let surface else { return false } 985 988 guard focused else { return false } 986 989 ··· 999 1002 return true 1000 1003 } 1001 1004 keyDown(with: event) 1005 + if isResetFontSizeShortcut { 1006 + onResetFontSizeShortcut?() 1007 + } 1002 1008 // Ghostty handled paste internally; broadcast the pasted text to followers. 1003 1009 if onCommittedText != nil, 1004 1010 event.modifierFlags.contains(.command), ··· 1030 1036 return false 1031 1037 } 1032 1038 keyDown(with: finalEvent) 1039 + if isResetFontSizeShortcut { 1040 + onResetFontSizeShortcut?() 1041 + } 1033 1042 return true 1043 + } 1044 + 1045 + private func matchesBindingShortcut(event: NSEvent, action: String) -> Bool { 1046 + guard let shortcut = runtime.keyboardShortcut(for: action) else { return false } 1047 + let normalizedEventModifiers = normalizedModifiers(from: event.modifierFlags) 1048 + guard normalizedEventModifiers == shortcut.modifiers else { return false } 1049 + let eventKey = (event.charactersIgnoringModifiers ?? "").lowercased() 1050 + let shortcutKey = String(shortcut.key.character).lowercased() 1051 + return !eventKey.isEmpty && eventKey == shortcutKey 1052 + } 1053 + 1054 + private func normalizedModifiers(from flags: NSEvent.ModifierFlags) -> SwiftUI.EventModifiers { 1055 + var normalized: SwiftUI.EventModifiers = [] 1056 + if flags.contains(.command) { normalized.insert(.command) } 1057 + if flags.contains(.shift) { normalized.insert(.shift) } 1058 + if flags.contains(.option) { normalized.insert(.option) } 1059 + if flags.contains(.control) { normalized.insert(.control) } 1060 + return normalized 1034 1061 } 1035 1062 1036 1063 private func bindingFlags(
+17
supacodeTests/GhosttySurfaceBridgeTests.swift
··· 29 29 #expect(received?.0 == "Title") 30 30 #expect(received?.1 == "Body") 31 31 } 32 + 33 + @Test func configChangeEmitsCallback() { 34 + let bridge = GhosttySurfaceBridge() 35 + var callbackCount = 0 36 + bridge.onConfigChange = { 37 + callbackCount += 1 38 + } 39 + 40 + var action = ghostty_action_s() 41 + action.tag = GHOSTTY_ACTION_CONFIG_CHANGE 42 + let target = ghostty_target_s() 43 + 44 + _ = bridge.handleAction(target: target, action: action) 45 + 46 + #expect(callbackCount == 1) 47 + #expect(bridge.state.configChangeCount == 1) 48 + } 32 49 }
+49
supacodeTests/WorktreeTerminalManagerTests.swift
··· 94 94 #expect(fontSizeEvents == [.fontSizeChanged(firstSize), .fontSizeChanged(secondSize)]) 95 95 } 96 96 97 + @Test func explicitResetAcrossStatesEmitsNilOverride() async { 98 + let runtime = GhosttyRuntime() 99 + let baseline = runtime.defaultFontSize() 100 + let manager = WorktreeTerminalManager(runtime: runtime, preferredFontSize: baseline + 2) 101 + let worktreeA = Worktree( 102 + id: "/tmp/repo-a/wt-a", 103 + name: "wt-a", 104 + detail: "detail", 105 + workingDirectory: URL(fileURLWithPath: "/tmp/repo-a/wt-a"), 106 + repositoryRootURL: URL(fileURLWithPath: "/tmp/repo-a") 107 + ) 108 + let worktreeB = Worktree( 109 + id: "/tmp/repo-b/wt-b", 110 + name: "wt-b", 111 + detail: "detail", 112 + workingDirectory: URL(fileURLWithPath: "/tmp/repo-b/wt-b"), 113 + repositoryRootURL: URL(fileURLWithPath: "/tmp/repo-b") 114 + ) 115 + _ = manager.state(for: worktreeA) 116 + _ = manager.state(for: worktreeB) 117 + 118 + let stream = manager.eventStream() 119 + var iterator = stream.makeAsyncIterator() 120 + 121 + manager.resetFontSizeAcrossStates() 122 + 123 + var event: TerminalClient.Event? 124 + while let next = await iterator.next() { 125 + if case .fontSizeChanged = next { 126 + event = next 127 + break 128 + } 129 + } 130 + 131 + #expect(event == .fontSizeChanged(nil)) 132 + } 133 + 134 + @Test func explicitResetWhenAlreadyDefaultDoesNotEmitEvent() async { 135 + let manager = WorktreeTerminalManager(runtime: GhosttyRuntime(), preferredFontSize: nil) 136 + _ = manager.state(for: makeWorktree()) 137 + let stream = manager.eventStream() 138 + var iterator = stream.makeAsyncIterator() 139 + 140 + manager.resetFontSizeAcrossStates() 141 + 142 + let first = await iterator.next() 143 + #expect(first == .notificationIndicatorChanged(count: 0)) 144 + } 145 + 97 146 @Test func cellSizeChangeSkipsFirstEventPerSurface() { 98 147 let state = WorktreeTerminalState(runtime: GhosttyRuntime(), worktree: makeWorktree()) 99 148 var captured: [Float32?] = []
+38
supacodeTests/WorktreeTerminalStateFontSizeTests.swift
··· 1 + import GhosttyKit 2 + import Testing 3 + 4 + @testable import supacode 5 + 6 + struct WorktreeTerminalStateFontSizeTests { 7 + @Test func tabContextUsesDefaultFontSizeOnly() { 8 + let resolvedWithDefault = WorktreeTerminalState.resolvedFontSizeForNewSurface( 9 + defaultFontSize: 14, 10 + inheritedFontSize: 18, 11 + context: GHOSTTY_SURFACE_CONTEXT_TAB 12 + ) 13 + #expect(resolvedWithDefault == 14) 14 + 15 + let resolvedWithoutDefault = WorktreeTerminalState.resolvedFontSizeForNewSurface( 16 + defaultFontSize: nil, 17 + inheritedFontSize: 18, 18 + context: GHOSTTY_SURFACE_CONTEXT_TAB 19 + ) 20 + #expect(resolvedWithoutDefault == nil) 21 + } 22 + 23 + @Test func splitContextPrefersInheritedFontSize() { 24 + let resolvedWithInherited = WorktreeTerminalState.resolvedFontSizeForNewSurface( 25 + defaultFontSize: 14, 26 + inheritedFontSize: 18, 27 + context: GHOSTTY_SURFACE_CONTEXT_SPLIT 28 + ) 29 + #expect(resolvedWithInherited == 18) 30 + 31 + let resolvedWithoutInherited = WorktreeTerminalState.resolvedFontSizeForNewSurface( 32 + defaultFontSize: 14, 33 + inheritedFontSize: nil, 34 + context: GHOSTTY_SURFACE_CONTEXT_SPLIT 35 + ) 36 + #expect(resolvedWithoutInherited == 14) 37 + } 38 + }