From 2af0263e563bbd595c08fcb6d6f4ef76b20cbfd4 Mon Sep 17 00:00:00 2001 From: onevcat Date: Fri, 27 Mar 2026 17:26:35 +0000 Subject: [PATCH] Fix Cmd+0 font reset persistence with minimal state sync --- supacodeTests/GhosttySurfaceBridgeTests.swift | 17 +++++++++++++++++ supacodeTests/WorktreeTerminalStateFontSizeTests.swift | 38 ++++++++++++++++++++++++++++++++++++++ supacode/Infrastructure/Ghostty/GhosttySurfaceBridge.swift | 2 ++ supacode/Infrastructure/Ghostty/GhosttySurfaceView.swift | 27 +++++++++++++++++++++++++++ supacode/Features/Terminal/Models/WorktreeTerminalState.swift | 26 +++++++++++++++++++++++++- 5 file(s) changed, 109 insertion(s)(+), 1 deletion(s)(-) diff --git a/supacodeTests/GhosttySurfaceBridgeTests.swift b/supacodeTests/GhosttySurfaceBridgeTests.swift --- a/supacodeTests/GhosttySurfaceBridgeTests.swift +++ b/supacodeTests/GhosttySurfaceBridgeTests.swift @@ -29,4 +29,21 @@ #expect(received?.0 == "Title") #expect(received?.1 == "Body") } + + @Test func configChangeEmitsCallback() { + let bridge = GhosttySurfaceBridge() + var callbackCount = 0 + bridge.onConfigChange = { + callbackCount += 1 + } + + var action = ghostty_action_s() + action.tag = GHOSTTY_ACTION_CONFIG_CHANGE + let target = ghostty_target_s() + + _ = bridge.handleAction(target: target, action: action) + + #expect(callbackCount == 1) + #expect(bridge.state.configChangeCount == 1) + } } diff --git a/supacodeTests/WorktreeTerminalStateFontSizeTests.swift b/supacodeTests/WorktreeTerminalStateFontSizeTests.swift new file mode 100644 --- /dev/null +++ b/supacodeTests/WorktreeTerminalStateFontSizeTests.swift @@ -0,0 +1,38 @@ +import GhosttyKit +import Testing + +@testable import supacode + +struct WorktreeTerminalStateFontSizeTests { + @Test func tabContextUsesDefaultFontSizeOnly() { + let resolvedWithDefault = WorktreeTerminalState.resolvedFontSizeForNewSurface( + defaultFontSize: 14, + inheritedFontSize: 18, + context: GHOSTTY_SURFACE_CONTEXT_TAB + ) + #expect(resolvedWithDefault == 14) + + let resolvedWithoutDefault = WorktreeTerminalState.resolvedFontSizeForNewSurface( + defaultFontSize: nil, + inheritedFontSize: 18, + context: GHOSTTY_SURFACE_CONTEXT_TAB + ) + #expect(resolvedWithoutDefault == nil) + } + + @Test func splitContextPrefersInheritedFontSize() { + let resolvedWithInherited = WorktreeTerminalState.resolvedFontSizeForNewSurface( + defaultFontSize: 14, + inheritedFontSize: 18, + context: GHOSTTY_SURFACE_CONTEXT_SPLIT + ) + #expect(resolvedWithInherited == 18) + + let resolvedWithoutInherited = WorktreeTerminalState.resolvedFontSizeForNewSurface( + defaultFontSize: 14, + inheritedFontSize: nil, + context: GHOSTTY_SURFACE_CONTEXT_SPLIT + ) + #expect(resolvedWithoutInherited == 14) + } +} diff --git a/supacode/Infrastructure/Ghostty/GhosttySurfaceBridge.swift b/supacode/Infrastructure/Ghostty/GhosttySurfaceBridge.swift --- a/supacode/Infrastructure/Ghostty/GhosttySurfaceBridge.swift +++ b/supacode/Infrastructure/Ghostty/GhosttySurfaceBridge.swift @@ -17,6 +17,7 @@ var onCommandPaletteToggle: (() -> Bool)? var onProgressReport: ((ghostty_action_progress_report_state_e) -> Void)? var onCellSizeChange: (() -> Void)? + var onConfigChange: (() -> Void)? var onDesktopNotification: ((String, String) -> Void)? var onCommandFinished: ((Int?, UInt64) -> Void)? var onPromptTitle: ((ghostty_action_prompt_title_e) -> Void)? @@ -436,6 +437,7 @@ case GHOSTTY_ACTION_CONFIG_CHANGE: state.configChangeCount += 1 + onConfigChange?() return true case GHOSTTY_ACTION_OPEN_CONFIG: diff --git a/supacode/Infrastructure/Ghostty/GhosttySurfaceView.swift b/supacode/Infrastructure/Ghostty/GhosttySurfaceView.swift --- a/supacode/Infrastructure/Ghostty/GhosttySurfaceView.swift +++ b/supacode/Infrastructure/Ghostty/GhosttySurfaceView.swift @@ -3,6 +3,7 @@ import CoreText import GhosttyKit import QuartzCore +import SwiftUI final class GhosttySurfaceView: NSView, Identifiable { struct OcclusionState { @@ -121,6 +122,7 @@ var onKeyInput: (() -> Void)? var onCommittedText: ((String) -> Void)? var onMirroredKey: ((MirroredTerminalKey) -> Void)? + var onResetFontSizeShortcut: (() -> Void)? private var accessibilityPaneIndexHelp: String? @@ -981,6 +983,7 @@ override func performKeyEquivalent(with event: NSEvent) -> Bool { guard event.type == .keyDown else { return false } + let isResetFontSizeShortcut = matchesBindingShortcut(event: event, action: "reset_font_size") guard let surface else { return false } guard focused else { return false } @@ -999,6 +1002,9 @@ return true } keyDown(with: event) + if isResetFontSizeShortcut { + onResetFontSizeShortcut?() + } // Ghostty handled paste internally; broadcast the pasted text to followers. if onCommittedText != nil, event.modifierFlags.contains(.command), @@ -1030,7 +1036,28 @@ return false } keyDown(with: finalEvent) + if isResetFontSizeShortcut { + onResetFontSizeShortcut?() + } return true + } + + private func matchesBindingShortcut(event: NSEvent, action: String) -> Bool { + guard let shortcut = runtime.keyboardShortcut(for: action) else { return false } + let normalizedEventModifiers = normalizedModifiers(from: event.modifierFlags) + guard normalizedEventModifiers == shortcut.modifiers else { return false } + let eventKey = (event.charactersIgnoringModifiers ?? "").lowercased() + let shortcutKey = String(shortcut.key.character).lowercased() + return !eventKey.isEmpty && eventKey == shortcutKey + } + + private func normalizedModifiers(from flags: NSEvent.ModifierFlags) -> SwiftUI.EventModifiers { + var normalized: SwiftUI.EventModifiers = [] + if flags.contains(.command) { normalized.insert(.command) } + if flags.contains(.shift) { normalized.insert(.shift) } + if flags.contains(.option) { normalized.insert(.option) } + if flags.contains(.control) { normalized.insert(.control) } + return normalized } private func bindingFlags( diff --git a/supacode/Features/Terminal/Models/WorktreeTerminalState.swift b/supacode/Features/Terminal/Models/WorktreeTerminalState.swift --- a/supacode/Features/Terminal/Models/WorktreeTerminalState.swift +++ b/supacode/Features/Terminal/Models/WorktreeTerminalState.swift @@ -651,11 +651,16 @@ context: ghostty_surface_context_e ) -> GhosttySurfaceView { let inherited = inheritedSurfaceConfig(fromSurfaceId: inheritingFromSurfaceId, context: context) + let resolvedFontSize = Self.resolvedFontSizeForNewSurface( + defaultFontSize: defaultFontSize, + inheritedFontSize: inherited.fontSize, + context: context + ) let view = GhosttySurfaceView( runtime: runtime, workingDirectory: inherited.workingDirectory ?? worktree.workingDirectory, initialInput: initialInput, - fontSize: inherited.fontSize ?? defaultFontSize, + fontSize: resolvedFontSize, context: context ) view.bridge.onTitleChange = { [weak self, weak view] title in @@ -694,6 +699,10 @@ guard let self, let view else { return } self.handleCellSizeChange(forSurfaceID: view.id) } + view.bridge.onConfigChange = { [weak self, weak view] in + guard let self, let view else { return } + self.handleCellSizeChange(forSurfaceID: view.id) + } view.bridge.onDesktopNotification = { [weak self, weak view] title, body in guard let self, let view else { return } self.appendNotification(title: title, body: body, surfaceId: view.id) @@ -723,8 +732,23 @@ self.recordKeyInput(forSurfaceID: view.id) self.markNotificationsRead(forSurfaceID: view.id) } + view.onResetFontSizeShortcut = { [weak self] in + guard let self else { return } + self.onFontSizeChanged?(nil) + } surfaces[view.id] = view return view + } + + static func resolvedFontSizeForNewSurface( + defaultFontSize: Float32?, + inheritedFontSize: Float32?, + context: ghostty_surface_context_e + ) -> Float32? { + if context == GHOSTTY_SURFACE_CONTEXT_SPLIT { + return inheritedFontSize ?? defaultFontSize + } + return defaultFontSize } private struct InheritedSurfaceConfig: Equatable { -- tangled.sh