diff --git a/.github/workflows/release-tip.yml b/.github/workflows/release-tip.yml index 27bbb5d1b..6786f436b 100644 --- a/.github/workflows/release-tip.yml +++ b/.github/workflows/release-tip.yml @@ -11,7 +11,7 @@ name: Release Tip jobs: build-macos: if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }} - runs-on: ubuntu-latest + runs-on: macos-12 env: # Needed for macos SDK AGREE: "true" @@ -22,45 +22,24 @@ jobs: submodules: recursive fetch-depth: 0 - # Install Nix and use that so our environment matches exactly. + # Install Nix and use that to run our tests so our environment matches exactly. - uses: cachix/install-nix-action@v19 with: nix_path: nixpkgs=channel:nixos-unstable - # Cross-compile the binary. We always use static building for this - # because its the only way to access the headers. - - name: Build aarch64 - run: | - nix develop -c zig build -Dcpu=baseline -Dstatic=true -Dtarget=aarch64-macos -Doptimize=ReleaseFast - mv zig-out/bin/ghostty zig-out/bin/ghostty-aarch64-macos - - name: Build x86_64 - run: | - nix develop -c zig build -Dcpu=baseline -Dstatic=true -Dtarget=x86_64-macos -Doptimize=ReleaseFast - mv zig-out/bin/ghostty zig-out/bin/ghostty-x86_64-macos + # GhosttyKit is the framework that is built from Zig for our native + # Mac app to access. Build this in release mode. + - name: Build GhosttyKit + run: nix develop -c zig build -Dstatic=true -Doptimize=ReleaseFast - - name: Create Universal Binary - run: | - # Lipo our binaries - nix develop -c \ - llvm-lipo \ - zig-out/bin/ghostty-aarch64-macos \ - zig-out/bin/ghostty-x86_64-macos \ - -create \ - -output zig-out/bin/ghostty-universal - - # Ensure the app is universal - cp zig-out/bin/ghostty-universal zig-out/Ghostty.app/Contents/MacOS/ghostty - - # Upload the App bundle so we can sign it later on macOS - - name: Store App Bundle Artifact - uses: actions/upload-artifact@v3 - with: - name: app-bundle - path: zig-out/ - retention-days: 5 + # The native app is built with native XCode tooling. This also does + # 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 - name: Zip Unsigned App - run: nix develop -c sh -c 'cd zig-out && zip -9 -r ../ghostty-macos-universal-unsigned.zip Ghostty.app' + run: nix develop -c sh -c 'cd macos/build/Release && zip -9 -r ../../../ghostty-macos-universal-unsigned.zip Ghostty.app' # Update Release - name: Release Unsigned @@ -80,25 +59,6 @@ jobs: message: "Latest Continuous Release" force_push_tag: true - sign-and-release: - runs-on: macos-12 - needs: build-macos - steps: - - name: Checkout code - uses: actions/checkout@v3 - with: - submodules: recursive - fetch-depth: 0 - - - uses: actions/download-artifact@v3 - with: - name: app-bundle - path: zig-out - - - name: Fix Permissions - run: | - chmod +x zig-out/Ghostty.app/Contents/MacOS/ghostty - - name: Codesign app bundle env: MACOS_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }} @@ -119,7 +79,7 @@ jobs: security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CI_KEYCHAIN_PWD" build.keychain # We finally codesign our app bundle, specifying the Hardened runtime option - /usr/bin/codesign --force -s "$MACOS_CERTIFICATE_NAME" --options runtime zig-out/Ghostty.app -v + /usr/bin/codesign --force -s "$MACOS_CERTIFICATE_NAME" --options runtime build/Release/Ghostty.app -v - name: "Notarize app bundle" env: @@ -136,7 +96,7 @@ jobs: # Therefore, we create a zip file containing our app bundle, so that we can send it to the # notarization service echo "Creating temp notarization archive" - ditto -c -k --keepParent "zig-out/Ghostty.app" "notarization.zip" + ditto -c -k --keepParent "build/Release/Ghostty.app" "notarization.zip" # Here we send the notarization request to the Apple's Notarization service, waiting for the result. # This typically takes a few seconds inside a CI environment, but it might take more depending on the App @@ -148,11 +108,11 @@ jobs: # Finally, we need to "attach the staple" to our executable, which will allow our app to be # validated by macOS even when an internet connection is not available. echo "Attach staple" - xcrun stapler staple "zig-out/Ghostty.app" + xcrun stapler staple "build/Release/Ghostty.app" # Zip up the app - name: Zip App - run: cd zig-out && zip -9 -r ../ghostty-macos-universal.zip Ghostty.app + run: cd build/Release && zip -9 -r ../../../ghostty-macos-universal.zip Ghostty.app # Update Release - name: Release diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7de0030c7..440ee8313 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,6 +40,35 @@ jobs: - name: Test Build run: nix develop -c zig build -Dstatic=true -Dtarget=${{ matrix.target }} + build-macos: + runs-on: macos-12 + needs: test + env: + # Needed for macos SDK + AGREE: "true" + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + submodules: recursive + fetch-depth: 0 + + # Install Nix and use that to run our tests so our environment matches exactly. + - uses: cachix/install-nix-action@v19 + with: + nix_path: nixpkgs=channel:nixos-unstable + + # GhosttyKit is the framework that is built from Zig for our native + # Mac app to access. + - name: Build GhosttyKit + run: nix develop -c zig build -Dstatic=true + + # The native app is built with native XCode tooling. This also does + # 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 + test: strategy: matrix: diff --git a/.gitignore b/.gitignore index 83d1031dc..2fef0f117 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.DS_Store .direnv/ zig-cache/ zig-out/ diff --git a/build.zig b/build.zig index 2c0fb4bc2..a7f42383e 100644 --- a/build.zig +++ b/build.zig @@ -21,6 +21,9 @@ const zlib = @import("pkg/zlib/build.zig"); const tracylib = @import("pkg/tracy/build.zig"); const system_sdk = @import("vendor/mach/libs/glfw/system_sdk.zig"); const WasmTarget = @import("src/os/wasm/target.zig").Target; +const LibtoolStep = @import("src/build/LibtoolStep.zig"); +const LipoStep = @import("src/build/LipoStep.zig"); +const XCFrameworkStep = @import("src/build/XCFrameworkStep.zig"); // Do a comptime Zig version requirement. The required Zig version is // somewhat arbitrary: it is meant to be a version that we feel works well, @@ -120,7 +123,7 @@ pub fn build(b: *std.build.Builder) !void { exe.install(); // Add the shared dependencies - try addDeps(b, exe, static); + _ = try addDeps(b, exe, static); } // App (Mac) @@ -131,6 +134,85 @@ pub fn build(b: *std.build.Builder) !void { b.installFile("dist/macos/Ghostty.icns", "Ghostty.app/Contents/Resources/Ghostty.icns"); } + // On Mac we can build the app. + if (builtin.target.isDarwin()) { + const static_lib_aarch64 = lib: { + const lib = b.addStaticLibrary(.{ + .name = "ghostty", + .root_source_file = .{ .path = "src/main_c.zig" }, + .target = try std.zig.CrossTarget.parse(.{ .arch_os_abi = "aarch64-macos" }), + .optimize = optimize, + }); + lib.bundle_compiler_rt = true; + lib.linkLibC(); + lib.addOptions("build_options", exe_options); + + // See the comment in this file + lib.addCSourceFile("src/renderer/metal_workaround.c", &.{}); + + // Create a single static lib with all our dependencies merged + var lib_list = try addDeps(b, lib, true); + try lib_list.append(.{ .generated = &lib.output_path_source }); + 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 = try std.zig.CrossTarget.parse(.{ .arch_os_abi = "x86_64-macos" }), + .optimize = optimize, + }); + lib.bundle_compiler_rt = true; + lib.linkLibC(); + lib.addOptions("build_options", exe_options); + + // See the comment in this file + lib.addCSourceFile("src/renderer/metal_workaround.c", &.{}); + + // Create a single static lib with all our dependencies merged + var lib_list = try addDeps(b, lib, true); + try lib_list.append(.{ .generated = &lib.output_path_source }); + 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 = .{ .generated = &static_lib_aarch64.out_path }, + .input_b = .{ .generated = &static_lib_x86_64.out_path }, + }); + static_lib_universal.step.dependOn(&static_lib_aarch64.step); + static_lib_universal.step.dependOn(&static_lib_x86_64.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 = .{ .generated = &static_lib_universal.out_path }, + .headers = .{ .path = "include" }, + }); + xcframework.step.dependOn(&static_lib_universal.step); + b.default_step.dependOn(&xcframework.step); + } + // wasm { // Build our Wasm target. @@ -179,7 +261,7 @@ pub fn build(b: *std.build.Builder) !void { wasm.stack_protector = false; // Wasm-specific deps - try addDeps(b, wasm, true); + _ = try addDeps(b, wasm, true); const step = b.step("wasm", "Build the wasm library"); step.dependOn(&wasm.step); @@ -194,7 +276,7 @@ pub fn build(b: *std.build.Builder) !void { .target = wasm_target, }); main_test.addOptions("build_options", exe_options); - try addDeps(b, main_test, true); + _ = try addDeps(b, main_test, true); test_step.dependOn(&main_test.step); } @@ -238,7 +320,7 @@ pub fn build(b: *std.build.Builder) !void { main_test_exe.install(); } main_test.setFilter(test_filter); - try addDeps(b, main_test, true); + _ = try addDeps(b, main_test, true); main_test.addOptions("build_options", exe_options); var before = b.addLog("\x1b[" ++ color_map.get("cyan").? ++ "\x1b[" ++ color_map.get("b").? ++ "[{s} tests]" ++ "\x1b[" ++ color_map.get("d").? ++ " ----" ++ "\x1b[0m", .{"ghostty"}); @@ -266,7 +348,7 @@ pub fn build(b: *std.build.Builder) !void { .target = target, }); - try addDeps(b, test_run, true); + _ = try addDeps(b, test_run, true); // if (pkg.dependencies) |children| { // test_.packages = std.ArrayList(std.build.Pkg).init(b.allocator); // try test_.packages.appendSlice(children); @@ -291,12 +373,18 @@ pub fn build(b: *std.build.Builder) !void { } } +/// Used to keep track of a list of file sources. +const FileSourceList = std.ArrayList(std.build.FileSource); + /// Adds and links all of the primary dependencies for the exe. fn addDeps( b: *std.build.Builder, step: *std.build.LibExeObjStep, static: bool, -) !void { +) !FileSourceList { + var static_libs = FileSourceList.init(b.allocator); + errdefer static_libs.deinit(); + // Wasm we do manually since it is such a different build. if (step.target.getCpuArch() == .wasm32) { // We link this package but its a no-op since Tracy @@ -308,15 +396,21 @@ fn addDeps( // utf8proc _ = try utf8proc.link(b, step); - return; + return static_libs; } + // If we're building a lib we have some different deps + const lib = step.kind == .lib; + + // We always require the system SDK so that our system headers are available. + // This makes things like `os/log.h` available for cross-compiling. + system_sdk.include(b, step, .{}); + // We always need the Zig packages if (enable_fontconfig) step.addModule("fontconfig", fontconfig.module(b)); step.addModule("freetype", freetype.module(b)); step.addModule("harfbuzz", harfbuzz.module(b)); step.addModule("imgui", imgui.module(b)); - step.addModule("glfw", glfw.module(b)); step.addModule("xev", libxev.module(b)); step.addModule("pixman", pixman.module(b)); step.addModule("stb_image_resize", stb_image_resize.module(b)); @@ -329,10 +423,6 @@ fn addDeps( _ = try macos.link(b, step, .{}); } - // We always statically compile glad - step.addIncludePath("vendor/glad/include/"); - step.addCSourceFile("vendor/glad/src/gl.c", &.{}); - // Tracy step.addModule("tracy", tracylib.module(b)); if (tracy) { @@ -341,17 +431,12 @@ fn addDeps( } // stb_image_resize - _ = try stb_image_resize.link(b, step, .{}); + const stb_image_resize_step = try stb_image_resize.link(b, step, .{}); + try static_libs.append(.{ .generated = &stb_image_resize_step.output_path_source }); // utf8proc - _ = try utf8proc.link(b, step); - - // Glfw - const glfw_opts: glfw.Options = .{ - .metal = step.target.isDarwin(), - .opengl = false, - }; - try glfw.link(b, step, glfw_opts); + const utf8proc_step = try utf8proc.link(b, step); + try static_libs.append(.{ .generated = &utf8proc_step.output_path_source }); // Imgui, we have to do this later since we need some information const imgui_backends = if (step.target.isDarwin()) @@ -379,12 +464,15 @@ fn addDeps( // Other dependencies, we may dynamically link if (static) { const zlib_step = try zlib.link(b, step); + try static_libs.append(.{ .generated = &zlib_step.output_path_source }); + const libpng_step = try libpng.link(b, step, .{ .zlib = .{ .step = zlib_step, .include = &zlib.include_paths, }, }); + try static_libs.append(.{ .generated = &libpng_step.output_path_source }); // Freetype const freetype_step = try freetype.link(b, step, .{ @@ -400,6 +488,7 @@ fn addDeps( .include = &zlib.include_paths, }, }); + try static_libs.append(.{ .generated = &freetype_step.output_path_source }); // Harfbuzz const harfbuzz_step = try harfbuzz.link(b, step, .{ @@ -414,10 +503,11 @@ fn addDeps( }, }); system_sdk.include(b, harfbuzz_step, .{}); + try static_libs.append(.{ .generated = &harfbuzz_step.output_path_source }); // Pixman const pixman_step = try pixman.link(b, step, .{}); - _ = pixman_step; + try static_libs.append(.{ .generated = &pixman_step.output_path_source }); // Only Linux gets fontconfig if (enable_fontconfig) { @@ -448,9 +538,26 @@ fn addDeps( imgui_opts.freetype.include = &freetype.include_paths; } - // Imgui - const imgui_step = try imgui.link(b, step, imgui_opts); - try glfw.link(b, imgui_step, glfw_opts); + if (!lib) { + step.addModule("glfw", glfw.module(b)); + + // We always statically compile glad + step.addIncludePath("vendor/glad/include/"); + step.addCSourceFile("vendor/glad/src/gl.c", &.{}); + + // Glfw + const glfw_opts: glfw.Options = .{ + .metal = step.target.isDarwin(), + .opengl = false, + }; + try glfw.link(b, step, glfw_opts); + + // Imgui + const imgui_step = try imgui.link(b, step, imgui_opts); + try glfw.link(b, imgui_step, glfw_opts); + } + + return static_libs; } fn benchSteps( @@ -490,7 +597,7 @@ fn benchSteps( }); c_exe.setMainPkgPath("./src"); c_exe.install(); - try addDeps(b, c_exe, true); + _ = try addDeps(b, c_exe, true); } } diff --git a/include/ghostty.h b/include/ghostty.h new file mode 100644 index 000000000..9bb88d65a --- /dev/null +++ b/include/ghostty.h @@ -0,0 +1,240 @@ +// Ghostty embedding API. The documentation for the embedding API is +// only within the Zig source files that define the implementations. This +// isn't meant to be a general purpose embedding API (yet) so there hasn't +// been documentation or example work beyond that. +// +// The only consumer of this API is the macOS app, but the API is built to +// be more general purpose. +#ifndef GHOSTTY_H +#define GHOSTTY_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +//------------------------------------------------------------------- +// Macros + +#define GHOSTTY_SUCCESS 0 + +//------------------------------------------------------------------- +// Types + +// Fully defined types. This MUST be kept in sync with equivalent Zig +// structs. To find the Zig struct, grep for this type name. The documentation +// for all of these types is available in the Zig source. +typedef void (*ghostty_runtime_wakeup_cb)(void *); +typedef void (*ghostty_runtime_set_title_cb)(void *, const char *); + +typedef struct { + void *userdata; + ghostty_runtime_wakeup_cb wakeup_cb; + ghostty_runtime_set_title_cb set_title_cb; +} ghostty_runtime_config_s; + +typedef struct { + void *userdata; + void *nsview; + double scale_factor; +} ghostty_surface_config_s; + +typedef enum { + GHOSTTY_MOUSE_RELEASE, + GHOSTTY_MOUSE_PRESS, +} ghostty_input_mouse_state_e; + +typedef enum { + GHOSTTY_MOUSE_LEFT = 1, + GHOSTTY_MOUSE_RIGHT, + GHOSTTY_MOUSE_MIDDLE, +} ghostty_input_mouse_button_e; + +typedef enum { + GHOSTTY_MODS_NONE = 0, + GHOSTTY_MODS_SHIFT = 1 << 0, + GHOSTTY_MODS_CTRL = 1 << 1, + GHOSTTY_MODS_ALT = 1 << 2, + GHOSTTY_MODS_SUPER = 1 << 3, + GHOSTTY_MODS_CAPS = 1 << 4, + GHOSTTY_MODS_NUM = 1 << 5, +} ghostty_input_mods_e; + +typedef enum { + GHOSTTY_ACTION_RELEASE, + GHOSTTY_ACTION_PRESS, + GHOSTTY_ACTION_REPEAT, +} ghostty_input_action_e; + +typedef enum { + GHOSTTY_KEY_INVALID, + + // a-z + GHOSTTY_KEY_A, + GHOSTTY_KEY_B, + GHOSTTY_KEY_C, + GHOSTTY_KEY_D, + GHOSTTY_KEY_E, + GHOSTTY_KEY_F, + GHOSTTY_KEY_G, + GHOSTTY_KEY_H, + GHOSTTY_KEY_I, + GHOSTTY_KEY_J, + GHOSTTY_KEY_K, + GHOSTTY_KEY_L, + GHOSTTY_KEY_M, + GHOSTTY_KEY_N, + GHOSTTY_KEY_O, + GHOSTTY_KEY_P, + GHOSTTY_KEY_Q, + GHOSTTY_KEY_R, + GHOSTTY_KEY_S, + GHOSTTY_KEY_T, + GHOSTTY_KEY_U, + GHOSTTY_KEY_V, + GHOSTTY_KEY_W, + GHOSTTY_KEY_X, + GHOSTTY_KEY_Y, + GHOSTTY_KEY_Z, + + // numbers + GHOSTTY_KEY_ZERO, + GHOSTTY_KEY_ONE, + GHOSTTY_KEY_TWO, + GHOSTTY_KEY_THREE, + GHOSTTY_KEY_FOUR, + GHOSTTY_KEY_FIVE, + GHOSTTY_KEY_SIX, + GHOSTTY_KEY_SEVEN, + GHOSTTY_KEY_EIGHT, + GHOSTTY_KEY_NINE, + + // puncuation + GHOSTTY_KEY_SEMICOLON, + GHOSTTY_KEY_SPACE, + GHOSTTY_KEY_APOSTROPHE, + GHOSTTY_KEY_COMMA, + GHOSTTY_KEY_GRAVE_ACCENT, // ` + GHOSTTY_KEY_PERIOD, + GHOSTTY_KEY_SLASH, + GHOSTTY_KEY_MINUS, + GHOSTTY_KEY_EQUAL, + GHOSTTY_KEY_LEFT_BRACKET, // [ + GHOSTTY_KEY_RIGHT_BRACKET, // ] + GHOSTTY_KEY_BACKSLASH, // / + + // control + GHOSTTY_KEY_UP, + GHOSTTY_KEY_DOWN, + GHOSTTY_KEY_RIGHT, + GHOSTTY_KEY_LEFT, + GHOSTTY_KEY_HOME, + GHOSTTY_KEY_END, + GHOSTTY_KEY_INSERT, + GHOSTTY_KEY_DELETE, + GHOSTTY_KEY_CAPS_LOCK, + GHOSTTY_KEY_SCROLL_LOCK, + GHOSTTY_KEY_NUM_LOCK, + GHOSTTY_KEY_PAGE_UP, + GHOSTTY_KEY_PAGE_DOWN, + GHOSTTY_KEY_ESCAPE, + GHOSTTY_KEY_ENTER, + GHOSTTY_KEY_TAB, + GHOSTTY_KEY_BACKSPACE, + GHOSTTY_KEY_PRINT_SCREEN, + GHOSTTY_KEY_PAUSE, + + // function keys + GHOSTTY_KEY_F1, + GHOSTTY_KEY_F2, + GHOSTTY_KEY_F3, + GHOSTTY_KEY_F4, + GHOSTTY_KEY_F5, + GHOSTTY_KEY_F6, + GHOSTTY_KEY_F7, + GHOSTTY_KEY_F8, + GHOSTTY_KEY_F9, + GHOSTTY_KEY_F10, + GHOSTTY_KEY_F11, + GHOSTTY_KEY_F12, + GHOSTTY_KEY_F13, + GHOSTTY_KEY_F14, + GHOSTTY_KEY_F15, + GHOSTTY_KEY_F16, + GHOSTTY_KEY_F17, + GHOSTTY_KEY_F18, + GHOSTTY_KEY_F19, + GHOSTTY_KEY_F20, + GHOSTTY_KEY_F21, + GHOSTTY_KEY_F22, + GHOSTTY_KEY_F23, + GHOSTTY_KEY_F24, + GHOSTTY_KEY_F25, + + // keypad + GHOSTTY_KEY_KP_0, + GHOSTTY_KEY_KP_1, + GHOSTTY_KEY_KP_2, + GHOSTTY_KEY_KP_3, + GHOSTTY_KEY_KP_4, + GHOSTTY_KEY_KP_5, + GHOSTTY_KEY_KP_6, + GHOSTTY_KEY_KP_7, + GHOSTTY_KEY_KP_8, + GHOSTTY_KEY_KP_9, + GHOSTTY_KEY_KP_DECIMAL, + GHOSTTY_KEY_KP_DIVIDE, + GHOSTTY_KEY_KP_MULTIPLY, + GHOSTTY_KEY_KP_SUBTRACT, + GHOSTTY_KEY_KP_ADD, + GHOSTTY_KEY_KP_ENTER, + GHOSTTY_KEY_KP_EQUAL, + + // modifiers + GHOSTTY_KEY_LEFT_SHIFT, + GHOSTTY_KEY_LEFT_CONTROL, + GHOSTTY_KEY_LEFT_ALT, + GHOSTTY_KEY_LEFT_SUPER, + GHOSTTY_KEY_RIGHT_SHIFT, + GHOSTTY_KEY_RIGHT_CONTROL, + GHOSTTY_KEY_RIGHT_ALT, + GHOSTTY_KEY_RIGHT_SUPER, +} ghostty_input_key_e; + +// Opaque types +typedef void *ghostty_app_t; +typedef void *ghostty_config_t; +typedef void *ghostty_surface_t; + +//------------------------------------------------------------------- +// Published API + +int ghostty_init(void); + +ghostty_config_t ghostty_config_new(); +void ghostty_config_free(ghostty_config_t); +void ghostty_config_load_string(ghostty_config_t, const char *, uintptr_t); +void ghostty_config_finalize(ghostty_config_t); + +ghostty_app_t ghostty_app_new(const ghostty_runtime_config_s *, ghostty_config_t); +void ghostty_app_free(ghostty_app_t); +int ghostty_app_tick(ghostty_app_t); + +ghostty_surface_t ghostty_surface_new(ghostty_app_t, ghostty_surface_config_s*); +void ghostty_surface_free(ghostty_surface_t); +void ghostty_surface_refresh(ghostty_surface_t); +void ghostty_surface_set_content_scale(ghostty_surface_t, double, double); +void ghostty_surface_set_focus(ghostty_surface_t, bool); +void ghostty_surface_set_size(ghostty_surface_t, uint32_t, uint32_t); +void ghostty_surface_key(ghostty_surface_t, ghostty_input_action_e, ghostty_input_key_e, ghostty_input_mods_e); +void ghostty_surface_char(ghostty_surface_t, uint32_t); +void ghostty_surface_mouse_button(ghostty_surface_t, ghostty_input_mouse_state_e, ghostty_input_mouse_button_e, ghostty_input_mods_e); +void ghostty_surface_mouse_pos(ghostty_surface_t, double, double); +void ghostty_surface_mouse_scroll(ghostty_surface_t, double, double); + +#ifdef __cplusplus +} +#endif + +#endif /* GHOSTTY_H */ diff --git a/include/module.modulemap b/include/module.modulemap new file mode 100644 index 000000000..8961f5c04 --- /dev/null +++ b/include/module.modulemap @@ -0,0 +1,7 @@ +// This makes Ghostty available to the XCode build for the macOS app. +// We append "Kit" to it not to be cute, but because targets have to have +// unique names and we use Ghostty for other things. +module GhosttyKit { + umbrella header "ghostty.h" + export * +} diff --git a/macos/.gitignore b/macos/.gitignore new file mode 100644 index 000000000..07895edd7 --- /dev/null +++ b/macos/.gitignore @@ -0,0 +1,5 @@ +.DS_Store +/*.xcframework +build/ +xcuserdata/ +DerivedData/ diff --git a/macos/Assets.xcassets/AccentColor.colorset/Contents.json b/macos/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 000000000..eb8789700 --- /dev/null +++ b/macos/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/macos/Assets.xcassets/AppIcon.appiconset/Contents.json b/macos/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..0425f20d1 --- /dev/null +++ b/macos/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "filename" : "Ghostty_256x256x32 6.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "filename" : "Ghostty_256x256x32 5.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "filename" : "Ghostty_256x256x32 4.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "filename" : "Ghostty_256x256x32 3.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "filename" : "Ghostty_256x256x32 2.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "filename" : "Ghostty_256x256x32 1.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "filename" : "Ghostty_256x256x32.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "filename" : "Ghostty_512x512x32.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "filename" : "Ghostty_512x512x32 1.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "filename" : "Ghostty_512x512x32 2.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/macos/Assets.xcassets/AppIcon.appiconset/Ghostty_256x256x32 1.png b/macos/Assets.xcassets/AppIcon.appiconset/Ghostty_256x256x32 1.png new file mode 100644 index 000000000..608ba84b0 Binary files /dev/null and b/macos/Assets.xcassets/AppIcon.appiconset/Ghostty_256x256x32 1.png differ diff --git a/macos/Assets.xcassets/AppIcon.appiconset/Ghostty_256x256x32 2.png b/macos/Assets.xcassets/AppIcon.appiconset/Ghostty_256x256x32 2.png new file mode 100644 index 000000000..608ba84b0 Binary files /dev/null and b/macos/Assets.xcassets/AppIcon.appiconset/Ghostty_256x256x32 2.png differ diff --git a/macos/Assets.xcassets/AppIcon.appiconset/Ghostty_256x256x32 3.png b/macos/Assets.xcassets/AppIcon.appiconset/Ghostty_256x256x32 3.png new file mode 100644 index 000000000..608ba84b0 Binary files /dev/null and b/macos/Assets.xcassets/AppIcon.appiconset/Ghostty_256x256x32 3.png differ diff --git a/macos/Assets.xcassets/AppIcon.appiconset/Ghostty_256x256x32 4.png b/macos/Assets.xcassets/AppIcon.appiconset/Ghostty_256x256x32 4.png new file mode 100644 index 000000000..608ba84b0 Binary files /dev/null and b/macos/Assets.xcassets/AppIcon.appiconset/Ghostty_256x256x32 4.png differ diff --git a/macos/Assets.xcassets/AppIcon.appiconset/Ghostty_256x256x32 5.png b/macos/Assets.xcassets/AppIcon.appiconset/Ghostty_256x256x32 5.png new file mode 100644 index 000000000..608ba84b0 Binary files /dev/null and b/macos/Assets.xcassets/AppIcon.appiconset/Ghostty_256x256x32 5.png differ diff --git a/macos/Assets.xcassets/AppIcon.appiconset/Ghostty_256x256x32 6.png b/macos/Assets.xcassets/AppIcon.appiconset/Ghostty_256x256x32 6.png new file mode 100644 index 000000000..608ba84b0 Binary files /dev/null and b/macos/Assets.xcassets/AppIcon.appiconset/Ghostty_256x256x32 6.png differ diff --git a/macos/Assets.xcassets/AppIcon.appiconset/Ghostty_256x256x32.png b/macos/Assets.xcassets/AppIcon.appiconset/Ghostty_256x256x32.png new file mode 100644 index 000000000..608ba84b0 Binary files /dev/null and b/macos/Assets.xcassets/AppIcon.appiconset/Ghostty_256x256x32.png differ diff --git a/macos/Assets.xcassets/AppIcon.appiconset/Ghostty_512x512x32 1.png b/macos/Assets.xcassets/AppIcon.appiconset/Ghostty_512x512x32 1.png new file mode 100644 index 000000000..2f89c3876 Binary files /dev/null and b/macos/Assets.xcassets/AppIcon.appiconset/Ghostty_512x512x32 1.png differ diff --git a/macos/Assets.xcassets/AppIcon.appiconset/Ghostty_512x512x32 2.png b/macos/Assets.xcassets/AppIcon.appiconset/Ghostty_512x512x32 2.png new file mode 100644 index 000000000..2f89c3876 Binary files /dev/null and b/macos/Assets.xcassets/AppIcon.appiconset/Ghostty_512x512x32 2.png differ diff --git a/macos/Assets.xcassets/AppIcon.appiconset/Ghostty_512x512x32.png b/macos/Assets.xcassets/AppIcon.appiconset/Ghostty_512x512x32.png new file mode 100644 index 000000000..2f89c3876 Binary files /dev/null and b/macos/Assets.xcassets/AppIcon.appiconset/Ghostty_512x512x32.png differ diff --git a/macos/Assets.xcassets/AppIconImage.imageset/199110421-9ff5fc30-a244-441e-9882-26070662adf9 1.png b/macos/Assets.xcassets/AppIconImage.imageset/199110421-9ff5fc30-a244-441e-9882-26070662adf9 1.png new file mode 100644 index 000000000..3c572d267 Binary files /dev/null and b/macos/Assets.xcassets/AppIconImage.imageset/199110421-9ff5fc30-a244-441e-9882-26070662adf9 1.png differ diff --git a/macos/Assets.xcassets/AppIconImage.imageset/199110421-9ff5fc30-a244-441e-9882-26070662adf9 2.png b/macos/Assets.xcassets/AppIconImage.imageset/199110421-9ff5fc30-a244-441e-9882-26070662adf9 2.png new file mode 100644 index 000000000..3c572d267 Binary files /dev/null and b/macos/Assets.xcassets/AppIconImage.imageset/199110421-9ff5fc30-a244-441e-9882-26070662adf9 2.png differ diff --git a/macos/Assets.xcassets/AppIconImage.imageset/199110421-9ff5fc30-a244-441e-9882-26070662adf9.png b/macos/Assets.xcassets/AppIconImage.imageset/199110421-9ff5fc30-a244-441e-9882-26070662adf9.png new file mode 100644 index 000000000..3c572d267 Binary files /dev/null and b/macos/Assets.xcassets/AppIconImage.imageset/199110421-9ff5fc30-a244-441e-9882-26070662adf9.png differ diff --git a/macos/Assets.xcassets/AppIconImage.imageset/Contents.json b/macos/Assets.xcassets/AppIconImage.imageset/Contents.json new file mode 100644 index 000000000..8a7f27f4f --- /dev/null +++ b/macos/Assets.xcassets/AppIconImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "199110421-9ff5fc30-a244-441e-9882-26070662adf9.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "199110421-9ff5fc30-a244-441e-9882-26070662adf9 1.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "199110421-9ff5fc30-a244-441e-9882-26070662adf9 2.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/macos/Assets.xcassets/Contents.json b/macos/Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/macos/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/macos/Ghostty.entitlements b/macos/Ghostty.entitlements new file mode 100644 index 000000000..0c67376eb --- /dev/null +++ b/macos/Ghostty.entitlements @@ -0,0 +1,5 @@ + + + + + diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj new file mode 100644 index 000000000..71f0d767e --- /dev/null +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -0,0 +1,376 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + A507573E299FF33C009D7DC7 /* TerminalSurfaceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A507573D299FF33C009D7DC7 /* TerminalSurfaceView.swift */; }; + A518502429A197C700E4CC4F /* TerminalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A518502329A197C700E4CC4F /* TerminalView.swift */; }; + A518502629A1A45100E4CC4F /* WindowTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = A518502529A1A45100E4CC4F /* WindowTracker.swift */; }; + A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A535B9D9299C569B0017E2E4 /* ErrorView.swift */; }; + A55685E029A03A9F004303CE /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55685DF29A03A9F004303CE /* AppError.swift */; }; + A5B30535299BEAAA0047F10C /* GhosttyApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5B30534299BEAAA0047F10C /* GhosttyApp.swift */; }; + A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; }; + A5D495A2299BEC7E00DD1313 /* GhosttyKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + A507573D299FF33C009D7DC7 /* TerminalSurfaceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalSurfaceView.swift; sourceTree = ""; }; + A518502329A197C700E4CC4F /* TerminalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalView.swift; sourceTree = ""; }; + A518502529A1A45100E4CC4F /* WindowTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowTracker.swift; sourceTree = ""; }; + A535B9D9299C569B0017E2E4 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = ""; }; + A55685DF29A03A9F004303CE /* AppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = ""; }; + A5B30531299BEAAA0047F10C /* Ghostty.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ghostty.app; sourceTree = BUILT_PRODUCTS_DIR; }; + A5B30534299BEAAA0047F10C /* GhosttyApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhosttyApp.swift; sourceTree = ""; }; + A5B30538299BEAAB0047F10C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + A5B3053D299BEAAB0047F10C /* Ghostty.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Ghostty.entitlements; sourceTree = ""; }; + A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = GhosttyKit.xcframework; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + A5B3052E299BEAAA0047F10C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + A5D495A2299BEC7E00DD1313 /* GhosttyKit.xcframework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + A54CD6ED299BEB14008C95BB /* Sources */ = { + isa = PBXGroup; + children = ( + A5D495A0299BEC2200DD1313 /* Preview Content */, + A5B30534299BEAAA0047F10C /* GhosttyApp.swift */, + A535B9D9299C569B0017E2E4 /* ErrorView.swift */, + A55685DF29A03A9F004303CE /* AppError.swift */, + A518502329A197C700E4CC4F /* TerminalView.swift */, + A507573D299FF33C009D7DC7 /* TerminalSurfaceView.swift */, + A518502529A1A45100E4CC4F /* WindowTracker.swift */, + ); + path = Sources; + sourceTree = ""; + }; + A5B30528299BEAAA0047F10C = { + isa = PBXGroup; + children = ( + A5B30538299BEAAB0047F10C /* Assets.xcassets */, + A5B3053D299BEAAB0047F10C /* Ghostty.entitlements */, + A54CD6ED299BEB14008C95BB /* Sources */, + A5D495A3299BECBA00DD1313 /* Frameworks */, + A5B30532299BEAAA0047F10C /* Products */, + ); + sourceTree = ""; + }; + A5B30532299BEAAA0047F10C /* Products */ = { + isa = PBXGroup; + children = ( + A5B30531299BEAAA0047F10C /* Ghostty.app */, + ); + name = Products; + sourceTree = ""; + }; + A5D495A0299BEC2200DD1313 /* Preview Content */ = { + isa = PBXGroup; + children = ( + ); + path = "Preview Content"; + sourceTree = ""; + }; + A5D495A3299BECBA00DD1313 /* Frameworks */ = { + isa = PBXGroup; + children = ( + A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + A5B30530299BEAAA0047F10C /* Ghostty */ = { + isa = PBXNativeTarget; + buildConfigurationList = A5B30540299BEAAB0047F10C /* Build configuration list for PBXNativeTarget "Ghostty" */; + buildPhases = ( + A5B3052D299BEAAA0047F10C /* Sources */, + A5B3052E299BEAAA0047F10C /* Frameworks */, + A5B3052F299BEAAA0047F10C /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Ghostty; + productName = Ghostty; + productReference = A5B30531299BEAAA0047F10C /* Ghostty.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + A5B30529299BEAAA0047F10C /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1420; + LastUpgradeCheck = 1420; + TargetAttributes = { + A5B30530299BEAAA0047F10C = { + CreatedOnToolsVersion = 14.2; + }; + }; + }; + buildConfigurationList = A5B3052C299BEAAA0047F10C /* Build configuration list for PBXProject "Ghostty" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = A5B30528299BEAAA0047F10C; + productRefGroup = A5B30532299BEAAA0047F10C /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + A5B30530299BEAAA0047F10C /* Ghostty */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + A5B3052F299BEAAA0047F10C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + A5B3052D299BEAAA0047F10C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A518502629A1A45100E4CC4F /* WindowTracker.swift in Sources */, + A518502429A197C700E4CC4F /* TerminalView.swift in Sources */, + A55685E029A03A9F004303CE /* AppError.swift in Sources */, + A507573E299FF33C009D7DC7 /* TerminalSurfaceView.swift in Sources */, + A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */, + A5B30535299BEAAA0047F10C /* GhosttyApp.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + A5B3053E299BEAAB0047F10C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 13.1; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + A5B3053F299BEAAB0047F10C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 13.1; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + A5B30541299BEAAB0047F10C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; + CODE_SIGN_ENTITLEMENTS = Ghostty.entitlements; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"Sources/Preview Content\""; + DEVELOPMENT_TEAM = ""; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = Ghostty; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MARKETING_VERSION = 0.1; + "OTHER_LDFLAGS[arch=*]" = "-lstdc++"; + PRODUCT_BUNDLE_IDENTIFIER = com.mitchellh.ghostty; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + A5B30542299BEAAB0047F10C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; + CODE_SIGN_ENTITLEMENTS = Ghostty.entitlements; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"Sources/Preview Content\""; + DEVELOPMENT_TEAM = ""; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + GCC_OPTIMIZATION_LEVEL = fast; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = Ghostty; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MARKETING_VERSION = 0.1; + "OTHER_LDFLAGS[arch=*]" = "-lstdc++"; + PRODUCT_BUNDLE_IDENTIFIER = com.mitchellh.ghostty; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + A5B3052C299BEAAA0047F10C /* Build configuration list for PBXProject "Ghostty" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A5B3053E299BEAAB0047F10C /* Debug */, + A5B3053F299BEAAB0047F10C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + A5B30540299BEAAB0047F10C /* Build configuration list for PBXNativeTarget "Ghostty" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A5B30541299BEAAB0047F10C /* Debug */, + A5B30542299BEAAB0047F10C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = A5B30529299BEAAA0047F10C /* Project object */; +} diff --git a/macos/Ghostty.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/macos/Ghostty.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..919434a62 --- /dev/null +++ b/macos/Ghostty.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/macos/Ghostty.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Ghostty.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/macos/Ghostty.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/macos/Sources/AppError.swift b/macos/Sources/AppError.swift new file mode 100644 index 000000000..55f191d3d --- /dev/null +++ b/macos/Sources/AppError.swift @@ -0,0 +1,3 @@ +enum AppError: Error { + case surfaceCreateError +} diff --git a/macos/Sources/ErrorView.swift b/macos/Sources/ErrorView.swift new file mode 100644 index 000000000..3a921667d --- /dev/null +++ b/macos/Sources/ErrorView.swift @@ -0,0 +1,24 @@ +import SwiftUI + +struct ErrorView: View { + var body: some View { + HStack { + Image("AppIconImage") + .resizable() + .scaledToFit() + .frame(width: 128, height: 128) + + VStack(alignment: .leading) { + Text("Oh, no. 😭").font(.title) + Text("Something went fatally wrong.\nCheck the logs and restart Ghostty.") + } + } + .padding() + } +} + +struct ErrorView_Previews: PreviewProvider { + static var previews: some View { + ErrorView() + } +} diff --git a/macos/Sources/GhosttyApp.swift b/macos/Sources/GhosttyApp.swift new file mode 100644 index 000000000..a68978c2b --- /dev/null +++ b/macos/Sources/GhosttyApp.swift @@ -0,0 +1,127 @@ +import OSLog +import SwiftUI +import GhosttyKit + +@main +struct GhosttyApp: App { + static let logger = Logger( + subsystem: Bundle.main.bundleIdentifier!, + category: String(describing: GhosttyApp.self) + ) + + /// The ghostty global state. Only one per process. + @StateObject private var ghostty = GhosttyState() + + var body: some Scene { + WindowGroup { + switch ghostty.readiness { + case .loading: + Text("Loading") + case .error: + ErrorView() + case .ready: + TerminalView(app: ghostty.app!) + .modifier(WindowObservationModifier()) + } + }.commands { + CommandGroup(after: .newItem) { + Button("New Tab", action: newTab).keyboardShortcut("t", modifiers: [.command]) + } + } + } + + // Create a new tab in the currently active window + func newTab() { + guard let currentWindow = NSApp.keyWindow else { return } + guard let windowController = currentWindow.windowController else { return } + windowController.newWindowForTab(nil) + if let newWindow = NSApp.keyWindow, currentWindow != newWindow { + currentWindow.addTabbedWindow(newWindow, ordered: .above) + } + } +} + +class GhosttyState: ObservableObject { + enum Readiness { + case loading, error, ready + } + + /// The readiness value of the state. + @Published var readiness: Readiness = .loading + + /// The ghostty global configuration. + var config: ghostty_config_t? = nil + + /// 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... + var app: ghostty_app_t? = nil + + init() { + // Initialize ghostty global state. This happens once per process. + guard ghostty_init() == GHOSTTY_SUCCESS else { + GhosttyApp.logger.critical("ghostty_init failed") + readiness = .error + return + } + + // Initialize the global configuration. + guard let cfg = ghostty_config_new() else { + GhosttyApp.logger.critical("ghostty_config_new failed") + readiness = .error + return + } + self.config = 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) + + // 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(), + wakeup_cb: { userdata in GhosttyState.wakeup(userdata) }, + set_title_cb: { userdata, title in GhosttyState.setTitle(userdata, title: title) }) + + // Create the ghostty app. + guard let app = ghostty_app_new(&runtime_cfg, cfg) else { + GhosttyApp.logger.critical("ghostty_app_new failed") + readiness = .error + return + } + self.app = app + + self.readiness = .ready + } + + func appTick() { + guard let app = self.app else { return } + ghostty_app_tick(app) + } + + 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 setTitle(_ userdata: UnsafeMutableRawPointer?, title: UnsafePointer?) { + let surfaceView = Unmanaged.fromOpaque(userdata!).takeUnretainedValue() + guard let titleStr = String(cString: title!, encoding: .utf8) else { return } + DispatchQueue.main.async { + surfaceView.title = titleStr + } + } + + deinit { + ghostty_app_free(app) + ghostty_config_free(config) + } +} diff --git a/macos/Sources/Preview Content/.gitkeep b/macos/Sources/Preview Content/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/macos/Sources/TerminalSurfaceView.swift b/macos/Sources/TerminalSurfaceView.swift new file mode 100644 index 000000000..ac3a05500 --- /dev/null +++ b/macos/Sources/TerminalSurfaceView.swift @@ -0,0 +1,448 @@ +import OSLog +import SwiftUI +import GhosttyKit + +/// A surface is terminology in Ghostty for a terminal surface, or a place where a terminal is actually drawn +/// and interacted with. The word "surface" is used because a surface may represent a window, a tab, +/// a split, a small preview pane, etc. It is ANYTHING that has a terminal drawn to it. +/// +/// We just wrap an AppKit NSView here at the moment so that we can behave as low level as possible +/// since that is what the Metal renderer in Ghostty expects. In the future, it may make more sense to +/// wrap an MTKView and use that, but for legacy reasons we didn't do that to begin with. +struct TerminalSurfaceView: NSViewRepresentable { + /// This should be set to true wen the surface has focus. This is up to the parent because + /// focus is also defined by window focus. It is important this is set correctly since if it is + /// false then the surface will idle at almost 0% CPU. + var hasFocus: Bool + + /// This is set to the title of the surface as defined by the pty. Callers should use this to + /// set the appropriate title of the window/tab/split/etc. if they care. + @Binding var title: String + + @StateObject private var state: TerminalSurfaceView_Real + + init(_ app: ghostty_app_t, hasFocus: Bool, title: Binding) { + self._state = StateObject(wrappedValue: TerminalSurfaceView_Real(app)) + self._title = title + self.hasFocus = hasFocus + } + + func makeNSView(context: Context) -> TerminalSurfaceView_Real { + // We need the view as part of the state to be created previously because + // the view is sent to the Ghostty API so that it can manipulate it + // directly since we draw on a render thread. + state.delegate = context.coordinator + return state; + } + + func updateNSView(_ view: TerminalSurfaceView_Real, context: Context) { + state.delegate = context.coordinator + state.focusDidChange(hasFocus) + } + + func makeCoordinator() -> Coordinator { + return Coordinator(self) + } + + class Coordinator : TerminalSurfaceDelegate { + let view: TerminalSurfaceView + + init(_ view: TerminalSurfaceView) { + self.view = view + } + + func titleDidChange(to: String) { + view.title = to + } + } +} + +/// We use the delegate pattern to receive notifications about important state changes in the surface. +protocol TerminalSurfaceDelegate: AnyObject { + func titleDidChange(to: String) +} + +/// The actual NSView implementation for the terminal surface. +class TerminalSurfaceView_Real: NSView, NSTextInputClient, ObservableObject { + weak var delegate: TerminalSurfaceDelegate? + + // The current title of the surface as defined by the pty. This can be + // changed with escape codes. + var title: String = "" { + didSet { + if let delegate = self.delegate { + delegate.titleDidChange(to: title) + } + } + } + + private var surface: ghostty_surface_t? = nil + private var error: Error? = nil + private var markedText: NSMutableAttributedString; + + // We need to support being a first responder so that we can get input events + override var acceptsFirstResponder: Bool { return true } + + // I don't thikn we need this but this lets us know we should redraw our layer + // so we'll use that to tell ghostty to refresh. + override var wantsUpdateLayer: Bool { return true } + + // Mapping of event keyCode to ghostty input key values. This is cribbed from + // glfw mostly since we started as a glfw-based app way back in the day! + static let keycodes: [UInt16 : ghostty_input_key_e] = [ + 0x1D: GHOSTTY_KEY_ZERO, + 0x12: GHOSTTY_KEY_ONE, + 0x13: GHOSTTY_KEY_TWO, + 0x14: GHOSTTY_KEY_THREE, + 0x15: GHOSTTY_KEY_FOUR, + 0x17: GHOSTTY_KEY_FIVE, + 0x16: GHOSTTY_KEY_SIX, + 0x1A: GHOSTTY_KEY_SEVEN, + 0x1C: GHOSTTY_KEY_EIGHT, + 0x19: GHOSTTY_KEY_NINE, + 0x00: GHOSTTY_KEY_A, + 0x0B: GHOSTTY_KEY_B, + 0x08: GHOSTTY_KEY_C, + 0x02: GHOSTTY_KEY_D, + 0x0E: GHOSTTY_KEY_E, + 0x03: GHOSTTY_KEY_F, + 0x05: GHOSTTY_KEY_G, + 0x04: GHOSTTY_KEY_H, + 0x22: GHOSTTY_KEY_I, + 0x26: GHOSTTY_KEY_J, + 0x28: GHOSTTY_KEY_K, + 0x25: GHOSTTY_KEY_L, + 0x2E: GHOSTTY_KEY_M, + 0x2D: GHOSTTY_KEY_N, + 0x1F: GHOSTTY_KEY_O, + 0x23: GHOSTTY_KEY_P, + 0x0C: GHOSTTY_KEY_Q, + 0x0F: GHOSTTY_KEY_R, + 0x01: GHOSTTY_KEY_S, + 0x11: GHOSTTY_KEY_T, + 0x20: GHOSTTY_KEY_U, + 0x09: GHOSTTY_KEY_V, + 0x0D: GHOSTTY_KEY_W, + 0x07: GHOSTTY_KEY_X, + 0x10: GHOSTTY_KEY_Y, + 0x06: GHOSTTY_KEY_Z, + + 0x27: GHOSTTY_KEY_APOSTROPHE, + 0x2A: GHOSTTY_KEY_BACKSLASH, + 0x2B: GHOSTTY_KEY_COMMA, + 0x18: GHOSTTY_KEY_EQUAL, + 0x32: GHOSTTY_KEY_GRAVE_ACCENT, + 0x21: GHOSTTY_KEY_LEFT_BRACKET, + 0x1B: GHOSTTY_KEY_MINUS, + 0x2F: GHOSTTY_KEY_PERIOD, + 0x1E: GHOSTTY_KEY_RIGHT_BRACKET, + 0x29: GHOSTTY_KEY_SEMICOLON, + 0x2C: GHOSTTY_KEY_SLASH, + + 0x33: GHOSTTY_KEY_BACKSPACE, + 0x39: GHOSTTY_KEY_CAPS_LOCK, + 0x75: GHOSTTY_KEY_DELETE, + 0x7D: GHOSTTY_KEY_DOWN, + 0x77: GHOSTTY_KEY_END, + 0x24: GHOSTTY_KEY_ENTER, + 0x35: GHOSTTY_KEY_ESCAPE, + 0x7A: GHOSTTY_KEY_F1, + 0x78: GHOSTTY_KEY_F2, + 0x63: GHOSTTY_KEY_F3, + 0x76: GHOSTTY_KEY_F4, + 0x60: GHOSTTY_KEY_F5, + 0x61: GHOSTTY_KEY_F6, + 0x62: GHOSTTY_KEY_F7, + 0x64: GHOSTTY_KEY_F8, + 0x65: GHOSTTY_KEY_F9, + 0x6D: GHOSTTY_KEY_F10, + 0x67: GHOSTTY_KEY_F11, + 0x6F: GHOSTTY_KEY_F12, + 0x69: GHOSTTY_KEY_PRINT_SCREEN, + 0x6B: GHOSTTY_KEY_F14, + 0x71: GHOSTTY_KEY_F15, + 0x6A: GHOSTTY_KEY_F16, + 0x40: GHOSTTY_KEY_F17, + 0x4F: GHOSTTY_KEY_F18, + 0x50: GHOSTTY_KEY_F19, + 0x5A: GHOSTTY_KEY_F20, + 0x73: GHOSTTY_KEY_HOME, + 0x72: GHOSTTY_KEY_INSERT, + 0x7B: GHOSTTY_KEY_LEFT, + 0x3A: GHOSTTY_KEY_LEFT_ALT, + 0x3B: GHOSTTY_KEY_LEFT_CONTROL, + 0x38: GHOSTTY_KEY_LEFT_SHIFT, + 0x37: GHOSTTY_KEY_LEFT_SUPER, + 0x47: GHOSTTY_KEY_NUM_LOCK, + 0x79: GHOSTTY_KEY_PAGE_DOWN, + 0x74: GHOSTTY_KEY_PAGE_UP, + 0x7C: GHOSTTY_KEY_RIGHT, + 0x3D: GHOSTTY_KEY_RIGHT_ALT, + 0x3E: GHOSTTY_KEY_RIGHT_CONTROL, + 0x3C: GHOSTTY_KEY_RIGHT_SHIFT, + 0x36: GHOSTTY_KEY_RIGHT_SUPER, + 0x31: GHOSTTY_KEY_SPACE, + 0x30: GHOSTTY_KEY_TAB, + 0x7E: GHOSTTY_KEY_UP, + + 0x52: GHOSTTY_KEY_KP_0, + 0x53: GHOSTTY_KEY_KP_1, + 0x54: GHOSTTY_KEY_KP_2, + 0x55: GHOSTTY_KEY_KP_3, + 0x56: GHOSTTY_KEY_KP_4, + 0x57: GHOSTTY_KEY_KP_5, + 0x58: GHOSTTY_KEY_KP_6, + 0x59: GHOSTTY_KEY_KP_7, + 0x5B: GHOSTTY_KEY_KP_8, + 0x5C: GHOSTTY_KEY_KP_9, + 0x45: GHOSTTY_KEY_KP_ADD, + 0x41: GHOSTTY_KEY_KP_DECIMAL, + 0x4B: GHOSTTY_KEY_KP_DIVIDE, + 0x4C: GHOSTTY_KEY_KP_ENTER, + 0x51: GHOSTTY_KEY_KP_EQUAL, + 0x43: GHOSTTY_KEY_KP_MULTIPLY, + 0x4E: GHOSTTY_KEY_KP_SUBTRACT, + ]; + + init(_ app: ghostty_app_t) { + self.markedText = NSMutableAttributedString() + + // Initialize with some default frame size. The important thing is that this + // is non-zero so that our layer bounds are non-zero so that our renderer + // can do SOMETHING. + super.init(frame: NSMakeRect(0, 0, 800, 600)) + + // Setup our surface. This will also initialize all the terminal IO. + var surface_cfg = ghostty_surface_config_s( + userdata: Unmanaged.passUnretained(self).toOpaque(), + nsview: Unmanaged.passUnretained(self).toOpaque(), + scale_factor: NSScreen.main!.backingScaleFactor) + guard let surface = ghostty_surface_new(app, &surface_cfg) else { + self.error = AppError.surfaceCreateError + return + } + self.surface = surface; + + // Setup our tracking area so we get mouse moved events + updateTrackingAreas() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) is not supported for this view") + } + + deinit { + trackingAreas.forEach { removeTrackingArea($0) } + + guard let surface = self.surface else { return } + ghostty_surface_free(surface) + } + + func focusDidChange(_ focused: Bool) { + guard let surface = self.surface else { return } + ghostty_surface_set_focus(surface, focused ? 1 : 0) + } + + override func resize(withOldSuperviewSize oldSize: NSSize) { + super.resize(withOldSuperviewSize: oldSize) + + if let surface = self.surface { + // Ghostty wants to know the actual framebuffer size... + let fbFrame = self.convertToBacking(self.frame); + ghostty_surface_set_size(surface, UInt32(fbFrame.size.width), UInt32(fbFrame.size.height)) + } + } + + override func updateTrackingAreas() { + // To update our tracking area we just recreate it all. + trackingAreas.forEach { removeTrackingArea($0) } + + // This tracking area is across the entire frame to notify us of mouse movements. + addTrackingArea(NSTrackingArea( + rect: frame, + options: [ + .mouseEnteredAndExited, + .mouseMoved, + .inVisibleRect, + + // It is possible this is incorrect when we have splits. This will make + // mouse events only happen while the terminal is focused. Is that what + // we want? + .activeWhenFirstResponder, + ], + owner: self, + userInfo: nil)) + } + + override func viewDidChangeBackingProperties() { + guard let surface = self.surface else { return } + + // Detect our X/Y scale factor so we can update our surface + let fbFrame = self.convertToBacking(self.frame) + let xScale = fbFrame.size.width / self.frame.size.width + let yScale = fbFrame.size.height / self.frame.size.height + ghostty_surface_set_content_scale(surface, xScale, yScale) + + // When our scale factor changes, so does our fb size so we send that too + ghostty_surface_set_size(surface, UInt32(fbFrame.size.width), UInt32(fbFrame.size.height)) + } + + override func updateLayer() { + guard let surface = self.surface else { return } + ghostty_surface_refresh(surface); + } + + override func mouseDown(with event: NSEvent) { + guard let surface = self.surface else { return } + let mods = Self.translateFlags(event.modifierFlags) + ghostty_surface_mouse_button(surface, GHOSTTY_MOUSE_PRESS, GHOSTTY_MOUSE_LEFT, mods) + } + + override func mouseUp(with event: NSEvent) { + guard let surface = self.surface else { return } + let mods = Self.translateFlags(event.modifierFlags) + ghostty_surface_mouse_button(surface, GHOSTTY_MOUSE_RELEASE, GHOSTTY_MOUSE_LEFT, mods) + } + + override func rightMouseDown(with event: NSEvent) { + guard let surface = self.surface else { return } + let mods = Self.translateFlags(event.modifierFlags) + ghostty_surface_mouse_button(surface, GHOSTTY_MOUSE_PRESS, GHOSTTY_MOUSE_RIGHT, mods) + } + + override func rightMouseUp(with event: NSEvent) { + guard let surface = self.surface else { return } + let mods = Self.translateFlags(event.modifierFlags) + ghostty_surface_mouse_button(surface, GHOSTTY_MOUSE_RELEASE, GHOSTTY_MOUSE_RIGHT, mods) + } + + override func mouseMoved(with event: NSEvent) { + guard let surface = self.surface else { return } + + // Convert window position to view position. Note (0, 0) is bottom left. + let pos = self.convert(event.locationInWindow, from: nil) + ghostty_surface_mouse_pos(surface, pos.x, frame.height - pos.y) + + } + + override func mouseDragged(with event: NSEvent) { + self.mouseMoved(with: event) + } + + override func scrollWheel(with event: NSEvent) { + guard let surface = self.surface else { return } + + var x = event.scrollingDeltaX + var y = event.scrollingDeltaY + if event.hasPreciseScrollingDeltas { + x *= 0.1 + y *= 0.1 + } + + ghostty_surface_mouse_scroll(surface, x, y) + } + + override func keyDown(with event: NSEvent) { + guard let surface = self.surface else { return } + let key = Self.keycodes[event.keyCode] ?? GHOSTTY_KEY_INVALID + let mods = Self.translateFlags(event.modifierFlags) + let action = event.isARepeat ? GHOSTTY_ACTION_REPEAT : GHOSTTY_ACTION_PRESS + ghostty_surface_key(surface, action, key, mods) + + self.interpretKeyEvents([event]) + } + + override func keyUp(with event: NSEvent) { + guard let surface = self.surface else { return } + let key = Self.keycodes[event.keyCode] ?? GHOSTTY_KEY_INVALID + let mods = Self.translateFlags(event.modifierFlags) + ghostty_surface_key(surface, GHOSTTY_ACTION_RELEASE, key, mods) + } + + // MARK: NSTextInputClient + + func hasMarkedText() -> Bool { + return markedText.length > 0 + } + + func markedRange() -> NSRange { + guard markedText.length > 0 else { return NSRange() } + return NSRange(0...(markedText.length-1)) + } + + func selectedRange() -> NSRange { + return NSRange() + } + + func setMarkedText(_ string: Any, selectedRange: NSRange, replacementRange: NSRange) { + switch string { + case let v as NSAttributedString: + self.markedText = NSMutableAttributedString(attributedString: v) + + case let v as String: + self.markedText = NSMutableAttributedString(string: v) + + default: + print("unknown marked text: \(string)") + } + } + + func unmarkText() { + self.markedText.mutableString.setString("") + } + + func validAttributesForMarkedText() -> [NSAttributedString.Key] { + return [] + } + + func attributedSubstring(forProposedRange range: NSRange, actualRange: NSRangePointer?) -> NSAttributedString? { + return nil + } + + func characterIndex(for point: NSPoint) -> Int { + return 0 + } + + func firstRect(forCharacterRange range: NSRange, actualRange: NSRangePointer?) -> NSRect { + return NSMakeRect(frame.origin.x, frame.origin.y, 0, 0) + } + + func insertText(_ string: Any, replacementRange: NSRange) { + // We must have an associated event + guard NSApp.currentEvent != nil else { return } + guard let surface = self.surface else { return } + + // We want the string view of the any value + var chars = "" + switch (string) { + case let v as NSAttributedString: + chars = v.string + case let v as String: + chars = v + default: + return + } + + for codepoint in chars.unicodeScalars { + ghostty_surface_char(surface, codepoint.value) + } + } + + override func doCommand(by selector: Selector) { + // This currently just prevents NSBeep from interpretKeyEvents but in the future + // we may want to make some of this work. + + print("SEL: \(selector)") + } + + private static func translateFlags(_ flags: NSEvent.ModifierFlags) -> ghostty_input_mods_e { + var mods: UInt32 = GHOSTTY_MODS_NONE.rawValue + if (flags.contains(.shift)) { mods |= GHOSTTY_MODS_SHIFT.rawValue } + if (flags.contains(.control)) { mods |= GHOSTTY_MODS_CTRL.rawValue } + if (flags.contains(.option)) { mods |= GHOSTTY_MODS_ALT.rawValue } + if (flags.contains(.command)) { mods |= GHOSTTY_MODS_SUPER.rawValue } + if (flags.contains(.capsLock)) { mods |= GHOSTTY_MODS_CAPS.rawValue } + + return ghostty_input_mods_e(mods) + } +} diff --git a/macos/Sources/TerminalView.swift b/macos/Sources/TerminalView.swift new file mode 100644 index 000000000..123388802 --- /dev/null +++ b/macos/Sources/TerminalView.swift @@ -0,0 +1,22 @@ +import SwiftUI +import GhosttyKit + +struct TerminalView: View { + let app: ghostty_app_t + @FocusState private var surfaceFocus: Bool + @Environment(\.isKeyWindow) private var isKeyWindow: Bool + @Environment(\.openWindow) private var openWindow + @State private var title: String = "Ghostty" + + // This is true if the terminal is considered "focused". The terminal is focused if + // it is both individually focused and the containing window is key. + private var hasFocus: Bool { surfaceFocus && isKeyWindow } + + var body: some View { + VStack { + TerminalSurfaceView(app, hasFocus: hasFocus, title: $title) + .focused($surfaceFocus) + .navigationTitle(title) + } + } +} diff --git a/macos/Sources/WindowTracker.swift b/macos/Sources/WindowTracker.swift new file mode 100644 index 000000000..c936db253 --- /dev/null +++ b/macos/Sources/WindowTracker.swift @@ -0,0 +1,80 @@ +import SwiftUI + +/// This modifier tracks whether the window is the key window in the isKeyWindow environment value. +struct WindowObservationModifier: ViewModifier { + @StateObject var windowObserver: WindowObserver = WindowObserver() + + func body(content: Content) -> some View { + content.background( + HostingWindowFinder { [weak windowObserver] window in + windowObserver?.window = window + } + ).environment(\.isKeyWindow, windowObserver.isKeyWindow) + } +} + +extension EnvironmentValues { + struct IsKeyWindowKey: EnvironmentKey { + static var defaultValue: Bool = false + typealias Value = Bool + } + + fileprivate(set) var isKeyWindow: Bool { + get { + self[IsKeyWindowKey.self] + } + set { + self[IsKeyWindowKey.self] = newValue + } + } +} + +class WindowObserver: ObservableObject { + @Published public private(set) var isKeyWindow: Bool = false + + private var becomeKeyobserver: NSObjectProtocol? + private var resignKeyobserver: NSObjectProtocol? + + weak var window: NSWindow? { + didSet { + self.isKeyWindow = window?.isKeyWindow ?? false + guard let window = window else { + self.becomeKeyobserver = nil + self.resignKeyobserver = nil + return + } + + self.becomeKeyobserver = NotificationCenter.default.addObserver( + forName: NSWindow.didBecomeKeyNotification, + object: window, + queue: .main + ) { (n) in + self.isKeyWindow = true + } + + self.resignKeyobserver = NotificationCenter.default.addObserver( + forName: NSWindow.didResignKeyNotification, + object: window, + queue: .main + ) { (n) in + self.isKeyWindow = false + } + } + } +} + +/// This view calls the callback with the window value that hosts the view. +struct HostingWindowFinder: NSViewRepresentable { + var callback: (NSWindow?) -> () + + func makeNSView(context: Self.Context) -> NSView { + let view = NSView() + view.translatesAutoresizingMaskIntoConstraints = false + DispatchQueue.main.async { [weak view] in + self.callback(view?.window) + } + return view + } + + func updateNSView(_ nsView: NSView, context: Context) {} +} diff --git a/pkg/pixman/build.zig b/pkg/pixman/build.zig index 16df1f85d..4a1650a21 100644 --- a/pkg/pixman/build.zig +++ b/pkg/pixman/build.zig @@ -138,7 +138,7 @@ const srcs = &.{ root ++ "pixman/pixman-region16.c", root ++ "pixman/pixman-region32.c", root ++ "pixman/pixman-solid-fill.c", - root ++ "pixman/pixman-timer.c", + //root ++ "pixman/pixman-timer.c", root ++ "pixman/pixman-trap.c", root ++ "pixman/pixman-utils.c", }; diff --git a/src/App.zig b/src/App.zig index e1e01bd00..5857154c0 100644 --- a/src/App.zig +++ b/src/App.zig @@ -5,10 +5,13 @@ const App = @This(); const std = @import("std"); const builtin = @import("builtin"); +const assert = std.debug.assert; const Allocator = std.mem.Allocator; +const build_config = @import("build_config.zig"); const apprt = @import("apprt.zig"); const Window = @import("Window.zig"); const tracy = @import("tracy"); +const input = @import("input.zig"); const Config = @import("config.zig").Config; const BlockingQueue = @import("./blocking_queue.zig").BlockingQueue; const renderer = @import("renderer.zig"); @@ -46,9 +49,11 @@ quit: bool, /// Mac settings darwin: if (Darwin.enabled) Darwin else void, -/// Mac-specific settings +/// Mac-specific settings. This is only enabled when the target is +/// Mac and the artifact is a standalone exe. We don't target libs because +/// the embedded API doesn't do windowing. pub const Darwin = struct { - pub const enabled = builtin.target.isDarwin(); + pub const enabled = builtin.target.isDarwin() and build_config.artifact == .exe; tabbing_id: *macos.foundation.String, @@ -61,9 +66,13 @@ pub const Darwin = struct { /// Initialize the main app instance. This creates the main window, sets /// up the renderer state, compiles the shaders, etc. This is the primary /// "startup" logic. -pub fn create(alloc: Allocator, config: *const Config) !*App { +pub fn create( + alloc: Allocator, + rt_opts: apprt.runtime.App.Options, + config: *const Config, +) !*App { // Initialize app runtime - var app_backend = try apprt.runtime.App.init(); + var app_backend = try apprt.runtime.App.init(rt_opts); errdefer app_backend.terminate(); // The mailbox for messaging this thread @@ -86,8 +95,9 @@ pub fn create(alloc: Allocator, config: *const Config) !*App { }; errdefer app.windows.deinit(alloc); - // On Mac, we enable window tabbing - if (comptime builtin.target.isDarwin()) { + // On Mac, we enable window tabbing. We only do this if we're building + // a standalone exe. In embedded mode the host app handles this for us. + if (Darwin.enabled) { const NSWindow = objc.Class.getClass("NSWindow").?; NSWindow.msgSend(void, objc.sel("setAllowsAutomaticWindowTabbing:"), .{true}); @@ -106,9 +116,6 @@ pub fn create(alloc: Allocator, config: *const Config) !*App { } errdefer if (comptime builtin.target.isDarwin()) app.darwin.deinit(); - // Create the first window - _ = try app.newWindow(.{}); - return app; } @@ -116,7 +123,7 @@ pub fn destroy(self: *App) void { // Clean up all our windows for (self.windows.items) |window| window.destroy(); self.windows.deinit(self.alloc); - if (comptime builtin.target.isDarwin()) self.darwin.deinit(); + if (Darwin.enabled) self.darwin.deinit(); self.mailbox.destroy(self.alloc); self.alloc.destroy(self); @@ -134,24 +141,61 @@ pub fn wakeup(self: App) void { /// application quits or every window is closed. pub fn run(self: *App) !void { while (!self.quit and self.windows.items.len > 0) { - // Block for any events. - try self.runtime.wait(); + try self.tick(); + } +} - // If any windows are closing, destroy them - var i: usize = 0; - while (i < self.windows.items.len) { - const window = self.windows.items[i]; - if (window.shouldClose()) { - window.destroy(); - _ = self.windows.swapRemove(i); - continue; - } +/// Tick ticks the app loop. This will drain our mailbox and process those +/// events. +pub fn tick(self: *App) !void { + // Block for any events. + try self.runtime.wait(); - i += 1; + // If any windows are closing, destroy them + var i: usize = 0; + while (i < self.windows.items.len) { + const window = self.windows.items[i]; + if (window.shouldClose()) { + window.destroy(); + _ = self.windows.swapRemove(i); + continue; } - // Drain our mailbox only if we're not quitting. - if (!self.quit) try self.drainMailbox(); + i += 1; + } + + // Drain our mailbox only if we're not quitting. + if (!self.quit) try self.drainMailbox(); +} + +/// Create a new window. This can be called only on the main thread. This +/// can be called prior to ever running the app loop. +pub fn newWindow(self: *App, msg: Message.NewWindow) !*Window { + var window = try Window.create(self.alloc, self, self.config, msg.runtime); + errdefer window.destroy(); + + try self.windows.append(self.alloc, window); + errdefer _ = self.windows.pop(); + + // Set initial font size if given + if (msg.font_size) |size| window.setFontSize(size); + + return window; +} + +/// Close a window and free all resources associated with it. This can +/// only be called from the main thread. +pub fn closeWindow(self: *App, window: *Window) void { + var i: usize = 0; + while (i < self.windows.items.len) { + const current = self.windows.items[i]; + if (window == current) { + window.destroy(); + _ = self.windows.swapRemove(i); + return; + } + + i += 1; } } @@ -168,19 +212,6 @@ fn drainMailbox(self: *App) !void { } } -/// Create a new window -fn newWindow(self: *App, msg: Message.NewWindow) !*Window { - var window = try Window.create(self.alloc, self, self.config); - errdefer window.destroy(); - try self.windows.append(self.alloc, window); - errdefer _ = self.windows.pop(); - - // Set initial font size if given - if (msg.font_size) |size| window.setFontSize(size); - - return window; -} - /// Create a new tab in the parent window fn newTab(self: *App, msg: Message.NewWindow) !void { if (comptime !builtin.target.isDarwin()) { @@ -188,6 +219,13 @@ fn newTab(self: *App, msg: Message.NewWindow) !void { return; } + // In embedded mode, it is up to the embedder to implement tabbing + // on their own. + if (comptime build_config.artifact != .exe) { + log.warn("tabbing is not supported in embedded mode", .{}); + return; + } + const parent = msg.parent orelse { log.warn("parent must be set in new_tab message", .{}); return; @@ -258,6 +296,9 @@ pub const Message = union(enum) { }, const NewWindow = struct { + /// Runtime-specific window options. + runtime: apprt.runtime.Window.Options = .{}, + /// The parent window, only used for new tabs. parent: ?*Window = null, @@ -273,8 +314,7 @@ pub const Wasm = if (!builtin.target.isWasm()) struct {} else struct { const alloc = wasm.alloc; // export fn app_new(config: *Config) ?*App { - // return app_new_(config) catch |err| { - // log.err("error initializing app err={}", .{err}); + // return app_new_(config) catch |err| { log.err("error initializing app err={}", .{err}); // return null; // }; // } @@ -295,3 +335,131 @@ pub const Wasm = if (!builtin.target.isWasm()) struct {} else struct { // } // } }; + +// C API +pub const CAPI = struct { + const global = &@import("main.zig").state; + + /// Create a new app. + export fn ghostty_app_new( + opts: *const apprt.runtime.App.Options, + config: *const Config, + ) ?*App { + return app_new_(opts, config) catch |err| { + log.err("error initializing app err={}", .{err}); + return null; + }; + } + + fn app_new_( + opts: *const apprt.runtime.App.Options, + config: *const Config, + ) !*App { + const app = try App.create(global.alloc, opts.*, config); + errdefer app.destroy(); + return app; + } + + /// Tick the event loop. This should be called whenever the "wakeup" + /// callback is invoked for the runtime. + export fn ghostty_app_tick(v: *App) void { + v.tick() catch |err| { + log.err("error app tick err={}", .{err}); + }; + } + + export fn ghostty_app_free(ptr: ?*App) void { + if (ptr) |v| { + v.destroy(); + v.alloc.destroy(v); + } + } + + /// Create a new surface as part of an app. + export fn ghostty_surface_new( + app: *App, + opts: *const apprt.runtime.Window.Options, + ) ?*Window { + return surface_new_(app, opts) catch |err| { + log.err("error initializing surface err={}", .{err}); + return null; + }; + } + + fn surface_new_( + app: *App, + opts: *const apprt.runtime.Window.Options, + ) !*Window { + const w = try app.newWindow(.{ + .runtime = opts.*, + }); + return w; + } + + export fn ghostty_surface_free(ptr: ?*Window) void { + if (ptr) |v| v.app.closeWindow(v); + } + + /// Tell the surface that it needs to schedule a render + export fn ghostty_surface_refresh(win: *Window) void { + win.window.refresh(); + } + + /// Update the size of a surface. This will trigger resize notifications + /// to the pty and the renderer. + export fn ghostty_surface_set_size(win: *Window, w: u32, h: u32) void { + win.window.updateSize(w, h); + } + + /// Update the content scale of the surface. + export fn ghostty_surface_set_content_scale(win: *Window, x: f64, y: f64) void { + win.window.updateContentScale(x, y); + } + + /// Update the focused state of a surface. + export fn ghostty_surface_set_focus(win: *Window, focused: bool) void { + win.window.focusCallback(focused); + } + + /// Tell the surface that it needs to schedule a render + export fn ghostty_surface_key( + win: *Window, + action: input.Action, + key: input.Key, + mods: c_int, + ) void { + win.window.keyCallback( + action, + key, + @bitCast(input.Mods, @truncate(u8, @bitCast(c_uint, mods))), + ); + } + + /// Tell the surface that it needs to schedule a render + export fn ghostty_surface_char(win: *Window, codepoint: u32) void { + win.window.charCallback(codepoint); + } + + /// Tell the surface that it needs to schedule a render + export fn ghostty_surface_mouse_button( + win: *Window, + action: input.MouseButtonState, + button: input.MouseButton, + mods: c_int, + ) void { + win.window.mouseButtonCallback( + action, + button, + @bitCast(input.Mods, @truncate(u8, @bitCast(c_uint, mods))), + ); + } + + /// Update the mouse position within the view. + export fn ghostty_surface_mouse_pos(win: *Window, x: f64, y: f64) void { + win.window.cursorPosCallback(x, y); + } + + export fn ghostty_surface_mouse_scroll(win: *Window, x: f64, y: f64) void { + win.window.scrollCallback(x, y); + } +}; diff --git a/src/DevMode.zig b/src/DevMode.zig index 2df187b82..2b037ce2a 100644 --- a/src/DevMode.zig +++ b/src/DevMode.zig @@ -4,6 +4,7 @@ const DevMode = @This(); const std = @import("std"); const builtin = @import("builtin"); +const build_config = @import("build_config.zig"); const imgui = @import("imgui"); const Allocator = std.mem.Allocator; const assert = std.debug.assert; @@ -15,7 +16,8 @@ const Config = @import("config.zig").Config; /// If this is false, the rest of the terminal will be compiled without /// dev mode support at all. -pub const enabled = !builtin.target.isWasm(); +/// TODO: remove this and use build_config everywhere +pub const enabled = build_config.devmode_enabled; /// The global DevMode instance that can be used app-wide. Assume all functions /// are NOT thread-safe unless otherwise noted. diff --git a/src/Pty.zig b/src/Pty.zig index a09b2519e..c61c9feb9 100644 --- a/src/Pty.zig +++ b/src/Pty.zig @@ -19,6 +19,8 @@ const c = switch (builtin.os.tag) { }), }; +const log = std.log.scoped(.pty); + // https://github.com/ziglang/zig/issues/13277 // Once above is fixed, use `c.TIOCSCTTY` const TIOCSCTTY = if (builtin.os.tag == .macos) 536900705 else c.TIOCSCTTY; @@ -102,8 +104,13 @@ pub fn childPreExec(self: Pty) !void { if (setsid() < 0) return error.ProcessGroupFailed; // Set controlling terminal - if (c.ioctl(self.slave, TIOCSCTTY, @as(c_ulong, 0)) < 0) - return error.SetControllingTerminalFailed; + switch (std.os.system.getErrno(c.ioctl(self.slave, TIOCSCTTY, @as(c_ulong, 0)))) { + .SUCCESS => {}, + else => |err| { + log.err("error setting controlling terminal errno={}", .{err}); + return error.SetControllingTerminalFailed; + }, + } // Can close master/slave pair now std.os.close(self.slave); diff --git a/src/Window.zig b/src/Window.zig index c43f68b08..a104b1ee6 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -128,12 +128,17 @@ const Mouse = struct { /// Create a new window. This allocates and returns a pointer because we /// need a stable pointer for user data callbacks. Therefore, a stack-only /// initialization is not currently possible. -pub fn create(alloc: Allocator, app: *App, config: *const Config) !*Window { +pub fn create( + alloc: Allocator, + app: *App, + config: *const Config, + rt_opts: apprt.runtime.Window.Options, +) !*Window { var self = try alloc.create(Window); errdefer alloc.destroy(self); // Create the windowing system - var window = try apprt.runtime.Window.init(app, self); + var window = try apprt.runtime.Window.init(app, self, rt_opts); errdefer window.deinit(); // Initialize our renderer with our initialized windowing system. @@ -1287,7 +1292,7 @@ pub fn mouseButtonCallback( } // Always record our latest mouse state - self.mouse.click_state[@enumToInt(button)] = action; + self.mouse.click_state[@intCast(usize, @enumToInt(button))] = action; self.mouse.mods = @bitCast(input.Mods, mods); self.renderer_state.mutex.lock(); diff --git a/src/apprt.zig b/src/apprt.zig index 4246d5a3c..36e2a2571 100644 --- a/src/apprt.zig +++ b/src/apprt.zig @@ -9,20 +9,22 @@ //! logic as possible, and to only reach out to platform-specific implementation //! code when absolutely necessary. const builtin = @import("builtin"); +const build_config = @import("build_config.zig"); pub usingnamespace @import("apprt/structs.zig"); pub const glfw = @import("apprt/glfw.zig"); pub const browser = @import("apprt/browser.zig"); +pub const embedded = @import("apprt/embedded.zig"); pub const Window = @import("apprt/Window.zig"); /// The implementation to use for the app runtime. This is comptime chosen /// so that every build has exactly one application runtime implementation. /// Note: it is very rare to use Runtime directly; most usage will use /// Window or something. -pub const runtime = if (builtin.target.isWasm()) - browser -else switch (builtin.os.tag) { - else => glfw, +pub const runtime = switch (build_config.artifact) { + .exe => glfw, + .lib => embedded, + .wasm_module => browser, }; test { diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig new file mode 100644 index 000000000..8a1f345b0 --- /dev/null +++ b/src/apprt/embedded.zig @@ -0,0 +1,238 @@ +//! Application runtime for the embedded version of Ghostty. The embedded +//! version is when Ghostty is embedded within a parent host application, +//! rather than owning the application lifecycle itself. This is used for +//! example for the macOS build of Ghostty so that we can use a native +//! Swift+XCode-based application. + +const std = @import("std"); +const builtin = @import("builtin"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const objc = @import("objc"); +const apprt = @import("../apprt.zig"); +const input = @import("../input.zig"); +const CoreApp = @import("../App.zig"); +const CoreWindow = @import("../Window.zig"); + +const log = std.log.scoped(.embedded_window); + +pub const App = struct { + /// Because we only expect the embedding API to be used in embedded + /// environments, the options are extern so that we can expose it + /// directly to a C callconv and not pay for any translation costs. + /// + /// C type: ghostty_runtime_config_s + pub const Options = extern struct { + /// Userdata that is passed to all the callbacks. + userdata: ?*anyopaque = null, + + /// Callback called to wakeup the event loop. This should trigger + /// a full tick of the app loop. + wakeup: *const fn (?*anyopaque) callconv(.C) void, + + /// Called to set the title of the window. + set_title: *const fn (?*anyopaque, [*]const u8) callconv(.C) void, + }; + + opts: Options, + + pub fn init(opts: Options) !App { + return .{ .opts = opts }; + } + + pub fn terminate(self: App) void { + _ = self; + } + + pub fn wakeup(self: App) !void { + self.opts.wakeup(self.opts.userdata); + } + + pub fn wait(self: App) !void { + _ = self; + } +}; + +pub const Window = struct { + nsview: objc.Object, + core_win: *CoreWindow, + content_scale: apprt.ContentScale, + size: apprt.WindowSize, + cursor_pos: apprt.CursorPos, + opts: Options, + + pub const Options = extern struct { + /// Userdata passed to some of the callbacks. + userdata: ?*anyopaque = null, + + /// The pointer to the backing NSView for the surface. + nsview: *anyopaque = undefined, + + /// The scale factor of the screen. + scale_factor: f64 = 1, + }; + + pub fn init(app: *const CoreApp, core_win: *CoreWindow, opts: Options) !Window { + _ = app; + + return .{ + .core_win = core_win, + .nsview = objc.Object.fromId(opts.nsview), + .content_scale = .{ + .x = @floatCast(f32, opts.scale_factor), + .y = @floatCast(f32, opts.scale_factor), + }, + .size = .{ .width = 800, .height = 600 }, + .cursor_pos = .{ .x = 0, .y = 0 }, + .opts = opts, + }; + } + + pub fn deinit(self: *Window) void { + _ = self; + } + + pub fn getContentScale(self: *const Window) !apprt.ContentScale { + return self.content_scale; + } + + pub fn getSize(self: *const Window) !apprt.WindowSize { + return self.size; + } + + pub fn setSizeLimits(self: *Window, min: apprt.WindowSize, max_: ?apprt.WindowSize) !void { + _ = self; + _ = min; + _ = max_; + } + + pub fn setTitle(self: *Window, slice: [:0]const u8) !void { + self.core_win.app.runtime.opts.set_title( + self.opts.userdata, + slice.ptr, + ); + } + + pub fn getClipboardString(self: *const Window) ![:0]const u8 { + _ = self; + return ""; + } + + pub fn setClipboardString(self: *const Window, val: [:0]const u8) !void { + _ = self; + _ = val; + } + + pub fn setShouldClose(self: *Window) void { + _ = self; + } + + pub fn shouldClose(self: *const Window) bool { + _ = self; + return false; + } + + pub fn getCursorPos(self: *const Window) !apprt.CursorPos { + return self.cursor_pos; + } + + pub fn refresh(self: *Window) void { + self.core_win.refreshCallback() catch |err| { + log.err("error in refresh callback err={}", .{err}); + return; + }; + } + + pub fn updateContentScale(self: *Window, x: f64, y: f64) void { + self.content_scale = .{ + .x = @floatCast(f32, x), + .y = @floatCast(f32, y), + }; + } + + pub fn updateSize(self: *Window, width: u32, height: u32) void { + self.size = .{ + .width = width, + .height = height, + }; + + // Call the primary callback. + self.core_win.sizeCallback(self.size) catch |err| { + log.err("error in size callback err={}", .{err}); + return; + }; + } + + pub fn mouseButtonCallback( + self: *const Window, + action: input.MouseButtonState, + button: input.MouseButton, + mods: input.Mods, + ) void { + self.core_win.mouseButtonCallback(action, button, mods) catch |err| { + log.err("error in mouse button callback err={}", .{err}); + return; + }; + } + + pub fn scrollCallback(self: *const Window, xoff: f64, yoff: f64) void { + self.core_win.scrollCallback(xoff, yoff) catch |err| { + log.err("error in scroll callback err={}", .{err}); + return; + }; + } + + pub fn cursorPosCallback(self: *Window, x: f64, y: f64) void { + // Convert our unscaled x/y to scaled. + self.cursor_pos = self.core_win.window.cursorPosToPixels(.{ + .x = @floatCast(f32, x), + .y = @floatCast(f32, y), + }) catch |err| { + log.err( + "error converting cursor pos to scaled pixels in cursor pos callback err={}", + .{err}, + ); + return; + }; + + self.core_win.cursorPosCallback(self.cursor_pos) catch |err| { + log.err("error in cursor pos callback err={}", .{err}); + return; + }; + } + + pub fn keyCallback( + self: *const Window, + action: input.Action, + key: input.Key, + mods: input.Mods, + ) void { + // log.warn("key action={} key={} mods={}", .{ action, key, mods }); + self.core_win.keyCallback(action, key, mods) catch |err| { + log.err("error in key callback err={}", .{err}); + return; + }; + } + + pub fn charCallback(self: *const Window, cp_: u32) void { + const cp = std.math.cast(u21, cp_) orelse return; + self.core_win.charCallback(cp) catch |err| { + log.err("error in char callback err={}", .{err}); + return; + }; + } + + pub fn focusCallback(self: *const Window, focused: bool) void { + self.core_win.focusCallback(focused) catch |err| { + log.err("error in focus callback err={}", .{err}); + return; + }; + } + + /// The cursor position from the host directly is in screen coordinates but + /// all our interface works in pixels. + fn cursorPosToPixels(self: *const Window, pos: apprt.CursorPos) !apprt.CursorPos { + const scale = try self.getContentScale(); + return .{ .x = pos.x * scale.x, .y = pos.y * scale.y }; + } +}; diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 0cb5fab8e..1e03f30e9 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -26,7 +26,9 @@ const glfwNative = glfw.Native(.{ const log = std.log.scoped(.glfw); pub const App = struct { - pub fn init() !App { + pub const Options = struct {}; + + pub fn init(_: Options) !App { if (!glfw.init(.{})) return error.GlfwInitFailed; return .{}; } @@ -56,7 +58,11 @@ pub const Window = struct { /// The glfw mouse cursor handle. cursor: glfw.Cursor, - pub fn init(app: *const CoreApp, core_win: *CoreWindow) !Window { + pub const Options = struct {}; + + pub fn init(app: *const CoreApp, core_win: *CoreWindow, opts: Options) !Window { + _ = opts; + // Create our window const win = glfw.Window.create( 640, diff --git a/src/build/LibtoolStep.zig b/src/build/LibtoolStep.zig new file mode 100644 index 000000000..1d114f097 --- /dev/null +++ b/src/build/LibtoolStep.zig @@ -0,0 +1,65 @@ +//! A zig builder step that runs "libtool" against a list of libraries +//! in order to create a single combined static library. +const LibtoolStep = @This(); + +const std = @import("std"); +const Step = std.build.Step; +const FileSource = std.build.FileSource; +const GeneratedFile = std.build.GeneratedFile; + +pub const Options = struct { + /// The name of this step. + name: []const u8, + + /// The filename (not the path) of the file to create. This will + /// be placed in a unique hashed directory. Use out_path to access. + out_name: []const u8, + + /// Library files (.a) to combine. + sources: []FileSource, +}; + +step: Step, +builder: *std.Build, + +/// Resulting binary +out_path: GeneratedFile, + +/// See Options +name: []const u8, +out_name: []const u8, +sources: []FileSource, + +pub fn create(builder: *std.Build, opts: Options) *LibtoolStep { + const self = builder.allocator.create(LibtoolStep) catch @panic("OOM"); + self.* = .{ + .step = Step.init(.custom, builder.fmt("lipo {s}", .{opts.name}), builder.allocator, make), + .builder = builder, + .name = opts.name, + .out_path = .{ .step = &self.step }, + .out_name = opts.out_name, + .sources = opts.sources, + }; + return self; +} + +fn make(step: *Step) !void { + const self = @fieldParentPtr(LibtoolStep, "step", step); + + // We use a RunStep here to ease our configuration. + const run = std.build.RunStep.create(self.builder, self.builder.fmt( + "libtool {s}", + .{self.name}, + )); + run.addArgs(&.{ + "libtool", + "-static", + "-o", + }); + try run.argv.append(.{ .output = .{ + .generated_file = &self.out_path, + .basename = self.out_name, + } }); + for (self.sources) |source| run.addFileSourceArg(source); + try run.step.make(); +} diff --git a/src/build/LipoStep.zig b/src/build/LipoStep.zig new file mode 100644 index 000000000..f073ca15f --- /dev/null +++ b/src/build/LipoStep.zig @@ -0,0 +1,64 @@ +//! A zig builder step that runs "lipo" on two binaries to create +//! a universal binary. +const LipoStep = @This(); + +const std = @import("std"); +const Step = std.build.Step; +const FileSource = std.build.FileSource; +const GeneratedFile = std.build.GeneratedFile; + +pub const Options = struct { + /// The name of the xcframework to create. + name: []const u8, + + /// The filename (not the path) of the file to create. + out_name: []const u8, + + /// Library file (dylib, a) to package. + input_a: FileSource, + input_b: FileSource, +}; + +step: Step, +builder: *std.build.Builder, + +/// Resulting binary +out_path: GeneratedFile, + +/// See Options +name: []const u8, +out_name: []const u8, +input_a: FileSource, +input_b: FileSource, + +pub fn create(builder: *std.build.Builder, opts: Options) *LipoStep { + const self = builder.allocator.create(LipoStep) catch @panic("OOM"); + self.* = .{ + .step = Step.init(.custom, builder.fmt("lipo {s}", .{opts.name}), builder.allocator, make), + .builder = builder, + .name = opts.name, + .out_path = .{ .step = &self.step }, + .out_name = opts.out_name, + .input_a = opts.input_a, + .input_b = opts.input_b, + }; + return self; +} + +fn make(step: *Step) !void { + const self = @fieldParentPtr(LipoStep, "step", step); + + // We use a RunStep here to ease our configuration. + const run = std.build.RunStep.create(self.builder, self.builder.fmt( + "lipo {s}", + .{self.name}, + )); + run.addArgs(&.{ "lipo", "-create", "-output" }); + try run.argv.append(.{ .output = .{ + .generated_file = &self.out_path, + .basename = self.out_name, + } }); + run.addFileSourceArg(self.input_a); + run.addFileSourceArg(self.input_b); + try run.step.make(); +} diff --git a/src/build/XCFrameworkStep.zig b/src/build/XCFrameworkStep.zig new file mode 100644 index 000000000..120609bf4 --- /dev/null +++ b/src/build/XCFrameworkStep.zig @@ -0,0 +1,81 @@ +//! A zig builder step that runs "swift build" in the context of +//! a Swift project managed with SwiftPM. This is primarily meant to build +//! executables currently since that is what we build. +const XCFrameworkStep = @This(); + +const std = @import("std"); +const Step = std.build.Step; +const GeneratedFile = std.build.GeneratedFile; + +pub const Options = struct { + /// The name of the xcframework to create. + name: []const u8, + + /// The path to write the framework + out_path: []const u8, + + /// Library file (dylib, a) to package. + library: std.build.FileSource, + + /// Path to a directory with the headers. + headers: std.build.FileSource, +}; + +step: Step, +builder: *std.build.Builder, + +/// See Options +name: []const u8, +out_path: []const u8, +library: std.build.FileSource, +headers: std.build.FileSource, + +pub fn create(builder: *std.build.Builder, opts: Options) *XCFrameworkStep { + const self = builder.allocator.create(XCFrameworkStep) catch @panic("OOM"); + self.* = .{ + .step = Step.init(.custom, builder.fmt( + "xcframework {s}", + .{opts.name}, + ), builder.allocator, make), + .builder = builder, + .name = opts.name, + .out_path = opts.out_path, + .library = opts.library, + .headers = opts.headers, + }; + return self; +} + +fn make(step: *Step) !void { + const self = @fieldParentPtr(XCFrameworkStep, "step", step); + + // TODO: use the zig cache system when it is in the stdlib + // https://github.com/ziglang/zig/pull/14571 + const output_path = self.out_path; + + // We use a RunStep here to ease our configuration. + { + const run = std.build.RunStep.create(self.builder, self.builder.fmt( + "xcframework delete {s}", + .{self.name}, + )); + run.condition = .always; + run.addArgs(&.{ "rm", "-rf", output_path }); + try run.step.make(); + } + { + const run = std.build.RunStep.create(self.builder, self.builder.fmt( + "xcframework {s}", + .{self.name}, + )); + run.condition = .always; + run.addArgs(&.{ "xcodebuild", "-create-xcframework" }); + run.addArg("-library"); + run.addFileSourceArg(self.library); + run.addArg("-headers"); + run.addFileSourceArg(self.headers); + run.addArg("-output"); + run.addArg(output_path); + try run.step.make(); + } +} diff --git a/src/build_config.zig b/src/build_config.zig new file mode 100644 index 000000000..2baaa48b8 --- /dev/null +++ b/src/build_config.zig @@ -0,0 +1,43 @@ +//! Build options, available at comptime. Used to configure features. This +//! will reproduce some of the fields from builtin and build_options just +//! so we can limit the amount of imports we need AND give us the ability +//! to shim logic and values into them later. +const std = @import("std"); +const builtin = @import("builtin"); +const assert = std.debug.assert; + +/// The artifact we're producing. This can be used to determine if we're +/// building a standalone exe, an embedded lib, etc. +pub const artifact = Artifact.detect(); + +/// Whether our devmode UI is enabled or not. This requires imgui to be +/// compiled. +pub const devmode_enabled = artifact == .exe; + +pub const Artifact = enum { + /// Standalone executable + exe, + + /// Embeddable library + lib, + + /// The WASM-targetted module. + wasm_module, + + pub fn detect() Artifact { + if (builtin.target.isWasm()) { + assert(builtin.output_mode == .Obj); + assert(builtin.link_mode == .Static); + return .wasm_module; + } + + return switch (builtin.output_mode) { + .Exe => .exe, + .Lib => .lib, + else => { + @compileLog(builtin.output_mode); + @compileError("unsupported artifact output mode"); + }, + }; + } +}; diff --git a/src/config.zig b/src/config.zig index 0e9ee8943..360959428 100644 --- a/src/config.zig +++ b/src/config.zig @@ -583,6 +583,58 @@ pub const Wasm = if (!builtin.target.isWasm()) struct {} else struct { } }; +// C API. +pub const CAPI = struct { + const global = &@import("main.zig").state; + const cli_args = @import("cli_args.zig"); + + /// Create a new configuration filled with the initial default values. + export fn ghostty_config_new() ?*Config { + const result = global.alloc.create(Config) catch |err| { + log.err("error allocating config err={}", .{err}); + return null; + }; + + result.* = Config.default(global.alloc) catch |err| { + log.err("error creating config err={}", .{err}); + return null; + }; + + return result; + } + + export fn ghostty_config_free(ptr: ?*Config) void { + if (ptr) |v| { + v.deinit(); + global.alloc.destroy(v); + } + } + + /// Load the configuration from a string in the same format as + /// the file-based syntax for the desktop version of the terminal. + export fn ghostty_config_load_string( + self: *Config, + str: [*]const u8, + len: usize, + ) void { + config_load_string_(self, str[0..len]) catch |err| { + log.err("error loading config err={}", .{err}); + }; + } + + fn config_load_string_(self: *Config, str: []const u8) !void { + var fbs = std.io.fixedBufferStream(str); + var iter = cli_args.lineIterator(fbs.reader()); + try cli_args.parse(Config, global.alloc, self, &iter); + } + + export fn ghostty_config_finalize(self: *Config) void { + self.finalize() catch |err| { + log.err("error finalizing config err={}", .{err}); + }; + } +}; + test { std.testing.refAllDecls(@This()); } diff --git a/src/input/key.zig b/src/input/key.zig index 78b5175da..cf0cb4bca 100644 --- a/src/input/key.zig +++ b/src/input/key.zig @@ -3,7 +3,9 @@ const Allocator = std.mem.Allocator; /// A bitmask for all key modifiers. This is taken directly from the /// GLFW representation, but we use this generically. -pub const Mods = packed struct { +/// +/// IMPORTANT: Any changes here update include/ghostty.h +pub const Mods = packed struct(u8) { shift: bool = false, ctrl: bool = false, alt: bool = false, @@ -11,10 +13,23 @@ pub const Mods = packed struct { caps_lock: bool = false, num_lock: bool = false, _padding: u2 = 0, + + // For our own understanding + test { + const testing = std.testing; + try testing.expectEqual(@bitCast(u8, Mods{}), @as(u8, 0b0)); + try testing.expectEqual( + @bitCast(u8, Mods{ .shift = true }), + @as(u8, 0b0000_0001), + ); + } }; -/// The action associated with an input event. -pub const Action = enum { +/// The action associated with an input event. This is backed by a c_int +/// so that we can use the enum as-is for our embedding API. +/// +/// IMPORTANT: Any changes here update include/ghostty.h +pub const Action = enum(c_int) { release, press, repeat, @@ -25,7 +40,11 @@ pub const Action = enum { /// this only needs to accomodate what maps to a key. If a key is not bound /// to anything and the key can be mapped to a printable character, then that /// unicode character is sent directly to the pty. -pub const Key = enum { +/// +/// This is backed by a c_int so we can use this as-is for our embedding API. +/// +/// IMPORTANT: Any changes here update include/ghostty.h +pub const Key = enum(c_int) { invalid, // a-z diff --git a/src/input/mouse.zig b/src/input/mouse.zig index 113521813..e1059e4c0 100644 --- a/src/input/mouse.zig +++ b/src/input/mouse.zig @@ -1,7 +1,11 @@ /// The state of a mouse button. -pub const MouseButtonState = enum(u1) { - release = 0, - press = 1, +/// +/// This is backed by a c_int so we can use this as-is for our embedding API. +/// +/// IMPORTANT: Any changes here update include/ghostty.h +pub const MouseButtonState = enum(c_int) { + release, + press, }; /// Possible mouse buttons. We only track up to 11 because thats the maximum @@ -10,7 +14,11 @@ pub const MouseButtonState = enum(u1) { /// /// Its a bit silly to name numbers like this but given its a restricted /// set, it feels better than passing around raw numeric literals. -pub const MouseButton = enum(u4) { +/// +/// This is backed by a c_int so we can use this as-is for our embedding API. +/// +/// IMPORTANT: Any changes here update include/ghostty.h +pub const MouseButton = enum(c_int) { const Self = @This(); /// The maximum value in this enum. This can be used to create a densely diff --git a/src/main.zig b/src/main.zig index 61dc5e2ba..b84322af6 100644 --- a/src/main.zig +++ b/src/main.zig @@ -2,72 +2,30 @@ const std = @import("std"); const builtin = @import("builtin"); const options = @import("build_options"); const glfw = @import("glfw"); -const fontconfig = @import("fontconfig"); -const freetype = @import("freetype"); -const harfbuzz = @import("harfbuzz"); const macos = @import("macos"); const tracy = @import("tracy"); +const internal_os = @import("os/main.zig"); const xev = @import("xev"); +const fontconfig = @import("fontconfig"); +const harfbuzz = @import("harfbuzz"); const renderer = @import("renderer.zig"); const xdg = @import("xdg.zig"); -const internal_os = @import("os/main.zig"); const App = @import("App.zig"); const cli_args = @import("cli_args.zig"); const Config = @import("config.zig").Config; +const Ghostty = @import("main_c.zig").Ghostty; + +/// Global process state. This is initialized in main() for exe artifacts +/// and by ghostty_init() for lib artifacts. This should ONLY be used by +/// the C API. The Zig API should NOT use any global state and should +/// rely on allocators being passed in as parameters. +pub var state: GlobalState = undefined; pub fn main() !void { - // Output some debug information right away - std.log.info("dependency harfbuzz={s}", .{harfbuzz.versionString()}); - if (options.fontconfig) { - std.log.info("dependency fontconfig={d}", .{fontconfig.version()}); - } - std.log.info("renderer={}", .{renderer.Renderer}); - std.log.info("libxev backend={}", .{xev.backend}); - - // First things first, we fix our file descriptors - internal_os.fixMaxFiles(); - - // We need to make sure the process locale is set properly. Locale - // affects a lot of behaviors in a shell. - internal_os.ensureLocale(); - - const GPA = std.heap.GeneralPurposeAllocator(.{}); - var gpa: ?GPA = gpa: { - // Use the libc allocator if it is available beacuse it is WAY - // faster than GPA. We only do this in release modes so that we - // can get easy memory leak detection in debug modes. - if (builtin.link_libc) { - if (switch (builtin.mode) { - .ReleaseSafe, .ReleaseFast => true, - - // We also use it if we can detect we're running under - // Valgrind since Valgrind only instruments the C allocator - else => std.valgrind.runningOnValgrind() > 0, - }) break :gpa null; - } - - break :gpa GPA{}; - }; - defer if (gpa) |*value| { - // We want to ensure that we deinit the GPA because this is - // the point at which it will output if there were safety violations. - _ = value.deinit(); - }; - - const alloc = alloc: { - const base = if (gpa) |*value| - value.allocator() - else if (builtin.link_libc) - std.heap.c_allocator - else - unreachable; - - // If we're tracing, wrap the allocator - if (!tracy.enabled) break :alloc base; - var tracy_alloc = tracy.allocator(base, null); - break :alloc tracy_alloc.allocator(); - }; + state.init(); + defer state.deinit(); + const alloc = state.alloc; // Try reading our config var config = try Config.default(alloc); @@ -133,9 +91,10 @@ pub fn main() !void { // We want to log all our errors glfw.setErrorCallback(glfwErrorCallback); - // Run our app - var app = try App.create(alloc, &config); + // Run our app with a single initial window to start. + var app = try App.create(alloc, .{}, &config); defer app.destroy(); + _ = try app.newWindow(.{}); try app.run(); } @@ -206,6 +165,80 @@ fn glfwErrorCallback(code: glfw.ErrorCode, desc: [:0]const u8) void { } } +/// This represents the global process state. There should only +/// be one of these at any given moment. This is extracted into a dedicated +/// struct because it is reused by main and the static C lib. +pub const GlobalState = struct { + const GPA = std.heap.GeneralPurposeAllocator(.{}); + + gpa: ?GPA, + alloc: std.mem.Allocator, + + pub fn init(self: *GlobalState) void { + // Output some debug information right away + std.log.info("dependency harfbuzz={s}", .{harfbuzz.versionString()}); + if (options.fontconfig) { + std.log.info("dependency fontconfig={d}", .{fontconfig.version()}); + } + std.log.info("renderer={}", .{renderer.Renderer}); + std.log.info("libxev backend={}", .{xev.backend}); + + // First things first, we fix our file descriptors + internal_os.fixMaxFiles(); + + // We need to make sure the process locale is set properly. Locale + // affects a lot of behaviors in a shell. + internal_os.ensureLocale(); + + // Initialize ourself to nothing so we don't have any extra state. + self.* = .{ + .gpa = null, + .alloc = undefined, + }; + errdefer self.deinit(); + + self.gpa = gpa: { + // Use the libc allocator if it is available beacuse it is WAY + // faster than GPA. We only do this in release modes so that we + // can get easy memory leak detection in debug modes. + if (builtin.link_libc) { + if (switch (builtin.mode) { + .ReleaseSafe, .ReleaseFast => true, + + // We also use it if we can detect we're running under + // Valgrind since Valgrind only instruments the C allocator + else => std.valgrind.runningOnValgrind() > 0, + }) break :gpa null; + } + + break :gpa GPA{}; + }; + + self.alloc = alloc: { + const base = if (self.gpa) |*value| + value.allocator() + else if (builtin.link_libc) + std.heap.c_allocator + else + unreachable; + + // If we're tracing, wrap the allocator + if (!tracy.enabled) break :alloc base; + var tracy_alloc = tracy.allocator(base, null); + break :alloc tracy_alloc.allocator(); + }; + } + + /// Cleans up the global state. This doesn't _need_ to be called but + /// doing so in dev modes will check for memory leaks. + pub fn deinit(self: *GlobalState) void { + if (self.gpa) |*value| { + // We want to ensure that we deinit the GPA because this is + // the point at which it will output if there were safety violations. + _ = value.deinit(); + } + } +}; test { _ = @import("Pty.zig"); _ = @import("Command.zig"); diff --git a/src/main_c.zig b/src/main_c.zig new file mode 100644 index 000000000..344d477bb --- /dev/null +++ b/src/main_c.zig @@ -0,0 +1,32 @@ +// This is the main file for the C API. The C API is used to embed Ghostty +// within other applications. Depending on the build settings some APIs +// may not be available (i.e. embedding into macOS exposes various Metal +// support). +// +// This currently isn't supported as a general purpose embedding API. +// This is currently used only to embed ghostty within a macOS app. However, +// it could be expanded to be general purpose in the future. +const std = @import("std"); +const assert = std.debug.assert; +const builtin = @import("builtin"); +const main = @import("main.zig"); + +// Some comptime assertions that our C API depends on. +comptime { + const apprt = @import("apprt.zig"); + assert(apprt.runtime == apprt.embedded); +} + +/// Global options so we can log. This is identical to main. +pub const std_options = main.std_options; + +pub usingnamespace @import("config.zig").CAPI; +pub usingnamespace @import("App.zig").CAPI; + +/// Initialize ghostty global state. It is possible to have more than +/// one global state but it has zero practical benefit. +export fn ghostty_init() c_int { + assert(builtin.link_libc); + main.state.init(); + return 0; +} diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 6ee656e6f..abd8ad06a 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -305,21 +305,42 @@ pub fn deinit(self: *Metal) void { /// This is called just prior to spinning up the renderer thread for /// final main thread setup requirements. pub fn finalizeWindowInit(self: *const Metal, win: apprt.runtime.Window) !void { - // Set our window backing layer to be our swapchain - const nswindow = switch (apprt.runtime) { - apprt.glfw => objc.Object.fromId(glfwNative.getCocoaWindow(win.window).?), + const Info = struct { + view: objc.Object, + scaleFactor: f64, + }; + + // Get the view and scale factor for our surface. + const info: Info = switch (apprt.runtime) { + apprt.glfw => info: { + // Everything in glfw is window-oriented so we grab the backing + // window, then derive everything from that. + const nswindow = objc.Object.fromId(glfwNative.getCocoaWindow(win.window).?); + const contentView = objc.Object.fromId(nswindow.getProperty(?*anyopaque, "contentView").?); + const scaleFactor = nswindow.getProperty(macos.graphics.c.CGFloat, "backingScaleFactor"); + break :info .{ + .view = contentView, + .scaleFactor = scaleFactor, + }; + }, + + apprt.embedded => .{ + .view = win.nsview, + .scaleFactor = @floatCast(f64, win.content_scale.x), + }, + else => @compileError("unsupported apprt for metal"), }; - const contentView = objc.Object.fromId(nswindow.getProperty(?*anyopaque, "contentView").?); - contentView.setProperty("layer", self.swapchain.value); - contentView.setProperty("wantsLayer", true); + + // Make our view layer-backed with our Metal layer + info.view.setProperty("layer", self.swapchain.value); + info.view.setProperty("wantsLayer", true); // Ensure that our metal layer has a content scale set to match the // scale factor of the window. This avoids magnification issues leading // to blurry rendering. - const layer = contentView.getProperty(objc.Object, "layer"); - const scaleFactor = nswindow.getProperty(macos.graphics.c.CGFloat, "backingScaleFactor"); - layer.setProperty("contentsScale", scaleFactor); + const layer = info.view.getProperty(objc.Object, "layer"); + layer.setProperty("contentsScale", info.scaleFactor); } /// This is called if this renderer runs DevMode. @@ -547,9 +568,14 @@ pub fn render( .{@as(c_ulong, 0)}, ); + // Texture is a property of CAMetalDrawable but if you run + // Ghostty in XCode in debug mode it returns a CaptureMTLDrawable + // which ironically doesn't implement CAMetalDrawable as a + // property so we just send a message. + const texture = surface.msgSend(objc.c.id, objc.sel("texture"), .{}); attachment.setProperty("loadAction", @enumToInt(MTLLoadAction.clear)); attachment.setProperty("storeAction", @enumToInt(MTLStoreAction.store)); - attachment.setProperty("texture", surface.getProperty(objc.c.id, "texture").?); + attachment.setProperty("texture", texture); attachment.setProperty("clearColor", MTLClearColor{ .red = @intToFloat(f32, critical.bg.r) / 255, .green = @intToFloat(f32, critical.bg.g) / 255, @@ -613,16 +639,18 @@ pub fn render( state.mutex.lock(); defer state.mutex.unlock(); - if (state.devmode) |dm| { - if (dm.visible) { - imgui.ImplMetal.newFrame(desc.value); - imgui.ImplGlfw.newFrame(); - try dm.update(); - imgui.ImplMetal.renderDrawData( - try dm.render(), - buffer.value, - encoder.value, - ); + if (DevMode.enabled) { + if (state.devmode) |dm| { + if (dm.visible) { + imgui.ImplMetal.newFrame(desc.value); + imgui.ImplGlfw.newFrame(); + try dm.update(); + imgui.ImplMetal.renderDrawData( + try dm.render(), + buffer.value, + encoder.value, + ); + } } } } @@ -650,18 +678,20 @@ fn drawCells( .{ buf.value, @as(c_ulong, 0), @as(c_ulong, 0) }, ); - encoder.msgSend( - void, - objc.sel("drawIndexedPrimitives:indexCount:indexType:indexBuffer:indexBufferOffset:instanceCount:"), - .{ - @enumToInt(MTLPrimitiveType.triangle), - @as(c_ulong, 6), - @enumToInt(MTLIndexType.uint16), - self.buf_instance.value, - @as(c_ulong, 0), - @as(c_ulong, cells.items.len), - }, - ); + if (cells.items.len > 0) { + encoder.msgSend( + void, + objc.sel("drawIndexedPrimitives:indexCount:indexType:indexBuffer:indexBufferOffset:instanceCount:"), + .{ + @enumToInt(MTLPrimitiveType.triangle), + @as(c_ulong, 6), + @enumToInt(MTLIndexType.uint16), + self.buf_instance.value, + @as(c_ulong, 0), + @as(c_ulong, cells.items.len), + }, + ); + } } /// Resize the screen. @@ -690,7 +720,8 @@ pub fn setScreenSize(self: *Metal, _: renderer.ScreenSize) !void { // and we split them equal across all boundaries. const padding = self.padding.explicit.add(if (self.padding.balance) renderer.Padding.balanced(dim, grid_size, self.cell_size) - else .{}); + else + .{}); const padded_dim = dim.subPadding(padding); // Update our shaper diff --git a/src/renderer/size.zig b/src/renderer/size.zig index a96ae160e..0be4e6476 100644 --- a/src/renderer/size.zig +++ b/src/renderer/size.zig @@ -106,10 +106,10 @@ pub const Padding = struct { const padding_bot = space_bot - padding_top; return .{ - .top = padding_top, - .bottom = padding_bot, - .right = padding_right, - .left = padding_left, + .top = @max(0, padding_top), + .bottom = @max(0, padding_bot), + .right = @max(0, padding_right), + .left = @max(0, padding_left), }; } @@ -124,6 +124,17 @@ pub const Padding = struct { } }; +test "Padding balanced on zero" { + // On some systems, our screen can be zero-sized for a bit, and we + // don't want to end up with negative padding. + const testing = std.testing; + const grid: GridSize = .{ .columns = 100, .rows = 37 }; + const cell: CellSize = .{ .width = 10, .height = 20 }; + const screen: ScreenSize = .{ .width = 0, .height = 0 }; + const padding = Padding.balanced(screen, grid, cell); + try testing.expectEqual(padding, .{}); +} + test "GridSize update exact" { const testing = std.testing; diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index c6e382750..01e0d300a 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -3,6 +3,7 @@ pub const Exec = @This(); const std = @import("std"); const builtin = @import("builtin"); +const build_config = @import("../build_config.zig"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; const termio = @import("../termio.zig"); @@ -361,6 +362,21 @@ const Subprocess = struct { try env.put("TERM", "xterm-256color"); try env.put("COLORTERM", "truecolor"); + // When embedding in macOS and running via XCode, XCode injects + // a bunch of things that break our shell process. We remove those. + if (comptime builtin.target.isDarwin() and build_config.artifact == .lib) { + if (env.get("__XCODE_BUILT_PRODUCTS_DIR_PATHS") != null) { + env.remove("__XCODE_BUILT_PRODUCTS_DIR_PATHS"); + env.remove("__XPC_DYLD_LIBRARY_PATH"); + env.remove("DYLD_FRAMEWORK_PATH"); + env.remove("DYLD_INSERT_LIBRARIES"); + env.remove("DYLD_LIBRARY_PATH"); + env.remove("LD_LIBRARY_PATH"); + env.remove("SECURITYSESSIONID"); + env.remove("XPC_SERVICE_NAME"); + } + } + return .{ .env = env, .cwd = opts.config.@"working-directory",