diff --git a/supacode/Features/Terminal/Models/WorktreeTerminalState.swift b/supacode/Features/Terminal/Models/WorktreeTerminalState.swift index f2773cbf..17b6398c 100644 --- a/supacode/Features/Terminal/Models/WorktreeTerminalState.swift +++ b/supacode/Features/Terminal/Models/WorktreeTerminalState.swift @@ -651,11 +651,16 @@ final class WorktreeTerminalState { 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 @@ final class WorktreeTerminalState { 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,10 +732,25 @@ final class WorktreeTerminalState { 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 { let workingDirectory: URL? let fontSize: Float32? diff --git a/supacode/Infrastructure/Ghostty/GhosttySurfaceBridge.swift b/supacode/Infrastructure/Ghostty/GhosttySurfaceBridge.swift index 799b84b0..7f1b4ad9 100644 --- a/supacode/Infrastructure/Ghostty/GhosttySurfaceBridge.swift +++ b/supacode/Infrastructure/Ghostty/GhosttySurfaceBridge.swift @@ -17,6 +17,7 @@ final class GhosttySurfaceBridge { 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 @@ final class GhosttySurfaceBridge { 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 index 0aab8e68..508173a6 100644 --- a/supacode/Infrastructure/Ghostty/GhosttySurfaceView.swift +++ b/supacode/Infrastructure/Ghostty/GhosttySurfaceView.swift @@ -3,6 +3,7 @@ import Carbon import CoreText import GhosttyKit import QuartzCore +import SwiftUI final class GhosttySurfaceView: NSView, Identifiable { struct OcclusionState { @@ -121,6 +122,7 @@ final class GhosttySurfaceView: NSView, Identifiable { var onKeyInput: (() -> Void)? var onCommittedText: ((String) -> Void)? var onMirroredKey: ((MirroredTerminalKey) -> Void)? + var onResetFontSizeShortcut: (() -> Void)? private var accessibilityPaneIndexHelp: String? @@ -981,6 +983,7 @@ final class GhosttySurfaceView: NSView, Identifiable { 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 @@ final class GhosttySurfaceView: NSView, Identifiable { 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,9 +1036,30 @@ final class GhosttySurfaceView: NSView, Identifiable { 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( for event: NSEvent, surface: ghostty_surface_t diff --git a/supacodeTests/GhosttySurfaceBridgeTests.swift b/supacodeTests/GhosttySurfaceBridgeTests.swift index 7955ac78..0d8bec15 100644 --- a/supacodeTests/GhosttySurfaceBridgeTests.swift +++ b/supacodeTests/GhosttySurfaceBridgeTests.swift @@ -29,4 +29,21 @@ struct GhosttySurfaceBridgeTests { #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 index 00000000..3921f49b --- /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) + } +}