From 3360a008cd137b428631fc8052f64d672a660240 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 13 Jan 2024 20:21:49 -0800 Subject: [PATCH 01/19] build: build produces a broken object file for iOS This gets `zig build -Dtarget=aarch64-ios` working. By "working" I mean it produces an object file without compiler errors. However, the object file certainly isn't useful since it uses a number of features that will not work in the iOS sandbox. This is just an experiment more than anything to see how hard it would be to get libghostty working within iOS to render a terminal. Note iOS doesn't support ptys so this wouldn't be a true on-device terminal. The challenge right now is to just get a terminal rendering (not usable). --- build.zig | 101 +++++++++++++++++++++++++++++++--- build.zig.zon | 4 +- pkg/apple-sdk/build.zig | 1 + pkg/cimgui/build.zig | 10 ++-- pkg/freetype/build.zig | 7 ++- pkg/freetype/build.zig.zon | 2 + pkg/glslang/build.zig | 13 ++++- pkg/glslang/build.zig.zon | 2 + pkg/harfbuzz/build.zig | 27 +++++---- pkg/harfbuzz/build.zig.zon | 1 + pkg/libpng/build.zig | 4 ++ pkg/libpng/build.zig.zon | 6 +- pkg/macos/build.zig | 6 +- pkg/oniguruma/build.zig | 7 ++- pkg/oniguruma/build.zig.zon | 2 + pkg/pixman/build.zig | 6 +- pkg/pixman/build.zig.zon | 3 + pkg/spirv-cross/build.zig | 8 ++- pkg/spirv-cross/build.zig.zon | 2 + pkg/zlib/build.zig | 5 ++ pkg/zlib/build.zig.zon | 3 + src/Command.zig | 2 +- src/input.zig | 2 +- src/input/keycodes.zig | 2 +- src/inspector/main.zig | 1 + src/os/desktop.zig | 3 + src/os/homedir.zig | 4 ++ src/os/open.zig | 1 + src/pty.zig | 39 +++++++++++-- 29 files changed, 228 insertions(+), 46 deletions(-) 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 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; 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=" 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; 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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

73dM%A=M1XGgy^ABWa!wV51m)IXd) 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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 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() } 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 }; +} 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 {} 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, 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, + }; } 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 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 */, 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) } 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; 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 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. 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); 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