diff --git a/build.zig b/build.zig index 8407d743c..21f6a0523 100644 --- a/build.zig +++ b/build.zig @@ -44,14 +44,31 @@ pub fn build(b: *std.Build) !void { // 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, - } }; + if (result.query.os_version_min == null) { + switch (result.result.os.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 => { + result.query.os_version_min = .{ .semver = .{ + .major = 12, + .minor = 0, + .patch = 0, + } }; + }, + + // iOS 17 picked arbitrarily + .ios => { + result.query.os_version_min = .{ .semver = .{ + .major = 17, + .minor = 0, + .patch = 0, + } }; + }, + + else => {}, + } } break :target result; @@ -542,6 +559,65 @@ pub fn build(b: *std.Build) !void { b.default_step.dependOn(xcframework.step); } + // iOS + if (builtin.target.isDarwin() and target.result.os.tag == .ios) { + 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" }, + .target = b.resolveTargetQuery(.{ + .cpu_arch = .aarch64, + .os_tag = .ios, + .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, 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); + b.default_step.dependOn(libtool.step); + + // Add our library to zig-out + const lib_install = b.addInstallLibFile( + libtool.output, + "libghostty.a", + ); + b.getInstallStep().dependOn(&lib_install.step); + + // Copy our ghostty.h to include + const header_install = b.addInstallHeaderFile( + "include/ghostty.h", + "ghostty.h", + ); + b.getInstallStep().dependOn(&header_install.step); + + // // The xcframework wraps our ghostty library so that we can link + // // it to the final app built with Swift. + // const xcframework = XCFrameworkStep.create(b, .{ + // .name = "GhosttyKit", + // .out_path = "macos/GhosttyKit.xcframework", + // .library = libtool.output, + // .headers = .{ .path = "include" }, + // }); + // xcframework.step.dependOn(libtool.step); + // b.default_step.dependOn(xcframework.step); + } + // wasm { // Build our Wasm target. @@ -830,7 +906,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/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/input.zig b/src/input.zig index 14140a524..47024ff67 100644 --- a/src/input.zig +++ b/src/input.zig @@ -17,7 +17,7 @@ pub const SplitResizeDirection = Binding.Action.SplitResizeDirection; // Keymap is only available on macOS right now. We could implement it // 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"), + .ios, .macos => @import("input/KeymapDarwin.zig"), else => struct {}, }; 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/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/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 -- 2.51.2 From 722348f552eb8da7da8582206620c1fd2d776827 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 13 Jan 2024 21:32:53 -0800 Subject: [PATCH 02/19] build: build iOS lib into XCFramework --- build.zig | 385 ++++++++++++++++++---------------- src/build/XCFrameworkStep.zig | 16 +- 2 files changed, 219 insertions(+), 182 deletions(-) diff --git a/build.zig b/build.zig index 21f6a0523..96f5d41aa 100644 --- a/build.zig +++ b/build.zig @@ -37,42 +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.query.os_version_min == null) { - switch (result.result.os.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 => { - result.query.os_version_min = .{ .semver = .{ - .major = 12, - .minor = 0, - .patch = 0, - } }; - }, - - // iOS 17 picked arbitrarily - .ios => { - result.query.os_version_min = .{ .semver = .{ - .major = 17, - .minor = 0, - .patch = 0, - } }; - }, - - else => {}, - } - } - - break :target result; - }; + const target = b.standardTargetOptions(.{}); const wasm_target: WasmTarget = .browser; @@ -455,92 +420,38 @@ 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, + 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); + + // 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", @@ -552,72 +463,23 @@ 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" }, + }, + }, }); - xcframework.step.dependOn(static_lib_universal.step); + xcframework.step.dependOn(ios_lib_step); + xcframework.step.dependOn(macos_lib_step); + xcframework.step.dependOn(&header_install.step); b.default_step.dependOn(xcframework.step); } - // iOS - if (builtin.target.isDarwin() and target.result.os.tag == .ios) { - 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" }, - .target = b.resolveTargetQuery(.{ - .cpu_arch = .aarch64, - .os_tag = .ios, - .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, 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); - b.default_step.dependOn(libtool.step); - - // Add our library to zig-out - const lib_install = b.addInstallLibFile( - libtool.output, - "libghostty.a", - ); - b.getInstallStep().dependOn(&lib_install.step); - - // Copy our ghostty.h to include - const header_install = b.addInstallHeaderFile( - "include/ghostty.h", - "ghostty.h", - ); - b.getInstallStep().dependOn(&header_install.step); - - // // The xcframework wraps our ghostty library so that we can link - // // it to the final app built with Swift. - // const xcframework = XCFrameworkStep.create(b, .{ - // .name = "GhosttyKit", - // .out_path = "macos/GhosttyKit.xcframework", - // .library = libtool.output, - // .headers = .{ .path = "include" }, - // }); - // xcframework.step.dependOn(libtool.step); - // b.default_step.dependOn(xcframework.step); - } - // wasm { // Build our Wasm target. @@ -748,6 +610,173 @@ 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, + 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), + }), + }); + 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); 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; -- 2.51.2 From 468ba9ef86dd8d8b53dc0f67db8e8531818af9db Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 13 Jan 2024 21:39:35 -0800 Subject: [PATCH 03/19] nix: update hash --- nix/zigCacheHash.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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=" -- 2.51.2 From 3c7fe08d875f74caed83193f1af71d32b39b81da Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 13 Jan 2024 22:11:13 -0800 Subject: [PATCH 04/19] build: add iOS simulator target --- build.zig | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/build.zig b/build.zig index 96f5d41aa..eb7dac7c8 100644 --- a/build.zig +++ b/build.zig @@ -438,6 +438,7 @@ pub fn build(b: *std.Build) !void { // Create the universal iOS lib. const ios_lib_step, const ios_lib_path = try createIOSLib( b, + null, optimize, config, exe_options, @@ -450,6 +451,22 @@ pub fn build(b: *std.Build) !void { ); 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_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( @@ -472,9 +489,14 @@ pub fn build(b: *std.Build) !void { .library = ios_lib_path, .headers = .{ .path = "include" }, }, + .{ + .library = ios_sim_lib_path, + .headers = .{ .path = "include" }, + }, }, }); 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); @@ -737,6 +759,7 @@ fn createMacOSLib( /// 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, @@ -755,6 +778,7 @@ fn createIOSLib( .cpu_arch = .aarch64, .os_tag = .ios, .os_version_min = osVersionMin(.ios), + .abi = abi, }), }); lib.bundle_compiler_rt = true; -- 2.51.2 From 48af1c6c99f0e09813ff6c55823c6fdde63b35da Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 13 Jan 2024 22:24:35 -0800 Subject: [PATCH 05/19] macos: add iOS target --- .../AppIcon.appiconset/Contents.json | 6 + .../icon_512x512@2x@2x 1.png | Bin 0 -> 94863 bytes macos/Ghostty.xcodeproj/project.pbxproj | 201 +++++++++++++++++- macos/Sources/App/iOS/iOSApp.swift | 26 +++ .../Sources/{ => App/macOS}/AppDelegate.swift | 0 macos/Sources/{ => App/macOS}/MainMenu.xib | 0 macos/Sources/{ => App/macOS}/main.swift | 0 7 files changed, 231 insertions(+), 2 deletions(-) create mode 100644 macos/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x@2x 1.png create mode 100644 macos/Sources/App/iOS/iOSApp.swift rename macos/Sources/{ => App/macOS}/AppDelegate.swift (100%) rename macos/Sources/{ => App/macOS}/MainMenu.xib (100%) rename macos/Sources/{ => App/macOS}/main.swift (100%) 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 0000000000000000000000000000000000000000..0368b4a422f4c487cf22ba0cdbd51f86107e6ece GIT binary patch literal 94863 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 z zdCm&i&%Q^0iT%TA|CtxO7OvxbTT|P#|J$?WckTaw_L2A}%voyiV|L=5t-@)7X~NG8 zUP!#&?6FNSN~2y`hIMlHiH6$mGao!Fy>HX?^YwM{QjOAQNB;;g8cObw-YFx%=l`^6 z)2+n^a;%R1&wg&^l=1A7tEIorslV|n{#*~Rt&#e1`gd&6(cm+jZ{_(Ib~*{)x_UGI ztNV%n|9Ei|7 z5~ce(PTHq-_Zci)(eAyjrew;e@9X$u>c0BVJnr7f@S(Tf$z1yX;@SK28p0R;m;T@C zE$9AUvW&^V?&Np-CokGRn*Tet-{f=3UAqgsbvs|Jw-Q+}oB6<`Njbs?JouwG9R77+ z<0NZ_mcR||6IkrnG#Bs&to&JefYIStf_oxIZ0TM7CgGHNruWXzE*?z${XTTbmBT+< zQ+UqYcS=3A`o`o9kM1{x{^Rr6R=xNBTR9Plga;M!bC~9;JQqk)QcB1YeD$yS{rU28 z+YiSy9_#;K-aP;R@B8veb_O@36Mvj%eV;dld9jA^q{FFg{~uQH&(ZoEaBuArfBiXJ zG4g*uoA)#Q^ZMOdclE3N@z3Q8DyU_P(|NqD9#p`WqA$802`-}myzYn$NGG%yrK4WlDzqGLZjZ5VZyKX^-mCS8t zl)s)}402(3Q^XKc{dm2B;_)M5LRKmboYfgleJ5)a?7uWHE||Oj%cl01l_!6!pUafI zO;-C~tD<&Y*J_4CdF>HZ5=_4&E%rCBGwn_>j@>`c;NaYvQ$K(Hbf5R$IqtDY)xTR( zeUl!9{_k46Wby^&9sEpBG!z`T?v~H{vS`nWReUVV_KnjaWK+t1CI;IZickLs1x^y5ZJ=eC z;NW%FUQRAx!SCnpl`F2kKIrf+nR!Ko8jUu+zdncj&l;IEaTGkN)U+faSVb z_FIhy@`d&7uRzL)1Nb3mHywe|D*Z8)S2q5xx1Xg!~ZD15iMqTadMI+gAmW30`{Y& ze_v038_w{IeZigwizgP?9%bB+^Yy)9rs(8P3>87#FP&w$eLl3VlV_K!K7aS~h1u`o z|EKb;H%We4Z`SiQZfDb33Bh{%C9_K@$2irPc$zAuChTHmS zb55Q5Kg_~(m#fEr0cD$oTz(mC4UU4m7d&UQdNgQmF?^6y{@yviw&{-|^Mm5>HI=u@ z88ZH`zTekWeEN*9*WdoQe-nb%7yUQ?r)zB9WvkxxJ#^jo`Xgku3ZnfRjT%^;pA_i-uR1hgrh7)=>TCAG za}F>aEK);L7Ju>i?%3&bV~k9_+`N$_zEl|KG0@irN^`%XQ~MOFCH+5Me?IcFwERXQFGp&= za!JC|N{5^i)81`7{+wCq$llpsZmP`g>V0?4COy6GpWTs-0smOP%=xlMDQHpjIxC|~ zwTzwp1e zKKr)Xm&taLtP}qJo&UJ%<#%&Xvv#@_lLF(ey-BkfUV&2JrSLyr`CiKFuVi-kp1>cv z^@`!gp`L~e{0OkfARdbI>R)bqm~R#Y`@qag(`CDJO9_IXA=0YoBpU_MnNqDTZ#V5 z&t0PLqRsvW&Swb!;(mYa35I*ZL9Zug@tj{~!B*BY8(`>BGx28`r%5 zul42ssvby_=2Jb>0_iVN|Nh5MWcZ?0!<2Ax(p$y_B_0oqB^V7t68seNgNh;F)|cvv`wzn}A^vmCe2gs1VdjD`=wfJcDw z!Hl|n7n0e0Jl@wxm~gDje>UG(Soq#|t)N%RmU7J>zRD@eZd#fvD}7JqoF30D#%EKG z*xM+0TWTEIS3dRf^S-R}r}iIMJmJ^RKQ&+G_=c=6PDuE2_&~=a`HA)$8kJJ!8Qs&+S%>?DvF)st&b8-uo3Mpp)c^D6i=WT&{JSHuW0_yaluuG+Giv_6{#^V~KC;f{ zuvymoEAKA;>z;Z;p#HMf?93OSrs* zM8ItrLTp3=l)iJE+T1isRS-~|;2l;1yJ zn`6Fn?)QChGe7*AFgb^zD&bJg`Uy_*9}6q@EB8Ic86CC3+4McCLUgv zW_x4eu9J6v9+-WQb4}9^PP+w9_dj2_J#)iczw_7IM01ZW@Z}f$(s=cL-J4ss?O)uk z+iuY1{(s|7E%WWfD@svJ507g8dHnfwynn{yUyrNK9Ev}yf8ed`MAaAU%!>ayvR>T% z%guiKeWmWsf1f{J|D8YYU;dx+fZq$tROZZLw!HUW@>jhTxVm53!*F5$%j<93e{e8% zGhCRYTFjtQldNpD@Rh;=!xNYG-1x+=@lwD((=Eemwt&6P681u$C-vzOUUzrjTO0lF z%>9YKrEk?5rK+0wU4l_tVk& z=l5++Y56*T;os*E^(|B0{y#RqKGY>AYt}vg&&utNso?JNX`9G%{~tU5+1inOn_0m& zauWvw=iLL-FW8pc`+7=}(LiI#g`-@WORhwEz4&=~-ElXmh0@v!dbfD}?%Vx5kxA{g z3bSy8O!Jk`r~kj4P`N(-XLOa>zn}YWs_f>deK7N!y_)^L?Bi{PYu|k_y#D0D`?>4o z<&D2Z)!ErBf6s6tG>-Y6zPib-*WrK6nCm?P#TWicEU127)_*_6J(Bfxyi0@0$A65W zEB^2K!&s>k{NMcF?e|r^i~TRmtMd33{`>EI<$BR$kZRcT*~uApSulKB@1@RXQOCltejB^mk)HBamw6qWC07a?kBZw0{qVD%TVKQC zkg9wjto+YM@h1=7JKO&MA*}bHmy7fBhdsU ze7EgYhd(dg^zSn7J)Xb!e-!`zct1Pzb^p)b(CB{J&2Zr@v;51S|BrF^K4aie_kPB} zQTMo?k0r9fT71cc!^ayZSu;FgFxFueV$-PX+tWTP{*~imw$-!t-0I_8$G&EolT$m7 z&xD2T{lBkI{xAGO`p(0*SMJR}XJ6Oa*qDF7+9SU>{@o_chwJ@PliU!kJn&&aB1~Csi|M**X@`2d;dp#)zjeJ zvJ4$|D}VjAcmBWr5Ofe_J+s4heU(RVd2h1>_)j~-D8TV^`mT2qFZchJ{jqrm=K`Yy zh4_+=nPo9|nH$10wS{Bk%eghbd}xXKr{D46XSs8J*4+pB)25z@*Y}q%O8@QWwA=dC zik+4}Hn-U6EPZ$V`18I;MOn%vou9lOuJ=#)lK(6^#`tP7+rO&Xjy>K2u}bxEMlt^{ zExh>VoXzuhUw)lGAOHTm<*)r8mwzkfw_}_UU1;~T`hWV1|FEJhMUa7!S?<%~8`8U) z57;q+vsR03Mi#qpN56o^MRkToajP#E3R4*_pYQ+P(H$D~rp|8tjGnSDIf9d~l(&jh zB{;odKHhX)yU+30gya97XNF(gy!C-(HRs=#e;$9TKT!VmZQgoY=}h(a*H@phSF%sE zwmlzLZSv;VyM1msGiDsK-&eaR_aNheugAYer2d-_6u$2G+qt)F=Lsg>IQWZQK8ML7 z^VKJY#aE9@gI&1gvlC@A|MX>qEbO{fq8tc2E8$Imw^Wo@>8^UpI;AtJ&n&8)yCb`kM2B z#MbXem3Fc4H6;A?W_MU^{p&^Fmv8gu@1JMy{@?uHl{%}PvYjWE{d@EN|CfLBA6NG7 z2X$Y%7(PtQd*JMK*N(BnW7-+U2`>_xd)$7?GVEfvFtrz`wY~3gJO4ggoU7bLmLGbG zOaB%6{aC;EV|Yr1@8bz&lOE0fX7caj(@0HiGp6#tSxhI@%74vRUHk7QkDPqX`^(4U zbe(SP=KQbG9Lv-NacsPDfCLDP@lUw4A(_WQcIw|Bpf33|1E z`h}Cv1(sKpKVEe|eBr;(AD(AB{(`pP^LG5Ls4flp`*;4r|KW+yF1IF=L$5?#Ysm?w z3*4Q148L*B&*o#Wjof6*aLIZB-^-=i`Y9w}9inep{$=tB|CFuZUYoT&bgF#{gA)H2<-P=W&&@X? z*4Y1V@x0P0Z+P*-o@KNCv|iab;g;q0^1Sc+1liVQv3LZZzI6qZ)c4m<{k{KVepaM- z$CZ;G{`tT9KQrNvB4~tnDzii6#{T-}xmJ9mHuT5i8y>t0RekM0E2n_`Quvl@_p|SUyB+{mP-x%3){~tePZ`1f;9e;wdMNQv@I2ZLlxA&KZx7zni{qS=UpTz%v@9&1h z@%~%&==jO_Da=cZ8XYS=+c~ekKDO#ZyWOJ4;W59B!=sH`Z`Byzs^$N6up#Jw?Tw9l zZ?HF{%$b235BhKZ&v;gN{ekMW`u{=2sJ}e4d*9E{@OFW|1Dlx4(f>@> z&Dk3!=|r+HSj};`pvZ9d{Q)sX0|AD5X%ixGPv3 z{Qhy__4`H7wcmX>JX=uhcHGZ@^Hb`L{g z?;kxsun#HRuD^744Jb&T;Y6D2i>$EH* zgYD6I8~hnAff_JJ%6EJbm{;V=u!L8DlX-$s0-rZOhrMo(RLc^xiG?ON=Wc!x=abg> zyEWwli@1>0k~7kF_uKcl-QDu^3r}fI!s!q7$Bv(iKmGnL|I@3%H}CWr%yqhb`|dMY z{ra3glI0WlIAt&Gmb?G`#p2r!buyJp9{l{Z>QKDjzAb;0a^$|A@SCf-uI|?-mVa5* zEDNfCo{y~1-=1F+WhDJ}pZNKepB(>+*LZ9bShh>pKWfH>f15w?+s$wK0!o#04+N^; z%YLyO(maE;A|jui^y1jRPu<}U(-K#P3{Yoy)yu};+OH=B>EBmt_|>5J^$R1VLUOwVZ!~ zZNKQaRwf!-WL}Z0|5N$$kD_ZN`)TVPA&wRkb`%F$x z|BCw6?6)rEc2|q7{_~&Y*Id89=IrGw>t1j8dEe*b=lZ`qQ|&)>_8O-PN^#p6&k9q! zQqT8qLgVvBxgBv?|IPp1vFrLS74~27YRF6FpZ7tH&|lEXeIm2Nng7k-nB|*As+#^D z>?mc3*pxDxA+3O8%5g@61h~-YPDW-g;d2_B*q;6_N89PJ4du_;_tY z%f6hm?@u_)ezpEzH}5T>pUV&b=2^hJ@a^uNTgRSF)%?@yQrXU21Hx z_Z|6{|L6RYvV(7#`_$i9U-T8~+jZyt|34lQ(2O-x{rBhpDa`kk7%oj#EoNB2{LAC} z2~CZO%nH&a{FPUJMlAiyzIfUN2jOd;?{?ppEs`$(bN6ncyxMaWN$b{>4~8fDTc*CA zSLj~9QIsu8nCI5HuEt|4#Ohyce0o*8$D!_B)gg=Pcej63{`md0c$I9o37M9`da(qd-wbQ$K;RwulwyMR9$jrTHo?N@Bg3qYcC5P*xz(!@>f=u zyN&|+{WF*y{HMJUZb)Dk+Qjg@oByU}EsH~LJ>v`cED=Yqxv6;?+Oscn3g6xTr+9aC z@#W7m7?{6vcb?Q=!e9A@`Ez{DQ{i_LnqO``=6{*SN?H)>hoE% z=Rf}zy(Ky4&vW-_OI#Wk{JZ^eb6zk<@|`coQa_%LR{fXQ9Oki5`=>Ifl~(4Eo7eOH z$`LuCT9x0Q_W%F<{r{4W=Rq@6`GO1+7(HgQTyIagq0UgXDMgUMvH4#6o0LnI0`CuM zXR|o$pWqbE*7M-!)(MS&eto^d*nK!~@6@Q4?Ba^zvn=I57Jkip`LzGik#D&VbQPDL z+hnA9jko39Ki|Chd(|%3swqy1mB=VyNO>S1{ZB*Rt$+I_+o=ZA_MNrAeRtE-^-}*= zeA-?5I{xlQ^*Lvc*;`Au^>#^FcxP2{)<*uQzJC3d{VA?MVbcPGt3K*JI-qMr(apL=NYHEgKXj+>mLVB<~@7!HScxo z`rm&Rwx8m!RM>v+r&MgZZan{eq3fNZNBK6+x;uSx{gIywx0wphpJlM#@ZIY8mnGBV zzAiZ*|0=P$r{8g1%y*rwmy37qU3B-*kM%QsUx>Ix%>VbtN?y?Pvfdnr$9Mnz_SFb$ zG=EoNx$Ix?Pj1;u%@*d5s+^8lv)FO}**S0JzWgUu?)GafUf%jp_d;m4-?yjr|DV7A z|MDleExB~2`tlpXUMxj7{uzJIpW{3C5h-xJZ$9BDb@wPWV#qYfKdl33& z)@7~9DQ|l2x)rVaakhTVq||5f!S)P2C*>x6KJuL5>+ZQ~?)4iFo_)LP=Gwhqmrj(l zIPmLsoYuL=pA*jrz4FXI=J)9HpMpm{f0WfC7-ZCpPO*Hwci?TqcN2v<&-`pY=4}6P z==X`jPfZv5}+?LYg%@8pWCtNVNE{;s3q zwl8-5&M#M=U$xJ=;QvR1KgSQ3Hy zhTXpZKk)qg>3;uwc698$Q$J0%bQj+7>-tu7sy_0?E_Smb>qp;0wygHGjxRiY?a#i9 zd}i)#MZXu9S%hv`dni!JY5jAr$MXYQf4vv|YJYou{g>(gUC+&t%saG1yZiaR50~8U z^scUdZkG4|cm1YvIg4|*9YomX7Omeu{q5e}x$)ZDeQn+RCQP_+yu#sxoI%sIm?RDx zp2Wgbdp`G1(tTT3C7f+mn!xUG|NMWpAMEz5^{o3@|Fhb&)wAsnI=Myn(U)b)ezO}6 zt^aS;Ru36|=snMHfz>9-{;~N#%}JlzH%?=SxVg2g;3U%9m+W_N(sN`Pt2aosQmrp_3J4 zn_hkSssBIcS16>-6%bu_Q=|TA&C2>8XNq!dH}M@)Vd}HxYbp45=#GK^nVUuX1mg3~ zKVBjFPmgOu`;Ki6pS%9ef3)5B;r|EMU;OuPI{!QTut&sA{iiigKmBKjC|_*m=dO3` zrQ}u1;6MF8%9Z@u_xzhK|NkdwD9QZaR~3e!+6g-oSG?}t(sV$J;Y-?CAqJy_7q6N2 z^S|~J~z4RW;_$ugZ0ky^>=PN zwq~;NB*vGwZn6HBvHoO{eEh#!Wc2x>=k?D2=2yjV|Cd+&cVCa&FXGpYR|`K+FRT)I zEb`wkbkd1w@8>-Ls-5D)SsPlkK77`m@K&ASn$c`k1|y@KQy*=v%x5^Uf8mSA z?YkGgJmjXv^sM8ubjz!y$9EoZ{XL8O__2F_w$E}+8=dE<#!upYFrPb?V;B28JNwTP z>>CfviCOabXkVd#aPl!_jX6phTo2~|{-=F?+V1aV=c*)))u+pTEz4mb zDq+DL|2xH}A3J?h2;|lpp2B?#@9&TJclUK)%jK+x@&CTodq{vrK&wBj*J7LyY<8~+WEd6))(Ef~nH(~^DOth%xPLKh8bd}@&xV)0F8dGi z6g6Ktu6?|;kXtT#M~0)tvnPEzkIv|xdOrVu<%3I~lna^CA9ytVF}L^kJ{_}ck^GeN z`;Y&69~G^1Q{{ukT8YgQK8Lc@zpS?qS&)so0;bn68zWe*%%w^1y_7-k&Y&a)% z+VbuDgqITkiks5gH)_1Ns{QI~n&I2^3un0Pp7n$I*xQwFZ@x$mwQ4^z_ao1P>G$i| z|Noo6|KG!Q`G1XU4*yv8-&bs4beOS#Ss}7OD|U-(RqpS9m9h-3MQ5!KRJ{+^-LWs> z5WoGO4~N0T{U`I28Fr}un6&&iXCH%r%VCBH|1V!P>th3UZ~MUWn&C#-lCQ?hPU~rEW^&` zyZQ-rb)g{}_%dcT!!$i?ub;vg@Ljl$`F@>3C*y*Ss&gFiL<5`O+% z^<~~s9(YLd|6liKvlYV`=EDj9A3k5qtiavG$FO>K&!pM!dF`1#%(1w1@^4Vck{c|a zuQ#bD&)pw?-~l`sALf7NH)8m9Kfc15k@)~yBjbkz#=lWNkFThFb>nP3`+?09^#dlc z_wDG3+qb~9-tNc4_WkF<(Z_H9XEHCt{{0g^Zd-Tv)ZaZU8^l@I4lMttzxdfl;riVV zmj6jQE%P{J@6SDl*S#UzE&wPNjk@3TY!pnQ~3_@cs z%sO3K`q536aYf&jXO-Nu{@1O0{2VC)?VruR%($Tb|MkDF-;UV72sp-I;F!R`B3%=E zKIFOV|ANe>UG|yl^Z2g{}Ag)Ft3R-5};0`Y-I9$7Y^})6<^ZTFj*U zc(+~GVJTJqXZxRj{5y-C^$in}JJ0=bwq^)9C-CA(BEsoc;TC&w~|v7S9vE!z@z{$w=U&t6s`*emVYuE31uugmZ z_v5-Yo2?jf4t{)9AJ4Vy+3~wzs~|-E?D@(JorwigKb{g~IK<9&;A+kXW#@#a|9;K+ z&#Iu|@&5a|!ly>Rm>w@J&bp_+qYe?Ibw6(ZS_52v zl52Fzv)Lsq4=#moK2ZDj=U$}5Dxb#igkR#fJKMSvhCkT?5)As;hIuca+O7Mu?bx4= zhG&O)7<484=RdyW>K0${=DWZD_rT)#1Ao4|_SW+w?Ec60UxRsp-NW_%Z{oxD?Y)2D zd`bSN3cEfA0XqvOfq5HG@-}Tcpf1VqRQz8v)5fmjI|C2&d`Z8M^ypdkpWpt_;tLc2 z?~mJ`VbGAS+26QbmU&f@Kmm&aM-v}IIKb%Egn7gxyCxX<68 zv#$F8`5?%_khF6DxqKSK3pU%=66cP3Gc+q0F!Y>ZGUL1dK$#)R;o6GIAcd5Wr=Nc| zJ)e3W$-er@^*;m|rpBGNo?!Z1mcdg%hVg`Wg!^HhhF2cd$8U-E#MJHk^?6Qs-23y$ z`E~z!`<2WJ^$T94^04$V2=pFc$aryk56gzY348kHzd!zb$Cgir>*A1-;`4Hbic&d` zBnAOB1BQRI;!>y7OkhmlYO4ub>YBduqW!za@@w~;+y1xqug_HREr0GKl@0sP+Mi^Y zu>b$%zfS4@8yOWGXB=RtsNLN^X?E)|!G=ApXT)XBzOLDpFzxr=KQ;Frwjrv8{Ri#; zaX4IO`0!hlhv5(lo5O|&v$%iMuh0Kpn8e;tIA`mMH8IDwEnZt$w_kgQ(r1*=d?267 z@Z$deS?@RRf571&$HL}tVbbUHjr*Q)SCp_H$lRTfIZu4TufLCyC%_rz5Pb z?_->h`(pR^*UfG`p#0M?-IODx=6Y1iW^0Bo;$^Eg_sn`;T*^IX|9*X>5aRv6{WZe` z+ryV`OWplD>oTJN%Rxbg$b?T*?r;5&Jx`h?ejW1z`ETc(-Sd{PA8N=ypI`a&+q*=4 zOS?AVOY@Bli~fFz+PZhCD8o7T28D-tU(c{Fn0{H+t8T?})|7`;;Rd^$*8X3X_k6wh zi#Z6F%K!6ZYS`_#qwe_q5{>D~3<FA!>8;y_EoCBcIENtH8GE3 zUwgipfB$a%yeGTNkcwjWkNrXncZ~l|lCO|k$jo5e#&AK@>_1Pvnt+cmLlxsj)<;`@ z{>rbrQ@wj%^*LtL+_hzKM9uTQ|6817|Py#=$_5H$D-bky+Pk1%|^#UT`c6WCBh#2 zKg}!+Uq2sGXRqt5vwP2QV2>ih8`&4}`|mG$V*Oa?s5&=;e>2m?`$D(pWxm!cHi`dH z(>aU%@$pZPtPIZ7_E8KK|9<^d_1bq?@I%z2)O+`8tQav~PxMOg}y30ORp7DTG8^eOG*JRkpZhCb5H1pA0l>rUKh?2kV$MXHm3qG+jE_k-KR7aVi;_wTG4_tG1y=Taf4*Hb+ zj-f&+@N;SF&p$u+EkN*Uuzk)-zg&}jro808vZoA&ks;ys5mUZ@G1Sz`fsuf&-j0cnSOu# zvnnX3jBCY_ch6C>0DCFJntKjs=DIV8@i;T=xNPrn#U}ruu>lp?z+3^i8bq^utm@I9eBcO8m0dn zxiL0}q2P~fef?n}#skuB>T(Q1`e~7-cT78hC5cS<{Al8*RPjfD_&O*vq!$}2dKPg`18;HPn(h~gT%CQh8N4; z4Bs)H^|2fRUP=C1K>z{|$oWWV?iLS-z&&$gg zX4wA_nthuoFmC7k`ntvzM7d`FT>cA(Lrq-7{CGS-pMO|R=?gX$M|Do&FTE;xX1q5-&qgEum5@O$E~V!cdHNq_Tl)S z#a0YgK3o5@VQNTRzK`X?uk6n0rY{*@yyK2oRKY&~`thr^jL1%VKL0*rf~^3L0E0n3 z2SfjM#^>b>Z$x<>?_*f{_<6k?Qa)AuKed!$&*~ReUE)?#1Q|BSIx}!Y{pyWBTF<;= z0mpOJfUjYR!;Fs^dxifzc+ZU} zO8*-$I@JIFE!|OP*Uyj;p~v7NJ&CVD-cGDlx^ve2;0>>Khv?k@^sn+~%~?o!4Yug~ z&*$X~Pu`tV=U|v&$kJf%VrcEP(D?CWmWH`a54hd*IZXBSJ>oXKLtJ14{I z=bT@ses;aU*pO($B#`6LIg7uJC1JV48quDFf0@_I?f?Bg04ZQWj;z1i&yW-NdFf|1 zCWdXv91Q>KSJdkVemeal_46<0y7~G)br==GTADw;^Uu$E^I13JSyktx`Aa*X*&Aen z{c>i9y8r9TdDP8S1Q`sHIT&7_zj1?C!)_O|g0_Ir>G~-eJI~#mIbTfS{im6y3XnR8 z_J0mrGyLK`Ue_M~Z|$B>b84@BC7`9md`^b2|M1)6}!KE-v^p^Iix|rJ9z3oy*$R{>cH@xXrt#seiH-unvL@6R_6u6s&CBCIQx7Li-DHK-L;EiDq}93 z`2{YQAlmHzDE?Pqe6WtW{%G008tGVclepz{9lWO>FNhOKHE5`_nM z-Awwa8Xp(G&h&LEQX>D*U&rE5vv=_g4u(C<2c+8=KJ3o^cwh2=?4l=mH8<*+f7FH- z>^=0FyK8@4?VjD|KEAtt8o9)l|7^YU|G#+q_$xO~Rkpe_Y+!w?+OX@Sn0b!;cDomX z49^|8Z+sV0<+pp^(!>c5DESYE?N>89+?_A~$AXEW?I}}3&H`J<1)t^?US?FV^I(`@ z|3FAJKCI_#l_TTrq+-ntjfdn=s`53=4u8*mKGIzty{4x7wJc+)E~IAr!K*P(yKFK` zgYD5@&)Rjp_5~DZxqWV)w)<++%+JVa>5ceZS%E?W#}t zn^_pvTV&rmUhDYrdinRye|P976#1TmMbwAG{Qor=7ySLFzy8lX*<)3`Zv_^BgDpV% zg#3%6&0=#4LCrjed%lhz>Jl1-_}BTP1lLc#hChA#pMVOhMurU^_!v4jMXuk+tPnn@ zKYWV+ca(~Iqb$R$_UD|J0~;6`-bykZI4Het(OV6PD~uoJYFvuGvS-ovAHRO-iJID{ z+WkRlEB#ksT=4&2ysiBjZnpL^h6IUzh8J_}_UC7u{r$CBj`4;jM`Gj4x<9}E>l#hh zsN5h2PYm^^_(4sadGdSo=jdxa-_Q7^?IPm?^BcQne|^Tm@QHozr>RhweVa8YA#wk0W{nkD6 z|6c-fV`KeG!G=F=kAHjXxHA;ADKgBt&iLVWw{D*hLyhAlvCR&Fd|WSnRk=+JZ$qk) zJkjC|Da>ZmeP4m9zghjl41eT*O#A)u-Rsw-=k4!{>_7=w zPo@TYf1UaEf2ueb?ipAyocor^bRhX>uglrLTlYQq@zo!>esbqzSh=2?g@fUo0ZYSN z-YqEo#?)c{S0eFR^)GAQE6tcKjGQuGhZ3uiJag5k1;Dl{XULoml+!p zS2H#2usp!7*K|;Z@qos$N=EK$`&Zfo?kIMZLkf$-_JIr$dlP5a3pFr2;B{sYU_F~} zaPz>^eARtxU+V>awzFHs{2=;g^FG(JzvXM91AGofXlP8oQ!oF%Y%hurHTO@x{l31s zVfxN$*%Qm0B^eJ$wlRG8DJE_E=EQBy)&I5OWeUe-eaHfIco+39Z`m|sG=UGbQb2bzkjT8XIR5@=hIiFCGWT= z{jb?SFLUMOKL$u1`mwx}VTW|rUz@<6%&g73#261qAG2oo)p$SO;p|^)mSy{x6@u0r z{oM5M`Rr+%_b@z%Cr0}}AI{gaH`vy{*}vq@8FvPQwvQYKs!rG6-t(`oUVgqZgN}$n z^n*N=tat0}FEulXAvFmO+j}v5c+PAa{Nr=$ZpX+zh6g;4RU5e3f4#GEzgx<2<|V_A zPu=-VO2R*a4vO$|Wo!()B7<=9KjoSk3>VHb+4d3_v2C8K1unn`x$Dg|1%YTeychy{!`c* zxz!C{nN;pO?z`>P`1_d$!qIg<*h?AiRL;w2Ih<&DdzcxbP2?Cqu-6psU+Bwz zaE?2}zK>k%<>U1uW<8&uZoj`e8nt~piDAN>gr0NK91J(+U1a?5=I(vF?;Ho#pSL@9 z>VD`MYo=%14EJgq1HNT9X19dh&de-{gbycX1`LB}`2AOdXQW3>N<{-P?b~{6ih% z>V7+>4Hq>nURA9P+3@ML^u+4c+vnele6~X_Z5H=4thoN2KhEF*2g5U7#s{D7+223H z^!nEVzJ~9eO}l$$iAP;nk};DhP^;tL=Zf#2zoOJkR~Z-FyC47Krv+0(A zXZS74@VTF1P2|OEckZvxdzLNoKo8l%N!tB^*X@(nLc^Ds5e`jr2dwO^M{v<|E zn->djxQ6&Yo4mhQl)>j<)q5i^Jyz$WCxZP4wC68Jv0H=jft&doA=P{i77m6RTFwkR zwtO$YuD9WczV{#d{Q{L``@2|nEl@9IcwzsbJ;Xuk^ZxKf&tB^#)FS7%<(v#(dH3E^ zuI2=dXEC&iS}}b4cTL~ozuSC1hUfbfz8*K{4%nZ7Jf8U2-cFWbl6ag&5evg%E~W$K z@$L*iW=AA!yfKMYwOe0Oe^H~Ql7SaPMSlFmvhUZqeI6MtvyUr6N+Q4ie6C};u=BQk z2rIJ{xM@3Ey!?OK&IfwOn^to(&HDX%@jvl~?{_l}c5D%Iy=_%|DPk4|1)ZkvRx9$e=AfEjy zW{2-Qv&!p>b@t0cqCIcjhPS$lpVb}AvWsG{c+wQ|C9Uy|J?b!Wp)dp2^{X!#G9=m< zHf#>AYQF!1LqITb62pYwZ7wHgKfZe0X8!ZfHS^AYuCzP<{HM+Q=jVkPp8T$_cw)Zu z;r7RZ3**I_@XLw$dP2N#kG3{aAi{#x4!nzwWqc$4WXbG$nG7r zPZu0g|E(UX&#X-vj6qN`ML}{{K5|g0 z{nr8Ii+1;UZ_Tg#{rBX-9~nl2HbsU%Pi}ID@0+4)v7Mn*(0g9&bXTjD%nxku)jD3Z zk$lfRZ$D}@-G4g&lkLs@8`tmu`2D&dLzq+>L&3X4-^5EDD_hTL7af1$&hVP6Z1I7g zO@YPtKZTW|q>aTa4Zo!SJ}d1555b(9bN`>yt^<0YI5t<2IC7(7qPt8v%L7~Y$I?Qo z`Dbp1tU=14`9Cy3?th;DrgXnzHT(U*Id9zcJ}NgHGw5T;skghm;r0C^zq{_v;bRCk zm=^eHvGxRY6}el_8{u7u(5UVB4NIm5O^f$^FSuLw-xue3-yU0lWKjNxej!lGf7rXmaOQAuwUCz<~`gLvXV}oXcp-=U zznWzYzkXgjjod_+VRYF4KRjMy)?e343w_udOnDd|yb-q#x&J^tkEP{AJ2!)S{DYo; ztq+I*VE&QL#qjIz{{089|FmGbV8FxpfcYwG1`G4!%Y6(bTt|$u>VAA*|Niqgls3XG z#s~2l^LFo$SDwBi@4Ir5J`=;XLjnwbdMph&jBf8lcmEP>`YFrM9$=snclQ2H`NPWo z^YrJ{$jx>Bd{`TK2$Elv;ZJ|t@4xN4#CcvY95^DtuO+?mxHi z=ho}#WehWz7VXrJxXX0)gBxQm!u=l(+j}#7_|5#^Eq+n0qhudL!&(EDhUe#N{)z5h z=)dQpqQMI02XlLwKAr1W+H^x>#-mSf9{pX$fYRQQzrq+$-~8U1*dn} zKLy$|z2Ls`i#uZf(zSmof7KxQw)&qVQ$x*vbLk)d{x#ocIKk`8@Z;Nh0S3R?cdq*n zMA_|UR`@6^WV*gTU;Ki^@2o3#;h#aQ%oq?*^P5#aX|Kbo**~Rq3uBSnYR@?szW#Np{&SOi zHwVKFFJ}gezdP3+b&9Oek&1OVow2?>N<6S;3&Vo0O&@nPt*sB<{`fVk*Jmfj>f==? z_Jiy9JIdAE#`BaJB*HA13SMoFzRA>W6L_PmYp&jpM0bW)U0ZbpUL9?^^4q4D=^9ED z^f45?ZHtdxzxCda(|;hvM7m#H+UGR!8N0aH;sR!ru`GDEZ-LCenr)yQejGV8SMW7_ z;&$C!Zy?J!QJEoOH4ozh^?8;*UlyBhK4_jN_G5`MLsWp$yWed`dUuEg+U$(~ul8N- z^UU8z&(`#wzy1t7gbN|+L)aZEoAp2D+>s6}+NozK!^jYw%E912|IeA3s}4P5VSaqL zk3nMT_K=2q`{WNRzZW-nznAF_ibp0f1iY2tyFXc@GSR+?p<%6&6+_&QGcToM8eXm1 zaaDA;qbx(E;|uL;dz)5177m|k^{zKU_W(-cS)G&NU+>-a_wR@?ek?40trRvB1j@?xE0`Vj z|9@S#@Z+)jf6^G38jN`uAFP}_zfLjpqdB+9rzR)$MF*G2Gp-TmsNZW4zq3K@^HB?X zyXw06$k`-Pmf_c>3;P|u=*!&w*P_gD3{(=?mwvj+s&+BwfvGvup%?OuZ!}J+tIWHx zUw{Ao^Y_2A`A(DxkQ;^ zDbv+F%gHM$ZxxB{l)tZ6@Zs=%M8vprGW_~$fAfI;I@cbXq)qz`e)C&_N1)Hw|Jo^i z@4@QRTX!iKEMr#C^jMa#lr>FKwfy?o&zF8C$GDuF{L|Ml4`qPWi@_s~!%e45=J|Ei zV|O?hZg4p>R6M%rZ#O&lLul(~?rS;)v2!l7FvJ^Nk!F3H``gf_$bNp0?>lkx$7erF zAvKFX@Cz~AnOV2FenmM)0SkB(>dEH$6>4HV<*_NcjS08%8TR;g2QSh%zO}c`(NgDV zjQ#pux+_qU%ky%EE75K0$WTACLh@QQ2Kvv)ra?TJH6GXGi^{yY2L{!bk>i3w66aXOb$^92 zgJ)ys9g!_=-I1<^X7i7ub_&e?t1v2je!`mkMtRfUmU>9zU_axBlgIxyU8%`Eb%({? z#P!yl^_7cR^X%K|*SefFFL|S=^$Dr^@E2v+CCga1--*4!oQLtj$L01Pf6Q!Mwe7>& zyMHBi9@%&P4TA?)S4B`lQchj{lh)TlM^~TE7D+&ASWEx8%+j!JdYv-^xHkB3!&tuF zB-SBXs8=CiNBty*1d)f^y{6dwW&gjndq!XQch)oZ`-2~tK*}6&Qm@zjGns{9e|^Rq z^G`no80I9FFxYI`pWv6TyZBiN!;^biio3Tx*i$O0>i_I=)5GJ{-#_nc;s9k*NI6(P zzrKdu!5OslgoPnlg6Y6<`MB%S|1Jw|W?fl7`zqsyu&-+xT;}y(S6Wh8UuVBB&Vm0S zO4rq$A!A>{n*09j4dy+{4F0u$6n8I-|J8S7ZbXCi@jc86`Vqg(zwT4ZSVYk2H;pdd|-G#iMm530x)cxT7uK=o(pXdK6SIJuUdt%Pv`h+@2 z5z%k=^WyK1iX{{8&hgfv06|FOI(gG&Aj-{XbvPcX{C!}lM&weboUSnbv@F9=Y$C$up-;Lo2~)1QCVEjh%Ck_2Md8*VMjVLZGC#hPQ48s z6TPSWWaDO7&F!<*!DGsu^Z9!N7D!!(bk@K*BmeVx5r#ki7TM3^12_1c`|E$4bMp2* z9%?W{g<;A61JAkgmwD(vK9oFfPr{3ev#+~0>nF@Z9x<@{KZCE~9=G#;d(UU?3@1Q+ zrh`9t8>}y}ri)m7+_9}^mVC&YW3r4N=81}Q&XSjSEm{;UaQME?$FqfR&z}6O>i_&R zA0mYJKc9bt@xlEY)9?S7{aS_*Gy`e#?cnXdZ1#}}f7lK*iWzsl+IL_k)3QpV`KAAD zbi1N-YER1|1xsyRAH$5-=Uy^^M=gFd_t$(d+t2X!>#FmD44ax3RuLwAqbhcm4eO0QQE8)cqH4fYy(K8r5H#w|^5)*t;}6guNkO(fGPA5j8U9TwH;Ex#r`vtWjI;4aO3cXOH{bl(a`eJn-|IUD_T+{uNJ7K3{8xoQ%c(8!#@| z_b~sRy8y!)P|fh=ll=N>$6v~fn^_p*6>1t+Yu>4=JGBnd!h?{ykrQ ze`US)F_6jO&*Q(xTHO!skW%$uH@hi`Ytpk!#!j2RHqsxrS2ceHN&1qYu<4?{okcMBTD2oEAldMv;Q~b8P`-iZdCyl z?|cp7^~?7&XxsTRXlNV|@^{(T`GR{w_=Zxy+Ayp6+77>X&R%|vR6uY4`Miwb%iTY0 z44`=+z6SC7-P;BFcQQPPJ~4?&wKBGG9jFZGw)u&a06$3Au{6w^Z?k=G&n#u|qL!N5 z;am*U>$7XQjTSwt`upaak>4u@57G3_S=FvLv~RgF`p0z5cyhC1Zm>-)Vg}s)Pwu}! zriQxz*Z1uI{(ys_&64rK>vif3cPj5cy}#$;(JS!>K3g$dVp_5@d`_+7i}UN_@)v&o zj+_*pEoWYE8#JW8)`+E{TEB}Spxlb#-VP_nX*)K?n`~*&S={H#H9=ZGk1ONT&!jn$ z@8?6BH{kN5zWM)*GKL=?>-IZ>S}A-D-TMW(b2e5e{o>v8th6y9g1tds;NIWA?{?)I z27iBC%QzXefpkTXVSnQK-GV%RN(>3Bc^Mz{?biQSRoeXi`0UrsEDbIfd;jpBUQro$ z;NFIwN$RYJ&g<)aH{+TkjqHT>`bi8A?msxbwd0|+(D&~mt>TkFebqjOm|v!w59;fj zNWD_?NswW2#O$V}tn1b{tbTnblJV0%iQV$k=U+xyp)CF9_-_t}Bgg8PL0!xehC9U% z!;=_eU+;RkUS9CR$BFun0~im8@cf+JuS{PsoHx0dG6-<{L7%hhgbKn{Qcn*;J^T?8Y_L=?;W(f)&1!-m!-mi{hJx3z&+AvN+wilG zQC;B7ONNT(z$x6UTF*{#_1PV^_rEK?<<;}c$g>q&Kk^GP{NX-y?4Q!dyZ>Dnb{HNO zVAvzgdj9tjP)9=0oU7<-UGN2|&yx0Uo+J6e-kTvK=f%?~M+1v?hK99fEDhG?^_sB- zX^eLDX_BV(Uw0jO{`g+U;X0NNU%C7LWcAqW%;WMZ`u`~Eh{X3_sZ;hC&Oh=CZO!_# z>P>b3zyIdmUw^@7=e`gB9T*OTNiZFVzgzlG{4%TRMUA|ZU+?s%vOC0Id>7mQXtJn} zZr9f)q~-nf^Xq-s9iI2rJ^rP~#IWtwk9>yn|C2T~Jj(rdb;V9Wo=bd`>Lz=w-STa6$&s!ToDDx#RsVUl@PCR>e;@# ziVFYoIY46x4ErDak^cMmHsgn1iyq~yD}Saf9??zWT}aS?mpObZ^NXJ^A&h zAOj0Swgi*It_Nk)0zaqJBs%*4;%&J8mD^=C!%C;WyHcmvRMwf4+BvnLT*U1Y@A&QdY*ybrUn`+aI8YZQ z{}cavhK!sKfB!w%c{%)_C?i93A_qf#;q9ICy~QWEoJ>8_2O6k2Cdc^U|AFjz;!9R7 z(z$p(yu7vzWzlBszn4r6_w9fD_0STK5n&88&nk z#WCdlymeh~@w3XtinT6h?|v5A&iZQW=@pe#;OYZP@PhVXon*WFMqlOCy+eKR&%*yn zG9Hj)EY*jLI4c z3=)S082!bfaXP%h5z~B|ff$U({TN>B)HN3Q4y*= zQu{xLui+K9>wl+98}5kskKfLJ zW<3YqI|mD+-+z+pSuW%}c7}zp5ca-k)KEZQg%@jrKpS znHa7e5nzz3&w9}HL;2son*2RcYtuftGg$XDJrL*F-%zR&6Mtuhe3y@;>38lq_V)8& zX#}*b_CtYYx18mdUmt&Se*N*+^<}*q!+}{6Ob3$vzs6qr)%0iXks?1yQ}g3btFA`r zP8MdU{^h>s*xH2F8Flvm|J6DAAqG(&w%dQ1X8GmB)+pV7jEncrFF3yYxBV|=riNSz zrURe&?eCQ?+<1_`-gUE_>36dq*S>z)8nsqep7DUb#LKR2^|kRA{R*FdUJjXuh2$#x zmAB?~GF({pcfTe>%U)NIGY(ffZ(0BS;(Gyx4Qg!+A08~8`&^Q1&f*hCxmk8K+`grj zs>JX{G5&>6cIwBKm8>WAIgWijUCGIM<@X+;y-d@Qm!j6c&^3NB(RqD-&V#vsn~t!3 z|0cfl{ER+^22(?p26nlh4|KAf<9Dq*VY+Mkfv;OnPG0^@ks;)J#;4f^owI&Fe)aNm z@cZ}TJ4z58!MY!t1sR;yzqdE&xm*9^ckj7ozmzFx<%f(Y}kjY`XVH_iVphZ_HUpShpRQMP?&RK4zX z9WwuJ^EvfXK12Cr<;1Tmc7_|QVN$t1;gMkAh9xIXY*iA;Sl{63^Au7rgOis16R_LA zn}7MJ&Qf!5!Tas@PLNdf_WPcqI~n2;%U#Y!OU}>V$7=kO^MKwl@m-4}cC((V-2dz) z*RIFM{vNrEJa@#uH((+gZ`WyLU5#sktA^iR{n%8w%9y8E+i@-;{siFVi#aTeE(%>i(;GB*5=e^?UO183e=opyBLHJ^>sGdXX(GKBc&c~y23C>_6l&;RGI`i z<%GB1)*9#k@l`XoS1BocV>!6xUVI%V#N_MuKAhOMR?i}K%dQz`m92`-y2Yp6oVw{p zQMUFr(|r%NdQ92r5?^@j`-!bV>*hx}tgiNw`4%-3lB>ZT6nnKllULnN1r?|9)&Gz1 z?%TXy;QjBb0!$3w3Y{5d__H>wUSIv=v##7m&vHS5u!zL3HY^RVwh6Mjb+yGi{ArpT z;Sl`%bv8=*yj}Em%0=b%8y>yd$S+X3uQ2Jr_jUW?zin4w5aD1Dz9zu1ZpW{C=V!SW zsq}pnoHsQ*QhTy6L-f`#ha;Co^H26RY3>Z3AoT1sx}nj}LHXqV%{A7)*~{-(EWGo* zJCB87O{XG5Ow~)-`D^r7>??A#l&(px)8xC+)VMEw>(%S$pX4*-MCx-q^xoYw>-OU^ z#$a=}$Q@SpkM+eAY}1Z43qf zj&9!~-4%1F>C5r!apjkN{1|fHX&?Ewqsr-^m&OFY#$v7_D`*P_octDUKLbj4bx$VN zCI5T>DzNBxW9t3$b{PT;Vgif_LQDr*%vl;XpWBsIp51KkyXe_Jw)@|C&F)wUGbUVC zWA$A0tj_WC{q=cbJ3g5{|GQ3lzWe)$C`D)V#N*DJF1+qP%6ud<_1*?~L)NBu`{wU& zW@zZm;b8FQ`CYVpZrRIoh4n|yi*Te&?0aLCKKbmYAM(q$COy6=!tt@CX~n#-gr}d? zL@tOR7fR0gGhO>-s%q<*<^MZeoPFQ;!)0@C#)M23h8z}W28(|`K9_G0o3a1k|G6%< zlOD(CpI%#j{t3&0wvAi`O^0IpCrN)k{{LTNXp$CkU~Ek~tW))5?Y4V&@B1IIPbgt+ zz1KK@uT{-|Q9%X+mWB`R`fdUYeKzmbYkHXv+}SsPuhy zi&deruCzaL;}@gv+*vg!d!lCUUmKwhu0mh_?yP8Bkp5x0zA}ReXe9N(;lBHq0xq6(idKh(P1i3xzLn#L`!SiL z(M9_eez6|c&v^G0X;F{=V{nQ6>G?OSQ}s#j-|I`=f6vAA*V(>~iE%+^A_qhEx}Eo; zzOr7mUM!bhHM9Li^_-^d{~eb7}ta41; z=ld^k;)W2*KZa|sPPwQoUvNS5qB?`h_v5_pCI1|3=3v;&>&!4?vj~IOEL8@c-&%A3 z?*Dzhquax!;enFSl%H!Ns%A#NzkZ$1`qLaAKZZ3Tt6AJdOgdk(p85XeH&Ty({(<``c_+AM&pg}scli~TY4PVZJH8^1J6YAg(B1rEV!!pr zp#PnFcmMC0fB)*|v(kbLIfn%prtvZzV846$zmETh=&&14Y?rpTX6Sp=zMXP;{aF`= zh$@k;O}hdERQ%_^ub123@QYO#DRxc%E!tEfX8TEVdHnPI8-M@0?Qyv9^{TcY!-meB zscK9I9&Ki6_`Z9&e}1a-|Aj{$B<6K2Q&_Ox^vBKT_vam*9vv8YNQ>jstg4nK-NKUV z_iMv+_MUN%cK9k)l#A5uKi;-~g$_8!{#~N}Pw+SQ*YEa!4!Iuw9-nw)x)5VR9!tZ` zHbsUtfAXH^*U4>s(8Ids-F$`}KQ{d~7Gx0X)eulD=kBQ78z6E@W2fP~7>CFGN04LS zSp1od8Dh4dj!w6Kko!Zmzv97*nD=kD|32@+a6rbHq2hIOe9iyEpEBz^uC>LBFEqbz z9a!*mYk&I3r(rXb9d|$Q=+TXsB>mX$;FLQ$35jRIPn>01cD(^oZGwWze(wIDNP}*< zpQp0tU-)PLw{ySYy6Q;}*6;au(368u4p=MASR zHgyN3K7ZZHF8})6gVVY{mP~1SA~K=s${jrqzsAeIkMEA(fF;{H_kZ2-o4MZRU(12R z{PsU9of&2{F&IqbWlT62#1Nsz!LWYM&*t*Ohd$kjZ4hKSXslnSE6liH16SD3qw^Lt zRDV`|wf8U6tGat_ck~Y!eY5{|PRpw==Hzd^S7ynxZY49l)6&w? z*4w^OpfPq#cK0!$FJSRk6O+JhNbuGQ za@oFYThAi5y0UxuPS5#gu928w`}tD4rY)rh$B z!Apk8;nCIBmw8PWL>~mSM(?z`r%9|L}|9>ig-ZpDwC67%IBV#DC(n73v(fokiYD zKGo36{rkH`?ZOn~E@b%A+i zeK)6zf55NL&A*Ym&Ij`!wMb+o2S2v{y5DO52W}Q=6|=&9hLxY5)ao)WNE2srSonyQ zfrs(I$>-b7Ut*8=q5tPm+`$Fm>;DDrdE(XTe3vD8_dEj|PB)v)EvoC(Z+z)OUa{Ty zC$!ah@rvH+ukYTk{U5u1-NzkYem;}FW_vCF-w*rA&--t*GF(_ViG_iOQ6Zm`;cb~J zgU!D$&$6A^XM79|o3hR2_TQ#M4-^+a5@7hme!@HE<08@SPlu*!d;a^M zm-0USMNXav8Tlw03+>8sjnH-i%F*!_CWhfD5 zTwwNbA^+Lf!hMn^t5ZH$2v?<^?9F$0&2{faVd0)9T|f8U5#Mz@JtA;YKtxlp&@`9j zcki^k+Eo*N|1U}q#-DmG>zZp**3W!}bKgJl#~=19TSJGQgi>EJk-F@y|aOwGfyRhi`efPkvYY0&<_0K6ULS*a4l^<)r?*Gqz zJfx|SgCRfPd!GFrWrh%ChK@uI2Jx7p$M;_{pDQhA*tKpw$B$p;{aue#!%y%|T4Hdw z>$0k%=ag+O&!cP~-kA6IYZcQ~VFM(8Xd?OJ{Vv<(9CwS)yuJTv^Yw5AhJ<)imWIA* z3>WM_Dz?kl)yy?>IIRBTSHr)T6Q8?$oOJs6FSFo-^Wyr-Zr9hI`n_APw#wUaa#ZP(fafH+j8o^vI;V=bunB}{&=oPXqGlZLc~)phR4U=?oR(v zU$kVdlY+(DGp`>DFxDe_lQp z&ij8%w|?eqoBA0X47D{(4yu2D^(!)L5oKJE{mAw-lfxtB@Vtuu_YbkndHqYRab0}T z(b~X0Pl|G_UUM$_^XR-z;>+*i`EOgKk=w}oS86U^(W}4rlU=H6VcG1S+xrqP%cp5G zIS4W`tS(_(5WkpV!?(NsWqd0>{Qpw@`}*|7SEptCnKtvh`-e%!_w=fdTAyxN694{X ze$TO|y9(@(YTeR*OG5e=ne8im_BrqG`}qgfZ(npb?^yh#>uY}hELCFQ5nxCykyJQ=eOm~A*~DF ze=I-ilm@8eSUz97Wj(`+{D}I6stjMm7#9dLF@$w8IUE#Xu6w}CvlGAYTsmsM zwt!B|jyFr5{B#y%*q0^2T{o`$So^hkQSJHT^7XIWXE6l4bZ1E7V5ogM z=ZxTa{Xd6m(i?W^)t(h+37fU;NYU=t-+U7}7=D{6GWu$}?|$noP&D^_)!({Dx?gLq z&pwWnDEF^aON4spUw+S*l|zc%?wuP0uN+yn z&EGzrV}IS-XZ+#@%@ay)sZWx2s>qjjhJWiS`7z1z>(7UO&mym=-akWL%Goz3 ztGfR0`t$m4&PNn;*Y4X`__{q_<-B|=LjsR8%Li_TS{DY1viEaX(|361ExCKg*Yb$1 zcf*m_YJZ$nm>kwFQh3ej{3}W)v6EfJKwIo-RA(2on_tfW%FOv$Cnd6yf^TPk-T(HQ z>AKhTiW_dt-nRGqB+HvVeHQQYIT>UH88&!{GIZ@0W>~lL-#mG?f9>&8EIP|>zx%FP zR5pv&a2RGP^_{if*V?QbSozB!@n zKR1O}sW7uTjAcobp~Cff6`!UC?s-#`YjuKq&sU*2)%IsEtDh+fvFSjxAm!_Vy^jS5 z&#yQ&)$6Z*;Ouyf{XcfkzrW+@RMu$>7x)YiX}drAk{*4Bmc3(K zu%4kH_Fe5s(eG|WPdw|~L+i4%Tz5PbeSfWX-ZMz|1{{Xc^Ru!}YCuEJW%t_i>U%y< zoLl(z*M0j0KQr%|4IB)nN(?0ej0>hzFfuNf)YHq*y{KlZFyn&Rx(q6IyAsb@&;P-` zpdzem?vFFpVlKtcGD{cw#XWntPJPLj>&t8FN-b}_71@JqPLxN-*gTn0OE2r{iE}pig;;+7<_dnbHAAYm8lwL9mywz{F zRd<$noaX-c#a-EV_FPfDl`DL7=~{Jz-=F{ej=p&BfLaVAzyFLwLMM>2RK4c@dXujo zlKubtto^UrcGi4~yoR_=hQ;YQIcn~6zL&BzWU(}OnM3N5{W=V1;(u-D-97ih`pGXO z4>;`ZS z^;i7-3+y>u8inVY`~Te#WT-lE?D<Rztq+VuSMCB7Z|UxTXm-itf#RBz|>N&4{}F-jobe*ek@!z+iD zE#v=x)V}p}Ov>41{r`4v+g9c;$iUXYaN({XL)SEhhR_y81`#3sPKFDPEDchE3~cIl zj~aJsujiccAiJqSPr{@6Ps@7uVvz;wex%JmaHVBc{rm=&gX&)vMy_x_ty{+n^6y^G;OG)sdY2g8m3l1vWb-V72`ess*-Jm2!5 z{lV+(OSUw#t?9J?b5e5RpZA*6_Z(4==5)R@dxOfW>#sRg{(io~vRpL<`Ph_l*4~rL z+Ikj>`DOjOy!BUnyrWL7;^$u9===YgEiaxCWZ2coaKSIkQT*rsdQi)6t1`nEQO1NH zACJ#BKg)CCjEH6Dz27IdKA&T6Jge7#6&L%Bg5*H)19}O9IAi%`|$tlKXc$nd+D1S zg)v4B|F14Tv68vqF8kAr7#60#pSPc@GMqP=q3ChhWQAo@7T+~96b=^-JJ@w;+KQ?w zP(dFLTM8M?bYMrus|UB!_TJd?D_)$XsjeX>;_vL;t_%{BD(bx%UUV=7yli6#_`<=! zoN~Fx&^v6;zo20D6&0Vu89EX^Tq)i^N!(pUov|YB!==2U{X6PvPq94lTu>A{?K`Ab zdOr_biij6AFJ9R^zvkU^-&Oxrqu!=*u{_e>bE9D1%?o#g7^>9ng|akk<6ww5I@6uu zh05&YFJg=d8IO{m>HhkxRqgO{9e+UX0rNGr`R*%2L{EA$*v?*WF*`-!VqW^R0+pFB z@2l^9v=({NW`2IKurqH?R%Nx{gqtn#VZs&m`M>gZre6E*!XTmZd-l~1a2gZ>mA~$c z+dzdC2g6n+hMu}FD|7jOvA(t8mSHXVfHPs9TtuGx0$ z7qd@W_u%EDzTLL`zE{?+MQgS1>}t}x`1x;;1ycAeOY2$s z&vfgaxc}+)^D`p8wm;WlOlbJCcjs}PS>X&(~bXq zE`9gE*8k1NGPc_`HSTjdAa$fzE>8R`x54#g-E(a&X&-y|_t)nT{`z@G*Fe1mF7xxV zjzzq>Z^^%&cisP9bCp`5rMeqCn+_~ejm|UtJww&*iXekkB@4qEC0@n_w*(n}so8n_ zvoEe}72fyw&y?%j};r!QP`MKp?H;?;#GhRLQ?^#eGvM#8f;mC$LJ9HVg zOkZkf`((cnbLz`8GnPO9Th?gRaF_ALuB*>wAB36M6yZ!Gs-T(H>Fj}a5{5Or^ zf*>Qq?Mw~^S9gXNdfusQQ&<};i?5$8Q~&&nB|iGoV(kiHwc5o--xx0lPncKmW?NnT z^|yAm3t!IXFF-20lz;u-zw+VKU-rA>|IFRb`zQMPy4dfVSQ_d!?T-u>WVjW!j-es3 zk0C*xHG`AESy;aIbnf{%EB#+wUe8eQyWi&7p^k5i8S?8Iw(nSEceAU^`o}Mk`^d>Z zW;Wl&D|?r<@vr}X`Mmn8@cln0-Q4f<%}`p0siBr7A-a#@$Po+91A+`ys&U%?=SqJ4 znlEo?d7Ck7cRlBYt>(6h`jag7tSY!LUD)GbSJ~~!2}|xi`S+^HMO|{|`Ok6sx!)`Q zv8V;YCe6ITXVk1eB`*?sw)$G0pk>VOAt>_Wj8R(-+;%vvggbcj)@!AeYsj+cmu(Y-yQl zrMNWUhWgaScgxamhny*JK~7o8W-Wc%me+W$64iN-fqB6w|f*7vk)|5w)h zyEMHn`QNrPf(;U46(S2XQtr;3%y6tnZtr`?lMjBK-@rIK&R|FGx0=d@F5k@=Y!hdc zS)MW#di#2_|AAkhFW-k0li&jLU&XZl-Lr4)4Xrnr^3C{j-M)7&DKAekZl@&=fkhQpp{yiE;I0X#QmAXp>pe;x5FjA{r6^1kXrn&_ENq5Z%I+4 z8M63L3xj1+*RQ-;RyN&gUuo?*W^VmS$_#6T8LlWZ81gVWh-}K+ZN>SZGrfN20iF#{ zv+w&8!Znj6fv;ceOd zet!GtIGv3Zik>{B@xBQ{!CLpS_Pb9ki(PkBXvXups`{fyozw8AuYG(af_t;GzMXkh z|Npska&q;{Q`_ym=k4Fl!7zuRAu)0;OTnR&;eQk|7-lzWUd&=&FRym)n6I=?n-_5aPWiVAPV#r{?XfBIwF z?8k6{m4WB6N`qePt~b&9f8DL$CSu;%wOr=G$$QM!T&+ndvl+`c!si&5{;k~QH2>A_ zP#sXq8C+uX?_bHf_*dcT2qV+#w6FXBi`RFC{hxe3XJ5X!I>Q&AawdkhLZJgMj(nfB z|H;yL(^CEhh46RkzZqPfhW@Ob)M9jTc7V@(m)&dC4E*=ooUXRN>pXAy=NbFD9zz-_ zpai7<^n2ghb*5XZu6^CVBh!1!t#8lM&sBXfxyf+Bj&Xt7$93n}8bD*C4#NIS5=!1# z-{$;2a_x8uLszw^W#;l1+v6DAWvdHI^iS2S+dZj3ZQA0y-(uIR``it_tH1X+QaZ}d zI&kCFt;PMjEmHrk=Vo~K_0fOZ%1>WDK6`)apD@D>27$wS)EOdis{?C z-`Ahn^>6w%m-ElXb*i79zsA;ZIOS{6{3E-q)fq%)D>5WVNHbo$#%H;q?yL2GA+rOv z%(uEuFFo&a|8Lco7qSnv8fJ-bW+Yigy=7>PS@hZF|6gvQuPoyK{ysuZuUq3wzUF+F z+Z(;+-}NvKn_pL+wV$~^MOZ#Jm*K|kdkh9gIT<95xiz>YR_FbbH*eLd`t`Nyf6xE_ zk1fI(pM1Y8lIeeF;tS7!OJ%|n^c_2w@3>dF@A0MQJMD_V85&IdTjexo|G(K%dzZed ze_IzGWBdBW2VRC|76zVViXtUN2OTfevURefePOY`!$AAh@^Sie!0X5gC~db(El{Xw<{iEDp) z8M4>(GPrjf+Lmu5S#SGRXns}ZYUVq;?zec}@A>p}X7=Ss?}(SN{u6Y6RX(k|h?JXW zN9iAvx_0H6ZrFeA@3TvfH?2-#K5(&g-+rLVT ze)KenFZkj4MYHBvl&j&MKUqJe9a`c}^_Pjf<$Rj4UDwqTl=!2VKGpBru=#EHhOf_G zat6Qb?~w<`1c=x_MOrrduh!}R(cAt_f3fX<^{xN^ua#`Jviul$jjdtt8(xO5|NkdT zFgaYj$8q5<`+_}!4_v3;ub#E~#LebA6+g8fZ~4Xf9r z<*nuhr?7ps3|X#`(o=uT+xv^(%0hC@f1TzVg{2L*b3-2*U;NfNdB5!1uBa2dU(`je zFV(ks`11Pe^XLElyVTc)9?bll=Gp zeqB(I_A@Ma!+hcLE%t;Z8o9r1t^e2Guzvl(Y}S%q_uZ4O@u`I??!k z>*VRLc#Vr?{Q73j{HgcF(D>c;x9damr{|q|{4pYEP3^8%D`$RMckS2TjhFk`>OZOe zWs09xRloepi}NqipWgp}^6T+aTNG1S3q`hh3PrB;ij80T)G*c0>+$E(yH&9#A6I%c z&z1l4fZhJbLH@cA>(#Bj0TLx^{j{`Id}-_VsQDcHi6j@ad+1^%HX#EPVU)*9X1+ zFKfo&qOgJAp*2^&!S{RFdgJdeZl=EfcsiRUbfw|Hs=GU-;aNuyXTq?cn{V;^yr6`F7FI`oBHh*Yo}yJ~2@_`pxVA z8yOXt9$sQhxXb?Fqkq-M$yd1F+&#QJHCp(~9`Sv>((^Zkszs=9URF(98FZDG^X#qW z+8;#aShPc1#4HDT2@6-EA^YFCT|AOR0Dte)}5&BIf-x~KBTDg8hHtAE+Db)i{* z|L1J|SFdyJzp}tOS%z=549g}zd-9F3;#u*%A3dk%Ka8`wCL1aiSMyGzwtZ8WT7-jV zI;*$Pgej}?cI0^_6wUuxxBJci^FMd}^ZT`X);m}{%l~;G&+_NZ-BWkxZ(`S<`rp3j zmwWt!$P*F&?LRaguw%$PZO6dvxpm1iWAVDb-oLq5OSd;>8tBFC7xkZEu=R}eO@qFs zuo5KJ`|egm5|;lHxM7ZM^v&Nv-|qcl zuMzxfKcD5B{^w`9QRg;KxcagFa_Y3Asi}YMSNAtMB*-@;URoTcYnx-5CExJtxXm+G z|3z&N>SN;)zOx-(BmBHhEK=O6o3|KH((#R08L zE?fBzDBWyW{50gde%*rjs7#5fOocbL4YjrR9?$=wlsDy6+{`O~_Q>UjzS>pYziZm@ zzdEnKZ;bg~8TaugIJbfljr|YfgMVBq0>9mPCja5+$N%!bUYy%5-S>aC(&hihdG36y z=V0k$mdUc_D|q|NIQt3fxAz~5ugq(X?>}td<8WB|$juE0jTyz?|MT*k7V~P?&&qk6 zJANPs=9{xO7Ek@3bHyO#{9Du3`^W#s@2Gm5f4pD*{=O#ta*i{en;*aX z*Q2ku|7T?geAzWOX65T=Y>w+ZH7D(Q_;a=VKa;#ULJKtdn!YuCS+RTlMeU;g{bEQd zzw_tf4RiNKD%ogM=YHN*qsRUJ|Ms_kFDc%uiShlfe+lA?HA=oly39Aqe?*J_P%WL$ zwB-HU`^yyh-~EZ1ND{>THo0YU2wi}>z_v65}}0(rfPjn=i-DjmTHCG)nyd_T(|vv@!lIKA$Rabx9YL- zO~<#LKcPG0|MOnfe5w*{meNETj%ckaQpsDDURP$6#lcN zerBG&W1?4px8oxMK;gHKHYdGSoiiW_w)bP zb1eQD&iofY-TI%u@Bj6ieEz3bvN1KLGF)v-W%#;en(z169ozHucdWa;UV3RdqsQKz zUmtd6O*nkzh?7df=ThD1{@pRFx7gqR{$IQslCr>wZ2sQxNqf|)vp+sg`(GP*%IW8S z?(2E~F84?lhn@OZf7*jPMeI|E?O%D2hgz-q0`!?@Oxw@1U}56bWWBikU*G?E^y_48 zub{cm_uRe5UQZFa#%bXCH7R>-z*5bJD~)tdMtQ|Pzohs3|MXplODF#QEjkCu?)dp% z=TF)C|ION)>C?S-^VNI}dAa<*vOolzftx<_h2@MBZk5UYsrv5va_i$uHx8!@+}B>Z zYWMy7JO6yVlcAcY<{(m5=v3@`K`#4U$jaBxUf<)4dh+4_ALPul_(|`d%{PK?|JnKQ z*MXn^PtX6k`G4)T&2gXCT=Mzvu5f`dA)ZZPIitC z@|HgzufJ5DdH(+2`v!maU#kCQ&(!Gg^}7i}(C>@E_CLeUpRWD8?Kj8%NI%;NtK~nI zg5UpFhb&zcZJ)pY-8rN*A>S(dX06amO?kJtTd$5sUJvz@ z^7#LMTX}9(_pE>WXP8V6X@2>?m7m3m(dl$1gYlXU>M7KDRV-UOO|rKQ&dyZ`;nG>&4f$@GQmXp@_%n>jyY@ruCp>C*e=ZRTW;+a|L3gy&VvTu z1#U@;-0fcRx_0lsCh`5v(i>aKm|P0Y)=X}>@%2B~(h~m{|Jn|IsQfzDrDp!ey4d%Z zzYFFeg=_l1oCyCqEw45GDg1BW9pAeD`}hCZ=JR5XS9eP;`|oe)^Z$I(ulhnz21tIw zI!8bbGs z-)`LgzyAC0xA)l*d3yd{;p!Dfez|`;mwwXux6jM(U;jT}fA7ccS+j1guFN+K+I#EI zdbe*1tEU}2kvQXD{2Ye|CJkI$c@{)XW4v-=*PNrD7HtdhpR?=Lou~GHzB=31l@~2t zSDZP;gefk&@80i&vUg^vMkQuV%Hg}QbgA$*%Z1u6@0AoEGO4i(S^B%9!RGd2_xp#K z>ppmM!qQi@>NlUx@4fF8->>=mOEol#{lS!)KmVt(ugjT!_P|V~vb3&g%+0 z)BIngoit5newfd3XTClYBHq*ckHl%dtea@N`6>I}-~2V<|L@nneP*v4dFjWBxO4yH zmz(`RJ^Nq3{Qp)C7AuA;X$_lr7EC|Ou-t$C_YcM4|9*V`#&)Ra?qc_k|8nQ|9X5E< zru&fVv5&#|IS#y=RAP2Jg}&PLwTf|G`U6IUo#$C>p32)z-23V8ZTn=&0_}$Q^Sl2Y z*mnMnY0kgt_22f-`#1mKm;X-eOs5%S%$QY5bN2oJ#P?f7@tSPtGpV_anGJH^Tf{w? z*;!Yvvfzr26<)KVpl$I9HkF;oktVzDcBvzC9Z==pJ5iYL_E{{M@a^N$^Nw<+n>I|3Ca5!hiN$QiiYcFTB3Dzdn|{VCoBelIU^}1I5d73Nc7Zwb9LwT^i!<~a_7_TC+^FobHn||kB=VR{vzrX(V*V?Qv`{!+mU=z*Gnf-qu z^MW6skT&9W$Yq;RuA;E=vv78Qe!-8%_ZOKn>+G(7m-!WDQ}Uv~Leyp1Oy#8x+b#vmrbxWl9 z+xb{8&c45I<@I9%?reP>n;&j!Qm;7O(e(BI>;DU1Gn+1Aghw&Ee!W@Dsd;So)`@Ra z|MvC1(t61~^0CD!wzc#1PG79wdG^1S_uilKOpRw5-nuhgJI#If>$2@@>)UraZuq7l zB_*@+`^SIW*H`GJdf!&o?Nt(4;&wbRLY?zQ?5^u^*So*k{GQ^Z_x3!ZS>N0?e=m2n zM1^Jd4es0QSMIRCh?)Lo-m8CC^R=|J!cJYR_kHzZ|4oZu@89vTq%o%Wx-&gH%e3O` zStf{Mf0hQZnsgr@t;YM9QvhTi--JxcerF|4dv&b*|o2@$wJPW6D1~{`v2J zZ~omMWv0LGFL3)`_(h({ki(4miaV2^;@ObR3{pj#8RmUEbA3-s+>ig)c-tIT1sZz9 z8sE>)zi{twW3C5NpE1*>heC@RFO>c+-JNJq8|NLrS%RhAbV8n%EyDWr{3gMA^S#fR zZ}-}t(`C+iXUwMa@9%?+@-@xu?5SV&&kR}J_iwh}tPJ~K{vjJ26a+MH>o8OXs-Nn% zR?io6V9z}I%{KmJS!Tc+-bq(vtym(o!)#eQ4^;LyEG$%;adtV|6V)$$F)LqRd;PD? z@oKg7K?Td&_}vdRkqWH-rfn9oXVUzc@}H@F+qaiz$Iaz$qS-#^{ENAoY4G9Y`Kq_w zyo>*T)?NJH;#a-_8`EY6spv4R&snq2+>pYvGJp z66uGn{tGgfSK6G<@%XVh=+&;f3ZhScH^GznH<>rKH<}N$@9egHe=*p;^y6hY`w!LE zztzUS+}88Y-w2enL2V5_hU<|Go5T+stJZ&0k`-{~)4xBv&OU!sV%2=)!NP{Jo4(7_ zCOc*XwmDASmA6kX?ZD9nkGo$gL(Tu!|Ngw>UE+)butvvz{d$pY8;*atb5TBN{poqv zZ~IsODh>NSb@u;Dmp*-1p72G!(c^#)L#dL(^B*5C%U3?T^#1>yy^bIE+sCi3eY|{v z4BIASg&BhBhuKcgI$+bRRQg@Bx$N+heaiyuUMC;$c!wyx=G(ehg&G&w|5E(6Yg*t> zC!PZ{*XQqff5^}D*Zma9zw&(->sgWv&M_Ix=h*OvS^nnN&d=BLj_TwI?0A#iTogBT zSKY$-RZkYrZ)P!k@S8Q{SYnpf98R&%^BC)tnS!?0?=!e`|DekA_w102o8Bn@XV0?v zdzq@)zVR`?abMtW^Xg{bz8i^N`xo2W7k@0Tx2yd0#eLfU7xqkzkqpK@n;9;>{NfeW z{YALC`Juh4r~Tz6o5kzp&(Do-cL_9DT2=Mw@{~K94>EcE@=cPnstQ@?r8`^ED1Q3$ zSI^sbUec)T|M|ORRmdg@gVW!=W#9?<4dV^&4eApD|B2KDHJX&i=~l-yf8WZ#VCLV? zx|(O1E4J~qe6O!K?p`S{;mW%6fA!~Y{m;RY#u#!^ry(;-?0}Ii_XAGNY~IaPQXPyFU#SRq-o2pk^7r)| z8`1Vesm{eqj9J?z1+JLX*Jx>*{>Lp*S#n<4Y{x=#pKrg<{?~tU>0zCwE${7Ecps?V zWv3;VRCQ75tfI1A&)e%npMSd4`^NcA>dnjF&i=pU_T}yO==3M^cK>Ow&&Z4ZyJgSC z|EWe67PC!!*?3o<%KjqXctEEiatZ6FL*|oid7ReP(vmB4(Pe#c@oMzWpsTi02JLdE z!uP&=>>*d4``jV*JcrYaSq^T=Qki9QIK__LOpu&8A?=+?!bZ!JQ@!}wylfY{@7(pb zF82NJ($H1^Q~&*EJGr&-39MY6U#$H+;@s9b#h#Zf*o*a^&8aE>ccT5q>8L;Xg;p## zb{GG=$N%of4ynKSidRoBcwi%`GUtuKuk%g{0_6{N8oUncG)P^Fx%qN)-xVF}hIJKh zgflb`OqKqhmop*c?%!Qm4NI<+>hV6}4W8h@maFdQBIWli#UxWka;j&c)zP`En^RUE zR5CKzS}0<2;$rLDT8YHW^HbekR@d+@`_D09w<8NA!A&=>7uhm#mgaxcFMk%CZ|hz; z=lsrzCUc8bpZi?OIJa=g3ZA+<$(QYS|NlBaF#Gf4-+eXTPk-4PcKSvAY#t`P$&Lpj zpBnz^cSsO(a68K+;VI=k%VCz5{;!%#K{tPH4lQGf{koEOxx9&4gwD$E^=%DPYXAJ% zscrt~*)?~Ci7IZ4n$B&@n@oCmE@)hm2r`|y=;IR4C7O38I;>LMY%AhmA!I}^3}}`g-j>Ta@Do`c#pj(sHtZMOOUy=%V7?|>1EDY|Mz&< zx!TFV$Cl|6k4J`dzPhpgtP#f!4>s29lnDiMr2ej#Y03UC$Wg+yB9h^-=i*fN^PZm;ou4Qk zFVDTBX&TcX`Ra7WSl3H$J~DGkn`}Ec=>bc5lEsk?rFSC3@5tVJ^zNNv5vQR_&AYZW zzw-)e-~E&OyX)P{J(rj7n_1F!`t086e_qeIKhx5BGG}(^|5fkSc};a^+-G1d()wAn z-u=FH@w-_sZdh_MvNC|c57Q-)=jZ5tjrhN6HS0NUF7D}aQNG26(k>s3AP|G2;H2NSMF2r5M6bDSt+JhH-Xe(ui*hm#Eloz?xO^*z47)A`|w^thE7 zufFeOz26bG)>?4`$O;CA{ViLxGQ-Y)UC;bCH=@CMR;BCxU+eq-zD_XwrXLd&9{rd9 z!4r8lXJ?1iu?)Vt$C%W#*$p$!F56>!YUd2?8F7UfCHjHaeXc)OvRbCf)DzDLHq~Qs z+Ah25L%*iqywMFH+-{HUC3FsyGiLBIN_!-Q z96xX2>3&bZz{0{pRr~$w^WAS{g^I22Fc$7$E{+qlE&4R6W0icb12foyKa7`_EeYJa zw0^FAxK6~T|IYV*oKJfbKSjkS`mg(Adx z#O~H(;s;ysfpdxL_IElfP5!RlxBNe!H`n?5f2y~By?^HifAar?4O#|b7xpP8NUv2) zNQ!iJ@H{nf^7Q(n+Ebp1)<2%e(0n+Bug~)AT5J6uDe7S37!I7W*YyZp_iyVzyWiRW zKYy!U{rr~6zwG?HKgW;%(@ZdkI3U8%!^^mJF~eGww;`n)maFW1qv7D=bNYEq>;1bA z_&n~0SGY4AKm7jUwAEg3IU$A?Je%LQVM)Jd7iLP zDW*+}84AnX8I9U}W(0*4{f_UN8)DKc>YY7D#&W`LVFTF(L12R%w%1>pXLoUb+jDm# z{r^$_B!hFe{o6iy%N42rw%K`qmNy(s@b`CiC=*c#3uV21OO|zpUG?cr#rFIDcZ3@C z@}07NuYINCK-gMqWrIpyu!(1W+Q0mDCjZO&f1j6g*YCKwCG$>k+syxGFZ|#KCCR@U z4#}4T89ZN2o2$B9D{A2rk@!8)Wm{e9kG~PPdVXcL-#_z3$(Nio1n|? z-G?Sc=N!FX|K3x>d%?vzOFZV{p)?oYx}VOt9uR>h#asHUC_Qt?OW(yhM;(v zpXY8L6@UMyx>7XYtf%4gqxZMpV_N!7{zH~ zukSnOoL_h0T#VPht!w{tPRhF$`~Ph8|3HR@g9RcBOs%;l#FU9ROxl)kvaxhmbMW_h z)*i)8@rvzx${tG!%E`4luz>?WizV~;*4gJGUi+86&;C2R{M)VOvbjQWfzSW3d;Hq4 z_J1yK?0@G8e~OzA7KkXgS#w6ku+51)Lz`DQo`Wln^IyAz<7dNN8kNRihT>tFJ zcUj9)*CShBb~QlEKhA;72fysU%z1Tz53~MfBT1>-Mu>_`u}V% zspQx8JZVNUtQ^Vij8R^D=7jHuv{)<^PXV&Z=I_d0n5I^SAw=PupUKURB?p z84maO6|8n0{T!LT?@ybxjeq!;pT&ITD{4-Q*anB2=kMR|#=?HxVS+N)Y=%tdOC9}d z?X`pduZxAqpF8&QKgG2(SLFU(-@NXB8C&y14ToIM6q=ue;}=t;PRu{L0TXkmzFAs!|%V^+Tkc)P%3hFP461_V79-@mfo- z_QhP|e|+<#Ev9~7`ojDHD~lb-=mssW%!6z8=-b|(xN6V?m$I3>_I}Q}+x*M>#>e%4Tnh>c*z$j+*S?ci{{M!TtvR�>8qkW%3UEp|x)o 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 literal 0 HcmV?d00001 diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 2e01d611f..86bc65636 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -20,6 +20,9 @@ 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 */; }; 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 */; }; @@ -69,6 +72,7 @@ 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 = ""; }; 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 = ""; }; @@ -99,6 +103,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 +122,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 +188,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 */, @@ -255,6 +293,7 @@ isa = PBXGroup; children = ( A5B30531299BEAAA0047F10C /* Ghostty.app */, + A5D4499D2B53AE7B000F5B83 /* Ghostty-iOS.app */, ); name = Products; sourceTree = ""; @@ -310,6 +349,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 +373,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 +401,7 @@ projectRoot = ""; targets = ( A5B30530299BEAAA0047F10C /* Ghostty */, + A5D4499C2B53AE7B000F5B83 /* Ghostty-iOS */, ); }; /* End PBXProject section */ @@ -363,6 +423,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + A5D4499B2B53AE7B000F5B83 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A53D0C952B53B4D800305CE6 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -406,6 +474,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + A5D449992B53AE7B000F5B83 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A53D0C942B53B43700305CE6 /* iOSApp.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin XCBuildConfiguration section */ @@ -682,6 +758,117 @@ }; 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 = ""; + 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; + 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 = ""; + 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; + 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 = ""; + 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; + 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 +892,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..da5a9fe5b --- /dev/null +++ b/macos/Sources/App/iOS/iOSApp.swift @@ -0,0 +1,26 @@ +import SwiftUI + +@main +struct Ghostty_iOSApp: App { + var body: some Scene { + WindowGroup { + iOS_ContentView() + } + } +} + +struct iOS_ContentView: View { + var body: some View { + VStack { + Image(systemName: "globe") + .imageScale(.large) + .foregroundStyle(.tint) + Text("Hello, world!") + } + .padding() + } +} + +#Preview { + iOS_ContentView() +} diff --git a/macos/Sources/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift similarity index 100% rename from macos/Sources/AppDelegate.swift rename to macos/Sources/App/macOS/AppDelegate.swift 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 -- 2.51.2 From 83b004b6e10e5bc13fd5220ccfaa863406a18fc5 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 13 Jan 2024 22:26:51 -0800 Subject: [PATCH 06/19] macos: show ghostty icon on main app loading --- macos/Sources/App/iOS/iOSApp.swift | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/macos/Sources/App/iOS/iOSApp.swift b/macos/Sources/App/iOS/iOSApp.swift index da5a9fe5b..d571a490b 100644 --- a/macos/Sources/App/iOS/iOSApp.swift +++ b/macos/Sources/App/iOS/iOSApp.swift @@ -12,10 +12,11 @@ struct Ghostty_iOSApp: App { struct iOS_ContentView: View { var body: some View { VStack { - Image(systemName: "globe") - .imageScale(.large) - .foregroundStyle(.tint) - Text("Hello, world!") + Image("AppIconImage") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxHeight: 96) + Text("Ghostty") } .padding() } -- 2.51.2 From 87f5d6f6a868c3f14614e80366c506db6c83e5c3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 14 Jan 2024 14:31:14 -0800 Subject: [PATCH 07/19] apprt/embedded: do not depend on macOS APIs on non-macOS --- src/apprt/embedded.zig | 3 +++ src/input.zig | 4 ++-- src/input/KeymapNoop.zig | 38 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 src/input/KeymapNoop.zig 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/input.zig b/src/input.zig index 47024ff67..814415fcb 100644 --- a/src/input.zig +++ b/src/input.zig @@ -17,8 +17,8 @@ pub const SplitResizeDirection = Binding.Action.SplitResizeDirection; // Keymap is only available on macOS right now. We could implement it // in theory for XKB too on Linux but we don't need it right now. pub const Keymap = switch (builtin.os.tag) { - .ios, .macos => @import("input/KeymapDarwin.zig"), - else => struct {}, + .macos => @import("input/KeymapDarwin.zig"), + 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 }; +} -- 2.51.2 From 4d9fd2beccd4fceff98d9a167a0f4a4a273e4dc9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 14 Jan 2024 14:44:16 -0800 Subject: [PATCH 08/19] macos: iOS app can initialize Ghostty --- macos/Ghostty.xcodeproj/project.pbxproj | 11 ++ macos/Sources/App/iOS/iOSApp.swift | 6 + macos/Sources/Ghostty/Ghostty.App.swift | 207 ++++++++++++++++++++++++ macos/Sources/Ghostty/Package.swift | 7 + 4 files changed, 231 insertions(+) create mode 100644 macos/Sources/Ghostty/Ghostty.App.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 86bc65636..948f9d7aa 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -23,6 +23,9 @@ A53D0C8E2B53B0EA00305CE6 /* GhosttyKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */; }; A53D0C942B53B43700305CE6 /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53D0C932B53B43700305CE6 /* iOSApp.swift */; }; A53D0C952B53B4D800305CE6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; }; + A53D0C9A2B543F3B00305CE6 /* Ghostty.App.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */; }; + A53D0C9B2B543F3B00305CE6 /* Ghostty.App.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */; }; + A53D0C9C2B543F7B00305CE6 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BB729B6F53A0055DE60 /* Package.swift */; }; A55685E029A03A9F004303CE /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55685DF29A03A9F004303CE /* AppError.swift */; }; A55B7BB629B6F47F0055DE60 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BB529B6F47F0055DE60 /* AppState.swift */; }; A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BB729B6F53A0055DE60 /* Package.swift */; }; @@ -73,6 +76,7 @@ A53426342A7DA53D00EBB7A2 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; A535B9D9299C569B0017E2E4 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = ""; }; A53D0C932B53B43700305CE6 /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = ""; }; + A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.App.swift; sourceTree = ""; }; A55685DF29A03A9F004303CE /* AppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = ""; }; A55B7BB529B6F47F0055DE60 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = ""; }; A55B7BB729B6F53A0055DE60 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; @@ -233,6 +237,7 @@ A55B7BB529B6F47F0055DE60 /* AppState.swift */, A55B7BBB29B6FC330055DE60 /* SurfaceView.swift */, A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */, + A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */, A5278A9A2AA05B2600CD3039 /* Ghostty.Input.swift */, A56D58852ACDDB4100508D2C /* Ghostty.Shell.swift */, A59630A32AF059BB00D64628 /* Ghostty.SplitNode.swift */, @@ -453,6 +458,7 @@ A5CDF1952AAFA19600513312 /* ConfigurationErrorsView.swift in Sources */, A55B7BBC29B6FC330055DE60 /* SurfaceView.swift in Sources */, A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */, + A53D0C9A2B543F3B00305CE6 /* Ghostty.App.swift in Sources */, A56D58862ACDDB4100508D2C /* Ghostty.Shell.swift in Sources */, A59630A22AF0415000D64628 /* Ghostty.TerminalSplit.swift in Sources */, A5FEB3002ABB69450068369E /* main.swift in Sources */, @@ -479,6 +485,8 @@ buildActionMask = 2147483647; files = ( A53D0C942B53B43700305CE6 /* iOSApp.swift in Sources */, + A53D0C9C2B543F7B00305CE6 /* Package.swift in Sources */, + A53D0C9B2B543F3B00305CE6 /* Ghostty.App.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -785,6 +793,7 @@ ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 0.1; + "OTHER_LDFLAGS[arch=*]" = "-lstdc++"; PRODUCT_BUNDLE_IDENTIFIER = "com.mitchellh.ghostty-ios"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -822,6 +831,7 @@ ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 0.1; + "OTHER_LDFLAGS[arch=*]" = "-lstdc++"; PRODUCT_BUNDLE_IDENTIFIER = "com.mitchellh.ghostty-ios"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -859,6 +869,7 @@ ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 0.1; + "OTHER_LDFLAGS[arch=*]" = "-lstdc++"; PRODUCT_BUNDLE_IDENTIFIER = "com.mitchellh.ghostty-ios"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; diff --git a/macos/Sources/App/iOS/iOSApp.swift b/macos/Sources/App/iOS/iOSApp.swift index d571a490b..bf581d6cd 100644 --- a/macos/Sources/App/iOS/iOSApp.swift +++ b/macos/Sources/App/iOS/iOSApp.swift @@ -2,14 +2,19 @@ import SwiftUI @main struct Ghostty_iOSApp: App { + @StateObject private var ghostty_app = Ghostty.App() + var body: some Scene { WindowGroup { iOS_ContentView() + .environmentObject(ghostty_app) } } } struct iOS_ContentView: View { + @EnvironmentObject private var ghostty_app: Ghostty.App + var body: some View { VStack { Image("AppIconImage") @@ -17,6 +22,7 @@ struct iOS_ContentView: View { .aspectRatio(contentMode: .fit) .frame(maxHeight: 96) Text("Ghostty") + Text("State: \(ghostty_app.readiness.rawValue)") } .padding() } diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift new file mode 100644 index 000000000..68cd77d3a --- /dev/null +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -0,0 +1,207 @@ +import SwiftUI +import GhosttyKit + +extension Ghostty { + // IMPORTANT: THIS IS NOT DONE. + // This is a refactor/redo of Ghostty.AppState so that it supports both macOS and iOS + class App: ObservableObject { + enum Readiness: String { + case loading, error, ready + } + + /// The readiness value of the state. + @Published var readiness: Readiness = .loading + + /// The ghostty global configuration. This should only be changed when it is definitely + /// safe to change. It is definitely safe to change only when the embedded app runtime + /// in Ghostty says so (usually, only in a reload configuration callback). + @Published var config: ghostty_config_t? = nil { + didSet { + // Free the old value whenever we change + guard let old = oldValue else { return } + ghostty_config_free(old) + } + } + + /// The ghostty app instance. We only have one of these for the entire app, although I guess + /// in theory you can have multiple... I don't know why you would... + @Published var app: ghostty_app_t? = nil { + didSet { + guard let old = oldValue else { return } + ghostty_app_free(old) + } + } + + init() { + // Initialize ghostty global state. This happens once per process. + guard ghostty_init() == GHOSTTY_SUCCESS else { + logger.critical("ghostty_init failed") + readiness = .error + return + } + + // Initialize the global configuration. + guard let cfg = Self.loadConfig() else { + readiness = .error + return + } + self.config = cfg; + + // Create our "runtime" config. The "runtime" is the configuration that ghostty + // uses to interface with the application runtime environment. + var runtime_cfg = ghostty_runtime_config_s( + userdata: Unmanaged.passUnretained(self).toOpaque(), + supports_selection_clipboard: false, + wakeup_cb: { userdata in App.wakeup(userdata) }, + reload_config_cb: { userdata in App.reloadConfig(userdata) }, + open_config_cb: { userdata in App.openConfig(userdata) }, + set_title_cb: { userdata, title in App.setTitle(userdata, title: title) }, + set_mouse_shape_cb: { userdata, shape in App.setMouseShape(userdata, shape: shape) }, + set_mouse_visibility_cb: { userdata, visible in App.setMouseVisibility(userdata, visible: visible) }, + read_clipboard_cb: { userdata, loc, state in App.readClipboard(userdata, location: loc, state: state) }, + confirm_read_clipboard_cb: { userdata, str, state, request in App.confirmReadClipboard(userdata, string: str, state: state, request: request ) }, + write_clipboard_cb: { userdata, str, loc, confirm in App.writeClipboard(userdata, string: str, location: loc, confirm: confirm) }, + new_split_cb: { userdata, direction, surfaceConfig in App.newSplit(userdata, direction: direction, config: surfaceConfig) }, + new_tab_cb: { userdata, surfaceConfig in App.newTab(userdata, config: surfaceConfig) }, + new_window_cb: { userdata, surfaceConfig in App.newWindow(userdata, config: surfaceConfig) }, + control_inspector_cb: { userdata, mode in App.controlInspector(userdata, mode: mode) }, + close_surface_cb: { userdata, processAlive in App.closeSurface(userdata, processAlive: processAlive) }, + focus_split_cb: { userdata, direction in App.focusSplit(userdata, direction: direction) }, + resize_split_cb: { userdata, direction, amount in + App.resizeSplit(userdata, direction: direction, amount: amount) }, + equalize_splits_cb: { userdata in + App.equalizeSplits(userdata) }, + toggle_split_zoom_cb: { userdata in App.toggleSplitZoom(userdata) }, + goto_tab_cb: { userdata, n in App.gotoTab(userdata, n: n) }, + toggle_fullscreen_cb: { userdata, nonNativeFullscreen in App.toggleFullscreen(userdata, nonNativeFullscreen: nonNativeFullscreen) }, + set_initial_window_size_cb: { userdata, width, height in App.setInitialWindowSize(userdata, width: width, height: height) }, + render_inspector_cb: { userdata in App.renderInspector(userdata) }, + set_cell_size_cb: { userdata, width, height in App.setCellSize(userdata, width: width, height: height) }, + show_desktop_notification_cb: { userdata, title, body in + App.showUserNotification(userdata, title: title, body: body) + } + ) + + // Create the ghostty app. + guard let app = ghostty_app_new(&runtime_cfg, cfg) else { + logger.critical("ghostty_app_new failed") + readiness = .error + return + } + self.app = app + + #if os(macOS) + // Subscribe to notifications for keyboard layout change so that we can update Ghostty. + NotificationCenter.default.addObserver( + self, + selector: #selector(self.keyboardSelectionDidChange(notification:)), + name: NSTextInputContext.keyboardSelectionDidChangeNotification, + object: nil) + #endif + + self.readiness = .ready + } + + deinit { + // This will force the didSet callbacks to run which free. + self.app = nil + self.config = nil + + #if os(macOS) + // Remove our observer + NotificationCenter.default.removeObserver( + self, + name: NSTextInputContext.keyboardSelectionDidChangeNotification, + object: nil) + #endif + } + + // MARK: - Config + + /// Initializes a new configuration and loads all the values. + static private func loadConfig() -> ghostty_config_t? { + // Initialize the global configuration. + guard let cfg = ghostty_config_new() else { + logger.critical("ghostty_config_new failed") + return nil + } + + // Load our configuration files from the home directory. + ghostty_config_load_default_files(cfg); + ghostty_config_load_cli_args(cfg); + ghostty_config_load_recursive_files(cfg); + + // TODO: we'd probably do some config loading here... for now we'd + // have to do this synchronously. When we support config updating we can do + // this async and update later. + + // Finalize will make our defaults available. + ghostty_config_finalize(cfg) + + // Log any configuration errors. These will be automatically shown in a + // pop-up window too. + let errCount = ghostty_config_errors_count(cfg) + if errCount > 0 { + logger.warning("config error: \(errCount) configuration errors on reload") + var errors: [String] = []; + for i in 0.. ghostty_config_t? { return nil } + static func openConfig(_ userdata: UnsafeMutableRawPointer?) {} + static func setTitle(_ userdata: UnsafeMutableRawPointer?, title: UnsafePointer?) {} + static func setMouseShape(_ userdata: UnsafeMutableRawPointer?, shape: ghostty_mouse_shape_e) {} + static func setMouseVisibility(_ userdata: UnsafeMutableRawPointer?, visible: Bool) {} + static func readClipboard( + _ userdata: UnsafeMutableRawPointer?, + location: ghostty_clipboard_e, + state: UnsafeMutableRawPointer? + ) {} + + static func confirmReadClipboard( + _ userdata: UnsafeMutableRawPointer?, + string: UnsafePointer?, + state: UnsafeMutableRawPointer?, + request: ghostty_clipboard_request_e + ) {} + + static func writeClipboard( + _ userdata: UnsafeMutableRawPointer?, + string: UnsafePointer?, + location: ghostty_clipboard_e, + confirm: Bool + ) {} + + static func newSplit( + _ userdata: UnsafeMutableRawPointer?, + direction: ghostty_split_direction_e, + config: ghostty_surface_config_s + ) {} + + static func newTab(_ userdata: UnsafeMutableRawPointer?, config: ghostty_surface_config_s) {} + static func newWindow(_ userdata: UnsafeMutableRawPointer?, config: ghostty_surface_config_s) {} + static func controlInspector(_ userdata: UnsafeMutableRawPointer?, mode: ghostty_inspector_mode_e) {} + static func closeSurface(_ userdata: UnsafeMutableRawPointer?, processAlive: Bool) {} + static func focusSplit(_ userdata: UnsafeMutableRawPointer?, direction: ghostty_split_focus_direction_e) {} + static func resizeSplit(_ userdata: UnsafeMutableRawPointer?, direction: ghostty_split_resize_direction_e, amount: UInt16) {} + static func equalizeSplits(_ userdata: UnsafeMutableRawPointer?) {} + static func toggleSplitZoom(_ userdata: UnsafeMutableRawPointer?) {} + static func gotoTab(_ userdata: UnsafeMutableRawPointer?, n: Int32) {} + static func toggleFullscreen(_ userdata: UnsafeMutableRawPointer?, nonNativeFullscreen: ghostty_non_native_fullscreen_e) {} + static func setInitialWindowSize(_ userdata: UnsafeMutableRawPointer?, width: UInt32, height: UInt32) {} + static func renderInspector(_ userdata: UnsafeMutableRawPointer?) {} + static func setCellSize(_ userdata: UnsafeMutableRawPointer?, width: UInt32, height: UInt32) {} + static func showUserNotification(_ userdata: UnsafeMutableRawPointer?, title: UnsafePointer?, body: UnsafePointer?) {} + } +} diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index e1f3f5e99..c5b0269c6 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -1,7 +1,14 @@ +import os import SwiftUI import GhosttyKit struct Ghostty { + // The primary logger used by the GhosttyKit libraries. + static let logger = Logger( + subsystem: Bundle.main.bundleIdentifier!, + category: "ghostty" + ) + // All the notifications that will be emitted will be put here. struct Notification {} -- 2.51.2 From 8c8838542f508d5fec910fa5d3e1d6e9a89cb734 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 14 Jan 2024 14:48:39 -0800 Subject: [PATCH 09/19] use Apple logging subsystem on all Darwin targets --- src/main.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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, -- 2.51.2 From 470b57f1942247f142d4e7137ed78b1f65aed454 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 14 Jan 2024 14:48:56 -0800 Subject: [PATCH 10/19] os: no mouse interval on ios --- src/os/mouse.zig | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) 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, + }; } -- 2.51.2 From 65fd02817ea277d8024235861e56f27414ae7063 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 14 Jan 2024 14:53:00 -0800 Subject: [PATCH 11/19] macos: only load config files on macos target --- macos/Sources/Ghostty/Ghostty.App.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index 68cd77d3a..b075bfe04 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -126,10 +126,14 @@ extension Ghostty { return nil } - // Load our configuration files from the home directory. + // 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 -- 2.51.2 From 33b93799b97c680695109f64b2c44cd302ef1b8c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 14 Jan 2024 15:00:00 -0800 Subject: [PATCH 12/19] macos: disable iOS file in macOS build --- macos/Ghostty.xcodeproj/project.pbxproj | 2 -- 1 file changed, 2 deletions(-) diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 948f9d7aa..43f3af015 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -23,7 +23,6 @@ A53D0C8E2B53B0EA00305CE6 /* GhosttyKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */; }; A53D0C942B53B43700305CE6 /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53D0C932B53B43700305CE6 /* iOSApp.swift */; }; A53D0C952B53B4D800305CE6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; }; - A53D0C9A2B543F3B00305CE6 /* Ghostty.App.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */; }; A53D0C9B2B543F3B00305CE6 /* Ghostty.App.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */; }; A53D0C9C2B543F7B00305CE6 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BB729B6F53A0055DE60 /* Package.swift */; }; A55685E029A03A9F004303CE /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55685DF29A03A9F004303CE /* AppError.swift */; }; @@ -458,7 +457,6 @@ A5CDF1952AAFA19600513312 /* ConfigurationErrorsView.swift in Sources */, A55B7BBC29B6FC330055DE60 /* SurfaceView.swift in Sources */, A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */, - A53D0C9A2B543F3B00305CE6 /* Ghostty.App.swift in Sources */, A56D58862ACDDB4100508D2C /* Ghostty.Shell.swift in Sources */, A59630A22AF0415000D64628 /* Ghostty.TerminalSplit.swift in Sources */, A5FEB3002ABB69450068369E /* main.swift in Sources */, -- 2.51.2 From eba3d5414d863d559b9073e1fbdbf7dd61ac8ab1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 14 Jan 2024 15:52:23 -0800 Subject: [PATCH 13/19] macos: Ghostty.Config to store all config-related operations --- macos/Ghostty.xcodeproj/project.pbxproj | 6 + macos/Sources/App/macOS/AppDelegate.swift | 19 +- .../Terminal/TerminalController.swift | 10 +- .../Features/Terminal/TerminalManager.swift | 4 +- .../Terminal/TerminalRestorable.swift | 2 +- macos/Sources/Ghostty/AppState.swift | 170 +------------ macos/Sources/Ghostty/Ghostty.Config.swift | 238 ++++++++++++++++++ macos/Sources/Ghostty/Ghostty.Input.swift | 15 -- macos/Sources/Ghostty/SurfaceView.swift | 32 +-- 9 files changed, 274 insertions(+), 222 deletions(-) create mode 100644 macos/Sources/Ghostty/Ghostty.Config.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 43f3af015..88c49ce83 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -11,6 +11,8 @@ 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 */; }; 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 */; }; @@ -65,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 = ""; }; @@ -237,6 +240,7 @@ 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 */, @@ -443,6 +447,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 */, @@ -483,6 +488,7 @@ 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 */, ); diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 4415dbf3f..50f7f5599 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -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? { @@ -357,7 +354,7 @@ class AppDelegate: NSObject, // 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/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index b49e502e8..4db4d715b 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -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 } diff --git a/macos/Sources/Features/Terminal/TerminalManager.swift b/macos/Sources/Features/Terminal/TerminalManager.swift index b919d5282..361ee2feb 100644 --- a/macos/Sources/Features/Terminal/TerminalManager.swift +++ b/macos/Sources/Features/Terminal/TerminalManager.swift @@ -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/Ghostty/AppState.swift b/macos/Sources/Ghostty/AppState.swift index e9c15ee95..c59f39795 100644 --- a/macos/Sources/Ghostty/AppState.swift +++ b/macos/Sources/Ghostty/AppState.swift @@ -36,16 +36,10 @@ extension Ghostty { /// 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) - } - } + /// 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... @@ -55,45 +49,6 @@ 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 { @@ -113,66 +68,19 @@ extension Ghostty { 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 { + AppDelegate.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. @@ -210,7 +118,7 @@ extension Ghostty { ) // Create the ghostty app. - guard let app = ghostty_app_new(&runtime_cfg, cfg) else { + guard let app = ghostty_app_new(&runtime_cfg, config.config) else { AppDelegate.logger.critical("ghostty_app_new failed") readiness = .error return @@ -230,7 +138,6 @@ extension Ghostty { deinit { // This will force the didSet callbacks to run which free. self.app = nil - self.config = nil // Remove our observer NotificationCenter.default.removeObserver( @@ -239,58 +146,6 @@ extension Ghostty { object: nil) } - /// 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? { - guard let newConfig = Self.loadConfig() else { + let newConfig = Config() + guard newConfig.loaded else { AppDelegate.logger.warning("failed to reload configuration") return nil } @@ -549,7 +405,7 @@ extension Ghostty { delegate.configDidReload(state) } - return newConfig + return newConfig.config } static func wakeup(_ userdata: UnsafeMutableRawPointer?) { @@ -662,7 +518,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" 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/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index 473a3a884..f9bc0f027 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -55,34 +55,6 @@ extension Ghostty { // 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) } -- 2.51.2 From 5e69b302401f613741296214bc08cb6ecd243084 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 14 Jan 2024 15:55:31 -0800 Subject: [PATCH 14/19] macos: iOS Ghostty.App converted to use Ghostty.Config --- macos/Sources/Ghostty/Ghostty.App.swift | 71 ++++--------------------- 1 file changed, 10 insertions(+), 61 deletions(-) diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index b075bfe04..add6fadb1 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -12,16 +12,10 @@ extension Ghostty { /// The readiness value of the state. @Published var readiness: Readiness = .loading - /// The ghostty global configuration. This should only be changed when it is definitely - /// safe to change. It is definitely safe to change only when the embedded app runtime - /// in Ghostty says so (usually, only in a reload configuration callback). - @Published var config: ghostty_config_t? = nil { - didSet { - // Free the old value whenever we change - guard let old = oldValue else { return } - ghostty_config_free(old) - } - } + /// The 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... @@ -34,18 +28,17 @@ extension Ghostty { init() { // Initialize ghostty global state. This happens once per process. - guard ghostty_init() == GHOSTTY_SUCCESS else { - 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. @@ -83,7 +76,7 @@ extension Ghostty { ) // Create the ghostty app. - guard let app = ghostty_app_new(&runtime_cfg, cfg) else { + guard let app = ghostty_app_new(&runtime_cfg, config.config) else { logger.critical("ghostty_app_new failed") readiness = .error return @@ -105,7 +98,6 @@ extension Ghostty { deinit { // This will force the didSet callbacks to run which free. self.app = nil - self.config = nil #if os(macOS) // Remove our observer @@ -116,49 +108,6 @@ extension Ghostty { #endif } - // MARK: - Config - - /// Initializes a new configuration and loads all the values. - static private func loadConfig() -> ghostty_config_t? { - // Initialize the global configuration. - guard let cfg = ghostty_config_new() else { - logger.critical("ghostty_config_new failed") - return nil - } - - // Load our configuration 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.. Date: Sun, 14 Jan 2024 19:06:01 -0800 Subject: [PATCH 15/19] ci: specifically target the main Ghostty target --- .github/workflows/release-tip.yml | 2 +- .github/workflows/test.yml | 2 +- macos/Ghostty.xcodeproj/project.pbxproj | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) 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..58b2f6cf8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.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 + run: cd macos && xcodebuild -target Ghostty build-windows: runs-on: windows-2019 diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 88c49ce83..94173ac54 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -779,6 +779,7 @@ 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; @@ -817,6 +818,7 @@ 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; @@ -855,6 +857,7 @@ 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; -- 2.51.2 From b17c33bfb06c142386f4e5ea32e6944f224aef2a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 14 Jan 2024 19:09:47 -0800 Subject: [PATCH 16/19] ci: try building iOS target in CI --- .github/workflows/test.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 58b2f6cf8..8883389da 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -90,6 +90,15 @@ jobs: - name: Build Ghostty.app run: cd macos && xcodebuild -target Ghostty + # Build the iOS target. This requires a team ID and we can reuse our + # release team ID. This doesn't upload anything so that's okay. + - name: Build Ghostty iOS + env: + PROD_MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }} + run: | + cd macos + xcodebuild -target Ghostty-iOS "DEVELOPMENT_TEAM=$PROD_MACOS_NOTARIZATION_TEAM_ID" + build-windows: runs-on: windows-2019 # this will not stop other jobs from running -- 2.51.2 From 875a774d4b3e8871aba369f5bc7dc632507049c7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 14 Jan 2024 19:35:57 -0800 Subject: [PATCH 17/19] macos: remove AppState and unify onto Ghostty.App cross-platform --- macos/Ghostty.xcodeproj/project.pbxproj | 6 +- macos/Sources/App/macOS/AppDelegate.swift | 8 +- .../Terminal/TerminalController.swift | 8 +- .../Features/Terminal/TerminalManager.swift | 4 +- .../Features/Terminal/TerminalView.swift | 4 +- macos/Sources/Ghostty/AppState.swift | 572 ------------------ macos/Sources/Ghostty/Ghostty.App.swift | 472 ++++++++++++++- macos/Sources/Ghostty/Package.swift | 20 + macos/Sources/Ghostty/SurfaceView.swift | 4 +- 9 files changed, 507 insertions(+), 591 deletions(-) delete mode 100644 macos/Sources/Ghostty/AppState.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 94173ac54..7322c3a08 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ 857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 857F63802A5E64F200CA4815 /* MainMenu.xib */; }; A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = A514C8D52B54A16400493A16 /* Ghostty.Config.swift */; }; A514C8D72B54A16400493A16 /* Ghostty.Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = A514C8D52B54A16400493A16 /* Ghostty.Config.swift */; }; + A514C8D82B54DC6800493A16 /* Ghostty.App.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */; }; A51B78472AF4B58B00F3EDB9 /* TerminalWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51B78462AF4B58B00F3EDB9 /* TerminalWindow.swift */; }; A51BFC1E2B2FB5CE00E92F16 /* About.xib in Resources */ = {isa = PBXBuildFile; fileRef = A51BFC1D2B2FB5CE00E92F16 /* About.xib */; }; A51BFC202B2FB64F00E92F16 /* AboutController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51BFC1F2B2FB64F00E92F16 /* AboutController.swift */; }; @@ -28,7 +29,6 @@ A53D0C9B2B543F3B00305CE6 /* Ghostty.App.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */; }; A53D0C9C2B543F7B00305CE6 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BB729B6F53A0055DE60 /* Package.swift */; }; A55685E029A03A9F004303CE /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55685DF29A03A9F004303CE /* AppError.swift */; }; - A55B7BB629B6F47F0055DE60 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BB529B6F47F0055DE60 /* AppState.swift */; }; A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BB729B6F53A0055DE60 /* Package.swift */; }; A55B7BBC29B6FC330055DE60 /* SurfaceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BBB29B6FC330055DE60 /* SurfaceView.swift */; }; A56B880B2A840447007A0E29 /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A56B880A2A840447007A0E29 /* Carbon.framework */; }; @@ -80,7 +80,6 @@ A53D0C932B53B43700305CE6 /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = ""; }; A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.App.swift; sourceTree = ""; }; A55685DF29A03A9F004303CE /* AppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = ""; }; - A55B7BB529B6F47F0055DE60 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = ""; }; A55B7BB729B6F53A0055DE60 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; A55B7BBB29B6FC330055DE60 /* SurfaceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurfaceView.swift; sourceTree = ""; }; A56B880A2A840447007A0E29 /* Carbon.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Carbon.framework; path = System/Library/Frameworks/Carbon.framework; sourceTree = SDKROOT; }; @@ -236,7 +235,6 @@ isa = PBXGroup; children = ( A55B7BB729B6F53A0055DE60 /* Package.swift */, - A55B7BB529B6F47F0055DE60 /* AppState.swift */, A55B7BBB29B6FC330055DE60 /* SurfaceView.swift */, A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */, A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */, @@ -467,7 +465,6 @@ A5FEB3002ABB69450068369E /* main.swift in Sources */, A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */, A51B78472AF4B58B00F3EDB9 /* TerminalWindow.swift in Sources */, - A55B7BB629B6F47F0055DE60 /* AppState.swift in Sources */, A5CEAFDC29B8009000646FDA /* SplitView.swift in Sources */, A5CDF1932AAF9E0800513312 /* ConfigurationErrorsController.swift in Sources */, A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */, @@ -480,6 +477,7 @@ A596309E2AEE1D6C00D64628 /* TerminalView.swift in Sources */, A5CEAFDE29B8058B00646FDA /* SplitView.Divider.swift in Sources */, A5E112972AF7401B00C6E0C2 /* ClipboardConfirmationView.swift in Sources */, + A514C8D82B54DC6800493A16 /* Ghostty.App.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 50f7f5599..348b8aceb 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -8,7 +8,7 @@ class AppDelegate: NSObject, ObservableObject, NSApplicationDelegate, UNUserNotificationCenterDelegate, - GhosttyAppStateDelegate + GhosttyAppDelegate { // The application logger. We should probably move this at some point to a dedicated // class/struct but for now it lives here! 🤷‍♂️ @@ -62,7 +62,7 @@ class AppDelegate: NSObject, private var applicationHasBecomeActive: Bool = false /// The ghostty global state. Only one per process. - let ghostty: Ghostty.AppState = Ghostty.AppState() + let ghostty: Ghostty.App = Ghostty.App() /// Manages our terminal windows. let terminalManager: TerminalManager @@ -338,7 +338,7 @@ class AppDelegate: NSObject, withCompletionHandler(options) } - //MARK: - GhosttyAppStateDelegate + //MARK: - GhosttyAppDelegate func findSurface(forUUID uuid: UUID) -> Ghostty.SurfaceView? { for c in terminalManager.windows { @@ -350,7 +350,7 @@ class AppDelegate: NSObject, return nil } - func configDidReload(_ state: Ghostty.AppState) { + func configDidReload(_ state: Ghostty.App) { // Depending on the "window-save-state" setting we have to set the NSQuitAlwaysKeepsWindows // configuration. This is the only way to carefully control whether macOS invokes the // state restoration system. diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 4db4d715b..27d42ef96 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -11,7 +11,7 @@ class TerminalController: NSWindowController, NSWindowDelegate, override var windowNibName: NSNib.Name? { "Terminal" } /// The app instance that this terminal view will represent. - let ghostty: Ghostty.AppState + let ghostty: Ghostty.App /// The currently focused surface. var focusedSurface: Ghostty.SurfaceView? = nil @@ -46,7 +46,7 @@ class TerminalController: NSWindowController, NSWindowDelegate, /// changes in the list. private var tabWindowsHash: Int = 0 - init(_ ghostty: Ghostty.AppState, + init(_ ghostty: Ghostty.App, withBaseConfig base: Ghostty.SurfaceConfiguration? = nil, withSurfaceTree tree: Ghostty.SplitNode? = nil ) { @@ -502,7 +502,7 @@ class TerminalController: NSWindowController, NSWindowDelegate, str = cc.contents } - Ghostty.AppState.completeClipboardRequest(cc.surface, data: str, state: cc.state, confirmed: true) + Ghostty.App.completeClipboardRequest(cc.surface, data: str, state: cc.state, confirmed: true) } } @@ -589,7 +589,7 @@ class TerminalController: NSWindowController, NSWindowDelegate, // If we already have a clipboard confirmation view up, we ignore this request. // This shouldn't be possible... guard self.clipboardConfirmation == nil else { - Ghostty.AppState.completeClipboardRequest(surface, data: "", state: state, confirmed: true) + Ghostty.App.completeClipboardRequest(surface, data: "", state: state, confirmed: true) return } diff --git a/macos/Sources/Features/Terminal/TerminalManager.swift b/macos/Sources/Features/Terminal/TerminalManager.swift index 361ee2feb..a59741d3f 100644 --- a/macos/Sources/Features/Terminal/TerminalManager.swift +++ b/macos/Sources/Features/Terminal/TerminalManager.swift @@ -11,7 +11,7 @@ class TerminalManager { let closePublisher: AnyCancellable } - let ghostty: Ghostty.AppState + let ghostty: Ghostty.App /// The currently focused surface of the main window. var focusedSurface: Ghostty.SurfaceView? { mainWindow?.controller.focusedSurface } @@ -37,7 +37,7 @@ class TerminalManager { return windows.last } - init(_ ghostty: Ghostty.AppState) { + init(_ ghostty: Ghostty.App) { self.ghostty = ghostty let center = NotificationCenter.default diff --git a/macos/Sources/Features/Terminal/TerminalView.swift b/macos/Sources/Features/Terminal/TerminalView.swift index ba3f86db6..d0766c7ab 100644 --- a/macos/Sources/Features/Terminal/TerminalView.swift +++ b/macos/Sources/Features/Terminal/TerminalView.swift @@ -37,7 +37,7 @@ protocol TerminalViewModel: ObservableObject { /// The main terminal view. This terminal view supports splits. struct TerminalView: View { - @ObservedObject var ghostty: Ghostty.AppState + @ObservedObject var ghostty: Ghostty.App // The required view model @ObservedObject var viewModel: ViewModel @@ -83,7 +83,7 @@ struct TerminalView: View { VStack(spacing: 0) { // If we're running in debug mode we show a warning so that users // know that performance will be degraded. - if (ghostty.info.mode == GHOSTTY_BUILD_MODE_DEBUG) { + if (Ghostty.info.mode == GHOSTTY_BUILD_MODE_DEBUG) { DebugBuildWarningView() } diff --git a/macos/Sources/Ghostty/AppState.swift b/macos/Sources/Ghostty/AppState.swift deleted file mode 100644 index c59f39795..000000000 --- a/macos/Sources/Ghostty/AppState.swift +++ /dev/null @@ -1,572 +0,0 @@ -import SwiftUI -import UserNotifications -import GhosttyKit - -protocol GhosttyAppStateDelegate: AnyObject { - /// Called when the configuration did finish reloading. - func configDidReload(_ state: Ghostty.AppState) - - /// Called when a callback needs access to a specific surface. This should return nil - /// when the surface is no longer valid. - func findSurface(forUUID uuid: UUID) -> Ghostty.SurfaceView? -} - -extension Ghostty { - enum AppReadiness { - case loading, error, ready - } - - enum FontSizeModification { - case increase(Int) - case decrease(Int) - case reset - } - - struct Info { - var mode: ghostty_build_mode_e - var version: String - } - - /// The AppState is the global state that is associated with the Swift app. This handles initially - /// initializing Ghostty, loading the configuration, etc. - class AppState: ObservableObject { - /// The readiness value of the state. - @Published var readiness: AppReadiness = .loading - - /// Optional delegate - weak var delegate: GhosttyAppStateDelegate? - - /// The global app configuration. This defines the app level configuration plus any behavior - /// for new windows, tabs, etc. Note that when creating a new window, it may inherit some - /// configuration (i.e. font size) from the previously focused window. This would override this. - private(set) var config: Config - - /// The ghostty app instance. We only have one of these for the entire app, although I guess - /// in theory you can have multiple... I don't know why you would... - @Published var app: ghostty_app_t? = nil { - didSet { - guard let old = oldValue else { return } - ghostty_app_free(old) - } - } - - /// True if we need to confirm before quitting. - var needsConfirmQuit: Bool { - guard let app = app else { return false } - return ghostty_app_needs_confirm_quit(app) - } - - /// Build information - var info: Info { - let raw = ghostty_info() - let version = NSString( - bytes: raw.version, - length: Int(raw.version_len), - encoding: NSUTF8StringEncoding - ) ?? "unknown" - - return Info(mode: raw.build_mode, version: String(version)) - } - - init() { - // Initialize ghostty global state. This happens once per process. - if ghostty_init() != GHOSTTY_SUCCESS { - AppDelegate.logger.critical("ghostty_init failed, weird things may happen") - readiness = .error - } - - // Initialize the global configuration. - self.config = Config() - if self.config.config == nil { - readiness = .error - return - } - - // Create our "runtime" config. The "runtime" is the configuration that ghostty - // uses to interface with the application runtime environment. - var runtime_cfg = ghostty_runtime_config_s( - userdata: Unmanaged.passUnretained(self).toOpaque(), - supports_selection_clipboard: false, - wakeup_cb: { userdata in AppState.wakeup(userdata) }, - reload_config_cb: { userdata in AppState.reloadConfig(userdata) }, - open_config_cb: { userdata in AppState.openConfig(userdata) }, - set_title_cb: { userdata, title in AppState.setTitle(userdata, title: title) }, - set_mouse_shape_cb: { userdata, shape in AppState.setMouseShape(userdata, shape: shape) }, - set_mouse_visibility_cb: { userdata, visible in AppState.setMouseVisibility(userdata, visible: visible) }, - read_clipboard_cb: { userdata, loc, state in AppState.readClipboard(userdata, location: loc, state: state) }, - confirm_read_clipboard_cb: { userdata, str, state, request in AppState.confirmReadClipboard(userdata, string: str, state: state, request: request ) }, - write_clipboard_cb: { userdata, str, loc, confirm in AppState.writeClipboard(userdata, string: str, location: loc, confirm: confirm) }, - new_split_cb: { userdata, direction, surfaceConfig in AppState.newSplit(userdata, direction: direction, config: surfaceConfig) }, - new_tab_cb: { userdata, surfaceConfig in AppState.newTab(userdata, config: surfaceConfig) }, - new_window_cb: { userdata, surfaceConfig in AppState.newWindow(userdata, config: surfaceConfig) }, - control_inspector_cb: { userdata, mode in AppState.controlInspector(userdata, mode: mode) }, - close_surface_cb: { userdata, processAlive in AppState.closeSurface(userdata, processAlive: processAlive) }, - focus_split_cb: { userdata, direction in AppState.focusSplit(userdata, direction: direction) }, - resize_split_cb: { userdata, direction, amount in - AppState.resizeSplit(userdata, direction: direction, amount: amount) }, - equalize_splits_cb: { userdata in - AppState.equalizeSplits(userdata) }, - toggle_split_zoom_cb: { userdata in AppState.toggleSplitZoom(userdata) }, - goto_tab_cb: { userdata, n in AppState.gotoTab(userdata, n: n) }, - toggle_fullscreen_cb: { userdata, nonNativeFullscreen in AppState.toggleFullscreen(userdata, nonNativeFullscreen: nonNativeFullscreen) }, - set_initial_window_size_cb: { userdata, width, height in AppState.setInitialWindowSize(userdata, width: width, height: height) }, - render_inspector_cb: { userdata in AppState.renderInspector(userdata) }, - set_cell_size_cb: { userdata, width, height in AppState.setCellSize(userdata, width: width, height: height) }, - show_desktop_notification_cb: { userdata, title, body in - AppState.showUserNotification(userdata, title: title, body: body) - } - ) - - // Create the ghostty app. - guard let app = ghostty_app_new(&runtime_cfg, config.config) else { - AppDelegate.logger.critical("ghostty_app_new failed") - readiness = .error - return - } - self.app = app - - // Subscribe to notifications for keyboard layout change so that we can update Ghostty. - NotificationCenter.default.addObserver( - self, - selector: #selector(self.keyboardSelectionDidChange(notification:)), - name: NSTextInputContext.keyboardSelectionDidChangeNotification, - object: nil) - - self.readiness = .ready - } - - deinit { - // This will force the didSet callbacks to run which free. - self.app = nil - - // Remove our observer - NotificationCenter.default.removeObserver( - self, - name: NSTextInputContext.keyboardSelectionDidChangeNotification, - object: nil) - } - - func appTick() { - guard let app = self.app else { return } - - // Tick our app, which lets us know if we want to quit - let exit = ghostty_app_tick(app) - if (!exit) { return } - - // We want to quit, start that process - NSApplication.shared.terminate(nil) - } - - func openConfig() { - guard let app = self.app else { return } - ghostty_app_open_config(app) - } - - func reloadConfig() { - guard let app = self.app else { return } - ghostty_app_reload_config(app) - } - - /// Request that the given surface is closed. This will trigger the full normal surface close event - /// cycle which will call our close surface callback. - func requestClose(surface: ghostty_surface_t) { - ghostty_surface_request_close(surface) - } - - func newTab(surface: ghostty_surface_t) { - let action = "new_tab" - if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) { - AppDelegate.logger.warning("action failed action=\(action)") - } - } - - func newWindow(surface: ghostty_surface_t) { - let action = "new_window" - if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) { - AppDelegate.logger.warning("action failed action=\(action)") - } - } - - func split(surface: ghostty_surface_t, direction: ghostty_split_direction_e) { - ghostty_surface_split(surface, direction) - } - - func splitMoveFocus(surface: ghostty_surface_t, direction: SplitFocusDirection) { - ghostty_surface_split_focus(surface, direction.toNative()) - } - - func splitResize(surface: ghostty_surface_t, direction: SplitResizeDirection, amount: UInt16) { - ghostty_surface_split_resize(surface, direction.toNative(), amount) - } - - func splitEqualize(surface: ghostty_surface_t) { - ghostty_surface_split_equalize(surface) - } - - func splitToggleZoom(surface: ghostty_surface_t) { - let action = "toggle_split_zoom" - if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) { - AppDelegate.logger.warning("action failed action=\(action)") - } - } - - func toggleFullscreen(surface: ghostty_surface_t) { - let action = "toggle_fullscreen" - if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) { - AppDelegate.logger.warning("action failed action=\(action)") - } - } - - func changeFontSize(surface: ghostty_surface_t, _ change: FontSizeModification) { - let action: String - switch change { - case .increase(let amount): - action = "increase_font_size:\(amount)" - case .decrease(let amount): - action = "decrease_font_size:\(amount)" - case .reset: - action = "reset_font_size" - } - if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) { - AppDelegate.logger.warning("action failed action=\(action)") - } - } - - func toggleTerminalInspector(surface: ghostty_surface_t) { - let action = "inspector:toggle" - if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) { - AppDelegate.logger.warning("action failed action=\(action)") - } - } - - // Called when the selected keyboard changes. We have to notify Ghostty so that - // it can reload the keyboard mapping for input. - @objc private func keyboardSelectionDidChange(notification: NSNotification) { - guard let app = self.app else { return } - ghostty_app_keyboard_changed(app) - } - - // MARK: Ghostty Callbacks - - static func newSplit(_ userdata: UnsafeMutableRawPointer?, direction: ghostty_split_direction_e, config: ghostty_surface_config_s) { - let surface = self.surfaceUserdata(from: userdata) - NotificationCenter.default.post(name: Notification.ghosttyNewSplit, object: surface, userInfo: [ - "direction": direction, - Notification.NewSurfaceConfigKey: SurfaceConfiguration(from: config), - ]) - } - - static func closeSurface(_ userdata: UnsafeMutableRawPointer?, processAlive: Bool) { - let surface = self.surfaceUserdata(from: userdata) - NotificationCenter.default.post(name: Notification.ghosttyCloseSurface, object: surface, userInfo: [ - "process_alive": processAlive, - ]) - } - - static func focusSplit(_ userdata: UnsafeMutableRawPointer?, direction: ghostty_split_focus_direction_e) { - let surface = self.surfaceUserdata(from: userdata) - guard let splitDirection = SplitFocusDirection.from(direction: direction) else { return } - NotificationCenter.default.post( - name: Notification.ghosttyFocusSplit, - object: surface, - userInfo: [ - Notification.SplitDirectionKey: splitDirection, - ] - ) - } - - static func resizeSplit(_ userdata: UnsafeMutableRawPointer?, direction: ghostty_split_resize_direction_e, amount: UInt16) { - let surface = self.surfaceUserdata(from: userdata) - guard let resizeDirection = SplitResizeDirection.from(direction: direction) else { return } - NotificationCenter.default.post( - name: Notification.didResizeSplit, - object: surface, - userInfo: [ - Notification.ResizeSplitDirectionKey: resizeDirection, - Notification.ResizeSplitAmountKey: amount, - ] - ) - } - - static func equalizeSplits(_ userdata: UnsafeMutableRawPointer?) { - let surface = self.surfaceUserdata(from: userdata) - NotificationCenter.default.post(name: Notification.didEqualizeSplits, object: surface) - } - - static func toggleSplitZoom(_ userdata: UnsafeMutableRawPointer?) { - let surface = self.surfaceUserdata(from: userdata) - - NotificationCenter.default.post( - name: Notification.didToggleSplitZoom, - object: surface - ) - } - - static func gotoTab(_ userdata: UnsafeMutableRawPointer?, n: Int32) { - let surface = self.surfaceUserdata(from: userdata) - NotificationCenter.default.post( - name: Notification.ghosttyGotoTab, - object: surface, - userInfo: [ - Notification.GotoTabKey: n, - ] - ) - } - - static func readClipboard(_ userdata: UnsafeMutableRawPointer?, location: ghostty_clipboard_e, state: UnsafeMutableRawPointer?) { - // If we don't even have a surface, something went terrible wrong so we have - // to leak "state". - let surfaceView = self.surfaceUserdata(from: userdata) - guard let surface = surfaceView.surface else { return } - - // We only support the standard clipboard - if (location != GHOSTTY_CLIPBOARD_STANDARD) { - return completeClipboardRequest(surface, data: "", state: state) - } - - // Get our string - let str = NSPasteboard.general.string(forType: .string) ?? "" - completeClipboardRequest(surface, data: str, state: state) - } - - static func confirmReadClipboard( - _ userdata: UnsafeMutableRawPointer?, - string: UnsafePointer?, - state: UnsafeMutableRawPointer?, - request: ghostty_clipboard_request_e - ) { - let surface = self.surfaceUserdata(from: userdata) - guard let valueStr = String(cString: string!, encoding: .utf8) else { return } - guard let request = Ghostty.ClipboardRequest.from(request: request) else { return } - NotificationCenter.default.post( - name: Notification.confirmClipboard, - object: surface, - userInfo: [ - Notification.ConfirmClipboardStrKey: valueStr, - Notification.ConfirmClipboardStateKey: state as Any, - Notification.ConfirmClipboardRequestKey: request, - ] - ) - } - - static func completeClipboardRequest( - _ surface: ghostty_surface_t, - data: String, - state: UnsafeMutableRawPointer?, - confirmed: Bool = false - ) { - data.withCString { ptr in - ghostty_surface_complete_clipboard_request(surface, ptr, state, confirmed) - } - } - - static func writeClipboard(_ userdata: UnsafeMutableRawPointer?, string: UnsafePointer?, location: ghostty_clipboard_e, confirm: Bool) { - let surface = self.surfaceUserdata(from: userdata) - - // We only support the standard clipboard - if (location != GHOSTTY_CLIPBOARD_STANDARD) { return } - - guard let valueStr = String(cString: string!, encoding: .utf8) else { return } - if !confirm { - let pb = NSPasteboard.general - pb.declareTypes([.string], owner: nil) - pb.setString(valueStr, forType: .string) - return - } - - NotificationCenter.default.post( - name: Notification.confirmClipboard, - object: surface, - userInfo: [ - Notification.ConfirmClipboardStrKey: valueStr, - Notification.ConfirmClipboardRequestKey: Ghostty.ClipboardRequest.osc_52_write, - ] - ) - } - - static func openConfig(_ userdata: UnsafeMutableRawPointer?) { - ghostty_config_open(); - } - - static func reloadConfig(_ userdata: UnsafeMutableRawPointer?) -> ghostty_config_t? { - let newConfig = Config() - guard newConfig.loaded else { - AppDelegate.logger.warning("failed to reload configuration") - return nil - } - - // Assign the new config. This will automatically free the old config. - // It is safe to free the old config from within this function call. - let state = Unmanaged.fromOpaque(userdata!).takeUnretainedValue() - state.config = newConfig - - // If we have a delegate, notify. - if let delegate = state.delegate { - delegate.configDidReload(state) - } - - return newConfig.config - } - - static func wakeup(_ userdata: UnsafeMutableRawPointer?) { - let state = Unmanaged.fromOpaque(userdata!).takeUnretainedValue() - - // Wakeup can be called from any thread so we schedule the app tick - // from the main thread. There is probably some improvements we can make - // to coalesce multiple ticks but I don't think it matters from a performance - // standpoint since we don't do this much. - DispatchQueue.main.async { state.appTick() } - } - - static func renderInspector(_ userdata: UnsafeMutableRawPointer?) { - let surface = self.surfaceUserdata(from: userdata) - NotificationCenter.default.post( - name: Notification.inspectorNeedsDisplay, - object: surface - ) - } - - static func setTitle(_ userdata: UnsafeMutableRawPointer?, title: UnsafePointer?) { - let surfaceView = self.surfaceUserdata(from: userdata) - guard let titleStr = String(cString: title!, encoding: .utf8) else { return } - DispatchQueue.main.async { - surfaceView.title = titleStr - } - } - - static func setMouseShape(_ userdata: UnsafeMutableRawPointer?, shape: ghostty_mouse_shape_e) { - let surfaceView = self.surfaceUserdata(from: userdata) - surfaceView.setCursorShape(shape) - } - - static func setMouseVisibility(_ userdata: UnsafeMutableRawPointer?, visible: Bool) { - let surfaceView = self.surfaceUserdata(from: userdata) - surfaceView.setCursorVisibility(visible) - } - - static func toggleFullscreen(_ userdata: UnsafeMutableRawPointer?, nonNativeFullscreen: ghostty_non_native_fullscreen_e) { - let surface = self.surfaceUserdata(from: userdata) - NotificationCenter.default.post( - name: Notification.ghosttyToggleFullscreen, - object: surface, - userInfo: [ - Notification.NonNativeFullscreenKey: nonNativeFullscreen, - ] - ) - } - - static func setInitialWindowSize(_ userdata: UnsafeMutableRawPointer?, width: UInt32, height: UInt32) { - // We need a window to set the frame - let surfaceView = self.surfaceUserdata(from: userdata) - surfaceView.initialSize = NSMakeSize(Double(width), Double(height)) - } - - static func setCellSize(_ userdata: UnsafeMutableRawPointer?, width: UInt32, height: UInt32) { - let surfaceView = self.surfaceUserdata(from: userdata) - let backingSize = NSSize(width: Double(width), height: Double(height)) - surfaceView.cellSize = surfaceView.convertFromBacking(backingSize) - } - - static func showUserNotification(_ userdata: UnsafeMutableRawPointer?, title: UnsafePointer?, body: UnsafePointer?) { - let surfaceView = self.surfaceUserdata(from: userdata) - guard let title = String(cString: title!, encoding: .utf8) else { return } - guard let body = String(cString: body!, encoding: .utf8) else { return } - - let center = UNUserNotificationCenter.current() - center.requestAuthorization(options: [.alert, .sound]) { _, error in - if let error = error { - AppDelegate.logger.error("Error while requesting notification authorization: \(error)") - } - } - - center.getNotificationSettings() { settings in - guard settings.authorizationStatus == .authorized else { return } - surfaceView.showUserNotification(title: title, body: body) - } - } - - /// Handle a received user notification. This is called when a user notification is clicked or dismissed by the user - func handleUserNotification(response: UNNotificationResponse) { - let userInfo = response.notification.request.content.userInfo - guard let uuidString = userInfo["surface"] as? String, - let uuid = UUID(uuidString: uuidString), - let surface = delegate?.findSurface(forUUID: uuid) else { return } - - switch (response.actionIdentifier) { - case UNNotificationDefaultActionIdentifier, Ghostty.userNotificationActionShow: - // The user clicked on a notification - surface.handleUserNotification(notification: response.notification, focus: true) - case UNNotificationDismissActionIdentifier: - // The user dismissed the notification - surface.handleUserNotification(notification: response.notification, focus: false) - default: - break - } - } - - /// Determine if a given notification should be presented to the user when Ghostty is running in the foreground. - func shouldPresentNotification(notification: UNNotification) -> Bool { - let userInfo = notification.request.content.userInfo - guard let uuidString = userInfo["surface"] as? String, - let uuid = UUID(uuidString: uuidString), - let surface = delegate?.findSurface(forUUID: uuid), - let window = surface.window else { return false } - return !window.isKeyWindow || !surface.focused - } - - static func newTab(_ userdata: UnsafeMutableRawPointer?, config: ghostty_surface_config_s) { - let surface = self.surfaceUserdata(from: userdata) - - guard let appState = self.appState(fromView: surface) else { return } - guard appState.config.windowDecorations else { - let alert = NSAlert() - alert.messageText = "Tabs are disabled" - alert.informativeText = "Enable window decorations to use tabs" - alert.addButton(withTitle: "OK") - alert.alertStyle = .warning - _ = alert.runModal() - return - } - - NotificationCenter.default.post( - name: Notification.ghosttyNewTab, - object: surface, - userInfo: [ - Notification.NewSurfaceConfigKey: SurfaceConfiguration(from: config), - ] - ) - } - - static func newWindow(_ userdata: UnsafeMutableRawPointer?, config: ghostty_surface_config_s) { - let surface = self.surfaceUserdata(from: userdata) - - NotificationCenter.default.post( - name: Notification.ghosttyNewWindow, - object: surface, - userInfo: [ - Notification.NewSurfaceConfigKey: SurfaceConfiguration(from: config), - ] - ) - } - - static func controlInspector(_ userdata: UnsafeMutableRawPointer?, mode: ghostty_inspector_mode_e) { - let surface = self.surfaceUserdata(from: userdata) - NotificationCenter.default.post(name: Notification.didControlInspector, object: surface, userInfo: [ - "mode": mode, - ]) - } - - /// Returns the GhosttyState from the given userdata value. - static private func appState(fromView view: SurfaceView) -> AppState? { - guard let surface = view.surface else { return nil } - guard let app = ghostty_surface_app(surface) else { return nil } - guard let app_ud = ghostty_app_userdata(app) else { return nil } - return Unmanaged.fromOpaque(app_ud).takeUnretainedValue() - } - - /// Returns the surface view from the userdata. - static private func surfaceUserdata(from userdata: UnsafeMutableRawPointer?) -> SurfaceView { - return Unmanaged.fromOpaque(userdata!).takeUnretainedValue() - } - } -} diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index add6fadb1..3afbc0870 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -1,6 +1,18 @@ import SwiftUI +import UserNotifications import GhosttyKit +protocol GhosttyAppDelegate: AnyObject { + /// Called when the configuration did finish reloading. + func configDidReload(_ app: Ghostty.App) + + #if os(macOS) + /// Called when a callback needs access to a specific surface. This should return nil + /// when the surface is no longer valid. + func findSurface(forUUID uuid: UUID) -> Ghostty.SurfaceView? + #endif +} + extension Ghostty { // IMPORTANT: THIS IS NOT DONE. // This is a refactor/redo of Ghostty.AppState so that it supports both macOS and iOS @@ -9,6 +21,9 @@ extension Ghostty { case loading, error, ready } + /// Optional delegate + weak var delegate: GhosttyAppDelegate? + /// The readiness value of the state. @Published var readiness: Readiness = .loading @@ -26,6 +41,12 @@ extension Ghostty { } } + /// True if we need to confirm before quitting. + var needsConfirmQuit: Bool { + guard let app = app else { return false } + return ghostty_app_needs_confirm_quit(app) + } + init() { // Initialize ghostty global state. This happens once per process. if ghostty_init() != GHOSTTY_SUCCESS { @@ -108,7 +129,119 @@ extension Ghostty { #endif } - // MARK: Ghostty Callbacks + // MARK: App Operations + + func appTick() { + guard let app = self.app else { return } + + // Tick our app, which lets us know if we want to quit + let exit = ghostty_app_tick(app) + if (!exit) { return } + + // On iOS, applications do not terminate programmatically like they do + // on macOS. On iOS, applications are only terminated when a user physically + // closes the application (i.e. going to the home screen). If we request + // exit on iOS we ignore it. + #if os(iOS) + logger.info("quit request received, ignoring on iOS") + #endif + + #if os(macOS) + // We want to quit, start that process + NSApplication.shared.terminate(nil) + #endif + } + + func openConfig() { + guard let app = self.app else { return } + ghostty_app_open_config(app) + } + + func reloadConfig() { + guard let app = self.app else { return } + ghostty_app_reload_config(app) + } + + /// Request that the given surface is closed. This will trigger the full normal surface close event + /// cycle which will call our close surface callback. + func requestClose(surface: ghostty_surface_t) { + ghostty_surface_request_close(surface) + } + + func newTab(surface: ghostty_surface_t) { + let action = "new_tab" + if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) { + logger.warning("action failed action=\(action)") + } + } + + func newWindow(surface: ghostty_surface_t) { + let action = "new_window" + if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) { + logger.warning("action failed action=\(action)") + } + } + + func split(surface: ghostty_surface_t, direction: ghostty_split_direction_e) { + ghostty_surface_split(surface, direction) + } + + func splitMoveFocus(surface: ghostty_surface_t, direction: SplitFocusDirection) { + ghostty_surface_split_focus(surface, direction.toNative()) + } + + func splitResize(surface: ghostty_surface_t, direction: SplitResizeDirection, amount: UInt16) { + ghostty_surface_split_resize(surface, direction.toNative(), amount) + } + + func splitEqualize(surface: ghostty_surface_t) { + ghostty_surface_split_equalize(surface) + } + + func splitToggleZoom(surface: ghostty_surface_t) { + let action = "toggle_split_zoom" + if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) { + logger.warning("action failed action=\(action)") + } + } + + func toggleFullscreen(surface: ghostty_surface_t) { + let action = "toggle_fullscreen" + if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) { + logger.warning("action failed action=\(action)") + } + } + + enum FontSizeModification { + case increase(Int) + case decrease(Int) + case reset + } + + func changeFontSize(surface: ghostty_surface_t, _ change: FontSizeModification) { + let action: String + switch change { + case .increase(let amount): + action = "increase_font_size:\(amount)" + case .decrease(let amount): + action = "decrease_font_size:\(amount)" + case .reset: + action = "reset_font_size" + } + if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) { + logger.warning("action failed action=\(action)") + } + } + + func toggleTerminalInspector(surface: ghostty_surface_t) { + let action = "inspector:toggle" + if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) { + logger.warning("action failed action=\(action)") + } + } + + #if os(iOS) + // MARK: Ghostty Callbacks (iOS) static func wakeup(_ userdata: UnsafeMutableRawPointer?) {} static func reloadConfig(_ userdata: UnsafeMutableRawPointer?) -> ghostty_config_t? { return nil } @@ -156,5 +289,342 @@ extension Ghostty { static func renderInspector(_ userdata: UnsafeMutableRawPointer?) {} static func setCellSize(_ userdata: UnsafeMutableRawPointer?, width: UInt32, height: UInt32) {} static func showUserNotification(_ userdata: UnsafeMutableRawPointer?, title: UnsafePointer?, body: UnsafePointer?) {} + #endif + + #if os(macOS) + + // MARK: Notifications + + // Called when the selected keyboard changes. We have to notify Ghostty so that + // it can reload the keyboard mapping for input. + @objc private func keyboardSelectionDidChange(notification: NSNotification) { + guard let app = self.app else { return } + ghostty_app_keyboard_changed(app) + } + + // MARK: Ghostty Callbacks (macOS) + + static func newSplit(_ userdata: UnsafeMutableRawPointer?, direction: ghostty_split_direction_e, config: ghostty_surface_config_s) { + let surface = self.surfaceUserdata(from: userdata) + NotificationCenter.default.post(name: Notification.ghosttyNewSplit, object: surface, userInfo: [ + "direction": direction, + Notification.NewSurfaceConfigKey: SurfaceConfiguration(from: config), + ]) + } + + static func closeSurface(_ userdata: UnsafeMutableRawPointer?, processAlive: Bool) { + let surface = self.surfaceUserdata(from: userdata) + NotificationCenter.default.post(name: Notification.ghosttyCloseSurface, object: surface, userInfo: [ + "process_alive": processAlive, + ]) + } + + static func focusSplit(_ userdata: UnsafeMutableRawPointer?, direction: ghostty_split_focus_direction_e) { + let surface = self.surfaceUserdata(from: userdata) + guard let splitDirection = SplitFocusDirection.from(direction: direction) else { return } + NotificationCenter.default.post( + name: Notification.ghosttyFocusSplit, + object: surface, + userInfo: [ + Notification.SplitDirectionKey: splitDirection, + ] + ) + } + + static func resizeSplit(_ userdata: UnsafeMutableRawPointer?, direction: ghostty_split_resize_direction_e, amount: UInt16) { + let surface = self.surfaceUserdata(from: userdata) + guard let resizeDirection = SplitResizeDirection.from(direction: direction) else { return } + NotificationCenter.default.post( + name: Notification.didResizeSplit, + object: surface, + userInfo: [ + Notification.ResizeSplitDirectionKey: resizeDirection, + Notification.ResizeSplitAmountKey: amount, + ] + ) + } + + static func equalizeSplits(_ userdata: UnsafeMutableRawPointer?) { + let surface = self.surfaceUserdata(from: userdata) + NotificationCenter.default.post(name: Notification.didEqualizeSplits, object: surface) + } + + static func toggleSplitZoom(_ userdata: UnsafeMutableRawPointer?) { + let surface = self.surfaceUserdata(from: userdata) + + NotificationCenter.default.post( + name: Notification.didToggleSplitZoom, + object: surface + ) + } + + static func gotoTab(_ userdata: UnsafeMutableRawPointer?, n: Int32) { + let surface = self.surfaceUserdata(from: userdata) + NotificationCenter.default.post( + name: Notification.ghosttyGotoTab, + object: surface, + userInfo: [ + Notification.GotoTabKey: n, + ] + ) + } + + static func readClipboard(_ userdata: UnsafeMutableRawPointer?, location: ghostty_clipboard_e, state: UnsafeMutableRawPointer?) { + // If we don't even have a surface, something went terrible wrong so we have + // to leak "state". + let surfaceView = self.surfaceUserdata(from: userdata) + guard let surface = surfaceView.surface else { return } + + // We only support the standard clipboard + if (location != GHOSTTY_CLIPBOARD_STANDARD) { + return completeClipboardRequest(surface, data: "", state: state) + } + + // Get our string + let str = NSPasteboard.general.string(forType: .string) ?? "" + completeClipboardRequest(surface, data: str, state: state) + } + + static func confirmReadClipboard( + _ userdata: UnsafeMutableRawPointer?, + string: UnsafePointer?, + state: UnsafeMutableRawPointer?, + request: ghostty_clipboard_request_e + ) { + let surface = self.surfaceUserdata(from: userdata) + guard let valueStr = String(cString: string!, encoding: .utf8) else { return } + guard let request = Ghostty.ClipboardRequest.from(request: request) else { return } + NotificationCenter.default.post( + name: Notification.confirmClipboard, + object: surface, + userInfo: [ + Notification.ConfirmClipboardStrKey: valueStr, + Notification.ConfirmClipboardStateKey: state as Any, + Notification.ConfirmClipboardRequestKey: request, + ] + ) + } + + static func completeClipboardRequest( + _ surface: ghostty_surface_t, + data: String, + state: UnsafeMutableRawPointer?, + confirmed: Bool = false + ) { + data.withCString { ptr in + ghostty_surface_complete_clipboard_request(surface, ptr, state, confirmed) + } + } + + static func writeClipboard(_ userdata: UnsafeMutableRawPointer?, string: UnsafePointer?, location: ghostty_clipboard_e, confirm: Bool) { + let surface = self.surfaceUserdata(from: userdata) + + // We only support the standard clipboard + if (location != GHOSTTY_CLIPBOARD_STANDARD) { return } + + guard let valueStr = String(cString: string!, encoding: .utf8) else { return } + if !confirm { + let pb = NSPasteboard.general + pb.declareTypes([.string], owner: nil) + pb.setString(valueStr, forType: .string) + return + } + + NotificationCenter.default.post( + name: Notification.confirmClipboard, + object: surface, + userInfo: [ + Notification.ConfirmClipboardStrKey: valueStr, + Notification.ConfirmClipboardRequestKey: Ghostty.ClipboardRequest.osc_52_write, + ] + ) + } + + static func openConfig(_ userdata: UnsafeMutableRawPointer?) { + ghostty_config_open(); + } + + static func reloadConfig(_ userdata: UnsafeMutableRawPointer?) -> ghostty_config_t? { + let newConfig = Config() + guard newConfig.loaded else { + AppDelegate.logger.warning("failed to reload configuration") + return nil + } + + // Assign the new config. This will automatically free the old config. + // It is safe to free the old config from within this function call. + let state = Unmanaged.fromOpaque(userdata!).takeUnretainedValue() + state.config = newConfig + + // If we have a delegate, notify. + if let delegate = state.delegate { + delegate.configDidReload(state) + } + + return newConfig.config + } + + static func wakeup(_ userdata: UnsafeMutableRawPointer?) { + let state = Unmanaged.fromOpaque(userdata!).takeUnretainedValue() + + // Wakeup can be called from any thread so we schedule the app tick + // from the main thread. There is probably some improvements we can make + // to coalesce multiple ticks but I don't think it matters from a performance + // standpoint since we don't do this much. + DispatchQueue.main.async { state.appTick() } + } + + static func renderInspector(_ userdata: UnsafeMutableRawPointer?) { + let surface = self.surfaceUserdata(from: userdata) + NotificationCenter.default.post( + name: Notification.inspectorNeedsDisplay, + object: surface + ) + } + + static func setTitle(_ userdata: UnsafeMutableRawPointer?, title: UnsafePointer?) { + let surfaceView = self.surfaceUserdata(from: userdata) + guard let titleStr = String(cString: title!, encoding: .utf8) else { return } + DispatchQueue.main.async { + surfaceView.title = titleStr + } + } + + static func setMouseShape(_ userdata: UnsafeMutableRawPointer?, shape: ghostty_mouse_shape_e) { + let surfaceView = self.surfaceUserdata(from: userdata) + surfaceView.setCursorShape(shape) + } + + static func setMouseVisibility(_ userdata: UnsafeMutableRawPointer?, visible: Bool) { + let surfaceView = self.surfaceUserdata(from: userdata) + surfaceView.setCursorVisibility(visible) + } + + static func toggleFullscreen(_ userdata: UnsafeMutableRawPointer?, nonNativeFullscreen: ghostty_non_native_fullscreen_e) { + let surface = self.surfaceUserdata(from: userdata) + NotificationCenter.default.post( + name: Notification.ghosttyToggleFullscreen, + object: surface, + userInfo: [ + Notification.NonNativeFullscreenKey: nonNativeFullscreen, + ] + ) + } + + static func setInitialWindowSize(_ userdata: UnsafeMutableRawPointer?, width: UInt32, height: UInt32) { + // We need a window to set the frame + let surfaceView = self.surfaceUserdata(from: userdata) + surfaceView.initialSize = NSMakeSize(Double(width), Double(height)) + } + + static func setCellSize(_ userdata: UnsafeMutableRawPointer?, width: UInt32, height: UInt32) { + let surfaceView = self.surfaceUserdata(from: userdata) + let backingSize = NSSize(width: Double(width), height: Double(height)) + surfaceView.cellSize = surfaceView.convertFromBacking(backingSize) + } + + static func showUserNotification(_ userdata: UnsafeMutableRawPointer?, title: UnsafePointer?, body: UnsafePointer?) { + let surfaceView = self.surfaceUserdata(from: userdata) + guard let title = String(cString: title!, encoding: .utf8) else { return } + guard let body = String(cString: body!, encoding: .utf8) else { return } + + let center = UNUserNotificationCenter.current() + center.requestAuthorization(options: [.alert, .sound]) { _, error in + if let error = error { + AppDelegate.logger.error("Error while requesting notification authorization: \(error)") + } + } + + center.getNotificationSettings() { settings in + guard settings.authorizationStatus == .authorized else { return } + surfaceView.showUserNotification(title: title, body: body) + } + } + + /// Handle a received user notification. This is called when a user notification is clicked or dismissed by the user + func handleUserNotification(response: UNNotificationResponse) { + let userInfo = response.notification.request.content.userInfo + guard let uuidString = userInfo["surface"] as? String, + let uuid = UUID(uuidString: uuidString), + let surface = delegate?.findSurface(forUUID: uuid) else { return } + + switch (response.actionIdentifier) { + case UNNotificationDefaultActionIdentifier, Ghostty.userNotificationActionShow: + // The user clicked on a notification + surface.handleUserNotification(notification: response.notification, focus: true) + case UNNotificationDismissActionIdentifier: + // The user dismissed the notification + surface.handleUserNotification(notification: response.notification, focus: false) + default: + break + } + } + + /// Determine if a given notification should be presented to the user when Ghostty is running in the foreground. + func shouldPresentNotification(notification: UNNotification) -> Bool { + let userInfo = notification.request.content.userInfo + guard let uuidString = userInfo["surface"] as? String, + let uuid = UUID(uuidString: uuidString), + let surface = delegate?.findSurface(forUUID: uuid), + let window = surface.window else { return false } + return !window.isKeyWindow || !surface.focused + } + + static func newTab(_ userdata: UnsafeMutableRawPointer?, config: ghostty_surface_config_s) { + let surface = self.surfaceUserdata(from: userdata) + + guard let appState = self.appState(fromView: surface) else { return } + guard appState.config.windowDecorations else { + let alert = NSAlert() + alert.messageText = "Tabs are disabled" + alert.informativeText = "Enable window decorations to use tabs" + alert.addButton(withTitle: "OK") + alert.alertStyle = .warning + _ = alert.runModal() + return + } + + NotificationCenter.default.post( + name: Notification.ghosttyNewTab, + object: surface, + userInfo: [ + Notification.NewSurfaceConfigKey: SurfaceConfiguration(from: config), + ] + ) + } + + static func newWindow(_ userdata: UnsafeMutableRawPointer?, config: ghostty_surface_config_s) { + let surface = self.surfaceUserdata(from: userdata) + + NotificationCenter.default.post( + name: Notification.ghosttyNewWindow, + object: surface, + userInfo: [ + Notification.NewSurfaceConfigKey: SurfaceConfiguration(from: config), + ] + ) + } + + static func controlInspector(_ userdata: UnsafeMutableRawPointer?, mode: ghostty_inspector_mode_e) { + let surface = self.surfaceUserdata(from: userdata) + NotificationCenter.default.post(name: Notification.didControlInspector, object: surface, userInfo: [ + "mode": mode, + ]) + } + + /// Returns the GhosttyState from the given userdata value. + static private func appState(fromView view: SurfaceView) -> App? { + guard let surface = view.surface else { return nil } + guard let app = ghostty_surface_app(surface) else { return nil } + guard let app_ud = ghostty_app_userdata(app) else { return nil } + return Unmanaged.fromOpaque(app_ud).takeUnretainedValue() + } + + /// Returns the surface view from the userdata. + static private func surfaceUserdata(from userdata: UnsafeMutableRawPointer?) -> SurfaceView { + return Unmanaged.fromOpaque(userdata!).takeUnretainedValue() + } + + #endif } } diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index c5b0269c6..9f8fe5237 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -19,6 +19,26 @@ struct Ghostty { static let userNotificationActionShow = "com.mitchellh.ghostty.userNotification.Show" } +// MARK: Build Info + +extension Ghostty { + struct Info { + var mode: ghostty_build_mode_e + var version: String + } + + static var info: Info { + let raw = ghostty_info() + let version = NSString( + bytes: raw.version, + length: Int(raw.version_len), + encoding: NSUTF8StringEncoding + ) ?? "unknown" + + return Info(mode: raw.build_mode, version: String(version)) + } +} + // MARK: Surface Notifications extension Ghostty { diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index f9bc0f027..0fb4c212d 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -5,7 +5,7 @@ import GhosttyKit extension Ghostty { /// Render a terminal for the active app in the environment. struct Terminal: View { - @EnvironmentObject private var ghostty: Ghostty.AppState + @EnvironmentObject private var ghostty: Ghostty.App @FocusedValue(\.ghosttySurfaceTitle) private var surfaceTitle: String? var body: some View { @@ -49,7 +49,7 @@ extension Ghostty { // Maintain whether our window has focus (is key) or not @State private var windowFocus: Bool = true - @EnvironmentObject private var ghostty: Ghostty.AppState + @EnvironmentObject private var ghostty: Ghostty.App // This is true if the terminal is considered "focused". The terminal is focused if // it is both individually focused and the containing window is key. -- 2.51.2 From 635e6808f66588c5ee063cb47058e6379a315733 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 14 Jan 2024 19:39:27 -0800 Subject: [PATCH 18/19] build: fix mistaken dependency for iOS simulator lib --- build.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.zig b/build.zig index eb7dac7c8..222b8ed37 100644 --- a/build.zig +++ b/build.zig @@ -462,7 +462,7 @@ pub fn build(b: *std.Build) !void { // Add our library to zig-out const ios_sim_lib_install = b.addInstallLibFile( - ios_lib_path, + ios_sim_lib_path, "libghostty-ios-simulator.a", ); b.getInstallStep().dependOn(&ios_sim_lib_install.step); -- 2.51.2 From 326a817bf05527a122f9d8a69d3cb360acb7e674 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 14 Jan 2024 19:48:41 -0800 Subject: [PATCH 19/19] ci: ios build does not use code signing --- .github/workflows/test.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8883389da..994f1a57b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -90,14 +90,11 @@ jobs: - name: Build Ghostty.app run: cd macos && xcodebuild -target Ghostty - # Build the iOS target. This requires a team ID and we can reuse our - # release team ID. This doesn't upload anything so that's okay. + # Build the iOS target without code signing just to verify it works. - name: Build Ghostty iOS - env: - PROD_MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }} run: | cd macos - xcodebuild -target Ghostty-iOS "DEVELOPMENT_TEAM=$PROD_MACOS_NOTARIZATION_TEAM_ID" + xcodebuild -target Ghostty-iOS "CODE_SIGNING_ALLOWED=NO" build-windows: runs-on: windows-2019