diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 86bc65636..948f9d7aa 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -23,6 +23,9 @@ A53D0C8E2B53B0EA00305CE6 /* GhosttyKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */; }; A53D0C942B53B43700305CE6 /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53D0C932B53B43700305CE6 /* iOSApp.swift */; }; A53D0C952B53B4D800305CE6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; }; + A53D0C9A2B543F3B00305CE6 /* Ghostty.App.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */; }; + 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 */; }; @@ -73,6 +76,7 @@ A53426342A7DA53D00EBB7A2 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; A535B9D9299C569B0017E2E4 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = ""; }; 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 = ""; }; @@ -233,6 +237,7 @@ A55B7BB529B6F47F0055DE60 /* AppState.swift */, A55B7BBB29B6FC330055DE60 /* SurfaceView.swift */, A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */, + A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */, A5278A9A2AA05B2600CD3039 /* Ghostty.Input.swift */, A56D58852ACDDB4100508D2C /* Ghostty.Shell.swift */, A59630A32AF059BB00D64628 /* Ghostty.SplitNode.swift */, @@ -453,6 +458,7 @@ A5CDF1952AAFA19600513312 /* ConfigurationErrorsView.swift in Sources */, A55B7BBC29B6FC330055DE60 /* SurfaceView.swift in Sources */, A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */, + A53D0C9A2B543F3B00305CE6 /* Ghostty.App.swift in Sources */, A56D58862ACDDB4100508D2C /* Ghostty.Shell.swift in Sources */, A59630A22AF0415000D64628 /* Ghostty.TerminalSplit.swift in Sources */, A5FEB3002ABB69450068369E /* main.swift in Sources */, @@ -479,6 +485,8 @@ buildActionMask = 2147483647; files = ( A53D0C942B53B43700305CE6 /* iOSApp.swift in Sources */, + A53D0C9C2B543F7B00305CE6 /* Package.swift in Sources */, + A53D0C9B2B543F3B00305CE6 /* Ghostty.App.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -785,6 +793,7 @@ ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 0.1; + "OTHER_LDFLAGS[arch=*]" = "-lstdc++"; PRODUCT_BUNDLE_IDENTIFIER = "com.mitchellh.ghostty-ios"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -822,6 +831,7 @@ ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 0.1; + "OTHER_LDFLAGS[arch=*]" = "-lstdc++"; PRODUCT_BUNDLE_IDENTIFIER = "com.mitchellh.ghostty-ios"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -859,6 +869,7 @@ ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 0.1; + "OTHER_LDFLAGS[arch=*]" = "-lstdc++"; PRODUCT_BUNDLE_IDENTIFIER = "com.mitchellh.ghostty-ios"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; diff --git a/macos/Sources/App/iOS/iOSApp.swift b/macos/Sources/App/iOS/iOSApp.swift index d571a490b..bf581d6cd 100644 --- a/macos/Sources/App/iOS/iOSApp.swift +++ b/macos/Sources/App/iOS/iOSApp.swift @@ -2,14 +2,19 @@ import SwiftUI @main struct Ghostty_iOSApp: App { + @StateObject private var ghostty_app = Ghostty.App() + var body: some Scene { WindowGroup { iOS_ContentView() + .environmentObject(ghostty_app) } } } struct iOS_ContentView: View { + @EnvironmentObject private var ghostty_app: Ghostty.App + var body: some View { VStack { Image("AppIconImage") @@ -17,6 +22,7 @@ struct iOS_ContentView: View { .aspectRatio(contentMode: .fit) .frame(maxHeight: 96) Text("Ghostty") + Text("State: \(ghostty_app.readiness.rawValue)") } .padding() } diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift new file mode 100644 index 000000000..68cd77d3a --- /dev/null +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -0,0 +1,207 @@ +import SwiftUI +import GhosttyKit + +extension Ghostty { + // IMPORTANT: THIS IS NOT DONE. + // This is a refactor/redo of Ghostty.AppState so that it supports both macOS and iOS + class App: ObservableObject { + enum Readiness: String { + case loading, error, ready + } + + /// The readiness value of the state. + @Published var readiness: Readiness = .loading + + /// The ghostty global configuration. This should only be changed when it is definitely + /// safe to change. It is definitely safe to change only when the embedded app runtime + /// in Ghostty says so (usually, only in a reload configuration callback). + @Published var config: ghostty_config_t? = nil { + didSet { + // Free the old value whenever we change + guard let old = oldValue else { return } + ghostty_config_free(old) + } + } + + /// 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) + } + } + + init() { + // Initialize ghostty global state. This happens once per process. + guard ghostty_init() == GHOSTTY_SUCCESS else { + logger.critical("ghostty_init failed") + readiness = .error + return + } + + // Initialize the global configuration. + guard let cfg = Self.loadConfig() else { + readiness = .error + return + } + self.config = cfg; + + // 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 App.wakeup(userdata) }, + reload_config_cb: { userdata in App.reloadConfig(userdata) }, + open_config_cb: { userdata in App.openConfig(userdata) }, + set_title_cb: { userdata, title in App.setTitle(userdata, title: title) }, + set_mouse_shape_cb: { userdata, shape in App.setMouseShape(userdata, shape: shape) }, + set_mouse_visibility_cb: { userdata, visible in App.setMouseVisibility(userdata, visible: visible) }, + read_clipboard_cb: { userdata, loc, state in App.readClipboard(userdata, location: loc, state: state) }, + confirm_read_clipboard_cb: { userdata, str, state, request in App.confirmReadClipboard(userdata, string: str, state: state, request: request ) }, + write_clipboard_cb: { userdata, str, loc, confirm in App.writeClipboard(userdata, string: str, location: loc, confirm: confirm) }, + new_split_cb: { userdata, direction, surfaceConfig in App.newSplit(userdata, direction: direction, config: surfaceConfig) }, + new_tab_cb: { userdata, surfaceConfig in App.newTab(userdata, config: surfaceConfig) }, + new_window_cb: { userdata, surfaceConfig in App.newWindow(userdata, config: surfaceConfig) }, + control_inspector_cb: { userdata, mode in App.controlInspector(userdata, mode: mode) }, + close_surface_cb: { userdata, processAlive in App.closeSurface(userdata, processAlive: processAlive) }, + focus_split_cb: { userdata, direction in App.focusSplit(userdata, direction: direction) }, + resize_split_cb: { userdata, direction, amount in + App.resizeSplit(userdata, direction: direction, amount: amount) }, + equalize_splits_cb: { userdata in + App.equalizeSplits(userdata) }, + toggle_split_zoom_cb: { userdata in App.toggleSplitZoom(userdata) }, + goto_tab_cb: { userdata, n in App.gotoTab(userdata, n: n) }, + toggle_fullscreen_cb: { userdata, nonNativeFullscreen in App.toggleFullscreen(userdata, nonNativeFullscreen: nonNativeFullscreen) }, + set_initial_window_size_cb: { userdata, width, height in App.setInitialWindowSize(userdata, width: width, height: height) }, + render_inspector_cb: { userdata in App.renderInspector(userdata) }, + set_cell_size_cb: { userdata, width, height in App.setCellSize(userdata, width: width, height: height) }, + show_desktop_notification_cb: { userdata, title, body in + App.showUserNotification(userdata, title: title, body: body) + } + ) + + // Create the ghostty app. + guard let app = ghostty_app_new(&runtime_cfg, cfg) else { + logger.critical("ghostty_app_new failed") + readiness = .error + return + } + self.app = app + + #if os(macOS) + // 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) + #endif + + self.readiness = .ready + } + + deinit { + // This will force the didSet callbacks to run which free. + self.app = nil + self.config = nil + + #if os(macOS) + // Remove our observer + NotificationCenter.default.removeObserver( + self, + name: NSTextInputContext.keyboardSelectionDidChangeNotification, + object: nil) + #endif + } + + // MARK: - Config + + /// Initializes a new configuration and loads all the values. + static private func loadConfig() -> ghostty_config_t? { + // Initialize the global configuration. + guard let cfg = ghostty_config_new() else { + logger.critical("ghostty_config_new failed") + return nil + } + + // Load our configuration files from the home directory. + ghostty_config_load_default_files(cfg); + ghostty_config_load_cli_args(cfg); + ghostty_config_load_recursive_files(cfg); + + // TODO: we'd probably do some config loading here... for now we'd + // have to do this synchronously. When we support config updating we can do + // this async and update later. + + // Finalize will make our defaults available. + ghostty_config_finalize(cfg) + + // Log any configuration errors. These will be automatically shown in a + // pop-up window too. + let errCount = ghostty_config_errors_count(cfg) + if errCount > 0 { + logger.warning("config error: \(errCount) configuration errors on reload") + var errors: [String] = []; + for i in 0.. ghostty_config_t? { return nil } + static func openConfig(_ userdata: UnsafeMutableRawPointer?) {} + static func setTitle(_ userdata: UnsafeMutableRawPointer?, title: UnsafePointer?) {} + static func setMouseShape(_ userdata: UnsafeMutableRawPointer?, shape: ghostty_mouse_shape_e) {} + static func setMouseVisibility(_ userdata: UnsafeMutableRawPointer?, visible: Bool) {} + static func readClipboard( + _ userdata: UnsafeMutableRawPointer?, + location: ghostty_clipboard_e, + state: UnsafeMutableRawPointer? + ) {} + + static func confirmReadClipboard( + _ userdata: UnsafeMutableRawPointer?, + string: UnsafePointer?, + state: UnsafeMutableRawPointer?, + request: ghostty_clipboard_request_e + ) {} + + static func writeClipboard( + _ userdata: UnsafeMutableRawPointer?, + string: UnsafePointer?, + location: ghostty_clipboard_e, + confirm: Bool + ) {} + + static func newSplit( + _ userdata: UnsafeMutableRawPointer?, + direction: ghostty_split_direction_e, + config: ghostty_surface_config_s + ) {} + + static func newTab(_ userdata: UnsafeMutableRawPointer?, config: ghostty_surface_config_s) {} + static func newWindow(_ userdata: UnsafeMutableRawPointer?, config: ghostty_surface_config_s) {} + static func controlInspector(_ userdata: UnsafeMutableRawPointer?, mode: ghostty_inspector_mode_e) {} + static func closeSurface(_ userdata: UnsafeMutableRawPointer?, processAlive: Bool) {} + static func focusSplit(_ userdata: UnsafeMutableRawPointer?, direction: ghostty_split_focus_direction_e) {} + static func resizeSplit(_ userdata: UnsafeMutableRawPointer?, direction: ghostty_split_resize_direction_e, amount: UInt16) {} + static func equalizeSplits(_ userdata: UnsafeMutableRawPointer?) {} + static func toggleSplitZoom(_ userdata: UnsafeMutableRawPointer?) {} + static func gotoTab(_ userdata: UnsafeMutableRawPointer?, n: Int32) {} + static func toggleFullscreen(_ userdata: UnsafeMutableRawPointer?, nonNativeFullscreen: ghostty_non_native_fullscreen_e) {} + static func setInitialWindowSize(_ userdata: UnsafeMutableRawPointer?, width: UInt32, height: UInt32) {} + static func renderInspector(_ userdata: UnsafeMutableRawPointer?) {} + static func setCellSize(_ userdata: UnsafeMutableRawPointer?, width: UInt32, height: UInt32) {} + static func showUserNotification(_ userdata: UnsafeMutableRawPointer?, title: UnsafePointer?, body: UnsafePointer?) {} + } +} diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index e1f3f5e99..c5b0269c6 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -1,7 +1,14 @@ +import os import SwiftUI import GhosttyKit struct Ghostty { + // The primary logger used by the GhosttyKit libraries. + static let logger = Logger( + subsystem: Bundle.main.bundleIdentifier!, + category: "ghostty" + ) + // All the notifications that will be emitted will be put here. struct Notification {}