diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 94173ac54..7322c3a08 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ 857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 857F63802A5E64F200CA4815 /* MainMenu.xib */; }; A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = A514C8D52B54A16400493A16 /* Ghostty.Config.swift */; }; A514C8D72B54A16400493A16 /* Ghostty.Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = A514C8D52B54A16400493A16 /* Ghostty.Config.swift */; }; + A514C8D82B54DC6800493A16 /* Ghostty.App.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */; }; A51B78472AF4B58B00F3EDB9 /* TerminalWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51B78462AF4B58B00F3EDB9 /* TerminalWindow.swift */; }; A51BFC1E2B2FB5CE00E92F16 /* About.xib in Resources */ = {isa = PBXBuildFile; fileRef = A51BFC1D2B2FB5CE00E92F16 /* About.xib */; }; A51BFC202B2FB64F00E92F16 /* AboutController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51BFC1F2B2FB64F00E92F16 /* AboutController.swift */; }; @@ -28,7 +29,6 @@ A53D0C9B2B543F3B00305CE6 /* Ghostty.App.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */; }; A53D0C9C2B543F7B00305CE6 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BB729B6F53A0055DE60 /* Package.swift */; }; A55685E029A03A9F004303CE /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55685DF29A03A9F004303CE /* AppError.swift */; }; - A55B7BB629B6F47F0055DE60 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BB529B6F47F0055DE60 /* AppState.swift */; }; A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BB729B6F53A0055DE60 /* Package.swift */; }; A55B7BBC29B6FC330055DE60 /* SurfaceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BBB29B6FC330055DE60 /* SurfaceView.swift */; }; A56B880B2A840447007A0E29 /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A56B880A2A840447007A0E29 /* Carbon.framework */; }; @@ -80,7 +80,6 @@ A53D0C932B53B43700305CE6 /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = ""; }; A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.App.swift; sourceTree = ""; }; A55685DF29A03A9F004303CE /* AppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = ""; }; - A55B7BB529B6F47F0055DE60 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = ""; }; A55B7BB729B6F53A0055DE60 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; A55B7BBB29B6FC330055DE60 /* SurfaceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurfaceView.swift; sourceTree = ""; }; A56B880A2A840447007A0E29 /* Carbon.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Carbon.framework; path = System/Library/Frameworks/Carbon.framework; sourceTree = SDKROOT; }; @@ -236,7 +235,6 @@ isa = PBXGroup; children = ( A55B7BB729B6F53A0055DE60 /* Package.swift */, - A55B7BB529B6F47F0055DE60 /* AppState.swift */, A55B7BBB29B6FC330055DE60 /* SurfaceView.swift */, A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */, A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */, @@ -467,7 +465,6 @@ A5FEB3002ABB69450068369E /* main.swift in Sources */, A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */, A51B78472AF4B58B00F3EDB9 /* TerminalWindow.swift in Sources */, - A55B7BB629B6F47F0055DE60 /* AppState.swift in Sources */, A5CEAFDC29B8009000646FDA /* SplitView.swift in Sources */, A5CDF1932AAF9E0800513312 /* ConfigurationErrorsController.swift in Sources */, A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */, @@ -480,6 +477,7 @@ A596309E2AEE1D6C00D64628 /* TerminalView.swift in Sources */, A5CEAFDE29B8058B00646FDA /* SplitView.Divider.swift in Sources */, A5E112972AF7401B00C6E0C2 /* ClipboardConfirmationView.swift in Sources */, + A514C8D82B54DC6800493A16 /* Ghostty.App.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 50f7f5599..348b8aceb 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -8,7 +8,7 @@ class AppDelegate: NSObject, ObservableObject, NSApplicationDelegate, UNUserNotificationCenterDelegate, - GhosttyAppStateDelegate + GhosttyAppDelegate { // The application logger. We should probably move this at some point to a dedicated // class/struct but for now it lives here! 🤷‍♂️ @@ -62,7 +62,7 @@ class AppDelegate: NSObject, private var applicationHasBecomeActive: Bool = false /// The ghostty global state. Only one per process. - let ghostty: Ghostty.AppState = Ghostty.AppState() + let ghostty: Ghostty.App = Ghostty.App() /// Manages our terminal windows. let terminalManager: TerminalManager @@ -338,7 +338,7 @@ class AppDelegate: NSObject, withCompletionHandler(options) } - //MARK: - GhosttyAppStateDelegate + //MARK: - GhosttyAppDelegate func findSurface(forUUID uuid: UUID) -> Ghostty.SurfaceView? { for c in terminalManager.windows { @@ -350,7 +350,7 @@ class AppDelegate: NSObject, return nil } - func configDidReload(_ state: Ghostty.AppState) { + func configDidReload(_ state: Ghostty.App) { // Depending on the "window-save-state" setting we have to set the NSQuitAlwaysKeepsWindows // configuration. This is the only way to carefully control whether macOS invokes the // state restoration system. diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 4db4d715b..27d42ef96 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -11,7 +11,7 @@ class TerminalController: NSWindowController, NSWindowDelegate, override var windowNibName: NSNib.Name? { "Terminal" } /// The app instance that this terminal view will represent. - let ghostty: Ghostty.AppState + let ghostty: Ghostty.App /// The currently focused surface. var focusedSurface: Ghostty.SurfaceView? = nil @@ -46,7 +46,7 @@ class TerminalController: NSWindowController, NSWindowDelegate, /// changes in the list. private var tabWindowsHash: Int = 0 - init(_ ghostty: Ghostty.AppState, + init(_ ghostty: Ghostty.App, withBaseConfig base: Ghostty.SurfaceConfiguration? = nil, withSurfaceTree tree: Ghostty.SplitNode? = nil ) { @@ -502,7 +502,7 @@ class TerminalController: NSWindowController, NSWindowDelegate, str = cc.contents } - Ghostty.AppState.completeClipboardRequest(cc.surface, data: str, state: cc.state, confirmed: true) + Ghostty.App.completeClipboardRequest(cc.surface, data: str, state: cc.state, confirmed: true) } } @@ -589,7 +589,7 @@ class TerminalController: NSWindowController, NSWindowDelegate, // If we already have a clipboard confirmation view up, we ignore this request. // This shouldn't be possible... guard self.clipboardConfirmation == nil else { - Ghostty.AppState.completeClipboardRequest(surface, data: "", state: state, confirmed: true) + Ghostty.App.completeClipboardRequest(surface, data: "", state: state, confirmed: true) return } diff --git a/macos/Sources/Features/Terminal/TerminalManager.swift b/macos/Sources/Features/Terminal/TerminalManager.swift index 361ee2feb..a59741d3f 100644 --- a/macos/Sources/Features/Terminal/TerminalManager.swift +++ b/macos/Sources/Features/Terminal/TerminalManager.swift @@ -11,7 +11,7 @@ class TerminalManager { let closePublisher: AnyCancellable } - let ghostty: Ghostty.AppState + let ghostty: Ghostty.App /// The currently focused surface of the main window. var focusedSurface: Ghostty.SurfaceView? { mainWindow?.controller.focusedSurface } @@ -37,7 +37,7 @@ class TerminalManager { return windows.last } - init(_ ghostty: Ghostty.AppState) { + init(_ ghostty: Ghostty.App) { self.ghostty = ghostty let center = NotificationCenter.default diff --git a/macos/Sources/Features/Terminal/TerminalView.swift b/macos/Sources/Features/Terminal/TerminalView.swift index ba3f86db6..d0766c7ab 100644 --- a/macos/Sources/Features/Terminal/TerminalView.swift +++ b/macos/Sources/Features/Terminal/TerminalView.swift @@ -37,7 +37,7 @@ protocol TerminalViewModel: ObservableObject { /// The main terminal view. This terminal view supports splits. struct TerminalView: View { - @ObservedObject var ghostty: Ghostty.AppState + @ObservedObject var ghostty: Ghostty.App // The required view model @ObservedObject var viewModel: ViewModel @@ -83,7 +83,7 @@ struct TerminalView: View { VStack(spacing: 0) { // If we're running in debug mode we show a warning so that users // know that performance will be degraded. - if (ghostty.info.mode == GHOSTTY_BUILD_MODE_DEBUG) { + if (Ghostty.info.mode == GHOSTTY_BUILD_MODE_DEBUG) { DebugBuildWarningView() } diff --git a/macos/Sources/Ghostty/AppState.swift b/macos/Sources/Ghostty/AppState.swift deleted file mode 100644 index c59f39795..000000000 --- a/macos/Sources/Ghostty/AppState.swift +++ /dev/null @@ -1,572 +0,0 @@ -import SwiftUI -import UserNotifications -import GhosttyKit - -protocol GhosttyAppStateDelegate: AnyObject { - /// Called when the configuration did finish reloading. - func configDidReload(_ state: Ghostty.AppState) - - /// Called when a callback needs access to a specific surface. This should return nil - /// when the surface is no longer valid. - func findSurface(forUUID uuid: UUID) -> Ghostty.SurfaceView? -} - -extension Ghostty { - enum AppReadiness { - case loading, error, ready - } - - enum FontSizeModification { - case increase(Int) - case decrease(Int) - case reset - } - - struct Info { - var mode: ghostty_build_mode_e - var version: String - } - - /// The AppState is the global state that is associated with the Swift app. This handles initially - /// initializing Ghostty, loading the configuration, etc. - class AppState: ObservableObject { - /// The readiness value of the state. - @Published var readiness: AppReadiness = .loading - - /// Optional delegate - weak var delegate: GhosttyAppStateDelegate? - - /// The global app configuration. This defines the app level configuration plus any behavior - /// for new windows, tabs, etc. Note that when creating a new window, it may inherit some - /// configuration (i.e. font size) from the previously focused window. This would override this. - private(set) var config: Config - - /// The ghostty app instance. We only have one of these for the entire app, although I guess - /// in theory you can have multiple... I don't know why you would... - @Published var app: ghostty_app_t? = nil { - didSet { - guard let old = oldValue else { return } - ghostty_app_free(old) - } - } - - /// True if we need to confirm before quitting. - var needsConfirmQuit: Bool { - guard let app = app else { return false } - return ghostty_app_needs_confirm_quit(app) - } - - /// Build information - var info: Info { - let raw = ghostty_info() - let version = NSString( - bytes: raw.version, - length: Int(raw.version_len), - encoding: NSUTF8StringEncoding - ) ?? "unknown" - - return Info(mode: raw.build_mode, version: String(version)) - } - - init() { - // Initialize ghostty global state. This happens once per process. - if ghostty_init() != GHOSTTY_SUCCESS { - AppDelegate.logger.critical("ghostty_init failed, weird things may happen") - readiness = .error - } - - // Initialize the global configuration. - self.config = Config() - if self.config.config == nil { - readiness = .error - return - } - - // Create our "runtime" config. The "runtime" is the configuration that ghostty - // uses to interface with the application runtime environment. - var runtime_cfg = ghostty_runtime_config_s( - userdata: Unmanaged.passUnretained(self).toOpaque(), - supports_selection_clipboard: false, - wakeup_cb: { userdata in AppState.wakeup(userdata) }, - reload_config_cb: { userdata in AppState.reloadConfig(userdata) }, - open_config_cb: { userdata in AppState.openConfig(userdata) }, - set_title_cb: { userdata, title in AppState.setTitle(userdata, title: title) }, - set_mouse_shape_cb: { userdata, shape in AppState.setMouseShape(userdata, shape: shape) }, - set_mouse_visibility_cb: { userdata, visible in AppState.setMouseVisibility(userdata, visible: visible) }, - read_clipboard_cb: { userdata, loc, state in AppState.readClipboard(userdata, location: loc, state: state) }, - confirm_read_clipboard_cb: { userdata, str, state, request in AppState.confirmReadClipboard(userdata, string: str, state: state, request: request ) }, - write_clipboard_cb: { userdata, str, loc, confirm in AppState.writeClipboard(userdata, string: str, location: loc, confirm: confirm) }, - new_split_cb: { userdata, direction, surfaceConfig in AppState.newSplit(userdata, direction: direction, config: surfaceConfig) }, - new_tab_cb: { userdata, surfaceConfig in AppState.newTab(userdata, config: surfaceConfig) }, - new_window_cb: { userdata, surfaceConfig in AppState.newWindow(userdata, config: surfaceConfig) }, - control_inspector_cb: { userdata, mode in AppState.controlInspector(userdata, mode: mode) }, - close_surface_cb: { userdata, processAlive in AppState.closeSurface(userdata, processAlive: processAlive) }, - focus_split_cb: { userdata, direction in AppState.focusSplit(userdata, direction: direction) }, - resize_split_cb: { userdata, direction, amount in - AppState.resizeSplit(userdata, direction: direction, amount: amount) }, - equalize_splits_cb: { userdata in - AppState.equalizeSplits(userdata) }, - toggle_split_zoom_cb: { userdata in AppState.toggleSplitZoom(userdata) }, - goto_tab_cb: { userdata, n in AppState.gotoTab(userdata, n: n) }, - toggle_fullscreen_cb: { userdata, nonNativeFullscreen in AppState.toggleFullscreen(userdata, nonNativeFullscreen: nonNativeFullscreen) }, - set_initial_window_size_cb: { userdata, width, height in AppState.setInitialWindowSize(userdata, width: width, height: height) }, - render_inspector_cb: { userdata in AppState.renderInspector(userdata) }, - set_cell_size_cb: { userdata, width, height in AppState.setCellSize(userdata, width: width, height: height) }, - show_desktop_notification_cb: { userdata, title, body in - AppState.showUserNotification(userdata, title: title, body: body) - } - ) - - // Create the ghostty app. - guard let app = ghostty_app_new(&runtime_cfg, config.config) else { - AppDelegate.logger.critical("ghostty_app_new failed") - readiness = .error - return - } - self.app = app - - // Subscribe to notifications for keyboard layout change so that we can update Ghostty. - NotificationCenter.default.addObserver( - self, - selector: #selector(self.keyboardSelectionDidChange(notification:)), - name: NSTextInputContext.keyboardSelectionDidChangeNotification, - object: nil) - - self.readiness = .ready - } - - deinit { - // This will force the didSet callbacks to run which free. - self.app = nil - - // Remove our observer - NotificationCenter.default.removeObserver( - self, - name: NSTextInputContext.keyboardSelectionDidChangeNotification, - object: nil) - } - - func appTick() { - guard let app = self.app else { return } - - // Tick our app, which lets us know if we want to quit - let exit = ghostty_app_tick(app) - if (!exit) { return } - - // We want to quit, start that process - NSApplication.shared.terminate(nil) - } - - func openConfig() { - guard let app = self.app else { return } - ghostty_app_open_config(app) - } - - func reloadConfig() { - guard let app = self.app else { return } - ghostty_app_reload_config(app) - } - - /// Request that the given surface is closed. This will trigger the full normal surface close event - /// cycle which will call our close surface callback. - func requestClose(surface: ghostty_surface_t) { - ghostty_surface_request_close(surface) - } - - func newTab(surface: ghostty_surface_t) { - let action = "new_tab" - if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) { - AppDelegate.logger.warning("action failed action=\(action)") - } - } - - func newWindow(surface: ghostty_surface_t) { - let action = "new_window" - if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) { - AppDelegate.logger.warning("action failed action=\(action)") - } - } - - func split(surface: ghostty_surface_t, direction: ghostty_split_direction_e) { - ghostty_surface_split(surface, direction) - } - - func splitMoveFocus(surface: ghostty_surface_t, direction: SplitFocusDirection) { - ghostty_surface_split_focus(surface, direction.toNative()) - } - - func splitResize(surface: ghostty_surface_t, direction: SplitResizeDirection, amount: UInt16) { - ghostty_surface_split_resize(surface, direction.toNative(), amount) - } - - func splitEqualize(surface: ghostty_surface_t) { - ghostty_surface_split_equalize(surface) - } - - func splitToggleZoom(surface: ghostty_surface_t) { - let action = "toggle_split_zoom" - if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) { - AppDelegate.logger.warning("action failed action=\(action)") - } - } - - func toggleFullscreen(surface: ghostty_surface_t) { - let action = "toggle_fullscreen" - if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) { - AppDelegate.logger.warning("action failed action=\(action)") - } - } - - func changeFontSize(surface: ghostty_surface_t, _ change: FontSizeModification) { - let action: String - switch change { - case .increase(let amount): - action = "increase_font_size:\(amount)" - case .decrease(let amount): - action = "decrease_font_size:\(amount)" - case .reset: - action = "reset_font_size" - } - if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) { - AppDelegate.logger.warning("action failed action=\(action)") - } - } - - func toggleTerminalInspector(surface: ghostty_surface_t) { - let action = "inspector:toggle" - if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) { - AppDelegate.logger.warning("action failed action=\(action)") - } - } - - // Called when the selected keyboard changes. We have to notify Ghostty so that - // it can reload the keyboard mapping for input. - @objc private func keyboardSelectionDidChange(notification: NSNotification) { - guard let app = self.app else { return } - ghostty_app_keyboard_changed(app) - } - - // MARK: Ghostty Callbacks - - static func newSplit(_ userdata: UnsafeMutableRawPointer?, direction: ghostty_split_direction_e, config: ghostty_surface_config_s) { - let surface = self.surfaceUserdata(from: userdata) - NotificationCenter.default.post(name: Notification.ghosttyNewSplit, object: surface, userInfo: [ - "direction": direction, - Notification.NewSurfaceConfigKey: SurfaceConfiguration(from: config), - ]) - } - - static func closeSurface(_ userdata: UnsafeMutableRawPointer?, processAlive: Bool) { - let surface = self.surfaceUserdata(from: userdata) - NotificationCenter.default.post(name: Notification.ghosttyCloseSurface, object: surface, userInfo: [ - "process_alive": processAlive, - ]) - } - - static func focusSplit(_ userdata: UnsafeMutableRawPointer?, direction: ghostty_split_focus_direction_e) { - let surface = self.surfaceUserdata(from: userdata) - guard let splitDirection = SplitFocusDirection.from(direction: direction) else { return } - NotificationCenter.default.post( - name: Notification.ghosttyFocusSplit, - object: surface, - userInfo: [ - Notification.SplitDirectionKey: splitDirection, - ] - ) - } - - static func resizeSplit(_ userdata: UnsafeMutableRawPointer?, direction: ghostty_split_resize_direction_e, amount: UInt16) { - let surface = self.surfaceUserdata(from: userdata) - guard let resizeDirection = SplitResizeDirection.from(direction: direction) else { return } - NotificationCenter.default.post( - name: Notification.didResizeSplit, - object: surface, - userInfo: [ - Notification.ResizeSplitDirectionKey: resizeDirection, - Notification.ResizeSplitAmountKey: amount, - ] - ) - } - - static func equalizeSplits(_ userdata: UnsafeMutableRawPointer?) { - let surface = self.surfaceUserdata(from: userdata) - NotificationCenter.default.post(name: Notification.didEqualizeSplits, object: surface) - } - - static func toggleSplitZoom(_ userdata: UnsafeMutableRawPointer?) { - let surface = self.surfaceUserdata(from: userdata) - - NotificationCenter.default.post( - name: Notification.didToggleSplitZoom, - object: surface - ) - } - - static func gotoTab(_ userdata: UnsafeMutableRawPointer?, n: Int32) { - let surface = self.surfaceUserdata(from: userdata) - NotificationCenter.default.post( - name: Notification.ghosttyGotoTab, - object: surface, - userInfo: [ - Notification.GotoTabKey: n, - ] - ) - } - - static func readClipboard(_ userdata: UnsafeMutableRawPointer?, location: ghostty_clipboard_e, state: UnsafeMutableRawPointer?) { - // If we don't even have a surface, something went terrible wrong so we have - // to leak "state". - let surfaceView = self.surfaceUserdata(from: userdata) - guard let surface = surfaceView.surface else { return } - - // We only support the standard clipboard - if (location != GHOSTTY_CLIPBOARD_STANDARD) { - return completeClipboardRequest(surface, data: "", state: state) - } - - // Get our string - let str = NSPasteboard.general.string(forType: .string) ?? "" - completeClipboardRequest(surface, data: str, state: state) - } - - static func confirmReadClipboard( - _ userdata: UnsafeMutableRawPointer?, - string: UnsafePointer?, - state: UnsafeMutableRawPointer?, - request: ghostty_clipboard_request_e - ) { - let surface = self.surfaceUserdata(from: userdata) - guard let valueStr = String(cString: string!, encoding: .utf8) else { return } - guard let request = Ghostty.ClipboardRequest.from(request: request) else { return } - NotificationCenter.default.post( - name: Notification.confirmClipboard, - object: surface, - userInfo: [ - Notification.ConfirmClipboardStrKey: valueStr, - Notification.ConfirmClipboardStateKey: state as Any, - Notification.ConfirmClipboardRequestKey: request, - ] - ) - } - - static func completeClipboardRequest( - _ surface: ghostty_surface_t, - data: String, - state: UnsafeMutableRawPointer?, - confirmed: Bool = false - ) { - data.withCString { ptr in - ghostty_surface_complete_clipboard_request(surface, ptr, state, confirmed) - } - } - - static func writeClipboard(_ userdata: UnsafeMutableRawPointer?, string: UnsafePointer?, location: ghostty_clipboard_e, confirm: Bool) { - let surface = self.surfaceUserdata(from: userdata) - - // We only support the standard clipboard - if (location != GHOSTTY_CLIPBOARD_STANDARD) { return } - - guard let valueStr = String(cString: string!, encoding: .utf8) else { return } - if !confirm { - let pb = NSPasteboard.general - pb.declareTypes([.string], owner: nil) - pb.setString(valueStr, forType: .string) - return - } - - NotificationCenter.default.post( - name: Notification.confirmClipboard, - object: surface, - userInfo: [ - Notification.ConfirmClipboardStrKey: valueStr, - Notification.ConfirmClipboardRequestKey: Ghostty.ClipboardRequest.osc_52_write, - ] - ) - } - - static func openConfig(_ userdata: UnsafeMutableRawPointer?) { - ghostty_config_open(); - } - - static func reloadConfig(_ userdata: UnsafeMutableRawPointer?) -> ghostty_config_t? { - let newConfig = Config() - guard newConfig.loaded else { - AppDelegate.logger.warning("failed to reload configuration") - return nil - } - - // Assign the new config. This will automatically free the old config. - // It is safe to free the old config from within this function call. - let state = Unmanaged.fromOpaque(userdata!).takeUnretainedValue() - state.config = newConfig - - // If we have a delegate, notify. - if let delegate = state.delegate { - delegate.configDidReload(state) - } - - return newConfig.config - } - - static func wakeup(_ userdata: UnsafeMutableRawPointer?) { - let state = Unmanaged.fromOpaque(userdata!).takeUnretainedValue() - - // Wakeup can be called from any thread so we schedule the app tick - // from the main thread. There is probably some improvements we can make - // to coalesce multiple ticks but I don't think it matters from a performance - // standpoint since we don't do this much. - DispatchQueue.main.async { state.appTick() } - } - - static func renderInspector(_ userdata: UnsafeMutableRawPointer?) { - let surface = self.surfaceUserdata(from: userdata) - NotificationCenter.default.post( - name: Notification.inspectorNeedsDisplay, - object: surface - ) - } - - static func setTitle(_ userdata: UnsafeMutableRawPointer?, title: UnsafePointer?) { - let surfaceView = self.surfaceUserdata(from: userdata) - guard let titleStr = String(cString: title!, encoding: .utf8) else { return } - DispatchQueue.main.async { - surfaceView.title = titleStr - } - } - - static func setMouseShape(_ userdata: UnsafeMutableRawPointer?, shape: ghostty_mouse_shape_e) { - let surfaceView = self.surfaceUserdata(from: userdata) - surfaceView.setCursorShape(shape) - } - - static func setMouseVisibility(_ userdata: UnsafeMutableRawPointer?, visible: Bool) { - let surfaceView = self.surfaceUserdata(from: userdata) - surfaceView.setCursorVisibility(visible) - } - - static func toggleFullscreen(_ userdata: UnsafeMutableRawPointer?, nonNativeFullscreen: ghostty_non_native_fullscreen_e) { - let surface = self.surfaceUserdata(from: userdata) - NotificationCenter.default.post( - name: Notification.ghosttyToggleFullscreen, - object: surface, - userInfo: [ - Notification.NonNativeFullscreenKey: nonNativeFullscreen, - ] - ) - } - - static func setInitialWindowSize(_ userdata: UnsafeMutableRawPointer?, width: UInt32, height: UInt32) { - // We need a window to set the frame - let surfaceView = self.surfaceUserdata(from: userdata) - surfaceView.initialSize = NSMakeSize(Double(width), Double(height)) - } - - static func setCellSize(_ userdata: UnsafeMutableRawPointer?, width: UInt32, height: UInt32) { - let surfaceView = self.surfaceUserdata(from: userdata) - let backingSize = NSSize(width: Double(width), height: Double(height)) - surfaceView.cellSize = surfaceView.convertFromBacking(backingSize) - } - - static func showUserNotification(_ userdata: UnsafeMutableRawPointer?, title: UnsafePointer?, body: UnsafePointer?) { - let surfaceView = self.surfaceUserdata(from: userdata) - guard let title = String(cString: title!, encoding: .utf8) else { return } - guard let body = String(cString: body!, encoding: .utf8) else { return } - - let center = UNUserNotificationCenter.current() - center.requestAuthorization(options: [.alert, .sound]) { _, error in - if let error = error { - AppDelegate.logger.error("Error while requesting notification authorization: \(error)") - } - } - - center.getNotificationSettings() { settings in - guard settings.authorizationStatus == .authorized else { return } - surfaceView.showUserNotification(title: title, body: body) - } - } - - /// Handle a received user notification. This is called when a user notification is clicked or dismissed by the user - func handleUserNotification(response: UNNotificationResponse) { - let userInfo = response.notification.request.content.userInfo - guard let uuidString = userInfo["surface"] as? String, - let uuid = UUID(uuidString: uuidString), - let surface = delegate?.findSurface(forUUID: uuid) else { return } - - switch (response.actionIdentifier) { - case UNNotificationDefaultActionIdentifier, Ghostty.userNotificationActionShow: - // The user clicked on a notification - surface.handleUserNotification(notification: response.notification, focus: true) - case UNNotificationDismissActionIdentifier: - // The user dismissed the notification - surface.handleUserNotification(notification: response.notification, focus: false) - default: - break - } - } - - /// Determine if a given notification should be presented to the user when Ghostty is running in the foreground. - func shouldPresentNotification(notification: UNNotification) -> Bool { - let userInfo = notification.request.content.userInfo - guard let uuidString = userInfo["surface"] as? String, - let uuid = UUID(uuidString: uuidString), - let surface = delegate?.findSurface(forUUID: uuid), - let window = surface.window else { return false } - return !window.isKeyWindow || !surface.focused - } - - static func newTab(_ userdata: UnsafeMutableRawPointer?, config: ghostty_surface_config_s) { - let surface = self.surfaceUserdata(from: userdata) - - guard let appState = self.appState(fromView: surface) else { return } - guard appState.config.windowDecorations else { - let alert = NSAlert() - alert.messageText = "Tabs are disabled" - alert.informativeText = "Enable window decorations to use tabs" - alert.addButton(withTitle: "OK") - alert.alertStyle = .warning - _ = alert.runModal() - return - } - - NotificationCenter.default.post( - name: Notification.ghosttyNewTab, - object: surface, - userInfo: [ - Notification.NewSurfaceConfigKey: SurfaceConfiguration(from: config), - ] - ) - } - - static func newWindow(_ userdata: UnsafeMutableRawPointer?, config: ghostty_surface_config_s) { - let surface = self.surfaceUserdata(from: userdata) - - NotificationCenter.default.post( - name: Notification.ghosttyNewWindow, - object: surface, - userInfo: [ - Notification.NewSurfaceConfigKey: SurfaceConfiguration(from: config), - ] - ) - } - - static func controlInspector(_ userdata: UnsafeMutableRawPointer?, mode: ghostty_inspector_mode_e) { - let surface = self.surfaceUserdata(from: userdata) - NotificationCenter.default.post(name: Notification.didControlInspector, object: surface, userInfo: [ - "mode": mode, - ]) - } - - /// Returns the GhosttyState from the given userdata value. - static private func appState(fromView view: SurfaceView) -> AppState? { - guard let surface = view.surface else { return nil } - guard let app = ghostty_surface_app(surface) else { return nil } - guard let app_ud = ghostty_app_userdata(app) else { return nil } - return Unmanaged.fromOpaque(app_ud).takeUnretainedValue() - } - - /// Returns the surface view from the userdata. - static private func surfaceUserdata(from userdata: UnsafeMutableRawPointer?) -> SurfaceView { - return Unmanaged.fromOpaque(userdata!).takeUnretainedValue() - } - } -} diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index add6fadb1..3afbc0870 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -1,6 +1,18 @@ import SwiftUI +import UserNotifications import GhosttyKit +protocol GhosttyAppDelegate: AnyObject { + /// Called when the configuration did finish reloading. + func configDidReload(_ app: Ghostty.App) + + #if os(macOS) + /// Called when a callback needs access to a specific surface. This should return nil + /// when the surface is no longer valid. + func findSurface(forUUID uuid: UUID) -> Ghostty.SurfaceView? + #endif +} + extension Ghostty { // IMPORTANT: THIS IS NOT DONE. // This is a refactor/redo of Ghostty.AppState so that it supports both macOS and iOS @@ -9,6 +21,9 @@ extension Ghostty { case loading, error, ready } + /// Optional delegate + weak var delegate: GhosttyAppDelegate? + /// The readiness value of the state. @Published var readiness: Readiness = .loading @@ -26,6 +41,12 @@ extension Ghostty { } } + /// True if we need to confirm before quitting. + var needsConfirmQuit: Bool { + guard let app = app else { return false } + return ghostty_app_needs_confirm_quit(app) + } + init() { // Initialize ghostty global state. This happens once per process. if ghostty_init() != GHOSTTY_SUCCESS { @@ -108,7 +129,119 @@ extension Ghostty { #endif } - // MARK: Ghostty Callbacks + // MARK: App Operations + + func appTick() { + guard let app = self.app else { return } + + // Tick our app, which lets us know if we want to quit + let exit = ghostty_app_tick(app) + if (!exit) { return } + + // On iOS, applications do not terminate programmatically like they do + // on macOS. On iOS, applications are only terminated when a user physically + // closes the application (i.e. going to the home screen). If we request + // exit on iOS we ignore it. + #if os(iOS) + logger.info("quit request received, ignoring on iOS") + #endif + + #if os(macOS) + // We want to quit, start that process + NSApplication.shared.terminate(nil) + #endif + } + + func openConfig() { + guard let app = self.app else { return } + ghostty_app_open_config(app) + } + + func reloadConfig() { + guard let app = self.app else { return } + ghostty_app_reload_config(app) + } + + /// Request that the given surface is closed. This will trigger the full normal surface close event + /// cycle which will call our close surface callback. + func requestClose(surface: ghostty_surface_t) { + ghostty_surface_request_close(surface) + } + + func newTab(surface: ghostty_surface_t) { + let action = "new_tab" + if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) { + logger.warning("action failed action=\(action)") + } + } + + func newWindow(surface: ghostty_surface_t) { + let action = "new_window" + if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) { + logger.warning("action failed action=\(action)") + } + } + + func split(surface: ghostty_surface_t, direction: ghostty_split_direction_e) { + ghostty_surface_split(surface, direction) + } + + func splitMoveFocus(surface: ghostty_surface_t, direction: SplitFocusDirection) { + ghostty_surface_split_focus(surface, direction.toNative()) + } + + func splitResize(surface: ghostty_surface_t, direction: SplitResizeDirection, amount: UInt16) { + ghostty_surface_split_resize(surface, direction.toNative(), amount) + } + + func splitEqualize(surface: ghostty_surface_t) { + ghostty_surface_split_equalize(surface) + } + + func splitToggleZoom(surface: ghostty_surface_t) { + let action = "toggle_split_zoom" + if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) { + logger.warning("action failed action=\(action)") + } + } + + func toggleFullscreen(surface: ghostty_surface_t) { + let action = "toggle_fullscreen" + if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) { + logger.warning("action failed action=\(action)") + } + } + + enum FontSizeModification { + case increase(Int) + case decrease(Int) + case reset + } + + func changeFontSize(surface: ghostty_surface_t, _ change: FontSizeModification) { + let action: String + switch change { + case .increase(let amount): + action = "increase_font_size:\(amount)" + case .decrease(let amount): + action = "decrease_font_size:\(amount)" + case .reset: + action = "reset_font_size" + } + if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) { + logger.warning("action failed action=\(action)") + } + } + + func toggleTerminalInspector(surface: ghostty_surface_t) { + let action = "inspector:toggle" + if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) { + logger.warning("action failed action=\(action)") + } + } + + #if os(iOS) + // MARK: Ghostty Callbacks (iOS) static func wakeup(_ userdata: UnsafeMutableRawPointer?) {} static func reloadConfig(_ userdata: UnsafeMutableRawPointer?) -> ghostty_config_t? { return nil } @@ -156,5 +289,342 @@ extension Ghostty { static func renderInspector(_ userdata: UnsafeMutableRawPointer?) {} static func setCellSize(_ userdata: UnsafeMutableRawPointer?, width: UInt32, height: UInt32) {} static func showUserNotification(_ userdata: UnsafeMutableRawPointer?, title: UnsafePointer?, body: UnsafePointer?) {} + #endif + + #if os(macOS) + + // MARK: Notifications + + // Called when the selected keyboard changes. We have to notify Ghostty so that + // it can reload the keyboard mapping for input. + @objc private func keyboardSelectionDidChange(notification: NSNotification) { + guard let app = self.app else { return } + ghostty_app_keyboard_changed(app) + } + + // MARK: Ghostty Callbacks (macOS) + + static func newSplit(_ userdata: UnsafeMutableRawPointer?, direction: ghostty_split_direction_e, config: ghostty_surface_config_s) { + let surface = self.surfaceUserdata(from: userdata) + NotificationCenter.default.post(name: Notification.ghosttyNewSplit, object: surface, userInfo: [ + "direction": direction, + Notification.NewSurfaceConfigKey: SurfaceConfiguration(from: config), + ]) + } + + static func closeSurface(_ userdata: UnsafeMutableRawPointer?, processAlive: Bool) { + let surface = self.surfaceUserdata(from: userdata) + NotificationCenter.default.post(name: Notification.ghosttyCloseSurface, object: surface, userInfo: [ + "process_alive": processAlive, + ]) + } + + static func focusSplit(_ userdata: UnsafeMutableRawPointer?, direction: ghostty_split_focus_direction_e) { + let surface = self.surfaceUserdata(from: userdata) + guard let splitDirection = SplitFocusDirection.from(direction: direction) else { return } + NotificationCenter.default.post( + name: Notification.ghosttyFocusSplit, + object: surface, + userInfo: [ + Notification.SplitDirectionKey: splitDirection, + ] + ) + } + + static func resizeSplit(_ userdata: UnsafeMutableRawPointer?, direction: ghostty_split_resize_direction_e, amount: UInt16) { + let surface = self.surfaceUserdata(from: userdata) + guard let resizeDirection = SplitResizeDirection.from(direction: direction) else { return } + NotificationCenter.default.post( + name: Notification.didResizeSplit, + object: surface, + userInfo: [ + Notification.ResizeSplitDirectionKey: resizeDirection, + Notification.ResizeSplitAmountKey: amount, + ] + ) + } + + static func equalizeSplits(_ userdata: UnsafeMutableRawPointer?) { + let surface = self.surfaceUserdata(from: userdata) + NotificationCenter.default.post(name: Notification.didEqualizeSplits, object: surface) + } + + static func toggleSplitZoom(_ userdata: UnsafeMutableRawPointer?) { + let surface = self.surfaceUserdata(from: userdata) + + NotificationCenter.default.post( + name: Notification.didToggleSplitZoom, + object: surface + ) + } + + static func gotoTab(_ userdata: UnsafeMutableRawPointer?, n: Int32) { + let surface = self.surfaceUserdata(from: userdata) + NotificationCenter.default.post( + name: Notification.ghosttyGotoTab, + object: surface, + userInfo: [ + Notification.GotoTabKey: n, + ] + ) + } + + static func readClipboard(_ userdata: UnsafeMutableRawPointer?, location: ghostty_clipboard_e, state: UnsafeMutableRawPointer?) { + // If we don't even have a surface, something went terrible wrong so we have + // to leak "state". + let surfaceView = self.surfaceUserdata(from: userdata) + guard let surface = surfaceView.surface else { return } + + // We only support the standard clipboard + if (location != GHOSTTY_CLIPBOARD_STANDARD) { + return completeClipboardRequest(surface, data: "", state: state) + } + + // Get our string + let str = NSPasteboard.general.string(forType: .string) ?? "" + completeClipboardRequest(surface, data: str, state: state) + } + + static func confirmReadClipboard( + _ userdata: UnsafeMutableRawPointer?, + string: UnsafePointer?, + state: UnsafeMutableRawPointer?, + request: ghostty_clipboard_request_e + ) { + let surface = self.surfaceUserdata(from: userdata) + guard let valueStr = String(cString: string!, encoding: .utf8) else { return } + guard let request = Ghostty.ClipboardRequest.from(request: request) else { return } + NotificationCenter.default.post( + name: Notification.confirmClipboard, + object: surface, + userInfo: [ + Notification.ConfirmClipboardStrKey: valueStr, + Notification.ConfirmClipboardStateKey: state as Any, + Notification.ConfirmClipboardRequestKey: request, + ] + ) + } + + static func completeClipboardRequest( + _ surface: ghostty_surface_t, + data: String, + state: UnsafeMutableRawPointer?, + confirmed: Bool = false + ) { + data.withCString { ptr in + ghostty_surface_complete_clipboard_request(surface, ptr, state, confirmed) + } + } + + static func writeClipboard(_ userdata: UnsafeMutableRawPointer?, string: UnsafePointer?, location: ghostty_clipboard_e, confirm: Bool) { + let surface = self.surfaceUserdata(from: userdata) + + // We only support the standard clipboard + if (location != GHOSTTY_CLIPBOARD_STANDARD) { return } + + guard let valueStr = String(cString: string!, encoding: .utf8) else { return } + if !confirm { + let pb = NSPasteboard.general + pb.declareTypes([.string], owner: nil) + pb.setString(valueStr, forType: .string) + return + } + + NotificationCenter.default.post( + name: Notification.confirmClipboard, + object: surface, + userInfo: [ + Notification.ConfirmClipboardStrKey: valueStr, + Notification.ConfirmClipboardRequestKey: Ghostty.ClipboardRequest.osc_52_write, + ] + ) + } + + static func openConfig(_ userdata: UnsafeMutableRawPointer?) { + ghostty_config_open(); + } + + static func reloadConfig(_ userdata: UnsafeMutableRawPointer?) -> ghostty_config_t? { + let newConfig = Config() + guard newConfig.loaded else { + AppDelegate.logger.warning("failed to reload configuration") + return nil + } + + // Assign the new config. This will automatically free the old config. + // It is safe to free the old config from within this function call. + let state = Unmanaged.fromOpaque(userdata!).takeUnretainedValue() + state.config = newConfig + + // If we have a delegate, notify. + if let delegate = state.delegate { + delegate.configDidReload(state) + } + + return newConfig.config + } + + static func wakeup(_ userdata: UnsafeMutableRawPointer?) { + let state = Unmanaged.fromOpaque(userdata!).takeUnretainedValue() + + // Wakeup can be called from any thread so we schedule the app tick + // from the main thread. There is probably some improvements we can make + // to coalesce multiple ticks but I don't think it matters from a performance + // standpoint since we don't do this much. + DispatchQueue.main.async { state.appTick() } + } + + static func renderInspector(_ userdata: UnsafeMutableRawPointer?) { + let surface = self.surfaceUserdata(from: userdata) + NotificationCenter.default.post( + name: Notification.inspectorNeedsDisplay, + object: surface + ) + } + + static func setTitle(_ userdata: UnsafeMutableRawPointer?, title: UnsafePointer?) { + let surfaceView = self.surfaceUserdata(from: userdata) + guard let titleStr = String(cString: title!, encoding: .utf8) else { return } + DispatchQueue.main.async { + surfaceView.title = titleStr + } + } + + static func setMouseShape(_ userdata: UnsafeMutableRawPointer?, shape: ghostty_mouse_shape_e) { + let surfaceView = self.surfaceUserdata(from: userdata) + surfaceView.setCursorShape(shape) + } + + static func setMouseVisibility(_ userdata: UnsafeMutableRawPointer?, visible: Bool) { + let surfaceView = self.surfaceUserdata(from: userdata) + surfaceView.setCursorVisibility(visible) + } + + static func toggleFullscreen(_ userdata: UnsafeMutableRawPointer?, nonNativeFullscreen: ghostty_non_native_fullscreen_e) { + let surface = self.surfaceUserdata(from: userdata) + NotificationCenter.default.post( + name: Notification.ghosttyToggleFullscreen, + object: surface, + userInfo: [ + Notification.NonNativeFullscreenKey: nonNativeFullscreen, + ] + ) + } + + static func setInitialWindowSize(_ userdata: UnsafeMutableRawPointer?, width: UInt32, height: UInt32) { + // We need a window to set the frame + let surfaceView = self.surfaceUserdata(from: userdata) + surfaceView.initialSize = NSMakeSize(Double(width), Double(height)) + } + + static func setCellSize(_ userdata: UnsafeMutableRawPointer?, width: UInt32, height: UInt32) { + let surfaceView = self.surfaceUserdata(from: userdata) + let backingSize = NSSize(width: Double(width), height: Double(height)) + surfaceView.cellSize = surfaceView.convertFromBacking(backingSize) + } + + static func showUserNotification(_ userdata: UnsafeMutableRawPointer?, title: UnsafePointer?, body: UnsafePointer?) { + let surfaceView = self.surfaceUserdata(from: userdata) + guard let title = String(cString: title!, encoding: .utf8) else { return } + guard let body = String(cString: body!, encoding: .utf8) else { return } + + let center = UNUserNotificationCenter.current() + center.requestAuthorization(options: [.alert, .sound]) { _, error in + if let error = error { + AppDelegate.logger.error("Error while requesting notification authorization: \(error)") + } + } + + center.getNotificationSettings() { settings in + guard settings.authorizationStatus == .authorized else { return } + surfaceView.showUserNotification(title: title, body: body) + } + } + + /// Handle a received user notification. This is called when a user notification is clicked or dismissed by the user + func handleUserNotification(response: UNNotificationResponse) { + let userInfo = response.notification.request.content.userInfo + guard let uuidString = userInfo["surface"] as? String, + let uuid = UUID(uuidString: uuidString), + let surface = delegate?.findSurface(forUUID: uuid) else { return } + + switch (response.actionIdentifier) { + case UNNotificationDefaultActionIdentifier, Ghostty.userNotificationActionShow: + // The user clicked on a notification + surface.handleUserNotification(notification: response.notification, focus: true) + case UNNotificationDismissActionIdentifier: + // The user dismissed the notification + surface.handleUserNotification(notification: response.notification, focus: false) + default: + break + } + } + + /// Determine if a given notification should be presented to the user when Ghostty is running in the foreground. + func shouldPresentNotification(notification: UNNotification) -> Bool { + let userInfo = notification.request.content.userInfo + guard let uuidString = userInfo["surface"] as? String, + let uuid = UUID(uuidString: uuidString), + let surface = delegate?.findSurface(forUUID: uuid), + let window = surface.window else { return false } + return !window.isKeyWindow || !surface.focused + } + + static func newTab(_ userdata: UnsafeMutableRawPointer?, config: ghostty_surface_config_s) { + let surface = self.surfaceUserdata(from: userdata) + + guard let appState = self.appState(fromView: surface) else { return } + guard appState.config.windowDecorations else { + let alert = NSAlert() + alert.messageText = "Tabs are disabled" + alert.informativeText = "Enable window decorations to use tabs" + alert.addButton(withTitle: "OK") + alert.alertStyle = .warning + _ = alert.runModal() + return + } + + NotificationCenter.default.post( + name: Notification.ghosttyNewTab, + object: surface, + userInfo: [ + Notification.NewSurfaceConfigKey: SurfaceConfiguration(from: config), + ] + ) + } + + static func newWindow(_ userdata: UnsafeMutableRawPointer?, config: ghostty_surface_config_s) { + let surface = self.surfaceUserdata(from: userdata) + + NotificationCenter.default.post( + name: Notification.ghosttyNewWindow, + object: surface, + userInfo: [ + Notification.NewSurfaceConfigKey: SurfaceConfiguration(from: config), + ] + ) + } + + static func controlInspector(_ userdata: UnsafeMutableRawPointer?, mode: ghostty_inspector_mode_e) { + let surface = self.surfaceUserdata(from: userdata) + NotificationCenter.default.post(name: Notification.didControlInspector, object: surface, userInfo: [ + "mode": mode, + ]) + } + + /// Returns the GhosttyState from the given userdata value. + static private func appState(fromView view: SurfaceView) -> App? { + guard let surface = view.surface else { return nil } + guard let app = ghostty_surface_app(surface) else { return nil } + guard let app_ud = ghostty_app_userdata(app) else { return nil } + return Unmanaged.fromOpaque(app_ud).takeUnretainedValue() + } + + /// Returns the surface view from the userdata. + static private func surfaceUserdata(from userdata: UnsafeMutableRawPointer?) -> SurfaceView { + return Unmanaged.fromOpaque(userdata!).takeUnretainedValue() + } + + #endif } } diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index c5b0269c6..9f8fe5237 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -19,6 +19,26 @@ struct Ghostty { static let userNotificationActionShow = "com.mitchellh.ghostty.userNotification.Show" } +// MARK: Build Info + +extension Ghostty { + struct Info { + var mode: ghostty_build_mode_e + var version: String + } + + static var info: Info { + let raw = ghostty_info() + let version = NSString( + bytes: raw.version, + length: Int(raw.version_len), + encoding: NSUTF8StringEncoding + ) ?? "unknown" + + return Info(mode: raw.build_mode, version: String(version)) + } +} + // MARK: Surface Notifications extension Ghostty { diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index f9bc0f027..0fb4c212d 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -5,7 +5,7 @@ import GhosttyKit extension Ghostty { /// Render a terminal for the active app in the environment. struct Terminal: View { - @EnvironmentObject private var ghostty: Ghostty.AppState + @EnvironmentObject private var ghostty: Ghostty.App @FocusedValue(\.ghosttySurfaceTitle) private var surfaceTitle: String? var body: some View { @@ -49,7 +49,7 @@ extension Ghostty { // Maintain whether our window has focus (is key) or not @State private var windowFocus: Bool = true - @EnvironmentObject private var ghostty: Ghostty.AppState + @EnvironmentObject private var ghostty: Ghostty.App // This is true if the terminal is considered "focused". The terminal is focused if // it is both individually focused and the containing window is key.