import AppKit import CGhostty // MARK: - Hilfen /// Ruft `body` mit einem C-String auf (oder nil, wenn der String nil ist). /// Der Zeiger ist nur innerhalb von `body` gültig. private func withOptionalCString(_ s: String?, _ body: (UnsafePointer?) -> R) -> R { if let s = s { return s.withCString { body($0) } } return body(nil) } // MARK: - libghostty-App /// Kapselt die globale libghostty-App: einmalige Initialisierung, geladene /// Konfiguration, Runtime-Callbacks und der Event-Tick. Eine Instanz pro Prozess. final class GhosttyApp { private(set) var app: ghostty_app_t! private(set) var config: ghostty_config_t! private static var didInit = false init?() { if !GhosttyApp.didInit { _ = ghostty_init(UInt(CommandLine.argc), CommandLine.unsafeArgv) GhosttyApp.didInit = true } guard let cfg = ghostty_config_new() else { return nil } ghostty_config_load_default_files(cfg) ghostty_config_load_recursive_files(cfg) ghostty_config_finalize(cfg) self.config = cfg var runtime = ghostty_runtime_config_s( userdata: Unmanaged.passUnretained(self).toOpaque(), supports_selection_clipboard: false, wakeup_cb: { userdata in guard let userdata = userdata else { return } let me = Unmanaged.fromOpaque(userdata).takeUnretainedValue() // libghostty signalisiert Arbeit -> Tick auf dem Main-Thread. DispatchQueue.main.async { me.tick() } }, action_cb: { _, _, _ in // Minimal: keine App-Aktionen (neue Fenster, Tabs, …) behandeln. return false }, read_clipboard_cb: { _, _, _ in return false }, confirm_read_clipboard_cb: { _, _, _, _ in }, write_clipboard_cb: { _, _, _, _, _ in }, close_surface_cb: { _, _ in DispatchQueue.main.async { NSApp.terminate(nil) } } ) guard let app = ghostty_app_new(&runtime, cfg) else { ghostty_config_free(cfg) return nil } self.app = app ghostty_app_set_focus(app, NSApp.isActive) } func tick() { guard let app = app else { return } ghostty_app_tick(app) } func setFocus(_ focused: Bool) { guard let app = app else { return } ghostty_app_set_focus(app, focused) } deinit { if let app = app { ghostty_app_free(app) } if let config = config { ghostty_config_free(config) } } } // MARK: - Surface-View /// NSView, in die libghostty per Metal rendert. libghostty erzeugt seinen /// CAMetalLayer selbst aus dem übergebenen NSView-Zeiger; wir leiten Größe, /// Skalierung, Fokus und Eingaben weiter. final class GhosttySurfaceView: NSView { private let ghostty: GhosttyApp private let appConfig: AppConfig private var surface: ghostty_surface_t! init(ghostty: GhosttyApp, appConfig: AppConfig, frame: NSRect) { self.ghostty = ghostty self.appConfig = appConfig super.init(frame: frame) wantsLayer = true layerContentsRedrawPolicy = .duringViewResize createSurface() } required init?(coder: NSCoder) { fatalError("init(coder:) not supported") } override var acceptsFirstResponder: Bool { true } override var isFlipped: Bool { true } // MARK: Surface-Lifecycle private func createSurface() { var c = ghostty_surface_config_new() c.platform_tag = GHOSTTY_PLATFORM_MACOS c.platform = ghostty_platform_u( macos: ghostty_platform_macos_s( nsview: Unmanaged.passUnretained(self).toOpaque())) c.userdata = Unmanaged.passUnretained(self).toOpaque() c.scale_factor = Double(window?.backingScaleFactor ?? NSScreen.main?.backingScaleFactor ?? 2.0) c.font_size = Float(appConfig.fontSize) c.context = GHOSTTY_SURFACE_CONTEXT_WINDOW withOptionalCString(surfaceCommand()) { cmd in withOptionalCString(appConfig.workingDirectory) { wd in c.command = cmd c.working_directory = wd self.surface = ghostty_surface_new(self.ghostty.app, &c) } } } /// Login-Shell-Verhalten wie in der SwiftTerm-Variante: leeres MTCommand => /// nil (libghostty startet selbst eine Login-Shell). Sonst Befehl ausführen /// und danach in eine interaktive Login-Shell übergehen, damit das Fenster /// offen bleibt. private func surfaceCommand() -> String? { let cmd = appConfig.command.trimmingCharacters(in: .whitespacesAndNewlines) if cmd.isEmpty { return nil } let shell = ProcessInfo.processInfo.environment["SHELL"].flatMap { $0.isEmpty ? nil : $0 } ?? "/bin/zsh" return "\(shell) -lc '\(cmd); exec \(shell) -l'" } // MARK: Größe / Skalierung override func setFrameSize(_ newSize: NSSize) { super.setFrameSize(newSize) updateSurfaceSize() } override func viewDidMoveToWindow() { super.viewDidMoveToWindow() updateSurfaceSize() if let window = window { ghostty_surface_set_content_scale(surface, Double(window.backingScaleFactor), Double(window.backingScaleFactor)) } } override func viewDidChangeBackingProperties() { super.viewDidChangeBackingProperties() guard let surface = surface, let window = window else { return } ghostty_surface_set_content_scale(surface, Double(window.backingScaleFactor), Double(window.backingScaleFactor)) updateSurfaceSize() } private func updateSurfaceSize() { guard let surface = surface else { return } let backing = convertToBacking(bounds).size let w = UInt32(max(0, backing.width)) let h = UInt32(max(0, backing.height)) ghostty_surface_set_size(surface, w, h) } // MARK: Fokus override func becomeFirstResponder() -> Bool { let ok = super.becomeFirstResponder() if let surface = surface { ghostty_surface_set_focus(surface, true) } return ok } override func resignFirstResponder() -> Bool { let ok = super.resignFirstResponder() if let surface = surface { ghostty_surface_set_focus(surface, false) } return ok } // MARK: Tastatur override func keyDown(with event: NSEvent) { // Shift+Return / Shift+Enter (Numpad): Zeilenvorschub statt Submit, // damit CLIs wie Claude Code mehrzeilige Eingaben erlauben. let isReturn = (event.keyCode == 36 || event.keyCode == 76) let mods = event.modifierFlags.intersection(.deviceIndependentFlagsMask) if isReturn && mods == .shift { "\n".withCString { ghostty_surface_text(surface, $0, UInt(strlen($0))) } return } sendKey(event, action: event.isARepeat ? GHOSTTY_ACTION_REPEAT : GHOSTTY_ACTION_PRESS) } override func keyUp(with event: NSEvent) { sendKey(event, action: GHOSTTY_ACTION_RELEASE) } override func flagsChanged(with event: NSEvent) { var k = ghostty_input_key_s() k.action = GHOSTTY_ACTION_PRESS k.mods = Self.translateMods(event.modifierFlags) k.keycode = UInt32(event.keyCode) _ = ghostty_surface_key(surface, k) } private func sendKey(_ event: NSEvent, action: ghostty_input_action_e) { guard let surface = surface else { return } var k = ghostty_input_key_s() k.action = action k.mods = Self.translateMods(event.modifierFlags) k.keycode = UInt32(event.keyCode) k.unshifted_codepoint = (event.charactersIgnoringModifiers?.unicodeScalars.first?.value) ?? 0 let text = event.characters ?? "" text.withCString { ptr in k.text = ptr _ = ghostty_surface_key(surface, k) } } private static func translateMods(_ flags: NSEvent.ModifierFlags) -> ghostty_input_mods_e { var m: UInt32 = GHOSTTY_MODS_NONE.rawValue if flags.contains(.shift) { m |= GHOSTTY_MODS_SHIFT.rawValue } if flags.contains(.control) { m |= GHOSTTY_MODS_CTRL.rawValue } if flags.contains(.option) { m |= GHOSTTY_MODS_ALT.rawValue } if flags.contains(.command) { m |= GHOSTTY_MODS_SUPER.rawValue } if flags.contains(.capsLock) { m |= GHOSTTY_MODS_CAPS.rawValue } return ghostty_input_mods_e(rawValue: m) } // MARK: Maus private func mousePos(_ event: NSEvent) -> (Double, Double) { let p = convert(event.locationInWindow, from: nil) return (Double(p.x), Double(p.y)) } override func mouseDown(with event: NSEvent) { mouseButton(event, .press, GHOSTTY_MOUSE_LEFT) } override func mouseUp(with event: NSEvent) { mouseButton(event, .release, GHOSTTY_MOUSE_LEFT) } override func rightMouseDown(with event: NSEvent) { mouseButton(event, .press, GHOSTTY_MOUSE_RIGHT) } override func rightMouseUp(with event: NSEvent) { mouseButton(event, .release, GHOSTTY_MOUSE_RIGHT) } override func otherMouseDown(with event: NSEvent) { mouseButton(event, .press, GHOSTTY_MOUSE_MIDDLE) } override func otherMouseUp(with event: NSEvent) { mouseButton(event, .release, GHOSTTY_MOUSE_MIDDLE) } private enum MouseAction { case press, release } private func mouseButton(_ event: NSEvent, _ a: MouseAction, _ button: ghostty_input_mouse_button_e) { guard let surface = surface else { return } let state: ghostty_input_mouse_state_e = (a == .press) ? GHOSTTY_MOUSE_PRESS : GHOSTTY_MOUSE_RELEASE _ = ghostty_surface_mouse_button(surface, state, button, Self.translateMods(event.modifierFlags)) } override func mouseMoved(with event: NSEvent) { mouseMove(event) } override func mouseDragged(with event: NSEvent) { mouseMove(event) } override func rightMouseDragged(with event: NSEvent) { mouseMove(event) } override func otherMouseDragged(with event: NSEvent) { mouseMove(event) } private func mouseMove(_ event: NSEvent) { guard let surface = surface else { return } let (x, y) = mousePos(event) ghostty_surface_mouse_pos(surface, x, y, Self.translateMods(event.modifierFlags)) } override func scrollWheel(with event: NSEvent) { guard let surface = surface else { return } // scroll_mods: einfacher Modus, 0 = keine Präzision/Momentum-Flags. ghostty_surface_mouse_scroll(surface, Double(event.scrollingDeltaX), Double(event.scrollingDeltaY), 0) } override func updateTrackingAreas() { super.updateTrackingAreas() trackingAreas.forEach { removeTrackingArea($0) } let area = NSTrackingArea( rect: bounds, options: [.activeInKeyWindow, .inVisibleRect, .mouseMoved, .mouseEnteredAndExited], owner: self, userInfo: nil) addTrackingArea(area) } deinit { if let surface = surface { ghostty_surface_free(surface) } } }