native macOS codings agent orchestrator
6
fork

Configure Feed

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

Extract GhosttySurfaceBridge and GhosttySurfaceState from GhosttySurfaceView

Separates Ghostty action handling into dedicated bridge and state classes:
- GhosttySurfaceBridge handles C callback routing and action dispatch
- GhosttySurfaceState provides @Observable container for terminal state
- GhosttySurfaceView now owns a bridge and supports initialInput parameter
- Tab titles update dynamically via bridge's onTitleChange callback

khoi b0e1dfc1 5a0e6039

+381 -11
+20 -6
supacode/GhosttyEmbed/GhosttyRuntime.swift
··· 99 99 return Unmanaged<GhosttyRuntime>.fromOpaque(userdata).takeUnretainedValue() 100 100 } 101 101 102 - private static func surfaceView(from userdata: UnsafeMutableRawPointer?) -> GhosttySurfaceView? { 102 + private static func surfaceBridge(fromUserdata userdata: UnsafeMutableRawPointer?) -> GhosttySurfaceBridge? { 103 103 guard let userdata else { return nil } 104 - return Unmanaged<GhosttySurfaceView>.fromOpaque(userdata).takeUnretainedValue() 104 + return Unmanaged<GhosttySurfaceBridge>.fromOpaque(userdata).takeUnretainedValue() 105 + } 106 + 107 + private static func surfaceBridge(fromSurface surface: ghostty_surface_t?) -> GhosttySurfaceBridge? { 108 + guard let surface, let userdata = ghostty_surface_userdata(surface) else { return nil } 109 + return Unmanaged<GhosttySurfaceBridge>.fromOpaque(userdata).takeUnretainedValue() 105 110 } 106 111 107 112 private static func wakeup(_ userdata: UnsafeMutableRawPointer?) { ··· 114 119 private static func action( 115 120 _ app: ghostty_app_t, target: ghostty_target_s, action: ghostty_action_s 116 121 ) -> Bool { 122 + guard target.tag == GHOSTTY_TARGET_SURFACE else { return false } 123 + guard let surface = target.target.surface else { return false } 124 + guard let bridge = surfaceBridge(fromSurface: surface) else { return false } 125 + if Thread.isMainThread { 126 + return bridge.handleAction(target: target, action: action) 127 + } 128 + Task { @MainActor in 129 + _ = bridge.handleAction(target: target, action: action) 130 + } 117 131 return false 118 132 } 119 133 ··· 122 136 location: ghostty_clipboard_e, 123 137 state: UnsafeMutableRawPointer? 124 138 ) { 125 - guard let surfaceView = surfaceView(from: userdata), let surface = surfaceView.surface else { 139 + guard let bridge = surfaceBridge(fromUserdata: userdata), let surface = bridge.surface else { 126 140 return 127 141 } 128 142 let value = NSPasteboard.ghostty(location)?.getOpinionatedStringContents() ?? "" ··· 139 153 state: UnsafeMutableRawPointer?, 140 154 request: ghostty_clipboard_request_e 141 155 ) { 142 - guard let surfaceView = surfaceView(from: userdata), let surface = surfaceView.surface else { 156 + guard let bridge = surfaceBridge(fromUserdata: userdata), let surface = bridge.surface else { 143 157 return 144 158 } 145 159 guard let string else { return } ··· 181 195 } 182 196 183 197 private static func closeSurface(_ userdata: UnsafeMutableRawPointer?, processAlive: Bool) { 184 - guard let surfaceView = surfaceView(from: userdata) else { return } 185 - surfaceView.closeSurface() 198 + guard let bridge = surfaceBridge(fromUserdata: userdata) else { return } 199 + bridge.closeSurface() 186 200 } 187 201 } 188 202
+271
supacode/GhosttyEmbed/GhosttySurfaceBridge.swift
··· 1 + import Foundation 2 + import GhosttyKit 3 + 4 + @MainActor 5 + final class GhosttySurfaceBridge { 6 + let state = GhosttySurfaceState() 7 + var surface: ghostty_surface_t? 8 + weak var surfaceView: GhosttySurfaceView? 9 + var onTitleChange: ((String) -> Void)? 10 + 11 + func handleAction(target: ghostty_target_s, action: ghostty_action_s) -> Bool { 12 + if handleTitleAndPath(action) { return false } 13 + if handleCommandStatus(action) { return false } 14 + if handleMouseAndLink(action) { return false } 15 + if handleSearchAndScroll(action) { return false } 16 + if handleSizeAndKey(action) { return false } 17 + if handleConfigAndShell(action) { return false } 18 + return false 19 + } 20 + 21 + func sendText(_ text: String) { 22 + guard let surface else { return } 23 + text.withCString { ptr in 24 + ghostty_surface_text(surface, ptr, UInt(text.lengthOfBytes(using: .utf8))) 25 + } 26 + } 27 + 28 + func sendCommand(_ command: String) { 29 + let finalCommand = command.hasSuffix("\n") ? command : "\(command)\n" 30 + sendText(finalCommand) 31 + } 32 + 33 + func closeSurface() { 34 + surfaceView?.closeSurface() 35 + } 36 + 37 + private func handleTitleAndPath(_ action: ghostty_action_s) -> Bool { 38 + switch action.tag { 39 + case GHOSTTY_ACTION_SET_TITLE: 40 + if let title = string(from: action.action.set_title.title) { 41 + state.title = title 42 + onTitleChange?(title) 43 + } 44 + return true 45 + 46 + case GHOSTTY_ACTION_PROMPT_TITLE: 47 + state.promptTitle = action.action.prompt_title 48 + return true 49 + 50 + case GHOSTTY_ACTION_PWD: 51 + state.pwd = string(from: action.action.pwd.pwd) 52 + return true 53 + 54 + case GHOSTTY_ACTION_DESKTOP_NOTIFICATION: 55 + let note = action.action.desktop_notification 56 + state.desktopNotificationTitle = string(from: note.title) 57 + state.desktopNotificationBody = string(from: note.body) 58 + return true 59 + 60 + default: 61 + return false 62 + } 63 + } 64 + 65 + private func handleCommandStatus(_ action: ghostty_action_s) -> Bool { 66 + switch action.tag { 67 + case GHOSTTY_ACTION_PROGRESS_REPORT: 68 + let report = action.action.progress_report 69 + state.progressState = report.state 70 + state.progressValue = report.progress == -1 ? nil : Int(report.progress) 71 + return true 72 + 73 + case GHOSTTY_ACTION_COMMAND_FINISHED: 74 + let info = action.action.command_finished 75 + state.commandExitCode = info.exit_code == -1 ? nil : Int(info.exit_code) 76 + state.commandDuration = info.duration 77 + return true 78 + 79 + case GHOSTTY_ACTION_SHOW_CHILD_EXITED: 80 + let info = action.action.child_exited 81 + state.childExitCode = info.exit_code 82 + state.childExitTimeMs = info.timetime_ms 83 + return true 84 + 85 + case GHOSTTY_ACTION_READONLY: 86 + state.readOnly = action.action.readonly 87 + return true 88 + 89 + case GHOSTTY_ACTION_RING_BELL: 90 + state.bellCount += 1 91 + return true 92 + 93 + default: 94 + return false 95 + } 96 + } 97 + 98 + private func handleMouseAndLink(_ action: ghostty_action_s) -> Bool { 99 + switch action.tag { 100 + case GHOSTTY_ACTION_MOUSE_SHAPE: 101 + state.mouseShape = action.action.mouse_shape 102 + return true 103 + 104 + case GHOSTTY_ACTION_MOUSE_VISIBILITY: 105 + state.mouseVisibility = action.action.mouse_visibility 106 + return true 107 + 108 + case GHOSTTY_ACTION_MOUSE_OVER_LINK: 109 + let link = action.action.mouse_over_link 110 + state.mouseOverLink = string(from: link.url, length: link.len) 111 + return true 112 + 113 + case GHOSTTY_ACTION_RENDERER_HEALTH: 114 + state.rendererHealth = action.action.renderer_health 115 + return true 116 + 117 + case GHOSTTY_ACTION_OPEN_URL: 118 + let openUrl = action.action.open_url 119 + state.openUrlKind = openUrl.kind 120 + state.openUrl = string(from: openUrl.url, length: openUrl.len) 121 + return true 122 + 123 + case GHOSTTY_ACTION_COLOR_CHANGE: 124 + let change = action.action.color_change 125 + state.colorChangeKind = change.kind 126 + state.colorChangeR = change.r 127 + state.colorChangeG = change.g 128 + state.colorChangeB = change.b 129 + return true 130 + 131 + default: 132 + return false 133 + } 134 + } 135 + 136 + private func handleSearchAndScroll(_ action: ghostty_action_s) -> Bool { 137 + switch action.tag { 138 + case GHOSTTY_ACTION_SCROLLBAR: 139 + let scroll = action.action.scrollbar 140 + state.scrollbarTotal = scroll.total 141 + state.scrollbarOffset = scroll.offset 142 + state.scrollbarLength = scroll.len 143 + return true 144 + 145 + case GHOSTTY_ACTION_START_SEARCH: 146 + state.searchNeedle = string(from: action.action.start_search.needle) 147 + return true 148 + 149 + case GHOSTTY_ACTION_END_SEARCH: 150 + state.searchNeedle = nil 151 + return true 152 + 153 + case GHOSTTY_ACTION_SEARCH_TOTAL: 154 + state.searchTotal = Int(action.action.search_total.total) 155 + return true 156 + 157 + case GHOSTTY_ACTION_SEARCH_SELECTED: 158 + state.searchSelected = Int(action.action.search_selected.selected) 159 + return true 160 + 161 + default: 162 + return false 163 + } 164 + } 165 + 166 + private func handleSizeAndKey(_ action: ghostty_action_s) -> Bool { 167 + switch action.tag { 168 + case GHOSTTY_ACTION_SIZE_LIMIT: 169 + let sizeLimit = action.action.size_limit 170 + state.sizeLimitMinWidth = sizeLimit.min_width 171 + state.sizeLimitMinHeight = sizeLimit.min_height 172 + state.sizeLimitMaxWidth = sizeLimit.max_width 173 + state.sizeLimitMaxHeight = sizeLimit.max_height 174 + return true 175 + 176 + case GHOSTTY_ACTION_INITIAL_SIZE: 177 + let initial = action.action.initial_size 178 + state.initialSizeWidth = initial.width 179 + state.initialSizeHeight = initial.height 180 + return true 181 + 182 + case GHOSTTY_ACTION_CELL_SIZE: 183 + let cell = action.action.cell_size 184 + state.cellSizeWidth = cell.width 185 + state.cellSizeHeight = cell.height 186 + return true 187 + 188 + case GHOSTTY_ACTION_RESET_WINDOW_SIZE: 189 + state.resetWindowSizeCount += 1 190 + return true 191 + 192 + case GHOSTTY_ACTION_KEY_SEQUENCE: 193 + let seq = action.action.key_sequence 194 + state.keySequenceActive = seq.active 195 + state.keySequenceTrigger = seq.trigger 196 + return true 197 + 198 + case GHOSTTY_ACTION_KEY_TABLE: 199 + let table = action.action.key_table 200 + state.keyTableTag = table.tag 201 + switch table.tag { 202 + case GHOSTTY_KEY_TABLE_ACTIVATE: 203 + state.keyTableName = string(from: table.value.activate.name, length: table.value.activate.len) 204 + default: 205 + state.keyTableName = nil 206 + } 207 + return true 208 + 209 + default: 210 + return false 211 + } 212 + } 213 + 214 + private func handleConfigAndShell(_ action: ghostty_action_s) -> Bool { 215 + switch action.tag { 216 + case GHOSTTY_ACTION_SECURE_INPUT: 217 + state.secureInput = action.action.secure_input 218 + return true 219 + 220 + case GHOSTTY_ACTION_FLOAT_WINDOW: 221 + state.floatWindow = action.action.float_window 222 + return true 223 + 224 + case GHOSTTY_ACTION_RELOAD_CONFIG: 225 + state.reloadConfigSoft = action.action.reload_config.soft 226 + return true 227 + 228 + case GHOSTTY_ACTION_CONFIG_CHANGE: 229 + state.configChangeCount += 1 230 + return true 231 + 232 + case GHOSTTY_ACTION_OPEN_CONFIG: 233 + state.openConfigCount += 1 234 + return true 235 + 236 + case GHOSTTY_ACTION_PRESENT_TERMINAL: 237 + state.presentTerminalCount += 1 238 + return true 239 + 240 + case GHOSTTY_ACTION_CLOSE_TAB: 241 + state.closeTabMode = action.action.close_tab_mode 242 + return true 243 + 244 + case GHOSTTY_ACTION_QUIT_TIMER: 245 + state.quitTimer = action.action.quit_timer 246 + return true 247 + 248 + default: 249 + return false 250 + } 251 + } 252 + 253 + private func string(from pointer: UnsafePointer<CChar>?) -> String? { 254 + guard let pointer else { return nil } 255 + return String(cString: pointer) 256 + } 257 + 258 + private func string(from pointer: UnsafePointer<CChar>?, length: Int) -> String? { 259 + guard let pointer, length > 0 else { return nil } 260 + let data = Data(bytes: pointer, count: length) 261 + return String(data: data, encoding: .utf8) 262 + } 263 + 264 + private func string(from pointer: UnsafePointer<CChar>?, length: UInt) -> String? { 265 + string(from: pointer, length: Int(length)) 266 + } 267 + 268 + private func string(from pointer: UnsafePointer<CChar>?, length: UInt64) -> String? { 269 + string(from: pointer, length: Int(length)) 270 + } 271 + }
+57
supacode/GhosttyEmbed/GhosttySurfaceState.swift
··· 1 + import GhosttyKit 2 + import Observation 3 + 4 + @MainActor 5 + @Observable 6 + final class GhosttySurfaceState { 7 + var title: String? 8 + var pwd: String? 9 + var promptTitle: ghostty_action_prompt_title_e? 10 + var progressState: ghostty_action_progress_report_state_e? 11 + var progressValue: Int? 12 + var commandExitCode: Int? 13 + var commandDuration: UInt64? 14 + var childExitCode: UInt32? 15 + var childExitTimeMs: UInt64? 16 + var readOnly: ghostty_action_readonly_e? 17 + var mouseShape: ghostty_action_mouse_shape_e? 18 + var mouseVisibility: ghostty_action_mouse_visibility_e? 19 + var mouseOverLink: String? 20 + var rendererHealth: ghostty_action_renderer_health_e? 21 + var openUrl: String? 22 + var openUrlKind: ghostty_action_open_url_kind_e? 23 + var colorChangeKind: ghostty_action_color_kind_e? 24 + var colorChangeR: UInt8? 25 + var colorChangeG: UInt8? 26 + var colorChangeB: UInt8? 27 + var scrollbarTotal: UInt64? 28 + var scrollbarOffset: UInt64? 29 + var scrollbarLength: UInt64? 30 + var searchNeedle: String? 31 + var searchTotal: Int? 32 + var searchSelected: Int? 33 + var sizeLimitMinWidth: UInt32? 34 + var sizeLimitMinHeight: UInt32? 35 + var sizeLimitMaxWidth: UInt32? 36 + var sizeLimitMaxHeight: UInt32? 37 + var initialSizeWidth: UInt32? 38 + var initialSizeHeight: UInt32? 39 + var cellSizeWidth: UInt32? 40 + var cellSizeHeight: UInt32? 41 + var keySequenceActive: Bool? 42 + var keySequenceTrigger: ghostty_input_trigger_s? 43 + var keyTableTag: ghostty_action_key_table_tag_e? 44 + var keyTableName: String? 45 + var secureInput: ghostty_action_secure_input_e? 46 + var floatWindow: ghostty_action_float_window_e? 47 + var reloadConfigSoft: Bool? 48 + var configChangeCount: Int = 0 49 + var bellCount: Int = 0 50 + var openConfigCount: Int = 0 51 + var presentTerminalCount: Int = 0 52 + var resetWindowSizeCount: Int = 0 53 + var desktopNotificationTitle: String? 54 + var desktopNotificationBody: String? 55 + var closeTabMode: ghostty_action_close_tab_mode_e? 56 + var quitTimer: ghostty_action_quit_timer_e? 57 + }
+17 -2
supacode/GhosttyEmbed/GhosttySurfaceView.swift
··· 3 3 4 4 final class GhosttySurfaceView: NSView { 5 5 private let runtime: GhosttyRuntime 6 + let bridge: GhosttySurfaceBridge 6 7 private(set) var surface: ghostty_surface_t? 7 8 private let workingDirectoryCString: UnsafeMutablePointer<CChar>? 9 + private let initialInputCString: UnsafeMutablePointer<CChar>? 8 10 private var trackingArea: NSTrackingArea? 9 11 private var lastBackingSize: CGSize = .zero 10 12 private var pendingFocus = false 11 13 12 14 override var acceptsFirstResponder: Bool { true } 13 15 14 - init(runtime: GhosttyRuntime, workingDirectory: URL?) { 16 + init(runtime: GhosttyRuntime, workingDirectory: URL?, initialInput: String? = nil) { 15 17 self.runtime = runtime 18 + self.bridge = GhosttySurfaceBridge() 16 19 if let workingDirectory { 17 20 let path = workingDirectory.path(percentEncoded: false) 18 21 workingDirectoryCString = path.withCString { strdup($0) } 19 22 } else { 20 23 workingDirectoryCString = nil 21 24 } 25 + if let initialInput { 26 + initialInputCString = initialInput.withCString { strdup($0) } 27 + } else { 28 + initialInputCString = nil 29 + } 22 30 super.init(frame: NSRect(x: 0, y: 0, width: 800, height: 600)) 23 31 wantsLayer = true 32 + bridge.surfaceView = self 24 33 createSurface() 25 34 } 26 35 ··· 33 42 if let workingDirectoryCString { 34 43 free(workingDirectoryCString) 35 44 } 45 + if let initialInputCString { 46 + free(initialInputCString) 47 + } 36 48 } 37 49 38 50 func closeSurface() { 39 51 if let surface { 40 52 ghostty_surface_free(surface) 41 53 self.surface = nil 54 + bridge.surface = nil 42 55 } 43 56 } 44 57 ··· 203 216 private func createSurface() { 204 217 guard let app = runtime.app else { return } 205 218 var config = ghostty_surface_config_new() 206 - config.userdata = Unmanaged.passUnretained(self).toOpaque() 219 + config.userdata = Unmanaged.passUnretained(bridge).toOpaque() 207 220 config.platform_tag = GHOSTTY_PLATFORM_MACOS 208 221 config.platform = ghostty_platform_u( 209 222 macos: ghostty_platform_macos_s( ··· 211 224 )) 212 225 config.scale_factor = backingScaleFactor() 213 226 config.working_directory = workingDirectoryCString.map { UnsafePointer($0) } 227 + config.initial_input = initialInputCString.map { UnsafePointer($0) } 214 228 config.context = GHOSTTY_SURFACE_CONTEXT_WINDOW 215 229 surface = ghostty_surface_new(app, &config) 230 + bridge.surface = surface 216 231 updateSurfaceSize() 217 232 } 218 233
+16 -3
supacode/Terminals/WorktreeTerminalState.swift
··· 52 52 return nil 53 53 } 54 54 controller.selectTab(tabId) 55 - surfaceView(for: tabId).requestFocus() 55 + surfaceView(for: tabId, initialInput: "echo \(title)\n").requestFocus() 56 56 return tabId 57 57 } 58 58 ··· 70 70 return closed 71 71 } 72 72 73 - func surfaceView(for tabId: TabID) -> GhosttySurfaceView { 73 + func surfaceView(for tabId: TabID, initialInput: String? = nil) -> GhosttySurfaceView { 74 74 if let existing = surfaces[tabId] { 75 75 return existing 76 76 } 77 - let view = GhosttySurfaceView(runtime: runtime, workingDirectory: worktree.workingDirectory) 77 + let resolvedInput = initialInput ?? defaultInitialInput(for: tabId) 78 + let view = GhosttySurfaceView( 79 + runtime: runtime, 80 + workingDirectory: worktree.workingDirectory, 81 + initialInput: resolvedInput 82 + ) 83 + view.bridge.onTitleChange = { [weak controller = controller] title in 84 + controller?.updateTab(tabId, title: title, icon: "terminal") 85 + } 78 86 surfaces[tabId] = view 79 87 return view 88 + } 89 + 90 + private func defaultInitialInput(for tabId: TabID) -> String? { 91 + guard let title = controller.tab(tabId)?.title else { return nil } 92 + return "echo \(title)\n" 80 93 } 81 94 82 95 func closeAllSurfaces() {