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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 z 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�>8qkW%3UEp|x)o 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 literal 0 HcmV?d00001 diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 2e01d611f..86bc65636 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -20,6 +20,9 @@ A5278A9B2AA05B2600CD3039 /* Ghostty.Input.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5278A9A2AA05B2600CD3039 /* Ghostty.Input.swift */; }; A53426352A7DA53D00EBB7A2 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53426342A7DA53D00EBB7A2 /* AppDelegate.swift */; }; A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A535B9D9299C569B0017E2E4 /* ErrorView.swift */; }; + A53D0C8E2B53B0EA00305CE6 /* GhosttyKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */; }; + A53D0C942B53B43700305CE6 /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53D0C932B53B43700305CE6 /* iOSApp.swift */; }; + A53D0C952B53B4D800305CE6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; }; A55685E029A03A9F004303CE /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55685DF29A03A9F004303CE /* AppError.swift */; }; A55B7BB629B6F47F0055DE60 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BB529B6F47F0055DE60 /* AppState.swift */; }; A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BB729B6F53A0055DE60 /* Package.swift */; }; @@ -69,6 +72,7 @@ A5278A9A2AA05B2600CD3039 /* Ghostty.Input.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Input.swift; sourceTree = ""; }; A53426342A7DA53D00EBB7A2 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; A535B9D9299C569B0017E2E4 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = ""; }; + A53D0C932B53B43700305CE6 /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = ""; }; A55685DF29A03A9F004303CE /* AppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = ""; }; A55B7BB529B6F47F0055DE60 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = ""; }; A55B7BB729B6F53A0055DE60 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; @@ -99,6 +103,7 @@ A5CEAFFE29C2410700646FDA /* Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backport.swift; sourceTree = ""; }; A5D0AF3A2B36A1DE00D21823 /* TerminalRestorable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalRestorable.swift; sourceTree = ""; }; A5D0AF3C2B37804400D21823 /* CodableBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodableBridge.swift; sourceTree = ""; }; + A5D4499D2B53AE7B000F5B83 /* Ghostty-iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Ghostty-iOS.app"; sourceTree = BUILT_PRODUCTS_DIR; }; A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = GhosttyKit.xcframework; sourceTree = ""; }; A5E112922AF73E6E00C6E0C2 /* ClipboardConfirmation.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ClipboardConfirmation.xib; sourceTree = ""; }; A5E112942AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardConfirmationController.swift; sourceTree = ""; }; @@ -117,6 +122,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + A5D4499A2B53AE7B000F5B83 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + A53D0C8E2B53B0EA00305CE6 /* GhosttyKit.xcframework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -175,12 +188,37 @@ path = Settings; sourceTree = ""; }; - A54CD6ED299BEB14008C95BB /* Sources */ = { + A53D0C912B53B41900305CE6 /* App */ = { + isa = PBXGroup; + children = ( + A53D0C962B53B57D00305CE6 /* macOS */, + A53D0C922B53B42000305CE6 /* iOS */, + ); + path = App; + sourceTree = ""; + }; + A53D0C922B53B42000305CE6 /* iOS */ = { + isa = PBXGroup; + children = ( + A53D0C932B53B43700305CE6 /* iOSApp.swift */, + ); + path = iOS; + sourceTree = ""; + }; + A53D0C962B53B57D00305CE6 /* macOS */ = { isa = PBXGroup; children = ( A5FEB2FF2ABB69450068369E /* main.swift */, A53426342A7DA53D00EBB7A2 /* AppDelegate.swift */, 857F63802A5E64F200CA4815 /* MainMenu.xib */, + ); + path = macOS; + sourceTree = ""; + }; + A54CD6ED299BEB14008C95BB /* Sources */ = { + isa = PBXGroup; + children = ( + A53D0C912B53B41900305CE6 /* App */, A53426362A7DC53000EBB7A2 /* Features */, A534263D2A7DCBB000EBB7A2 /* Helpers */, A55B7BB429B6F4410055DE60 /* Ghostty */, @@ -255,6 +293,7 @@ isa = PBXGroup; children = ( A5B30531299BEAAA0047F10C /* Ghostty.app */, + A5D4499D2B53AE7B000F5B83 /* Ghostty-iOS.app */, ); name = Products; sourceTree = ""; @@ -310,6 +349,23 @@ productReference = A5B30531299BEAAA0047F10C /* Ghostty.app */; productType = "com.apple.product-type.application"; }; + A5D4499C2B53AE7B000F5B83 /* Ghostty-iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = A5D449AB2B53AE7B000F5B83 /* Build configuration list for PBXNativeTarget "Ghostty-iOS" */; + buildPhases = ( + A5D449992B53AE7B000F5B83 /* Sources */, + A5D4499A2B53AE7B000F5B83 /* Frameworks */, + A5D4499B2B53AE7B000F5B83 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "Ghostty-iOS"; + productName = "Ghostty-iOS"; + productReference = A5D4499D2B53AE7B000F5B83 /* Ghostty-iOS.app */; + productType = "com.apple.product-type.application"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -317,12 +373,15 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1420; + LastSwiftUpdateCheck = 1520; LastUpgradeCheck = 1420; TargetAttributes = { A5B30530299BEAAA0047F10C = { CreatedOnToolsVersion = 14.2; }; + A5D4499C2B53AE7B000F5B83 = { + CreatedOnToolsVersion = 15.2; + }; }; }; buildConfigurationList = A5B3052C299BEAAA0047F10C /* Build configuration list for PBXProject "Ghostty" */; @@ -342,6 +401,7 @@ projectRoot = ""; targets = ( A5B30530299BEAAA0047F10C /* Ghostty */, + A5D4499C2B53AE7B000F5B83 /* Ghostty-iOS */, ); }; /* End PBXProject section */ @@ -363,6 +423,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + A5D4499B2B53AE7B000F5B83 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A53D0C952B53B4D800305CE6 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -406,6 +474,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + A5D449992B53AE7B000F5B83 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A53D0C942B53B43700305CE6 /* iOSApp.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin XCBuildConfiguration section */ @@ -682,6 +758,117 @@ }; name = Release; }; + A5D449A82B53AE7B000F5B83 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = ""; + ENABLE_PREVIEWS = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = Ghostty; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 0.1; + PRODUCT_BUNDLE_IDENTIFIER = "com.mitchellh.ghostty-ios"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + A5D449A92B53AE7B000F5B83 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = ""; + ENABLE_PREVIEWS = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = Ghostty; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 0.1; + PRODUCT_BUNDLE_IDENTIFIER = "com.mitchellh.ghostty-ios"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + A5D449AA2B53AE7B000F5B83 /* ReleaseLocal */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = ""; + ENABLE_PREVIEWS = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = Ghostty; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 0.1; + PRODUCT_BUNDLE_IDENTIFIER = "com.mitchellh.ghostty-ios"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = ReleaseLocal; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -705,6 +892,16 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = ReleaseLocal; }; + A5D449AB2B53AE7B000F5B83 /* Build configuration list for PBXNativeTarget "Ghostty-iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A5D449A82B53AE7B000F5B83 /* Debug */, + A5D449A92B53AE7B000F5B83 /* Release */, + A5D449AA2B53AE7B000F5B83 /* ReleaseLocal */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseLocal; + }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ diff --git a/macos/Sources/App/iOS/iOSApp.swift b/macos/Sources/App/iOS/iOSApp.swift new file mode 100644 index 000000000..da5a9fe5b --- /dev/null +++ b/macos/Sources/App/iOS/iOSApp.swift @@ -0,0 +1,26 @@ +import SwiftUI + +@main +struct Ghostty_iOSApp: App { + var body: some Scene { + WindowGroup { + iOS_ContentView() + } + } +} + +struct iOS_ContentView: View { + var body: some View { + VStack { + Image(systemName: "globe") + .imageScale(.large) + .foregroundStyle(.tint) + Text("Hello, world!") + } + .padding() + } +} + +#Preview { + iOS_ContentView() +} diff --git a/macos/Sources/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift similarity index 100% rename from macos/Sources/AppDelegate.swift rename to macos/Sources/App/macOS/AppDelegate.swift diff --git a/macos/Sources/MainMenu.xib b/macos/Sources/App/macOS/MainMenu.xib similarity index 100% rename from macos/Sources/MainMenu.xib rename to macos/Sources/App/macOS/MainMenu.xib diff --git a/macos/Sources/main.swift b/macos/Sources/App/macOS/main.swift similarity index 100% rename from macos/Sources/main.swift rename to macos/Sources/App/macOS/main.swift 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