72
.github/workflows/release-tip.yml
vendored
@ -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
|
||||
|
29
.github/workflows/test.yml
vendored
@ -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
@ -1,3 +1,4 @@
|
||||
.DS_Store
|
||||
.direnv/
|
||||
zig-cache/
|
||||
zig-out/
|
||||
|
159
build.zig
@ -21,6 +21,9 @@ const zlib = @import("pkg/zlib/build.zig");
|
||||
const tracylib = @import("pkg/tracy/build.zig");
|
||||
const system_sdk = @import("vendor/mach/libs/glfw/system_sdk.zig");
|
||||
const WasmTarget = @import("src/os/wasm/target.zig").Target;
|
||||
const LibtoolStep = @import("src/build/LibtoolStep.zig");
|
||||
const LipoStep = @import("src/build/LipoStep.zig");
|
||||
const XCFrameworkStep = @import("src/build/XCFrameworkStep.zig");
|
||||
|
||||
// Do a comptime Zig version requirement. The required Zig version is
|
||||
// somewhat arbitrary: it is meant to be a version that we feel works well,
|
||||
@ -120,7 +123,7 @@ pub fn build(b: *std.build.Builder) !void {
|
||||
exe.install();
|
||||
|
||||
// Add the shared dependencies
|
||||
try addDeps(b, exe, static);
|
||||
_ = try addDeps(b, exe, static);
|
||||
}
|
||||
|
||||
// App (Mac)
|
||||
@ -131,6 +134,85 @@ pub fn build(b: *std.build.Builder) !void {
|
||||
b.installFile("dist/macos/Ghostty.icns", "Ghostty.app/Contents/Resources/Ghostty.icns");
|
||||
}
|
||||
|
||||
// On Mac we can build the app.
|
||||
if (builtin.target.isDarwin()) {
|
||||
const static_lib_aarch64 = lib: {
|
||||
const lib = b.addStaticLibrary(.{
|
||||
.name = "ghostty",
|
||||
.root_source_file = .{ .path = "src/main_c.zig" },
|
||||
.target = try std.zig.CrossTarget.parse(.{ .arch_os_abi = "aarch64-macos" }),
|
||||
.optimize = optimize,
|
||||
});
|
||||
lib.bundle_compiler_rt = true;
|
||||
lib.linkLibC();
|
||||
lib.addOptions("build_options", exe_options);
|
||||
|
||||
// See the comment in this file
|
||||
lib.addCSourceFile("src/renderer/metal_workaround.c", &.{});
|
||||
|
||||
// Create a single static lib with all our dependencies merged
|
||||
var lib_list = try addDeps(b, lib, true);
|
||||
try lib_list.append(.{ .generated = &lib.output_path_source });
|
||||
const libtool = LibtoolStep.create(b, .{
|
||||
.name = "ghostty",
|
||||
.out_name = "libghostty-aarch64-fat.a",
|
||||
.sources = lib_list.items,
|
||||
});
|
||||
libtool.step.dependOn(&lib.step);
|
||||
b.default_step.dependOn(&libtool.step);
|
||||
|
||||
break :lib libtool;
|
||||
};
|
||||
|
||||
const static_lib_x86_64 = lib: {
|
||||
const lib = b.addStaticLibrary(.{
|
||||
.name = "ghostty",
|
||||
.root_source_file = .{ .path = "src/main_c.zig" },
|
||||
.target = try std.zig.CrossTarget.parse(.{ .arch_os_abi = "x86_64-macos" }),
|
||||
.optimize = optimize,
|
||||
});
|
||||
lib.bundle_compiler_rt = true;
|
||||
lib.linkLibC();
|
||||
lib.addOptions("build_options", exe_options);
|
||||
|
||||
// See the comment in this file
|
||||
lib.addCSourceFile("src/renderer/metal_workaround.c", &.{});
|
||||
|
||||
// Create a single static lib with all our dependencies merged
|
||||
var lib_list = try addDeps(b, lib, true);
|
||||
try lib_list.append(.{ .generated = &lib.output_path_source });
|
||||
const libtool = LibtoolStep.create(b, .{
|
||||
.name = "ghostty",
|
||||
.out_name = "libghostty-x86_64-fat.a",
|
||||
.sources = lib_list.items,
|
||||
});
|
||||
libtool.step.dependOn(&lib.step);
|
||||
b.default_step.dependOn(&libtool.step);
|
||||
|
||||
break :lib libtool;
|
||||
};
|
||||
|
||||
const static_lib_universal = LipoStep.create(b, .{
|
||||
.name = "ghostty",
|
||||
.out_name = "libghostty.a",
|
||||
.input_a = .{ .generated = &static_lib_aarch64.out_path },
|
||||
.input_b = .{ .generated = &static_lib_x86_64.out_path },
|
||||
});
|
||||
static_lib_universal.step.dependOn(&static_lib_aarch64.step);
|
||||
static_lib_universal.step.dependOn(&static_lib_x86_64.step);
|
||||
|
||||
// The xcframework wraps our ghostty library so that we can link
|
||||
// it to the final app built with Swift.
|
||||
const xcframework = XCFrameworkStep.create(b, .{
|
||||
.name = "GhosttyKit",
|
||||
.out_path = "macos/GhosttyKit.xcframework",
|
||||
.library = .{ .generated = &static_lib_universal.out_path },
|
||||
.headers = .{ .path = "include" },
|
||||
});
|
||||
xcframework.step.dependOn(&static_lib_universal.step);
|
||||
b.default_step.dependOn(&xcframework.step);
|
||||
}
|
||||
|
||||
// wasm
|
||||
{
|
||||
// Build our Wasm target.
|
||||
@ -179,7 +261,7 @@ pub fn build(b: *std.build.Builder) !void {
|
||||
wasm.stack_protector = false;
|
||||
|
||||
// Wasm-specific deps
|
||||
try addDeps(b, wasm, true);
|
||||
_ = try addDeps(b, wasm, true);
|
||||
|
||||
const step = b.step("wasm", "Build the wasm library");
|
||||
step.dependOn(&wasm.step);
|
||||
@ -194,7 +276,7 @@ pub fn build(b: *std.build.Builder) !void {
|
||||
.target = wasm_target,
|
||||
});
|
||||
main_test.addOptions("build_options", exe_options);
|
||||
try addDeps(b, main_test, true);
|
||||
_ = try addDeps(b, main_test, true);
|
||||
test_step.dependOn(&main_test.step);
|
||||
}
|
||||
|
||||
@ -238,7 +320,7 @@ pub fn build(b: *std.build.Builder) !void {
|
||||
main_test_exe.install();
|
||||
}
|
||||
main_test.setFilter(test_filter);
|
||||
try addDeps(b, main_test, true);
|
||||
_ = try addDeps(b, main_test, true);
|
||||
main_test.addOptions("build_options", exe_options);
|
||||
|
||||
var before = b.addLog("\x1b[" ++ color_map.get("cyan").? ++ "\x1b[" ++ color_map.get("b").? ++ "[{s} tests]" ++ "\x1b[" ++ color_map.get("d").? ++ " ----" ++ "\x1b[0m", .{"ghostty"});
|
||||
@ -266,7 +348,7 @@ pub fn build(b: *std.build.Builder) !void {
|
||||
.target = target,
|
||||
});
|
||||
|
||||
try addDeps(b, test_run, true);
|
||||
_ = try addDeps(b, test_run, true);
|
||||
// if (pkg.dependencies) |children| {
|
||||
// test_.packages = std.ArrayList(std.build.Pkg).init(b.allocator);
|
||||
// try test_.packages.appendSlice(children);
|
||||
@ -291,12 +373,18 @@ pub fn build(b: *std.build.Builder) !void {
|
||||
}
|
||||
}
|
||||
|
||||
/// Used to keep track of a list of file sources.
|
||||
const FileSourceList = std.ArrayList(std.build.FileSource);
|
||||
|
||||
/// Adds and links all of the primary dependencies for the exe.
|
||||
fn addDeps(
|
||||
b: *std.build.Builder,
|
||||
step: *std.build.LibExeObjStep,
|
||||
static: bool,
|
||||
) !void {
|
||||
) !FileSourceList {
|
||||
var static_libs = FileSourceList.init(b.allocator);
|
||||
errdefer static_libs.deinit();
|
||||
|
||||
// Wasm we do manually since it is such a different build.
|
||||
if (step.target.getCpuArch() == .wasm32) {
|
||||
// We link this package but its a no-op since Tracy
|
||||
@ -308,15 +396,21 @@ fn addDeps(
|
||||
// utf8proc
|
||||
_ = try utf8proc.link(b, step);
|
||||
|
||||
return;
|
||||
return static_libs;
|
||||
}
|
||||
|
||||
// If we're building a lib we have some different deps
|
||||
const lib = step.kind == .lib;
|
||||
|
||||
// We always require the system SDK so that our system headers are available.
|
||||
// This makes things like `os/log.h` available for cross-compiling.
|
||||
system_sdk.include(b, step, .{});
|
||||
|
||||
// We always need the Zig packages
|
||||
if (enable_fontconfig) step.addModule("fontconfig", fontconfig.module(b));
|
||||
step.addModule("freetype", freetype.module(b));
|
||||
step.addModule("harfbuzz", harfbuzz.module(b));
|
||||
step.addModule("imgui", imgui.module(b));
|
||||
step.addModule("glfw", glfw.module(b));
|
||||
step.addModule("xev", libxev.module(b));
|
||||
step.addModule("pixman", pixman.module(b));
|
||||
step.addModule("stb_image_resize", stb_image_resize.module(b));
|
||||
@ -329,10 +423,6 @@ fn addDeps(
|
||||
_ = try macos.link(b, step, .{});
|
||||
}
|
||||
|
||||
// We always statically compile glad
|
||||
step.addIncludePath("vendor/glad/include/");
|
||||
step.addCSourceFile("vendor/glad/src/gl.c", &.{});
|
||||
|
||||
// Tracy
|
||||
step.addModule("tracy", tracylib.module(b));
|
||||
if (tracy) {
|
||||
@ -341,17 +431,12 @@ fn addDeps(
|
||||
}
|
||||
|
||||
// stb_image_resize
|
||||
_ = try stb_image_resize.link(b, step, .{});
|
||||
const stb_image_resize_step = try stb_image_resize.link(b, step, .{});
|
||||
try static_libs.append(.{ .generated = &stb_image_resize_step.output_path_source });
|
||||
|
||||
// utf8proc
|
||||
_ = try utf8proc.link(b, step);
|
||||
|
||||
// Glfw
|
||||
const glfw_opts: glfw.Options = .{
|
||||
.metal = step.target.isDarwin(),
|
||||
.opengl = false,
|
||||
};
|
||||
try glfw.link(b, step, glfw_opts);
|
||||
const utf8proc_step = try utf8proc.link(b, step);
|
||||
try static_libs.append(.{ .generated = &utf8proc_step.output_path_source });
|
||||
|
||||
// Imgui, we have to do this later since we need some information
|
||||
const imgui_backends = if (step.target.isDarwin())
|
||||
@ -379,12 +464,15 @@ fn addDeps(
|
||||
// Other dependencies, we may dynamically link
|
||||
if (static) {
|
||||
const zlib_step = try zlib.link(b, step);
|
||||
try static_libs.append(.{ .generated = &zlib_step.output_path_source });
|
||||
|
||||
const libpng_step = try libpng.link(b, step, .{
|
||||
.zlib = .{
|
||||
.step = zlib_step,
|
||||
.include = &zlib.include_paths,
|
||||
},
|
||||
});
|
||||
try static_libs.append(.{ .generated = &libpng_step.output_path_source });
|
||||
|
||||
// Freetype
|
||||
const freetype_step = try freetype.link(b, step, .{
|
||||
@ -400,6 +488,7 @@ fn addDeps(
|
||||
.include = &zlib.include_paths,
|
||||
},
|
||||
});
|
||||
try static_libs.append(.{ .generated = &freetype_step.output_path_source });
|
||||
|
||||
// Harfbuzz
|
||||
const harfbuzz_step = try harfbuzz.link(b, step, .{
|
||||
@ -414,10 +503,11 @@ fn addDeps(
|
||||
},
|
||||
});
|
||||
system_sdk.include(b, harfbuzz_step, .{});
|
||||
try static_libs.append(.{ .generated = &harfbuzz_step.output_path_source });
|
||||
|
||||
// Pixman
|
||||
const pixman_step = try pixman.link(b, step, .{});
|
||||
_ = pixman_step;
|
||||
try static_libs.append(.{ .generated = &pixman_step.output_path_source });
|
||||
|
||||
// Only Linux gets fontconfig
|
||||
if (enable_fontconfig) {
|
||||
@ -448,9 +538,26 @@ fn addDeps(
|
||||
imgui_opts.freetype.include = &freetype.include_paths;
|
||||
}
|
||||
|
||||
// Imgui
|
||||
const imgui_step = try imgui.link(b, step, imgui_opts);
|
||||
try glfw.link(b, imgui_step, glfw_opts);
|
||||
if (!lib) {
|
||||
step.addModule("glfw", glfw.module(b));
|
||||
|
||||
// We always statically compile glad
|
||||
step.addIncludePath("vendor/glad/include/");
|
||||
step.addCSourceFile("vendor/glad/src/gl.c", &.{});
|
||||
|
||||
// Glfw
|
||||
const glfw_opts: glfw.Options = .{
|
||||
.metal = step.target.isDarwin(),
|
||||
.opengl = false,
|
||||
};
|
||||
try glfw.link(b, step, glfw_opts);
|
||||
|
||||
// Imgui
|
||||
const imgui_step = try imgui.link(b, step, imgui_opts);
|
||||
try glfw.link(b, imgui_step, glfw_opts);
|
||||
}
|
||||
|
||||
return static_libs;
|
||||
}
|
||||
|
||||
fn benchSteps(
|
||||
@ -490,7 +597,7 @@ fn benchSteps(
|
||||
});
|
||||
c_exe.setMainPkgPath("./src");
|
||||
c_exe.install();
|
||||
try addDeps(b, c_exe, true);
|
||||
_ = try addDeps(b, c_exe, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
240
include/ghostty.h
Normal 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
@ -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
@ -0,0 +1,5 @@
|
||||
.DS_Store
|
||||
/*.xcframework
|
||||
build/
|
||||
xcuserdata/
|
||||
DerivedData/
|
11
macos/Assets.xcassets/AccentColor.colorset/Contents.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
68
macos/Assets.xcassets/AppIcon.appiconset/Contents.json
Normal 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
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 20 KiB |
BIN
macos/Assets.xcassets/AppIcon.appiconset/Ghostty_256x256x32.png
Normal file
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 45 KiB |
After Width: | Height: | Size: 45 KiB |
BIN
macos/Assets.xcassets/AppIcon.appiconset/Ghostty_512x512x32.png
Normal file
After Width: | Height: | Size: 45 KiB |
BIN
macos/Assets.xcassets/AppIconImage.imageset/199110421-9ff5fc30-a244-441e-9882-26070662adf9 1.png
vendored
Normal file
After Width: | Height: | Size: 242 KiB |
BIN
macos/Assets.xcassets/AppIconImage.imageset/199110421-9ff5fc30-a244-441e-9882-26070662adf9 2.png
vendored
Normal file
After Width: | Height: | Size: 242 KiB |
BIN
macos/Assets.xcassets/AppIconImage.imageset/199110421-9ff5fc30-a244-441e-9882-26070662adf9.png
vendored
Normal file
After Width: | Height: | Size: 242 KiB |
23
macos/Assets.xcassets/AppIconImage.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
6
macos/Assets.xcassets/Contents.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
5
macos/Ghostty.entitlements
Normal 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>
|
376
macos/Ghostty.xcodeproj/project.pbxproj
Normal 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 */;
|
||||
}
|
7
macos/Ghostty.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
@ -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>
|
3
macos/Sources/AppError.swift
Normal file
@ -0,0 +1,3 @@
|
||||
enum AppError: Error {
|
||||
case surfaceCreateError
|
||||
}
|
24
macos/Sources/ErrorView.swift
Normal 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()
|
||||
}
|
||||
}
|
127
macos/Sources/GhosttyApp.swift
Normal 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)
|
||||
}
|
||||
}
|
0
macos/Sources/Preview Content/.gitkeep
Normal file
448
macos/Sources/TerminalSurfaceView.swift
Normal 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)
|
||||
}
|
||||
}
|
22
macos/Sources/TerminalView.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
80
macos/Sources/WindowTracker.swift
Normal 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) {}
|
||||
}
|
@ -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",
|
||||
};
|
||||
|
246
src/App.zig
@ -5,10 +5,13 @@ const App = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const build_config = @import("build_config.zig");
|
||||
const apprt = @import("apprt.zig");
|
||||
const Window = @import("Window.zig");
|
||||
const tracy = @import("tracy");
|
||||
const input = @import("input.zig");
|
||||
const Config = @import("config.zig").Config;
|
||||
const BlockingQueue = @import("./blocking_queue.zig").BlockingQueue;
|
||||
const renderer = @import("renderer.zig");
|
||||
@ -46,9 +49,11 @@ quit: bool,
|
||||
/// Mac settings
|
||||
darwin: if (Darwin.enabled) Darwin else void,
|
||||
|
||||
/// Mac-specific settings
|
||||
/// Mac-specific settings. This is only enabled when the target is
|
||||
/// Mac and the artifact is a standalone exe. We don't target libs because
|
||||
/// the embedded API doesn't do windowing.
|
||||
pub const Darwin = struct {
|
||||
pub const enabled = builtin.target.isDarwin();
|
||||
pub const enabled = builtin.target.isDarwin() and build_config.artifact == .exe;
|
||||
|
||||
tabbing_id: *macos.foundation.String,
|
||||
|
||||
@ -61,9 +66,13 @@ pub const Darwin = struct {
|
||||
/// Initialize the main app instance. This creates the main window, sets
|
||||
/// up the renderer state, compiles the shaders, etc. This is the primary
|
||||
/// "startup" logic.
|
||||
pub fn create(alloc: Allocator, config: *const Config) !*App {
|
||||
pub fn create(
|
||||
alloc: Allocator,
|
||||
rt_opts: apprt.runtime.App.Options,
|
||||
config: *const Config,
|
||||
) !*App {
|
||||
// Initialize app runtime
|
||||
var app_backend = try apprt.runtime.App.init();
|
||||
var app_backend = try apprt.runtime.App.init(rt_opts);
|
||||
errdefer app_backend.terminate();
|
||||
|
||||
// The mailbox for messaging this thread
|
||||
@ -86,8 +95,9 @@ pub fn create(alloc: Allocator, config: *const Config) !*App {
|
||||
};
|
||||
errdefer app.windows.deinit(alloc);
|
||||
|
||||
// On Mac, we enable window tabbing
|
||||
if (comptime builtin.target.isDarwin()) {
|
||||
// On Mac, we enable window tabbing. We only do this if we're building
|
||||
// a standalone exe. In embedded mode the host app handles this for us.
|
||||
if (Darwin.enabled) {
|
||||
const NSWindow = objc.Class.getClass("NSWindow").?;
|
||||
NSWindow.msgSend(void, objc.sel("setAllowsAutomaticWindowTabbing:"), .{true});
|
||||
|
||||
@ -106,9 +116,6 @@ pub fn create(alloc: Allocator, config: *const Config) !*App {
|
||||
}
|
||||
errdefer if (comptime builtin.target.isDarwin()) app.darwin.deinit();
|
||||
|
||||
// Create the first window
|
||||
_ = try app.newWindow(.{});
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
@ -116,7 +123,7 @@ pub fn destroy(self: *App) void {
|
||||
// Clean up all our windows
|
||||
for (self.windows.items) |window| window.destroy();
|
||||
self.windows.deinit(self.alloc);
|
||||
if (comptime builtin.target.isDarwin()) self.darwin.deinit();
|
||||
if (Darwin.enabled) self.darwin.deinit();
|
||||
self.mailbox.destroy(self.alloc);
|
||||
self.alloc.destroy(self);
|
||||
|
||||
@ -134,24 +141,61 @@ pub fn wakeup(self: App) void {
|
||||
/// application quits or every window is closed.
|
||||
pub fn run(self: *App) !void {
|
||||
while (!self.quit and self.windows.items.len > 0) {
|
||||
// Block for any events.
|
||||
try self.runtime.wait();
|
||||
try self.tick();
|
||||
}
|
||||
}
|
||||
|
||||
// If any windows are closing, destroy them
|
||||
var i: usize = 0;
|
||||
while (i < self.windows.items.len) {
|
||||
const window = self.windows.items[i];
|
||||
if (window.shouldClose()) {
|
||||
window.destroy();
|
||||
_ = self.windows.swapRemove(i);
|
||||
continue;
|
||||
}
|
||||
/// Tick ticks the app loop. This will drain our mailbox and process those
|
||||
/// events.
|
||||
pub fn tick(self: *App) !void {
|
||||
// Block for any events.
|
||||
try self.runtime.wait();
|
||||
|
||||
i += 1;
|
||||
// If any windows are closing, destroy them
|
||||
var i: usize = 0;
|
||||
while (i < self.windows.items.len) {
|
||||
const window = self.windows.items[i];
|
||||
if (window.shouldClose()) {
|
||||
window.destroy();
|
||||
_ = self.windows.swapRemove(i);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Drain our mailbox only if we're not quitting.
|
||||
if (!self.quit) try self.drainMailbox();
|
||||
i += 1;
|
||||
}
|
||||
|
||||
// Drain our mailbox only if we're not quitting.
|
||||
if (!self.quit) try self.drainMailbox();
|
||||
}
|
||||
|
||||
/// Create a new window. This can be called only on the main thread. This
|
||||
/// can be called prior to ever running the app loop.
|
||||
pub fn newWindow(self: *App, msg: Message.NewWindow) !*Window {
|
||||
var window = try Window.create(self.alloc, self, self.config, msg.runtime);
|
||||
errdefer window.destroy();
|
||||
|
||||
try self.windows.append(self.alloc, window);
|
||||
errdefer _ = self.windows.pop();
|
||||
|
||||
// Set initial font size if given
|
||||
if (msg.font_size) |size| window.setFontSize(size);
|
||||
|
||||
return window;
|
||||
}
|
||||
|
||||
/// Close a window and free all resources associated with it. This can
|
||||
/// only be called from the main thread.
|
||||
pub fn closeWindow(self: *App, window: *Window) void {
|
||||
var i: usize = 0;
|
||||
while (i < self.windows.items.len) {
|
||||
const current = self.windows.items[i];
|
||||
if (window == current) {
|
||||
window.destroy();
|
||||
_ = self.windows.swapRemove(i);
|
||||
return;
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
@ -168,19 +212,6 @@ fn drainMailbox(self: *App) !void {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new window
|
||||
fn newWindow(self: *App, msg: Message.NewWindow) !*Window {
|
||||
var window = try Window.create(self.alloc, self, self.config);
|
||||
errdefer window.destroy();
|
||||
try self.windows.append(self.alloc, window);
|
||||
errdefer _ = self.windows.pop();
|
||||
|
||||
// Set initial font size if given
|
||||
if (msg.font_size) |size| window.setFontSize(size);
|
||||
|
||||
return window;
|
||||
}
|
||||
|
||||
/// Create a new tab in the parent window
|
||||
fn newTab(self: *App, msg: Message.NewWindow) !void {
|
||||
if (comptime !builtin.target.isDarwin()) {
|
||||
@ -188,6 +219,13 @@ fn newTab(self: *App, msg: Message.NewWindow) !void {
|
||||
return;
|
||||
}
|
||||
|
||||
// In embedded mode, it is up to the embedder to implement tabbing
|
||||
// on their own.
|
||||
if (comptime build_config.artifact != .exe) {
|
||||
log.warn("tabbing is not supported in embedded mode", .{});
|
||||
return;
|
||||
}
|
||||
|
||||
const parent = msg.parent orelse {
|
||||
log.warn("parent must be set in new_tab message", .{});
|
||||
return;
|
||||
@ -258,6 +296,9 @@ pub const Message = union(enum) {
|
||||
},
|
||||
|
||||
const NewWindow = struct {
|
||||
/// Runtime-specific window options.
|
||||
runtime: apprt.runtime.Window.Options = .{},
|
||||
|
||||
/// The parent window, only used for new tabs.
|
||||
parent: ?*Window = null,
|
||||
|
||||
@ -273,8 +314,7 @@ pub const Wasm = if (!builtin.target.isWasm()) struct {} else struct {
|
||||
const alloc = wasm.alloc;
|
||||
|
||||
// export fn app_new(config: *Config) ?*App {
|
||||
// return app_new_(config) catch |err| {
|
||||
// log.err("error initializing app err={}", .{err});
|
||||
// return app_new_(config) catch |err| { log.err("error initializing app err={}", .{err});
|
||||
// return null;
|
||||
// };
|
||||
// }
|
||||
@ -295,3 +335,131 @@ pub const Wasm = if (!builtin.target.isWasm()) struct {} else struct {
|
||||
// }
|
||||
// }
|
||||
};
|
||||
|
||||
// C API
|
||||
pub const CAPI = struct {
|
||||
const global = &@import("main.zig").state;
|
||||
|
||||
/// Create a new app.
|
||||
export fn ghostty_app_new(
|
||||
opts: *const apprt.runtime.App.Options,
|
||||
config: *const Config,
|
||||
) ?*App {
|
||||
return app_new_(opts, config) catch |err| {
|
||||
log.err("error initializing app err={}", .{err});
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
fn app_new_(
|
||||
opts: *const apprt.runtime.App.Options,
|
||||
config: *const Config,
|
||||
) !*App {
|
||||
const app = try App.create(global.alloc, opts.*, config);
|
||||
errdefer app.destroy();
|
||||
return app;
|
||||
}
|
||||
|
||||
/// Tick the event loop. This should be called whenever the "wakeup"
|
||||
/// callback is invoked for the runtime.
|
||||
export fn ghostty_app_tick(v: *App) void {
|
||||
v.tick() catch |err| {
|
||||
log.err("error app tick err={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
export fn ghostty_app_free(ptr: ?*App) void {
|
||||
if (ptr) |v| {
|
||||
v.destroy();
|
||||
v.alloc.destroy(v);
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new surface as part of an app.
|
||||
export fn ghostty_surface_new(
|
||||
app: *App,
|
||||
opts: *const apprt.runtime.Window.Options,
|
||||
) ?*Window {
|
||||
return surface_new_(app, opts) catch |err| {
|
||||
log.err("error initializing surface err={}", .{err});
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
fn surface_new_(
|
||||
app: *App,
|
||||
opts: *const apprt.runtime.Window.Options,
|
||||
) !*Window {
|
||||
const w = try app.newWindow(.{
|
||||
.runtime = opts.*,
|
||||
});
|
||||
return w;
|
||||
}
|
||||
|
||||
export fn ghostty_surface_free(ptr: ?*Window) void {
|
||||
if (ptr) |v| v.app.closeWindow(v);
|
||||
}
|
||||
|
||||
/// Tell the surface that it needs to schedule a render
|
||||
export fn ghostty_surface_refresh(win: *Window) void {
|
||||
win.window.refresh();
|
||||
}
|
||||
|
||||
/// Update the size of a surface. This will trigger resize notifications
|
||||
/// to the pty and the renderer.
|
||||
export fn ghostty_surface_set_size(win: *Window, w: u32, h: u32) void {
|
||||
win.window.updateSize(w, h);
|
||||
}
|
||||
|
||||
/// Update the content scale of the surface.
|
||||
export fn ghostty_surface_set_content_scale(win: *Window, x: f64, y: f64) void {
|
||||
win.window.updateContentScale(x, y);
|
||||
}
|
||||
|
||||
/// Update the focused state of a surface.
|
||||
export fn ghostty_surface_set_focus(win: *Window, focused: bool) void {
|
||||
win.window.focusCallback(focused);
|
||||
}
|
||||
|
||||
/// Tell the surface that it needs to schedule a render
|
||||
export fn ghostty_surface_key(
|
||||
win: *Window,
|
||||
action: input.Action,
|
||||
key: input.Key,
|
||||
mods: c_int,
|
||||
) void {
|
||||
win.window.keyCallback(
|
||||
action,
|
||||
key,
|
||||
@bitCast(input.Mods, @truncate(u8, @bitCast(c_uint, mods))),
|
||||
);
|
||||
}
|
||||
|
||||
/// Tell the surface that it needs to schedule a render
|
||||
export fn ghostty_surface_char(win: *Window, codepoint: u32) void {
|
||||
win.window.charCallback(codepoint);
|
||||
}
|
||||
|
||||
/// Tell the surface that it needs to schedule a render
|
||||
export fn ghostty_surface_mouse_button(
|
||||
win: *Window,
|
||||
action: input.MouseButtonState,
|
||||
button: input.MouseButton,
|
||||
mods: c_int,
|
||||
) void {
|
||||
win.window.mouseButtonCallback(
|
||||
action,
|
||||
button,
|
||||
@bitCast(input.Mods, @truncate(u8, @bitCast(c_uint, mods))),
|
||||
);
|
||||
}
|
||||
|
||||
/// Update the mouse position within the view.
|
||||
export fn ghostty_surface_mouse_pos(win: *Window, x: f64, y: f64) void {
|
||||
win.window.cursorPosCallback(x, y);
|
||||
}
|
||||
|
||||
export fn ghostty_surface_mouse_scroll(win: *Window, x: f64, y: f64) void {
|
||||
win.window.scrollCallback(x, y);
|
||||
}
|
||||
};
|
||||
|
@ -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.
|
||||
|
11
src/Pty.zig
@ -19,6 +19,8 @@ const c = switch (builtin.os.tag) {
|
||||
}),
|
||||
};
|
||||
|
||||
const log = std.log.scoped(.pty);
|
||||
|
||||
// https://github.com/ziglang/zig/issues/13277
|
||||
// Once above is fixed, use `c.TIOCSCTTY`
|
||||
const TIOCSCTTY = if (builtin.os.tag == .macos) 536900705 else c.TIOCSCTTY;
|
||||
@ -102,8 +104,13 @@ pub fn childPreExec(self: Pty) !void {
|
||||
if (setsid() < 0) return error.ProcessGroupFailed;
|
||||
|
||||
// Set controlling terminal
|
||||
if (c.ioctl(self.slave, TIOCSCTTY, @as(c_ulong, 0)) < 0)
|
||||
return error.SetControllingTerminalFailed;
|
||||
switch (std.os.system.getErrno(c.ioctl(self.slave, TIOCSCTTY, @as(c_ulong, 0)))) {
|
||||
.SUCCESS => {},
|
||||
else => |err| {
|
||||
log.err("error setting controlling terminal errno={}", .{err});
|
||||
return error.SetControllingTerminalFailed;
|
||||
},
|
||||
}
|
||||
|
||||
// Can close master/slave pair now
|
||||
std.os.close(self.slave);
|
||||
|
@ -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();
|
||||
|
@ -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
@ -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 };
|
||||
}
|
||||
};
|
@ -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
@ -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
@ -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();
|
||||
}
|
81
src/build/XCFrameworkStep.zig
Normal 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
@ -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");
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
@ -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());
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
147
src/main.zig
@ -2,72 +2,30 @@ const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const options = @import("build_options");
|
||||
const glfw = @import("glfw");
|
||||
const fontconfig = @import("fontconfig");
|
||||
const freetype = @import("freetype");
|
||||
const harfbuzz = @import("harfbuzz");
|
||||
const macos = @import("macos");
|
||||
const tracy = @import("tracy");
|
||||
const internal_os = @import("os/main.zig");
|
||||
const xev = @import("xev");
|
||||
const fontconfig = @import("fontconfig");
|
||||
const harfbuzz = @import("harfbuzz");
|
||||
const renderer = @import("renderer.zig");
|
||||
const xdg = @import("xdg.zig");
|
||||
const internal_os = @import("os/main.zig");
|
||||
|
||||
const App = @import("App.zig");
|
||||
const cli_args = @import("cli_args.zig");
|
||||
const Config = @import("config.zig").Config;
|
||||
const Ghostty = @import("main_c.zig").Ghostty;
|
||||
|
||||
/// Global process state. This is initialized in main() for exe artifacts
|
||||
/// and by ghostty_init() for lib artifacts. This should ONLY be used by
|
||||
/// the C API. The Zig API should NOT use any global state and should
|
||||
/// rely on allocators being passed in as parameters.
|
||||
pub var state: GlobalState = undefined;
|
||||
|
||||
pub fn main() !void {
|
||||
// Output some debug information right away
|
||||
std.log.info("dependency harfbuzz={s}", .{harfbuzz.versionString()});
|
||||
if (options.fontconfig) {
|
||||
std.log.info("dependency fontconfig={d}", .{fontconfig.version()});
|
||||
}
|
||||
std.log.info("renderer={}", .{renderer.Renderer});
|
||||
std.log.info("libxev backend={}", .{xev.backend});
|
||||
|
||||
// First things first, we fix our file descriptors
|
||||
internal_os.fixMaxFiles();
|
||||
|
||||
// We need to make sure the process locale is set properly. Locale
|
||||
// affects a lot of behaviors in a shell.
|
||||
internal_os.ensureLocale();
|
||||
|
||||
const GPA = std.heap.GeneralPurposeAllocator(.{});
|
||||
var gpa: ?GPA = gpa: {
|
||||
// Use the libc allocator if it is available beacuse it is WAY
|
||||
// faster than GPA. We only do this in release modes so that we
|
||||
// can get easy memory leak detection in debug modes.
|
||||
if (builtin.link_libc) {
|
||||
if (switch (builtin.mode) {
|
||||
.ReleaseSafe, .ReleaseFast => true,
|
||||
|
||||
// We also use it if we can detect we're running under
|
||||
// Valgrind since Valgrind only instruments the C allocator
|
||||
else => std.valgrind.runningOnValgrind() > 0,
|
||||
}) break :gpa null;
|
||||
}
|
||||
|
||||
break :gpa GPA{};
|
||||
};
|
||||
defer if (gpa) |*value| {
|
||||
// We want to ensure that we deinit the GPA because this is
|
||||
// the point at which it will output if there were safety violations.
|
||||
_ = value.deinit();
|
||||
};
|
||||
|
||||
const alloc = alloc: {
|
||||
const base = if (gpa) |*value|
|
||||
value.allocator()
|
||||
else if (builtin.link_libc)
|
||||
std.heap.c_allocator
|
||||
else
|
||||
unreachable;
|
||||
|
||||
// If we're tracing, wrap the allocator
|
||||
if (!tracy.enabled) break :alloc base;
|
||||
var tracy_alloc = tracy.allocator(base, null);
|
||||
break :alloc tracy_alloc.allocator();
|
||||
};
|
||||
state.init();
|
||||
defer state.deinit();
|
||||
const alloc = state.alloc;
|
||||
|
||||
// Try reading our config
|
||||
var config = try Config.default(alloc);
|
||||
@ -133,9 +91,10 @@ pub fn main() !void {
|
||||
// We want to log all our errors
|
||||
glfw.setErrorCallback(glfwErrorCallback);
|
||||
|
||||
// Run our app
|
||||
var app = try App.create(alloc, &config);
|
||||
// Run our app with a single initial window to start.
|
||||
var app = try App.create(alloc, .{}, &config);
|
||||
defer app.destroy();
|
||||
_ = try app.newWindow(.{});
|
||||
try app.run();
|
||||
}
|
||||
|
||||
@ -206,6 +165,80 @@ fn glfwErrorCallback(code: glfw.ErrorCode, desc: [:0]const u8) void {
|
||||
}
|
||||
}
|
||||
|
||||
/// This represents the global process state. There should only
|
||||
/// be one of these at any given moment. This is extracted into a dedicated
|
||||
/// struct because it is reused by main and the static C lib.
|
||||
pub const GlobalState = struct {
|
||||
const GPA = std.heap.GeneralPurposeAllocator(.{});
|
||||
|
||||
gpa: ?GPA,
|
||||
alloc: std.mem.Allocator,
|
||||
|
||||
pub fn init(self: *GlobalState) void {
|
||||
// Output some debug information right away
|
||||
std.log.info("dependency harfbuzz={s}", .{harfbuzz.versionString()});
|
||||
if (options.fontconfig) {
|
||||
std.log.info("dependency fontconfig={d}", .{fontconfig.version()});
|
||||
}
|
||||
std.log.info("renderer={}", .{renderer.Renderer});
|
||||
std.log.info("libxev backend={}", .{xev.backend});
|
||||
|
||||
// First things first, we fix our file descriptors
|
||||
internal_os.fixMaxFiles();
|
||||
|
||||
// We need to make sure the process locale is set properly. Locale
|
||||
// affects a lot of behaviors in a shell.
|
||||
internal_os.ensureLocale();
|
||||
|
||||
// Initialize ourself to nothing so we don't have any extra state.
|
||||
self.* = .{
|
||||
.gpa = null,
|
||||
.alloc = undefined,
|
||||
};
|
||||
errdefer self.deinit();
|
||||
|
||||
self.gpa = gpa: {
|
||||
// Use the libc allocator if it is available beacuse it is WAY
|
||||
// faster than GPA. We only do this in release modes so that we
|
||||
// can get easy memory leak detection in debug modes.
|
||||
if (builtin.link_libc) {
|
||||
if (switch (builtin.mode) {
|
||||
.ReleaseSafe, .ReleaseFast => true,
|
||||
|
||||
// We also use it if we can detect we're running under
|
||||
// Valgrind since Valgrind only instruments the C allocator
|
||||
else => std.valgrind.runningOnValgrind() > 0,
|
||||
}) break :gpa null;
|
||||
}
|
||||
|
||||
break :gpa GPA{};
|
||||
};
|
||||
|
||||
self.alloc = alloc: {
|
||||
const base = if (self.gpa) |*value|
|
||||
value.allocator()
|
||||
else if (builtin.link_libc)
|
||||
std.heap.c_allocator
|
||||
else
|
||||
unreachable;
|
||||
|
||||
// If we're tracing, wrap the allocator
|
||||
if (!tracy.enabled) break :alloc base;
|
||||
var tracy_alloc = tracy.allocator(base, null);
|
||||
break :alloc tracy_alloc.allocator();
|
||||
};
|
||||
}
|
||||
|
||||
/// Cleans up the global state. This doesn't _need_ to be called but
|
||||
/// doing so in dev modes will check for memory leaks.
|
||||
pub fn deinit(self: *GlobalState) void {
|
||||
if (self.gpa) |*value| {
|
||||
// We want to ensure that we deinit the GPA because this is
|
||||
// the point at which it will output if there were safety violations.
|
||||
_ = value.deinit();
|
||||
}
|
||||
}
|
||||
};
|
||||
test {
|
||||
_ = @import("Pty.zig");
|
||||
_ = @import("Command.zig");
|
||||
|
32
src/main_c.zig
Normal 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;
|
||||
}
|
@ -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
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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",
|
||||
|