diff --git a/.github/workflows/release-tip.yml b/.github/workflows/release-tip.yml index 4553dde90..4b590e612 100644 --- a/.github/workflows/release-tip.yml +++ b/.github/workflows/release-tip.yml @@ -88,7 +88,7 @@ jobs: # codesigning. IMPORTANT: this must NOT run in a Nix environment. # Nix breaks xcodebuild so this has to be run outside. - name: Build Ghostty.app - run: cd macos && xcodebuild -configuration Release + run: cd macos && xcodebuild -target Ghostty -configuration Release # We inject the "build number" as simply the number of commits since HEAD. # This will be a monotonically always increasing build number that we use. diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 75dc9a04c..994f1a57b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -88,7 +88,13 @@ jobs: # codesigning. IMPORTANT: this must NOT run in a Nix environment. # Nix breaks xcodebuild so this has to be run outside. - name: Build Ghostty.app - run: cd macos && xcodebuild + run: cd macos && xcodebuild -target Ghostty + + # Build the iOS target without code signing just to verify it works. + - name: Build Ghostty iOS + run: | + cd macos + xcodebuild -target Ghostty-iOS "CODE_SIGNING_ALLOWED=NO" build-windows: runs-on: windows-2019 diff --git a/build.zig b/build.zig index 8407d743c..222b8ed37 100644 --- a/build.zig +++ b/build.zig @@ -37,25 +37,7 @@ const app_version = std.SemanticVersion{ .major = 0, .minor = 1, .patch = 0 }; pub fn build(b: *std.Build) !void { const optimize = b.standardOptimizeOption(.{}); - const target = target: { - var result = b.standardTargetOptions(.{}); - - // On macOS, we specify a minimum supported version. This is important - // to set since header files will use this to determine the availability - // of certain APIs and I believe it is also encoded in the Mach-O - // binaries. - if (result.result.os.tag == .macos and - result.query.os_version_min == null) - { - result.query.os_version_min = .{ .semver = .{ - .major = 12, - .minor = 0, - .patch = 0, - } }; - } - - break :target result; - }; + const target = b.standardTargetOptions(.{}); const wasm_target: WasmTarget = .browser; @@ -438,92 +420,55 @@ pub fn build(b: *std.Build) !void { // On Mac we can build the embedding library. This only handles the macOS lib. if (builtin.target.isDarwin() and target.result.os.tag == .macos) { - // Modify our build configuration for macOS builds. - const macos_config: BuildConfig = config: { - var copy = config; - - // Always static for the macOS app because we want all of our - // dependencies in a fat static library. - copy.static = true; - - break :config copy; - }; - - const static_lib_aarch64 = lib: { - const lib = b.addStaticLibrary(.{ - .name = "ghostty", - .root_source_file = .{ .path = "src/main_c.zig" }, - .target = b.resolveTargetQuery(.{ - .cpu_arch = .aarch64, - .os_tag = .macos, - .os_version_min = target.query.os_version_min, - }), - .optimize = optimize, - }); - lib.bundle_compiler_rt = true; - lib.linkLibC(); - lib.root_module.addOptions("build_options", exe_options); - - // Create a single static lib with all our dependencies merged - var lib_list = try addDeps(b, lib, macos_config); - try lib_list.append(lib.getEmittedBin()); - const libtool = LibtoolStep.create(b, .{ - .name = "ghostty", - .out_name = "libghostty-aarch64-fat.a", - .sources = lib_list.items, - }); - libtool.step.dependOn(&lib.step); - b.default_step.dependOn(libtool.step); - - break :lib libtool; - }; - - const static_lib_x86_64 = lib: { - const lib = b.addStaticLibrary(.{ - .name = "ghostty", - .root_source_file = .{ .path = "src/main_c.zig" }, - .target = b.resolveTargetQuery(.{ - .cpu_arch = .x86_64, - .os_tag = .macos, - .os_version_min = target.query.os_version_min, - }), - .optimize = optimize, - }); - lib.bundle_compiler_rt = true; - lib.linkLibC(); - lib.root_module.addOptions("build_options", exe_options); - - // Create a single static lib with all our dependencies merged - var lib_list = try addDeps(b, lib, macos_config); - try lib_list.append(lib.getEmittedBin()); - const libtool = LibtoolStep.create(b, .{ - .name = "ghostty", - .out_name = "libghostty-x86_64-fat.a", - .sources = lib_list.items, - }); - libtool.step.dependOn(&lib.step); - b.default_step.dependOn(libtool.step); - - break :lib libtool; - }; - - const static_lib_universal = LipoStep.create(b, .{ - .name = "ghostty", - .out_name = "libghostty.a", - .input_a = static_lib_aarch64.output, - .input_b = static_lib_x86_64.output, - }); - static_lib_universal.step.dependOn(static_lib_aarch64.step); - static_lib_universal.step.dependOn(static_lib_x86_64.step); + // Create the universal macOS lib. + const macos_lib_step, const macos_lib_path = try createMacOSLib( + b, + optimize, + config, + exe_options, + ); // Add our library to zig-out const lib_install = b.addInstallLibFile( - static_lib_universal.output, - "libghostty.a", + macos_lib_path, + "libghostty-macos.a", ); b.getInstallStep().dependOn(&lib_install.step); - // Copy our ghostty.h to include + // Create the universal iOS lib. + const ios_lib_step, const ios_lib_path = try createIOSLib( + b, + null, + optimize, + config, + exe_options, + ); + + // Add our library to zig-out + const ios_lib_install = b.addInstallLibFile( + ios_lib_path, + "libghostty-ios.a", + ); + b.getInstallStep().dependOn(&ios_lib_install.step); + + // Create the iOS simulator lib. + const ios_sim_lib_step, const ios_sim_lib_path = try createIOSLib( + b, + .simulator, + optimize, + config, + exe_options, + ); + + // Add our library to zig-out + const ios_sim_lib_install = b.addInstallLibFile( + ios_sim_lib_path, + "libghostty-ios-simulator.a", + ); + b.getInstallStep().dependOn(&ios_sim_lib_install.step); + + // Copy our ghostty.h to include. The header file is shared by + // all embedded targets. const header_install = b.addInstallHeaderFile( "include/ghostty.h", "ghostty.h", @@ -535,10 +480,25 @@ pub fn build(b: *std.Build) !void { const xcframework = XCFrameworkStep.create(b, .{ .name = "GhosttyKit", .out_path = "macos/GhosttyKit.xcframework", - .library = static_lib_universal.output, - .headers = .{ .path = "include" }, + .libraries = &.{ + .{ + .library = macos_lib_path, + .headers = .{ .path = "include" }, + }, + .{ + .library = ios_lib_path, + .headers = .{ .path = "include" }, + }, + .{ + .library = ios_sim_lib_path, + .headers = .{ .path = "include" }, + }, + }, }); - xcframework.step.dependOn(static_lib_universal.step); + xcframework.step.dependOn(ios_lib_step); + xcframework.step.dependOn(ios_sim_lib_step); + xcframework.step.dependOn(macos_lib_step); + xcframework.step.dependOn(&header_install.step); b.default_step.dependOn(xcframework.step); } @@ -672,6 +632,175 @@ pub fn build(b: *std.Build) !void { } } +/// Returns the minimum OS version for the given OS tag. This shouldn't +/// be used generally, it should only be used for Darwin-based OS currently. +fn osVersionMin(tag: std.Target.Os.Tag) ?std.Target.Query.OsVersion { + return switch (tag) { + // The lowest supported version of macOS is 12.x because + // this is the first version to support Apple Silicon so it is + // the earliest version we can virtualize to test (I only have + // an Apple Silicon machine for macOS). + .macos => .{ .semver = .{ + .major = 12, + .minor = 0, + .patch = 0, + } }, + + // iOS 17 picked arbitrarily + .ios => .{ .semver = .{ + .major = 17, + .minor = 0, + .patch = 0, + } }, + + // This should never happen currently. If we add a new target then + // we should add a new case here. + else => @panic("unhandled os version min os tag"), + }; +} + +/// Creates a universal macOS libghostty library and returns the path +/// to the final library. +/// +/// The library is always a fat static library currently because this is +/// expected to be used directly with Xcode and Swift. In the future, we +/// probably want to change this because it makes it harder to use the +/// library in other contexts. +fn createMacOSLib( + b: *std.Build, + optimize: std.builtin.OptimizeMode, + config: BuildConfig, + exe_options: *std.Build.Step.Options, +) !struct { *std.Build.Step, std.Build.LazyPath } { + // Modify our build configuration for macOS builds. + const macos_config: BuildConfig = config: { + var copy = config; + + // Always static for the macOS app because we want all of our + // dependencies in a fat static library. + copy.static = true; + + break :config copy; + }; + + const static_lib_aarch64 = lib: { + const lib = b.addStaticLibrary(.{ + .name = "ghostty", + .root_source_file = .{ .path = "src/main_c.zig" }, + .target = b.resolveTargetQuery(.{ + .cpu_arch = .aarch64, + .os_tag = .macos, + .os_version_min = osVersionMin(.macos), + }), + .optimize = optimize, + }); + lib.bundle_compiler_rt = true; + lib.linkLibC(); + lib.root_module.addOptions("build_options", exe_options); + + // Create a single static lib with all our dependencies merged + var lib_list = try addDeps(b, lib, macos_config); + try lib_list.append(lib.getEmittedBin()); + const libtool = LibtoolStep.create(b, .{ + .name = "ghostty", + .out_name = "libghostty-aarch64-fat.a", + .sources = lib_list.items, + }); + libtool.step.dependOn(&lib.step); + b.default_step.dependOn(libtool.step); + + break :lib libtool; + }; + + const static_lib_x86_64 = lib: { + const lib = b.addStaticLibrary(.{ + .name = "ghostty", + .root_source_file = .{ .path = "src/main_c.zig" }, + .target = b.resolveTargetQuery(.{ + .cpu_arch = .x86_64, + .os_tag = .macos, + .os_version_min = osVersionMin(.macos), + }), + .optimize = optimize, + }); + lib.bundle_compiler_rt = true; + lib.linkLibC(); + lib.root_module.addOptions("build_options", exe_options); + + // Create a single static lib with all our dependencies merged + var lib_list = try addDeps(b, lib, macos_config); + try lib_list.append(lib.getEmittedBin()); + const libtool = LibtoolStep.create(b, .{ + .name = "ghostty", + .out_name = "libghostty-x86_64-fat.a", + .sources = lib_list.items, + }); + libtool.step.dependOn(&lib.step); + b.default_step.dependOn(libtool.step); + + break :lib libtool; + }; + + const static_lib_universal = LipoStep.create(b, .{ + .name = "ghostty", + .out_name = "libghostty.a", + .input_a = static_lib_aarch64.output, + .input_b = static_lib_x86_64.output, + }); + static_lib_universal.step.dependOn(static_lib_aarch64.step); + static_lib_universal.step.dependOn(static_lib_x86_64.step); + + return .{ + static_lib_universal.step, + static_lib_universal.output, + }; +} + +/// Create an Apple iOS/iPadOS build. +fn createIOSLib( + b: *std.Build, + abi: ?std.Target.Abi, + optimize: std.builtin.OptimizeMode, + config: BuildConfig, + exe_options: *std.Build.Step.Options, +) !struct { *std.Build.Step, std.Build.LazyPath } { + const ios_config: BuildConfig = config: { + var copy = config; + copy.static = true; + break :config copy; + }; + + const lib = b.addStaticLibrary(.{ + .name = "ghostty", + .root_source_file = .{ .path = "src/main_c.zig" }, + .optimize = optimize, + .target = b.resolveTargetQuery(.{ + .cpu_arch = .aarch64, + .os_tag = .ios, + .os_version_min = osVersionMin(.ios), + .abi = abi, + }), + }); + lib.bundle_compiler_rt = true; + lib.linkLibC(); + lib.root_module.addOptions("build_options", exe_options); + + // Create a single static lib with all our dependencies merged + var lib_list = try addDeps(b, lib, ios_config); + try lib_list.append(lib.getEmittedBin()); + const libtool = LibtoolStep.create(b, .{ + .name = "ghostty", + .out_name = "libghostty-ios-fat.a", + .sources = lib_list.items, + }); + libtool.step.dependOn(&lib.step); + + return .{ + libtool.step, + libtool.output, + }; +} + /// Used to keep track of a list of file sources. const LazyPathList = std.ArrayList(std.Build.LazyPath); @@ -830,7 +959,14 @@ fn addDeps( // Mac Stuff if (step.rootModuleTarget().isDarwin()) { - step.root_module.addImport("objc", objc_dep.module("objc")); + // This is a bit of a hack that should probably be fixed upstream + // in zig-objc, but we need to add the apple SDK paths to the + // zig-objc module so that it can find the objc runtime headers. + const module = objc_dep.module("objc"); + module.resolved_target = step.root_module.resolved_target; + try @import("apple_sdk").addPaths(b, module); + step.root_module.addImport("objc", module); + step.root_module.addImport("macos", macos_dep.module("macos")); step.linkLibrary(macos_dep.artifact("macos")); try static_libs.append(macos_dep.artifact("macos").getEmittedBin()); diff --git a/build.zig.zon b/build.zig.zon index 3db6b645e..43aab7d27 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -5,8 +5,8 @@ .dependencies = .{ // Zig libs .libxev = .{ - .url = "https://github.com/mitchellh/libxev/archive/74bc7aea4a8f88210f0ad4215108613ab7e7af1a.tar.gz", - .hash = "122029743e5d96aa1b57a1b99ff58bf13ff9ed6d8f624ac3ae8074062feb91c5bd8d", + .url = "https://github.com/mitchellh/libxev/archive/4e6781895e4e6c477597c8c2713d36cd82b57d07.tar.gz", + .hash = "12203f87e00caa6c07c02a748f234a5c0ee2ca5c334ec464e88810d93e7b5495a56f", }, .mach_glfw = .{ .url = "https://github.com/der-teufel-programming/mach-glfw/archive/a9aae000cdc104dabe75d829ff9dab6809e47604.tar.gz", diff --git a/macos/Assets.xcassets/AppIcon.appiconset/Contents.json b/macos/Assets.xcassets/AppIcon.appiconset/Contents.json index b8bd46272..eb3bbadd8 100644 --- a/macos/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/macos/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,5 +1,11 @@ { "images" : [ + { + "filename" : "icon_512x512@2x@2x 1.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, { "filename" : "icon_16x16.png", "idiom" : "mac", diff --git a/macos/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x@2x 1.png b/macos/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x@2x 1.png new file mode 100644 index 000000000..0368b4a42 Binary files /dev/null and b/macos/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x@2x 1.png differ diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 2e01d611f..7322c3a08 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -11,6 +11,9 @@ 552964E62B34A9B400030505 /* vim in Resources */ = {isa = PBXBuildFile; fileRef = 552964E52B34A9B400030505 /* vim */; }; 8503D7C72A549C66006CFF3D /* FullScreenHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8503D7C62A549C66006CFF3D /* FullScreenHandler.swift */; }; 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 */; }; @@ -20,8 +23,12 @@ A5278A9B2AA05B2600CD3039 /* Ghostty.Input.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5278A9A2AA05B2600CD3039 /* Ghostty.Input.swift */; }; A53426352A7DA53D00EBB7A2 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53426342A7DA53D00EBB7A2 /* AppDelegate.swift */; }; A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A535B9D9299C569B0017E2E4 /* ErrorView.swift */; }; + 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 */; }; + 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 */; }; @@ -60,6 +67,7 @@ 552964E52B34A9B400030505 /* vim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = vim; path = "../zig-out/share/vim"; sourceTree = ""; }; 8503D7C62A549C66006CFF3D /* FullScreenHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenHandler.swift; sourceTree = ""; }; 857F63802A5E64F200CA4815 /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = ""; }; + A514C8D52B54A16400493A16 /* Ghostty.Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Config.swift; sourceTree = ""; }; A51B78462AF4B58B00F3EDB9 /* TerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalWindow.swift; sourceTree = ""; }; A51BFC1D2B2FB5CE00E92F16 /* About.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = About.xib; sourceTree = ""; }; A51BFC1F2B2FB64F00E92F16 /* AboutController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutController.swift; sourceTree = ""; }; @@ -69,8 +77,9 @@ A5278A9A2AA05B2600CD3039 /* Ghostty.Input.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Input.swift; sourceTree = ""; }; 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 = ""; }; 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; }; @@ -99,6 +108,7 @@ A5CEAFFE29C2410700646FDA /* Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backport.swift; sourceTree = ""; }; A5D0AF3A2B36A1DE00D21823 /* TerminalRestorable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalRestorable.swift; sourceTree = ""; }; A5D0AF3C2B37804400D21823 /* CodableBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodableBridge.swift; sourceTree = ""; }; + A5D4499D2B53AE7B000F5B83 /* Ghostty-iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Ghostty-iOS.app"; sourceTree = BUILT_PRODUCTS_DIR; }; A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = GhosttyKit.xcframework; sourceTree = ""; }; A5E112922AF73E6E00C6E0C2 /* ClipboardConfirmation.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ClipboardConfirmation.xib; sourceTree = ""; }; A5E112942AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardConfirmationController.swift; sourceTree = ""; }; @@ -117,6 +127,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + A5D4499A2B53AE7B000F5B83 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + A53D0C8E2B53B0EA00305CE6 /* GhosttyKit.xcframework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -175,12 +193,37 @@ path = Settings; sourceTree = ""; }; - A54CD6ED299BEB14008C95BB /* Sources */ = { + A53D0C912B53B41900305CE6 /* App */ = { + isa = PBXGroup; + children = ( + A53D0C962B53B57D00305CE6 /* macOS */, + A53D0C922B53B42000305CE6 /* iOS */, + ); + path = App; + sourceTree = ""; + }; + A53D0C922B53B42000305CE6 /* iOS */ = { + isa = PBXGroup; + children = ( + A53D0C932B53B43700305CE6 /* iOSApp.swift */, + ); + path = iOS; + sourceTree = ""; + }; + A53D0C962B53B57D00305CE6 /* macOS */ = { isa = PBXGroup; children = ( A5FEB2FF2ABB69450068369E /* main.swift */, A53426342A7DA53D00EBB7A2 /* AppDelegate.swift */, 857F63802A5E64F200CA4815 /* MainMenu.xib */, + ); + path = macOS; + sourceTree = ""; + }; + A54CD6ED299BEB14008C95BB /* Sources */ = { + isa = PBXGroup; + children = ( + A53D0C912B53B41900305CE6 /* App */, A53426362A7DC53000EBB7A2 /* Features */, A534263D2A7DCBB000EBB7A2 /* Helpers */, A55B7BB429B6F4410055DE60 /* Ghostty */, @@ -192,9 +235,10 @@ isa = PBXGroup; children = ( A55B7BB729B6F53A0055DE60 /* Package.swift */, - A55B7BB529B6F47F0055DE60 /* AppState.swift */, A55B7BBB29B6FC330055DE60 /* SurfaceView.swift */, A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */, + A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */, + A514C8D52B54A16400493A16 /* Ghostty.Config.swift */, A5278A9A2AA05B2600CD3039 /* Ghostty.Input.swift */, A56D58852ACDDB4100508D2C /* Ghostty.Shell.swift */, A59630A32AF059BB00D64628 /* Ghostty.SplitNode.swift */, @@ -255,6 +299,7 @@ isa = PBXGroup; children = ( A5B30531299BEAAA0047F10C /* Ghostty.app */, + A5D4499D2B53AE7B000F5B83 /* Ghostty-iOS.app */, ); name = Products; sourceTree = ""; @@ -310,6 +355,23 @@ productReference = A5B30531299BEAAA0047F10C /* Ghostty.app */; productType = "com.apple.product-type.application"; }; + A5D4499C2B53AE7B000F5B83 /* Ghostty-iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = A5D449AB2B53AE7B000F5B83 /* Build configuration list for PBXNativeTarget "Ghostty-iOS" */; + buildPhases = ( + A5D449992B53AE7B000F5B83 /* Sources */, + A5D4499A2B53AE7B000F5B83 /* Frameworks */, + A5D4499B2B53AE7B000F5B83 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "Ghostty-iOS"; + productName = "Ghostty-iOS"; + productReference = A5D4499D2B53AE7B000F5B83 /* Ghostty-iOS.app */; + productType = "com.apple.product-type.application"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -317,12 +379,15 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1420; + LastSwiftUpdateCheck = 1520; LastUpgradeCheck = 1420; TargetAttributes = { A5B30530299BEAAA0047F10C = { CreatedOnToolsVersion = 14.2; }; + A5D4499C2B53AE7B000F5B83 = { + CreatedOnToolsVersion = 15.2; + }; }; }; buildConfigurationList = A5B3052C299BEAAA0047F10C /* Build configuration list for PBXProject "Ghostty" */; @@ -342,6 +407,7 @@ projectRoot = ""; targets = ( A5B30530299BEAAA0047F10C /* Ghostty */, + A5D4499C2B53AE7B000F5B83 /* Ghostty-iOS */, ); }; /* End PBXProject section */ @@ -363,6 +429,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + A5D4499B2B53AE7B000F5B83 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A53D0C952B53B4D800305CE6 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -371,6 +445,7 @@ buildActionMask = 2147483647; files = ( A59630A42AF059BB00D64628 /* Ghostty.SplitNode.swift in Sources */, + A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */, A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */, A5D0AF3D2B37804400D21823 /* CodableBridge.swift in Sources */, A5D0AF3B2B36A1DE00D21823 /* TerminalRestorable.swift in Sources */, @@ -390,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 */, @@ -403,6 +477,18 @@ A596309E2AEE1D6C00D64628 /* TerminalView.swift in Sources */, A5CEAFDE29B8058B00646FDA /* SplitView.Divider.swift in Sources */, A5E112972AF7401B00C6E0C2 /* ClipboardConfirmationView.swift in Sources */, + A514C8D82B54DC6800493A16 /* Ghostty.App.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + A5D449992B53AE7B000F5B83 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A53D0C942B53B43700305CE6 /* iOSApp.swift in Sources */, + A514C8D72B54A16400493A16 /* Ghostty.Config.swift in Sources */, + A53D0C9C2B543F7B00305CE6 /* Package.swift in Sources */, + A53D0C9B2B543F3B00305CE6 /* Ghostty.App.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -682,6 +768,123 @@ }; name = Release; }; + A5D449A82B53AE7B000F5B83 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = ""; + DEVELOPMENT_TEAM = ""; + ENABLE_PREVIEWS = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = Ghostty; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + 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; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + A5D449A92B53AE7B000F5B83 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = ""; + DEVELOPMENT_TEAM = ""; + ENABLE_PREVIEWS = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = Ghostty; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + 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; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + A5D449AA2B53AE7B000F5B83 /* ReleaseLocal */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = ""; + DEVELOPMENT_TEAM = ""; + ENABLE_PREVIEWS = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = Ghostty; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + 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; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = ReleaseLocal; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -705,6 +908,16 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = ReleaseLocal; }; + A5D449AB2B53AE7B000F5B83 /* Build configuration list for PBXNativeTarget "Ghostty-iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A5D449A82B53AE7B000F5B83 /* Debug */, + A5D449A92B53AE7B000F5B83 /* Release */, + A5D449AA2B53AE7B000F5B83 /* ReleaseLocal */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseLocal; + }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ diff --git a/macos/Sources/App/iOS/iOSApp.swift b/macos/Sources/App/iOS/iOSApp.swift new file mode 100644 index 000000000..bf581d6cd --- /dev/null +++ b/macos/Sources/App/iOS/iOSApp.swift @@ -0,0 +1,33 @@ +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") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxHeight: 96) + Text("Ghostty") + Text("State: \(ghostty_app.readiness.rawValue)") + } + .padding() + } +} + +#Preview { + iOS_ContentView() +} diff --git a/macos/Sources/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift similarity index 95% rename from macos/Sources/AppDelegate.swift rename to macos/Sources/App/macOS/AppDelegate.swift index 4415dbf3f..348b8aceb 100644 --- a/macos/Sources/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 @@ -143,7 +143,7 @@ class AppDelegate: NSObject, } func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { - return ghostty.shouldQuitAfterLastWindowClosed + return ghostty.config.shouldQuitAfterLastWindowClosed } func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { @@ -242,7 +242,7 @@ class AppDelegate: NSObject, /// Sync all of our menu item keyboard shortcuts with the Ghostty configuration. private func syncMenuShortcuts() { - guard ghostty.config != nil else { return } + guard ghostty.readiness == .ready else { return } syncMenuShortcut(action: "open_config", menuItem: self.menuOpenConfig) syncMenuShortcut(action: "reload_config", menuItem: self.menuReloadConfig) @@ -286,19 +286,16 @@ class AppDelegate: NSObject, /// Syncs a single menu shortcut for the given action. The action string is the same /// action string used for the Ghostty configuration. private func syncMenuShortcut(action: String, menuItem: NSMenuItem?) { - guard let cfg = ghostty.config else { return } guard let menu = menuItem else { return } - - let trigger = ghostty_config_trigger(cfg, action, UInt(action.count)) - guard let equiv = Ghostty.keyEquivalent(key: trigger.key) else { + guard let equiv = ghostty.config.keyEquivalent(for: action) else { // No shortcut, clear the menu item menu.keyEquivalent = "" menu.keyEquivalentModifierMask = [] return } - menu.keyEquivalent = equiv - menu.keyEquivalentModifierMask = Ghostty.eventModifierFlags(mods: trigger.mods) + menu.keyEquivalent = equiv.key + menu.keyEquivalentModifierMask = equiv.modifiers } private func focusedSurface() -> ghostty_surface_t? { @@ -341,7 +338,7 @@ class AppDelegate: NSObject, withCompletionHandler(options) } - //MARK: - GhosttyAppStateDelegate + //MARK: - GhosttyAppDelegate func findSurface(forUUID uuid: UUID) -> Ghostty.SurfaceView? { for c in terminalManager.windows { @@ -353,11 +350,11 @@ 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. - switch (ghostty.windowSaveState) { + switch (ghostty.config.windowSaveState) { case "never": UserDefaults.standard.setValue(false, forKey: "NSQuitAlwaysKeepsWindows") case "always": UserDefaults.standard.setValue(true, forKey: "NSQuitAlwaysKeepsWindows") case "default": fallthrough @@ -373,7 +370,7 @@ class AppDelegate: NSObject, // If we have configuration errors, we need to show them. let c = ConfigurationErrorsController.sharedInstance - c.errors = state.configErrors() + c.errors = state.config.errors if (c.errors.count > 0) { if (c.window == nil || !c.window!.isVisible) { c.showWindow(self) @@ -383,7 +380,7 @@ class AppDelegate: NSObject, /// Sync the appearance of our app with the theme specified in the config. private func syncAppearance() { - guard let theme = ghostty.windowTheme else { return } + guard let theme = ghostty.config.windowTheme else { return } switch (theme) { case "dark": let appearance = NSAppearance(named: .darkAqua) diff --git a/macos/Sources/MainMenu.xib b/macos/Sources/App/macOS/MainMenu.xib similarity index 100% rename from macos/Sources/MainMenu.xib rename to macos/Sources/App/macOS/MainMenu.xib diff --git a/macos/Sources/main.swift b/macos/Sources/App/macOS/main.swift similarity index 100% rename from macos/Sources/main.swift rename to macos/Sources/App/macOS/main.swift diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index b49e502e8..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 ) { @@ -101,7 +101,6 @@ class TerminalController: NSWindowController, NSWindowDelegate, tabListenForFrame = false guard let windows = self.window?.tabbedWindows else { return } - guard let cfg = ghostty.config else { return } // We only listen for frame changes if we have more than 1 window, // otherwise the accessory view doesn't matter. @@ -109,8 +108,7 @@ class TerminalController: NSWindowController, NSWindowDelegate, for (index, window) in windows.enumerated().prefix(9) { let action = "goto_tab:\(index + 1)" - let trigger = ghostty_config_trigger(cfg, action, UInt(action.count)) - guard let equiv = Ghostty.keyEquivalentLabel(key: trigger.key, mods: trigger.mods) else { + guard let equiv = ghostty.config.keyEquivalent(for: action) else { continue } @@ -157,13 +155,13 @@ class TerminalController: NSWindowController, NSWindowDelegate, window.identifier = .init(String(describing: TerminalWindowRestoration.self)) // If window decorations are disabled, remove our title - if (!ghostty.windowDecorations) { window.styleMask.remove(.titled) } + if (!ghostty.config.windowDecorations) { window.styleMask.remove(.titled) } // Terminals typically operate in sRGB color space and macOS defaults // to "native" which is typically P3. There is a lot more resources // covered in thie GitHub issue: https://github.com/mitchellh/ghostty/pull/376 // Ghostty defaults to sRGB but this can be overridden. - switch (ghostty.windowColorspace) { + switch (ghostty.config.windowColorspace) { case "display-p3": window.colorSpace = .displayP3 case "srgb": @@ -462,7 +460,7 @@ class TerminalController: NSWindowController, NSWindowDelegate, } func cellSizeDidChange(to: NSSize) { - guard ghostty.windowStepResize else { return } + guard ghostty.config.windowStepResize else { return } self.window?.contentResizeIncrements = to } @@ -504,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) } } @@ -591,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 b919d5282..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 @@ -66,7 +66,7 @@ class TerminalManager { let window = c.window! // We want to go fullscreen if we're configured for new windows to go fullscreen - var toggleFullScreen = ghostty.windowFullscreen + var toggleFullScreen = ghostty.config.windowFullscreen // If the previous focused window prior to creating this window is fullscreen, // then this window also becomes fullscreen. @@ -130,7 +130,7 @@ class TerminalManager { controller.showWindow(self) // Add the window to the tab group and show it. - switch ghostty.windowNewTabPosition { + switch ghostty.config.windowNewTabPosition { case "end": // If we already have a tab group and we want the new tab to open at the end, // then we use the last window in the tab group as the parent. diff --git a/macos/Sources/Features/Terminal/TerminalRestorable.swift b/macos/Sources/Features/Terminal/TerminalRestorable.swift index 7b70220b4..b808e5701 100644 --- a/macos/Sources/Features/Terminal/TerminalRestorable.swift +++ b/macos/Sources/Features/Terminal/TerminalRestorable.swift @@ -66,7 +66,7 @@ class TerminalWindowRestoration: NSObject, NSWindowRestoration { // If our configuration is "never" then we never restore the state // no matter what. - if (appDelegate.terminalManager.ghostty.windowSaveState == "never") { + if (appDelegate.terminalManager.ghostty.config.windowSaveState == "never") { completionHandler(nil, nil) return } 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/Ghostty.App.swift similarity index 67% rename from macos/Sources/Ghostty/AppState.swift rename to macos/Sources/Ghostty/Ghostty.App.swift index e9c15ee95..3afbc0870 100644 --- a/macos/Sources/Ghostty/AppState.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -2,51 +2,36 @@ import SwiftUI import UserNotifications import GhosttyKit -protocol GhosttyAppStateDelegate: AnyObject { +protocol GhosttyAppDelegate: AnyObject { /// Called when the configuration did finish reloading. - func configDidReload(_ state: Ghostty.AppState) + 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 { - 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 ghostty global configuration. This should only be changed when it is definitely - /// safe to change. It is definite 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) - } + // 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 } - + + /// Optional delegate + weak var delegate: GhosttyAppDelegate? + + /// The readiness value of the state. + @Published var readiness: Readiness = .loading + + /// 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 { @@ -55,253 +40,118 @@ extension Ghostty { ghostty_app_free(old) } } - - /// True if we should quit when the last window is closed. - var shouldQuitAfterLastWindowClosed: Bool { - guard let config = self.config else { return true } - var v = false; - let key = "quit-after-last-window-closed" - _ = ghostty_config_get(config, &v, key, UInt(key.count)) - return v - } - - /// window-colorspace - var windowColorspace: String { - guard let config = self.config else { return "" } - var v: UnsafePointer? = nil - let key = "window-colorspace" - guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return "" } - guard let ptr = v else { return "" } - return String(cString: ptr) - } - - /// window-save-state - var windowSaveState: String { - guard let config = self.config else { return "" } - var v: UnsafePointer? = nil - let key = "window-save-state" - guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return "" } - guard let ptr = v else { return "" } - return String(cString: ptr) - } - - /// window-new-tab-position - var windowNewTabPosition: String { - guard let config = self.config else { return "" } - var v: UnsafePointer? = nil - let key = "window-new-tab-position" - guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return "" } - guard let ptr = v else { return "" } - return String(cString: ptr) - } /// 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)) - } - - /// True if we want to render window decorations - var windowDecorations: Bool { - guard let config = self.config else { return true } - var v = false; - let key = "window-decoration" - _ = ghostty_config_get(config, &v, key, UInt(key.count)) - return v; - } - - /// The window theme as a string. - var windowTheme: String? { - guard let config = self.config else { return nil } - var v: UnsafePointer? = nil - let key = "window-theme" - guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return nil } - guard let ptr = v else { return nil } - return String(cString: ptr) - } - - /// Whether to resize windows in discrete steps or use "fluid" resizing - var windowStepResize: Bool { - guard let config = self.config else { return true } - var v = false - let key = "window-step-resize" - _ = ghostty_config_get(config, &v, key, UInt(key.count)) - return v - } - - /// Whether to open new windows in fullscreen. - var windowFullscreen: Bool { - guard let config = self.config else { return true } - var v = false - let key = "fullscreen" - _ = ghostty_config_get(config, &v, key, UInt(key.count)) - return v - } - - /// The background opacity. - var backgroundOpacity: Double { - guard let config = self.config else { return 1 } - var v: Double = 1 - let key = "background-opacity" - _ = ghostty_config_get(config, &v, key, UInt(key.count)) - return v; - } init() { // Initialize ghostty global state. This happens once per process. - guard ghostty_init() == GHOSTTY_SUCCESS else { - AppDelegate.logger.critical("ghostty_init failed") + if ghostty_init() != GHOSTTY_SUCCESS { + logger.critical("ghostty_init failed, weird things may happen") readiness = .error - return } // Initialize the global configuration. - guard let cfg = Self.loadConfig() else { + self.config = Config() + if self.config.config == nil { 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 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) }, + 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 - AppState.resizeSplit(userdata, direction: direction, amount: amount) }, + App.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) }, + 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 - AppState.showUserNotification(userdata, title: title, body: body) + App.showUserNotification(userdata, title: title, body: body) } ) - + // Create the ghostty app. - guard let app = ghostty_app_new(&runtime_cfg, cfg) else { - AppDelegate.logger.critical("ghostty_app_new failed") + guard let app = ghostty_app_new(&runtime_cfg, config.config) 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 } - - /// Initializes a new configuration and loads all the values. - static func loadConfig() -> ghostty_config_t? { - // Initialize the global configuration. - guard let cfg = ghostty_config_new() else { - AppDelegate.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 { - AppDelegate.logger.warning("config error: \(errCount) configuration errors on reload") - var errors: [String] = []; - for i in 0.. [String] { - guard let cfg = self.config else { return [] } - - var errors: [String] = []; - let errCount = ghostty_config_errors_count(cfg) - 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?) {} + #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 + + // 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) @@ -534,14 +445,15 @@ extension Ghostty { } static func reloadConfig(_ userdata: UnsafeMutableRawPointer?) -> ghostty_config_t? { - guard let newConfig = Self.loadConfig() else { + 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() + let state = Unmanaged.fromOpaque(userdata!).takeUnretainedValue() state.config = newConfig // If we have a delegate, notify. @@ -549,11 +461,11 @@ extension Ghostty { delegate.configDidReload(state) } - return newConfig + return newConfig.config } static func wakeup(_ userdata: UnsafeMutableRawPointer?) { - let state = Unmanaged.fromOpaque(userdata!).takeUnretainedValue() + 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 @@ -662,7 +574,7 @@ extension Ghostty { let surface = self.surfaceUserdata(from: userdata) guard let appState = self.appState(fromView: surface) else { return } - guard appState.windowDecorations else { + guard appState.config.windowDecorations else { let alert = NSAlert() alert.messageText = "Tabs are disabled" alert.informativeText = "Enable window decorations to use tabs" @@ -701,16 +613,18 @@ extension Ghostty { } /// Returns the GhosttyState from the given userdata value. - static private func appState(fromView view: SurfaceView) -> AppState? { + 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() + 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/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift new file mode 100644 index 000000000..5cf05694b --- /dev/null +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -0,0 +1,238 @@ +import SwiftUI +import GhosttyKit + +extension Ghostty { + /// Maps to a `ghostty_config_t` and the various operations on that. + class Config: ObservableObject { + // The underlying C pointer to the Ghostty config structure. This + // should never be accessed directly. Any operations on this should + // be called from the functions on this or another class. + private(set) var config: ghostty_config_t? = nil { + didSet { + // Free the old value whenever we change + guard let old = oldValue else { return } + ghostty_config_free(old) + } + } + + /// True if the configuration is loaded + var loaded: Bool { config != nil } + + /// Return the errors found while loading the configuration. + var errors: [String] { + guard let cfg = self.config else { return [] } + + var errors: [String] = []; + let errCount = ghostty_config_errors_count(cfg) + for i in 0.. 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 from files, CLI args, and then any referenced files. + // We only do this on macOS because other Apple platforms do not have the + // same filesystem concept. +#if os(macOS) + ghostty_config_load_default_files(cfg); + ghostty_config_load_cli_args(cfg); + ghostty_config_load_recursive_files(cfg); +#endif + + // 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.. KeyEquivalent? { + guard let cfg = self.config else { return nil } + + let trigger = ghostty_config_trigger(cfg, action, UInt(action.count)) + guard let equiv = Ghostty.keyEquivalent(key: trigger.key) else { return nil } + + return KeyEquivalent( + key: equiv, + modifiers: Ghostty.eventModifierFlags(mods: trigger.mods) + ) + } +#endif + + // MARK: - Configuration Values + + /// For all of the configuration values below, see the associated Ghostty documentation for + /// details on what each means. We only add documentation if there is a strange conversion + /// due to the embedded library and Swift. + + var shouldQuitAfterLastWindowClosed: Bool { + guard let config = self.config else { return true } + var v = false; + let key = "quit-after-last-window-closed" + _ = ghostty_config_get(config, &v, key, UInt(key.count)) + return v + } + + var windowColorspace: String { + guard let config = self.config else { return "" } + var v: UnsafePointer? = nil + let key = "window-colorspace" + guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return "" } + guard let ptr = v else { return "" } + return String(cString: ptr) + } + + var windowSaveState: String { + guard let config = self.config else { return "" } + var v: UnsafePointer? = nil + let key = "window-save-state" + guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return "" } + guard let ptr = v else { return "" } + return String(cString: ptr) + } + + var windowNewTabPosition: String { + guard let config = self.config else { return "" } + var v: UnsafePointer? = nil + let key = "window-new-tab-position" + guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return "" } + guard let ptr = v else { return "" } + return String(cString: ptr) + } + + var windowDecorations: Bool { + guard let config = self.config else { return true } + var v = false; + let key = "window-decoration" + _ = ghostty_config_get(config, &v, key, UInt(key.count)) + return v; + } + + var windowTheme: String? { + guard let config = self.config else { return nil } + var v: UnsafePointer? = nil + let key = "window-theme" + guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return nil } + guard let ptr = v else { return nil } + return String(cString: ptr) + } + + var windowStepResize: Bool { + guard let config = self.config else { return true } + var v = false + let key = "window-step-resize" + _ = ghostty_config_get(config, &v, key, UInt(key.count)) + return v + } + + var windowFullscreen: Bool { + guard let config = self.config else { return true } + var v = false + let key = "fullscreen" + _ = ghostty_config_get(config, &v, key, UInt(key.count)) + return v + } + + var backgroundOpacity: Double { + guard let config = self.config else { return 1 } + var v: Double = 1 + let key = "background-opacity" + _ = ghostty_config_get(config, &v, key, UInt(key.count)) + return v; + } + + var unfocusedSplitOpacity: Double { + guard let config = self.config else { return 1 } + var opacity: Double = 0.85 + let key = "unfocused-split-opacity" + _ = ghostty_config_get(config, &opacity, key, UInt(key.count)) + return 1 - opacity + } + + var unfocusedSplitFill: Color { + guard let config = self.config else { return .white } + + var rgb: UInt32 = 16777215 // white default + let key = "unfocused-split-fill" + if (!ghostty_config_get(config, &rgb, key, UInt(key.count))) { + let bg_key = "background" + _ = ghostty_config_get(config, &rgb, bg_key, UInt(bg_key.count)); + } + + let red = Double(rgb & 0xff) + let green = Double((rgb >> 8) & 0xff) + let blue = Double((rgb >> 16) & 0xff) + + return Color( + red: red / 255, + green: green / 255, + blue: blue / 255 + ) + } + } +} diff --git a/macos/Sources/Ghostty/Ghostty.Input.swift b/macos/Sources/Ghostty/Ghostty.Input.swift index dd71d2ed2..182e0dad1 100644 --- a/macos/Sources/Ghostty/Ghostty.Input.swift +++ b/macos/Sources/Ghostty/Ghostty.Input.swift @@ -7,21 +7,6 @@ extension Ghostty { return Self.keyToEquivalent[key] } - /// Returns the keyEquivalent label that includes the mods. - static func keyEquivalentLabel(key: ghostty_input_key_e, mods: ghostty_input_mods_e) -> String? { - guard var key = Self.keyEquivalent(key: key) else { return nil } - let flags = Self.eventModifierFlags(mods: mods) - - // Note: the order below matters; it matches the ordering modifiers show for - // macOS menu shortcut labels. - if flags.contains(.command) { key = "⌘\(key)" } - if flags.contains(.shift) { key = "⇧\(key)" } - if flags.contains(.option) { key = "⌥\(key)" } - if flags.contains(.control) { key = "⌃\(key)" } - - return key - } - /// Returns the event modifier flags set for the Ghostty mods enum. static func eventModifierFlags(mods: ghostty_input_mods_e) -> NSEvent.ModifierFlags { var flags = NSEvent.ModifierFlags(rawValue: 0); diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index e1f3f5e99..9f8fe5237 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 {} @@ -12,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 473a3a884..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,40 +49,12 @@ 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. private var hasFocus: Bool { surfaceFocus && windowFocus } - // The opacity of the rectangle when unfocused. - private var unfocusedOpacity: Double { - var opacity: Double = 0.85 - let key = "unfocused-split-opacity" - _ = ghostty_config_get(ghostty.config, &opacity, key, UInt(key.count)) - return 1 - opacity - } - - // The color for the rectangle overlay when unfocused. - private var unfocusedFill: Color { - var rgb: UInt32 = 16777215 // white default - let key = "unfocused-split-fill" - if (!ghostty_config_get(ghostty.config, &rgb, key, UInt(key.count))) { - let bg_key = "background" - _ = ghostty_config_get(ghostty.config, &rgb, bg_key, UInt(bg_key.count)); - } - - let red = Double(rgb & 0xff) - let green = Double((rgb >> 8) & 0xff) - let blue = Double((rgb >> 16) & 0xff) - - return Color( - red: red / 255, - green: green / 255, - blue: blue / 255 - ) - } - var body: some View { ZStack { // We use a GeometryReader to get the frame bounds so that our metal surface @@ -175,10 +147,10 @@ extension Ghostty { // because we want to keep our focused surface dark even if we don't have window // focus. if (isSplit && !surfaceFocus) { - let overlayOpacity = unfocusedOpacity; + let overlayOpacity = ghostty.config.unfocusedSplitOpacity; if (overlayOpacity > 0) { Rectangle() - .fill(unfocusedFill) + .fill(ghostty.config.unfocusedSplitFill) .allowsHitTesting(false) .opacity(overlayOpacity) } diff --git a/nix/zigCacheHash.nix b/nix/zigCacheHash.nix index 3129d2b76..63ca546e6 100644 --- a/nix/zigCacheHash.nix +++ b/nix/zigCacheHash.nix @@ -1,3 +1,3 @@ # This file is auto-generated! check build-support/check-zig-cache-hash.sh for # more details. -"sha256-hE4MNVZx/kA90MPHEraJDayBtLw29HZfnFChLdXPS0g=" +"sha256-to0V9rCefIs8KcWsx+nopgQO4i7O3gb06LGNc6NXN2M=" diff --git a/pkg/apple-sdk/build.zig b/pkg/apple-sdk/build.zig index 62f1372c6..ffb1671da 100644 --- a/pkg/apple-sdk/build.zig +++ b/pkg/apple-sdk/build.zig @@ -45,6 +45,7 @@ const SDK = struct { pub fn fromTarget(target: std.Target) !SDK { return switch (target.os.tag) { + .ios => .{ .platform = "iPhoneOS", .version = "" }, .macos => .{ .platform = "MacOSX", .version = "14" }, else => { std.log.err("unsupported os={}", .{target.os.tag}); diff --git a/pkg/cimgui/build.zig b/pkg/cimgui/build.zig index 34585bac9..3e397f955 100644 --- a/pkg/cimgui/build.zig +++ b/pkg/cimgui/build.zig @@ -71,10 +71,12 @@ pub fn build(b: *std.Build) !void { .file = imgui.path("backends/imgui_impl_metal.mm"), .flags = flags.items, }); - lib.addCSourceFile(.{ - .file = imgui.path("backends/imgui_impl_osx.mm"), - .flags = flags.items, - }); + if (target.result.os.tag == .macos) { + lib.addCSourceFile(.{ + .file = imgui.path("backends/imgui_impl_osx.mm"), + .flags = flags.items, + }); + } } lib.installHeadersDirectoryOptions(.{ diff --git a/pkg/freetype/build.zig b/pkg/freetype/build.zig index 1770e3e49..8716c572a 100644 --- a/pkg/freetype/build.zig +++ b/pkg/freetype/build.zig @@ -15,6 +15,11 @@ pub fn build(b: *std.Build) !void { }); lib.linkLibC(); lib.addIncludePath(upstream.path("include")); + if (target.result.isDarwin()) { + const apple_sdk = @import("apple_sdk"); + try apple_sdk.addPaths(b, &lib.root_module); + } + module.addIncludePath(upstream.path("include")); module.addIncludePath(.{ .path = "" }); @@ -86,7 +91,7 @@ pub fn build(b: *std.Build) !void { b.installArtifact(lib); - { + if (target.query.isNative()) { const test_exe = b.addTest(.{ .name = "test", .root_source_file = .{ .path = "main.zig" }, diff --git a/pkg/freetype/build.zig.zon b/pkg/freetype/build.zig.zon index 29b694973..5c6538fd5 100644 --- a/pkg/freetype/build.zig.zon +++ b/pkg/freetype/build.zig.zon @@ -1,12 +1,14 @@ .{ .name = "freetype", .version = "2.13.2", + .paths = .{""}, .dependencies = .{ .freetype = .{ .url = "https://github.com/freetype/freetype/archive/refs/tags/VER-2-13-2.tar.gz", .hash = "1220b81f6ecfb3fd222f76cf9106fecfa6554ab07ec7fdc4124b9bb063ae2adf969d", }, + .apple_sdk = .{ .path = "../apple-sdk" }, .libpng = .{ .path = "../libpng" }, .zlib = .{ .path = "../zlib" }, }, diff --git a/pkg/glslang/build.zig b/pkg/glslang/build.zig index d73306071..8d6fc1ff1 100644 --- a/pkg/glslang/build.zig +++ b/pkg/glslang/build.zig @@ -12,8 +12,15 @@ pub fn build(b: *std.Build) !void { module.addIncludePath(upstream.path("")); module.addIncludePath(.{ .path = "override" }); + if (target.result.isDarwin()) { + // See pkg/harfbuzz/build.zig + module.resolved_target = target; + defer module.resolved_target = null; + const apple_sdk = @import("apple_sdk"); + try apple_sdk.addPaths(b, module); + } - { + if (target.query.isNative()) { const test_exe = b.addTest(.{ .name = "test", .root_source_file = .{ .path = "main.zig" }, @@ -45,6 +52,10 @@ fn buildGlslang( lib.linkLibCpp(); lib.addIncludePath(upstream.path("")); lib.addIncludePath(.{ .path = "override" }); + if (target.result.isDarwin()) { + const apple_sdk = @import("apple_sdk"); + try apple_sdk.addPaths(b, &lib.root_module); + } var flags = std.ArrayList([]const u8).init(b.allocator); defer flags.deinit(); diff --git a/pkg/glslang/build.zig.zon b/pkg/glslang/build.zig.zon index d1ffcfa5c..d4b469204 100644 --- a/pkg/glslang/build.zig.zon +++ b/pkg/glslang/build.zig.zon @@ -7,5 +7,7 @@ .url = "https://github.com/KhronosGroup/glslang/archive/refs/tags/13.1.1.tar.gz", .hash = "1220481fe19def1172cd0728743019c0f440181a6342b62d03e24d05c70141516799", }, + + .apple_sdk = .{ .path = "../apple-sdk" }, }, } diff --git a/pkg/harfbuzz/build.zig b/pkg/harfbuzz/build.zig index 51aeb6f81..9b5778584 100644 --- a/pkg/harfbuzz/build.zig +++ b/pkg/harfbuzz/build.zig @@ -34,6 +34,18 @@ pub fn build(b: *std.Build) !void { lib.addIncludePath(upstream.path("src")); module.addIncludePath(upstream.path("src")); + if (target.result.isDarwin()) { + // This is definitely super sketchy and not right but without this + // zig build test breaks on macOS. We have to look into what exactly + // is going on here but this getting comitted in the interest of + // unblocking zig build test. + module.resolved_target = target; + defer module.resolved_target = null; + + try apple_sdk.addPaths(b, &lib.root_module); + try apple_sdk.addPaths(b, module); + } + const freetype_dep = b.dependency("freetype", .{ .target = target, .optimize = optimize }); lib.linkLibrary(freetype_dep.artifact("freetype")); module.addIncludePath(freetype_dep.builder.dependency("freetype", .{}).path("include")); @@ -59,19 +71,10 @@ pub fn build(b: *std.Build) !void { "-DHAVE_FT_DONE_MM_VAR=1", "-DHAVE_FT_GET_TRANSFORM=1", }); - if (coretext_enabled and target.result.isDarwin()) { - // This is definitely super sketchy and not right but without this - // zig build test breaks on macOS. We have to look into what exactly - // is going on here but this getting comitted in the interest of - // unblocking zig build test. - module.resolved_target = target; - defer module.resolved_target = null; - + if (coretext_enabled) { try flags.appendSlice(&.{"-DHAVE_CORETEXT=1"}); - try apple_sdk.addPaths(b, &lib.root_module); - try apple_sdk.addPaths(b, module); - lib.linkFramework("ApplicationServices"); - module.linkFramework("ApplicationServices", .{}); + lib.linkFramework("CoreText"); + module.linkFramework("CoreText", .{}); } lib.addCSourceFile(.{ diff --git a/pkg/harfbuzz/build.zig.zon b/pkg/harfbuzz/build.zig.zon index a9bc2ba7e..1fa67abc9 100644 --- a/pkg/harfbuzz/build.zig.zon +++ b/pkg/harfbuzz/build.zig.zon @@ -1,6 +1,7 @@ .{ .name = "harfbuzz", .version = "8.2.2", + .paths = .{""}, .dependencies = .{ .harfbuzz = .{ .url = "https://github.com/harfbuzz/harfbuzz/archive/refs/tags/8.2.2.tar.gz", diff --git a/pkg/libpng/build.zig b/pkg/libpng/build.zig index 10785c07d..accbdd9cc 100644 --- a/pkg/libpng/build.zig +++ b/pkg/libpng/build.zig @@ -15,6 +15,10 @@ pub fn build(b: *std.Build) !void { if (target.result.os.tag == .linux) { lib.linkSystemLibrary("m"); } + if (target.result.isDarwin()) { + const apple_sdk = @import("apple_sdk"); + try apple_sdk.addPaths(b, &lib.root_module); + } const zlib_dep = b.dependency("zlib", .{ .target = target, .optimize = optimize }); lib.linkLibrary(zlib_dep.artifact("z")); diff --git a/pkg/libpng/build.zig.zon b/pkg/libpng/build.zig.zon index ebad1dcb4..6f0985812 100644 --- a/pkg/libpng/build.zig.zon +++ b/pkg/libpng/build.zig.zon @@ -1,14 +1,14 @@ .{ .name = "libpng", .version = "1.6.40", + .paths = .{""}, .dependencies = .{ .libpng = .{ .url = "https://github.com/glennrp/libpng/archive/refs/tags/v1.6.40.tar.gz", .hash = "12203d2722e3af6f9556503b114c25fe3eead528a93f5f26eefcb187a460d1548e07", }, - .zlib = .{ - .path = "../zlib", - }, + .zlib = .{ .path = "../zlib" }, + .apple_sdk = .{ .path = "../apple-sdk" }, }, } diff --git a/pkg/macos/build.zig b/pkg/macos/build.zig index 3891553f2..1a43c8daf 100644 --- a/pkg/macos/build.zig +++ b/pkg/macos/build.zig @@ -28,14 +28,16 @@ pub fn build(b: *std.Build) !void { .file = .{ .path = "text/ext.c" }, .flags = flags.items, }); - lib.linkFramework("Carbon"); lib.linkFramework("CoreFoundation"); lib.linkFramework("CoreGraphics"); lib.linkFramework("CoreText"); lib.linkFramework("CoreVideo"); + if (target.result.os.tag == .macos) { + lib.linkFramework("Carbon"); + module.linkFramework("Carbon", .{}); + } if (target.result.isDarwin()) { - module.linkFramework("Carbon", .{}); module.linkFramework("CoreFoundation", .{}); module.linkFramework("CoreGraphics", .{}); module.linkFramework("CoreText", .{}); diff --git a/pkg/oniguruma/build.zig b/pkg/oniguruma/build.zig index 9fa8772cd..0b5d43e83 100644 --- a/pkg/oniguruma/build.zig +++ b/pkg/oniguruma/build.zig @@ -12,7 +12,7 @@ pub fn build(b: *std.Build) !void { module.addIncludePath(upstream.path("src")); b.installArtifact(lib); - { + if (target.query.isNative()) { const test_exe = b.addTest(.{ .name = "test", .root_source_file = .{ .path = "main.zig" }, @@ -44,6 +44,11 @@ fn buildOniguruma( lib.linkLibC(); lib.addIncludePath(upstream.path("src")); + if (target.result.isDarwin()) { + const apple_sdk = @import("apple_sdk"); + try apple_sdk.addPaths(b, &lib.root_module); + } + lib.addConfigHeader(b.addConfigHeader(.{ .style = .{ .cmake = upstream.path("src/config.h.cmake.in") }, }, .{ diff --git a/pkg/oniguruma/build.zig.zon b/pkg/oniguruma/build.zig.zon index 8e08a0ad2..2120f77ae 100644 --- a/pkg/oniguruma/build.zig.zon +++ b/pkg/oniguruma/build.zig.zon @@ -7,5 +7,7 @@ .url = "https://github.com/kkos/oniguruma/archive/refs/tags/v6.9.9.tar.gz", .hash = "1220c15e72eadd0d9085a8af134904d9a0f5dfcbed5f606ad60edc60ebeccd9706bb", }, + + .apple_sdk = .{ .path = "../apple-sdk" }, }, } diff --git a/pkg/pixman/build.zig b/pkg/pixman/build.zig index a74fece29..42c514e9d 100644 --- a/pkg/pixman/build.zig +++ b/pkg/pixman/build.zig @@ -16,6 +16,10 @@ pub fn build(b: *std.Build) !void { if (target.result.os.tag != .windows) { lib.linkSystemLibrary("pthread"); } + if (target.result.isDarwin()) { + const apple_sdk = @import("apple_sdk"); + try apple_sdk.addPaths(b, &lib.root_module); + } lib.addIncludePath(upstream.path("")); lib.addIncludePath(.{ .path = "" }); @@ -68,7 +72,7 @@ pub fn build(b: *std.Build) !void { b.installArtifact(lib); - { + if (target.query.isNative()) { const test_exe = b.addTest(.{ .name = "test", .root_source_file = .{ .path = "main.zig" }, diff --git a/pkg/pixman/build.zig.zon b/pkg/pixman/build.zig.zon index c4ed35a62..af6813e07 100644 --- a/pkg/pixman/build.zig.zon +++ b/pkg/pixman/build.zig.zon @@ -1,10 +1,13 @@ .{ .name = "pixman", .version = "0.42.2", + .paths = .{""}, .dependencies = .{ .pixman = .{ .url = "https://deps.files.ghostty.dev/pixman-pixman-0.42.2.tar.gz", .hash = "12209b9206f9a5d31ccd9a2312cc72cb9dfc3e034aee1883c549dc1d753fae457230", }, + + .apple_sdk = .{ .path = "../apple-sdk" }, }, } diff --git a/pkg/spirv-cross/build.zig b/pkg/spirv-cross/build.zig index 76b29a279..37da13eee 100644 --- a/pkg/spirv-cross/build.zig +++ b/pkg/spirv-cross/build.zig @@ -12,7 +12,7 @@ pub fn build(b: *std.Build) !void { const lib = try buildSpirvCross(b, upstream, target, optimize); b.installArtifact(lib); - { + if (target.query.isNative()) { const test_exe = b.addTest(.{ .name = "test", .root_source_file = .{ .path = "main.zig" }, @@ -42,8 +42,10 @@ fn buildSpirvCross( }); lib.linkLibC(); lib.linkLibCpp(); - //lib.addIncludePath(upstream.path("")); - //lib.addIncludePath(.{ .path = "override" }); + if (target.result.isDarwin()) { + const apple_sdk = @import("apple_sdk"); + try apple_sdk.addPaths(b, &lib.root_module); + } var flags = std.ArrayList([]const u8).init(b.allocator); defer flags.deinit(); diff --git a/pkg/spirv-cross/build.zig.zon b/pkg/spirv-cross/build.zig.zon index 8338b7a61..9100bb967 100644 --- a/pkg/spirv-cross/build.zig.zon +++ b/pkg/spirv-cross/build.zig.zon @@ -7,5 +7,7 @@ .url = "https://github.com/KhronosGroup/SPIRV-Cross/archive/4818f7e7ef7b7078a3a7a5a52c4a338e0dda22f4.tar.gz", .hash = "1220b2d8a6cff1926ef28a29e312a0a503b555ebc2f082230b882410f49e672ac9c6", }, + + .apple_sdk = .{ .path = "../apple-sdk" }, }, } diff --git a/pkg/zlib/build.zig b/pkg/zlib/build.zig index de00f4b73..695ebcb40 100644 --- a/pkg/zlib/build.zig +++ b/pkg/zlib/build.zig @@ -13,6 +13,11 @@ pub fn build(b: *std.Build) !void { }); lib.linkLibC(); lib.addIncludePath(upstream.path("")); + if (target.result.isDarwin()) { + const apple_sdk = @import("apple_sdk"); + try apple_sdk.addPaths(b, &lib.root_module); + } + lib.installHeadersDirectoryOptions(.{ .source_dir = upstream.path(""), .install_dir = .header, diff --git a/pkg/zlib/build.zig.zon b/pkg/zlib/build.zig.zon index 7550da4a3..1f23bd588 100644 --- a/pkg/zlib/build.zig.zon +++ b/pkg/zlib/build.zig.zon @@ -1,10 +1,13 @@ .{ .name = "zlib", .version = "1.3.0", + .paths = .{""}, .dependencies = .{ .zlib = .{ .url = "https://github.com/madler/zlib/archive/refs/tags/v1.3.tar.gz", .hash = "12207d353609d95cee9da7891919e6d9582e97b7aa2831bd50f33bf523a582a08547", }, + + .apple_sdk = .{ .path = "../apple-sdk" }, }, } diff --git a/src/Command.zig b/src/Command.zig index 4a15d1229..af3979b3e 100644 --- a/src/Command.zig +++ b/src/Command.zig @@ -290,7 +290,7 @@ fn setupFd(src: File.Handle, target: i32) !void { } } }, - .macos => { + .ios, .macos => { // Mac doesn't support dup3 so we use dup2. We purposely clear // CLO_ON_EXEC for this fd. const flags = try os.fcntl(src, os.F.GETFD, 0); diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 9b79fefca..a88aa55b0 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -1664,6 +1664,9 @@ pub const CAPI = struct { ptr: *Surface, window: *anyopaque, ) void { + // This is only supported on macOS + if (comptime builtin.target.os.tag != .macos) return; + const config = ptr.app.config; // Do nothing if we don't have background transparency enabled diff --git a/src/build/XCFrameworkStep.zig b/src/build/XCFrameworkStep.zig index a611edc4b..823e5aac4 100644 --- a/src/build/XCFrameworkStep.zig +++ b/src/build/XCFrameworkStep.zig @@ -15,6 +15,12 @@ pub const Options = struct { /// The path to write the framework out_path: []const u8, + /// The libraries to bundle + libraries: []const Library, +}; + +/// A single library to bundle into the xcframework. +pub const Library = struct { /// Library file (dylib, a) to package. library: LazyPath, @@ -41,10 +47,12 @@ pub fn create(b: *std.Build, opts: Options) *XCFrameworkStep { const run = RunStep.create(b, b.fmt("xcframework {s}", .{opts.name})); run.has_side_effects = true; run.addArgs(&.{ "xcodebuild", "-create-xcframework" }); - run.addArg("-library"); - run.addFileArg(opts.library); - run.addArg("-headers"); - run.addFileArg(opts.headers); + for (opts.libraries) |lib| { + run.addArg("-library"); + run.addFileArg(lib.library); + run.addArg("-headers"); + run.addFileArg(lib.headers); + } run.addArg("-output"); run.addArg(opts.out_path); break :run run; diff --git a/src/input.zig b/src/input.zig index 14140a524..814415fcb 100644 --- a/src/input.zig +++ b/src/input.zig @@ -18,7 +18,7 @@ pub const SplitResizeDirection = Binding.Action.SplitResizeDirection; // in theory for XKB too on Linux but we don't need it right now. pub const Keymap = switch (builtin.os.tag) { .macos => @import("input/KeymapDarwin.zig"), - else => struct {}, + else => @import("input/KeymapNoop.zig"), }; test { diff --git a/src/input/KeymapNoop.zig b/src/input/KeymapNoop.zig new file mode 100644 index 000000000..414c52954 --- /dev/null +++ b/src/input/KeymapNoop.zig @@ -0,0 +1,38 @@ +//! A noop implementation of the keymap interface so that the embedded +//! library can compile on non-macOS platforms. +const KeymapNoop = @This(); + +const Mods = @import("key.zig").Mods; + +pub const State = struct {}; +pub const Translation = struct { + text: []const u8, + composing: bool, +}; + +pub fn init() !KeymapNoop { + return .{}; +} + +pub fn deinit(self: *const KeymapNoop) void { + _ = self; +} + +pub fn reload(self: *KeymapNoop) !void { + _ = self; +} + +pub fn translate( + self: *const KeymapNoop, + out: []u8, + state: *State, + code: u16, + mods: Mods, +) !Translation { + _ = self; + _ = out; + _ = state; + _ = code; + _ = mods; + return .{ .text = "", .composing = false }; +} diff --git a/src/input/keycodes.zig b/src/input/keycodes.zig index 170739aa9..82f526818 100644 --- a/src/input/keycodes.zig +++ b/src/input/keycodes.zig @@ -9,7 +9,7 @@ const Key = @import("key.zig").Key; /// The full list of entries for the current platform. pub const entries: []const Entry = entries: { const native_idx = switch (builtin.os.tag) { - .macos => 4, // mac + .ios, .macos => 4, // mac .windows => 3, // win .linux => 2, // xkb else => @compileError("unsupported platform"), diff --git a/src/inspector/main.zig b/src/inspector/main.zig index 57612ddee..920491dd8 100644 --- a/src/inspector/main.zig +++ b/src/inspector/main.zig @@ -1,3 +1,4 @@ +const std = @import("std"); pub const cursor = @import("cursor.zig"); pub const key = @import("key.zig"); pub const termio = @import("termio.zig"); diff --git a/src/main.zig b/src/main.zig index a761f359e..fe3c24925 100644 --- a/src/main.zig +++ b/src/main.zig @@ -130,7 +130,7 @@ pub const std_options = struct { // // sudo log stream --level debug --predicate 'subsystem=="com.mitchellh.ghostty"' // - if (builtin.os.tag == .macos) { + if (builtin.target.isDarwin()) { // Convert our levels to Mac levels const mac_level: macos.os.LogType = switch (level) { .debug => .debug, diff --git a/src/os/desktop.zig b/src/os/desktop.zig index 6475c278e..efc3b1541 100644 --- a/src/os/desktop.zig +++ b/src/os/desktop.zig @@ -52,6 +52,9 @@ pub fn launchedFromDesktop() bool { // TODO: This should have some logic to detect this. Perhaps std.builtin.subsystem .windows => false, + // iPhone/iPad is always launched from the "desktop" + .ios => true, + else => @compileError("unsupported platform"), }; } diff --git a/src/os/homedir.zig b/src/os/homedir.zig index ab60cdf26..854c7d62c 100644 --- a/src/os/homedir.zig +++ b/src/os/homedir.zig @@ -14,6 +14,10 @@ pub inline fn home(buf: []u8) !?[]u8 { return switch (builtin.os.tag) { inline .linux, .macos => try homeUnix(buf), .windows => try homeWindows(buf), + + // iOS doesn't have a user-writable home directory + .ios => null, + else => @compileError("unimplemented"), }; } diff --git a/src/os/mouse.zig b/src/os/mouse.zig index 1774399c9..fa39882c7 100644 --- a/src/os/mouse.zig +++ b/src/os/mouse.zig @@ -7,18 +7,20 @@ const log = std.log.scoped(.os); /// The system-configured double-click interval if its available. pub fn clickInterval() ?u32 { - // On macOS, we can ask the system. - if (comptime builtin.target.isDarwin()) { - const NSEvent = objc.getClass("NSEvent") orelse { - log.err("NSEvent class not found. Can't get click interval.", .{}); - return null; - }; + return switch (builtin.os.tag) { + // On macOS, we can ask the system. + .macos => macos: { + const NSEvent = objc.getClass("NSEvent") orelse { + log.err("NSEvent class not found. Can't get click interval.", .{}); + return null; + }; - // Get the interval and convert to ms - const interval = NSEvent.msgSend(f64, objc.sel("doubleClickInterval"), .{}); - const ms = @as(u32, @intFromFloat(@ceil(interval * 1000))); - return ms; - } + // Get the interval and convert to ms + const interval = NSEvent.msgSend(f64, objc.sel("doubleClickInterval"), .{}); + const ms = @as(u32, @intFromFloat(@ceil(interval * 1000))); + break :macos ms; + }, - return null; + else => null, + }; } diff --git a/src/os/open.zig b/src/os/open.zig index 14e21111f..8bf8bb7ca 100644 --- a/src/os/open.zig +++ b/src/os/open.zig @@ -8,6 +8,7 @@ pub fn open(alloc: Allocator, url: []const u8) !void { .linux => &.{ "xdg-open", url }, .macos => &.{ "open", url }, .windows => &.{ "rundll32", "url.dll,FileProtocolHandler", url }, + .ios => return error.Unimplemented, else => @compileError("unsupported OS"), }; diff --git a/src/pty.zig b/src/pty.zig index f31d5f97d..66014dc57 100644 --- a/src/pty.zig +++ b/src/pty.zig @@ -14,10 +14,41 @@ pub const winsize = extern struct { ws_ypixel: u16 = 600, }; -pub const Pty = if (builtin.os.tag == .windows) - WindowsPty -else - PosixPty; +pub const Pty = switch (builtin.os.tag) { + .windows => WindowsPty, + .ios => NullPty, + else => PosixPty, +}; + +// A pty implementation that does nothing. +// +// TODO: This should be removed. This is only temporary until we have +// a termio that doesn't use a pty. This isn't used in any user-facing +// artifacts, this is just a stopgap to get compilation to work on iOS. +const NullPty = struct { + pub const Fd = std.os.fd_t; + + master: Fd, + slave: Fd, + + pub fn open(size: winsize) !Pty { + _ = size; + return .{ .master = 0, .slave = 0 }; + } + + pub fn deinit(self: *Pty) void { + _ = self; + } + + pub fn setSize(self: *Pty, size: winsize) !void { + _ = self; + _ = size; + } + + pub fn childPreExec(self: Pty) !void { + _ = self; + } +}; /// Linux PTY creation and management. This is just a thin layer on top /// of Linux syscalls. The caller is responsible for detail-oriented handling