Merge pull request #72 from mitchellh/macos

Swift-based macOS App
This commit is contained in:
Mitchell Hashimoto
2023-02-19 10:45:37 -08:00
committed by GitHub
55 changed files with 2676 additions and 236 deletions

View File

@ -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

View File

@ -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:

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
.DS_Store
.direnv/
zig-cache/
zig-out/

159
build.zig
View File

@ -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);
}
}

240
include/ghostty.h Normal file
View File

@ -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 <stdint.h>
//-------------------------------------------------------------------
// 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 */

7
include/module.modulemap Normal file
View File

@ -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 *
}

5
macos/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
.DS_Store
/*.xcframework
build/
xcuserdata/
DerivedData/

View File

@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

View File

@ -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
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>

View File

@ -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 = "<group>"; };
A518502329A197C700E4CC4F /* TerminalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalView.swift; sourceTree = "<group>"; };
A518502529A1A45100E4CC4F /* WindowTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowTracker.swift; sourceTree = "<group>"; };
A535B9D9299C569B0017E2E4 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = "<group>"; };
A55685DF29A03A9F004303CE /* AppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = "<group>"; };
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 = "<group>"; };
A5B30538299BEAAB0047F10C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
A5B3053D299BEAAB0047F10C /* Ghostty.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Ghostty.entitlements; sourceTree = "<group>"; };
A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = GhosttyKit.xcframework; sourceTree = "<group>"; };
/* 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 = "<group>";
};
A5B30528299BEAAA0047F10C = {
isa = PBXGroup;
children = (
A5B30538299BEAAB0047F10C /* Assets.xcassets */,
A5B3053D299BEAAB0047F10C /* Ghostty.entitlements */,
A54CD6ED299BEB14008C95BB /* Sources */,
A5D495A3299BECBA00DD1313 /* Frameworks */,
A5B30532299BEAAA0047F10C /* Products */,
);
sourceTree = "<group>";
};
A5B30532299BEAAA0047F10C /* Products */ = {
isa = PBXGroup;
children = (
A5B30531299BEAAA0047F10C /* Ghostty.app */,
);
name = Products;
sourceTree = "<group>";
};
A5D495A0299BEC2200DD1313 /* Preview Content */ = {
isa = PBXGroup;
children = (
);
path = "Preview Content";
sourceTree = "<group>";
};
A5D495A3299BECBA00DD1313 /* Frameworks */ = {
isa = PBXGroup;
children = (
A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* 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 */;
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,3 @@
enum AppError: Error {
case surfaceCreateError
}

View File

@ -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()
}
}

View File

@ -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<GhosttyState>.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<CChar>?) {
let surfaceView = Unmanaged<TerminalSurfaceView_Real>.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)
}
}

View File

View File

@ -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<String>) {
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)
}
}

View File

@ -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)
}
}
}

View File

@ -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) {}
}

View File

@ -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",
};

View File

@ -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);
}
};

View File

@ -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.

View File

@ -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);

View File

@ -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();

View File

@ -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 {

238
src/apprt/embedded.zig Normal file
View File

@ -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 };
}
};

View File

@ -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,

65
src/build/LibtoolStep.zig Normal file
View File

@ -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();
}

64
src/build/LipoStep.zig Normal file
View File

@ -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();
}

View File

@ -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();
}
}

43
src/build_config.zig Normal file
View File

@ -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");
},
};
}
};

View File

@ -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());
}

View File

@ -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

View File

@ -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

View File

@ -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");

32
src/main_c.zig Normal file
View File

@ -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;
}

View File

@ -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

View File

@ -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;

View File

@ -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",