72
.github/workflows/release-tip.yml
vendored
@ -11,7 +11,7 @@ name: Release Tip
|
|||||||
jobs:
|
jobs:
|
||||||
build-macos:
|
build-macos:
|
||||||
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }}
|
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: macos-12
|
||||||
env:
|
env:
|
||||||
# Needed for macos SDK
|
# Needed for macos SDK
|
||||||
AGREE: "true"
|
AGREE: "true"
|
||||||
@ -22,45 +22,24 @@ jobs:
|
|||||||
submodules: recursive
|
submodules: recursive
|
||||||
fetch-depth: 0
|
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
|
- uses: cachix/install-nix-action@v19
|
||||||
with:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
|
|
||||||
# Cross-compile the binary. We always use static building for this
|
# GhosttyKit is the framework that is built from Zig for our native
|
||||||
# because its the only way to access the headers.
|
# Mac app to access. Build this in release mode.
|
||||||
- name: Build aarch64
|
- name: Build GhosttyKit
|
||||||
run: |
|
run: nix develop -c zig build -Dstatic=true -Doptimize=ReleaseFast
|
||||||
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
|
|
||||||
|
|
||||||
- name: Create Universal Binary
|
# The native app is built with native XCode tooling. This also does
|
||||||
run: |
|
# codesigning. IMPORTANT: this must NOT run in a Nix environment.
|
||||||
# Lipo our binaries
|
# Nix breaks xcodebuild so this has to be run outside.
|
||||||
nix develop -c \
|
- name: Build Ghostty.app
|
||||||
llvm-lipo \
|
run: cd macos && xcodebuild -configuration Release
|
||||||
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
|
|
||||||
|
|
||||||
- name: Zip Unsigned App
|
- 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
|
# Update Release
|
||||||
- name: Release Unsigned
|
- name: Release Unsigned
|
||||||
@ -80,25 +59,6 @@ jobs:
|
|||||||
message: "Latest Continuous Release"
|
message: "Latest Continuous Release"
|
||||||
force_push_tag: true
|
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
|
- name: Codesign app bundle
|
||||||
env:
|
env:
|
||||||
MACOS_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }}
|
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
|
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
|
# 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"
|
- name: "Notarize app bundle"
|
||||||
env:
|
env:
|
||||||
@ -136,7 +96,7 @@ jobs:
|
|||||||
# Therefore, we create a zip file containing our app bundle, so that we can send it to the
|
# Therefore, we create a zip file containing our app bundle, so that we can send it to the
|
||||||
# notarization service
|
# notarization service
|
||||||
echo "Creating temp notarization archive"
|
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.
|
# 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
|
# 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
|
# 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.
|
# validated by macOS even when an internet connection is not available.
|
||||||
echo "Attach staple"
|
echo "Attach staple"
|
||||||
xcrun stapler staple "zig-out/Ghostty.app"
|
xcrun stapler staple "build/Release/Ghostty.app"
|
||||||
|
|
||||||
# Zip up the app
|
# Zip up the app
|
||||||
- name: Zip 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
|
# Update Release
|
||||||
- name: Release
|
- name: Release
|
||||||
|
29
.github/workflows/test.yml
vendored
@ -40,6 +40,35 @@ jobs:
|
|||||||
- name: Test Build
|
- name: Test Build
|
||||||
run: nix develop -c zig build -Dstatic=true -Dtarget=${{ matrix.target }}
|
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:
|
test:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
|
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
|||||||
|
.DS_Store
|
||||||
.direnv/
|
.direnv/
|
||||||
zig-cache/
|
zig-cache/
|
||||||
zig-out/
|
zig-out/
|
||||||
|
159
build.zig
@ -21,6 +21,9 @@ const zlib = @import("pkg/zlib/build.zig");
|
|||||||
const tracylib = @import("pkg/tracy/build.zig");
|
const tracylib = @import("pkg/tracy/build.zig");
|
||||||
const system_sdk = @import("vendor/mach/libs/glfw/system_sdk.zig");
|
const system_sdk = @import("vendor/mach/libs/glfw/system_sdk.zig");
|
||||||
const WasmTarget = @import("src/os/wasm/target.zig").Target;
|
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
|
// 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,
|
// 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();
|
exe.install();
|
||||||
|
|
||||||
// Add the shared dependencies
|
// Add the shared dependencies
|
||||||
try addDeps(b, exe, static);
|
_ = try addDeps(b, exe, static);
|
||||||
}
|
}
|
||||||
|
|
||||||
// App (Mac)
|
// 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");
|
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
|
// wasm
|
||||||
{
|
{
|
||||||
// Build our Wasm target.
|
// Build our Wasm target.
|
||||||
@ -179,7 +261,7 @@ pub fn build(b: *std.build.Builder) !void {
|
|||||||
wasm.stack_protector = false;
|
wasm.stack_protector = false;
|
||||||
|
|
||||||
// Wasm-specific deps
|
// Wasm-specific deps
|
||||||
try addDeps(b, wasm, true);
|
_ = try addDeps(b, wasm, true);
|
||||||
|
|
||||||
const step = b.step("wasm", "Build the wasm library");
|
const step = b.step("wasm", "Build the wasm library");
|
||||||
step.dependOn(&wasm.step);
|
step.dependOn(&wasm.step);
|
||||||
@ -194,7 +276,7 @@ pub fn build(b: *std.build.Builder) !void {
|
|||||||
.target = wasm_target,
|
.target = wasm_target,
|
||||||
});
|
});
|
||||||
main_test.addOptions("build_options", exe_options);
|
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);
|
test_step.dependOn(&main_test.step);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -238,7 +320,7 @@ pub fn build(b: *std.build.Builder) !void {
|
|||||||
main_test_exe.install();
|
main_test_exe.install();
|
||||||
}
|
}
|
||||||
main_test.setFilter(test_filter);
|
main_test.setFilter(test_filter);
|
||||||
try addDeps(b, main_test, true);
|
_ = try addDeps(b, main_test, true);
|
||||||
main_test.addOptions("build_options", exe_options);
|
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"});
|
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,
|
.target = target,
|
||||||
});
|
});
|
||||||
|
|
||||||
try addDeps(b, test_run, true);
|
_ = try addDeps(b, test_run, true);
|
||||||
// if (pkg.dependencies) |children| {
|
// if (pkg.dependencies) |children| {
|
||||||
// test_.packages = std.ArrayList(std.build.Pkg).init(b.allocator);
|
// test_.packages = std.ArrayList(std.build.Pkg).init(b.allocator);
|
||||||
// try test_.packages.appendSlice(children);
|
// 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.
|
/// Adds and links all of the primary dependencies for the exe.
|
||||||
fn addDeps(
|
fn addDeps(
|
||||||
b: *std.build.Builder,
|
b: *std.build.Builder,
|
||||||
step: *std.build.LibExeObjStep,
|
step: *std.build.LibExeObjStep,
|
||||||
static: bool,
|
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.
|
// Wasm we do manually since it is such a different build.
|
||||||
if (step.target.getCpuArch() == .wasm32) {
|
if (step.target.getCpuArch() == .wasm32) {
|
||||||
// We link this package but its a no-op since Tracy
|
// We link this package but its a no-op since Tracy
|
||||||
@ -308,15 +396,21 @@ fn addDeps(
|
|||||||
// utf8proc
|
// utf8proc
|
||||||
_ = try utf8proc.link(b, step);
|
_ = 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
|
// We always need the Zig packages
|
||||||
if (enable_fontconfig) step.addModule("fontconfig", fontconfig.module(b));
|
if (enable_fontconfig) step.addModule("fontconfig", fontconfig.module(b));
|
||||||
step.addModule("freetype", freetype.module(b));
|
step.addModule("freetype", freetype.module(b));
|
||||||
step.addModule("harfbuzz", harfbuzz.module(b));
|
step.addModule("harfbuzz", harfbuzz.module(b));
|
||||||
step.addModule("imgui", imgui.module(b));
|
step.addModule("imgui", imgui.module(b));
|
||||||
step.addModule("glfw", glfw.module(b));
|
|
||||||
step.addModule("xev", libxev.module(b));
|
step.addModule("xev", libxev.module(b));
|
||||||
step.addModule("pixman", pixman.module(b));
|
step.addModule("pixman", pixman.module(b));
|
||||||
step.addModule("stb_image_resize", stb_image_resize.module(b));
|
step.addModule("stb_image_resize", stb_image_resize.module(b));
|
||||||
@ -329,10 +423,6 @@ fn addDeps(
|
|||||||
_ = try macos.link(b, step, .{});
|
_ = try macos.link(b, step, .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
// We always statically compile glad
|
|
||||||
step.addIncludePath("vendor/glad/include/");
|
|
||||||
step.addCSourceFile("vendor/glad/src/gl.c", &.{});
|
|
||||||
|
|
||||||
// Tracy
|
// Tracy
|
||||||
step.addModule("tracy", tracylib.module(b));
|
step.addModule("tracy", tracylib.module(b));
|
||||||
if (tracy) {
|
if (tracy) {
|
||||||
@ -341,17 +431,12 @@ fn addDeps(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// stb_image_resize
|
// 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
|
// utf8proc
|
||||||
_ = try utf8proc.link(b, step);
|
const utf8proc_step = try utf8proc.link(b, step);
|
||||||
|
try static_libs.append(.{ .generated = &utf8proc_step.output_path_source });
|
||||||
// Glfw
|
|
||||||
const glfw_opts: glfw.Options = .{
|
|
||||||
.metal = step.target.isDarwin(),
|
|
||||||
.opengl = false,
|
|
||||||
};
|
|
||||||
try glfw.link(b, step, glfw_opts);
|
|
||||||
|
|
||||||
// Imgui, we have to do this later since we need some information
|
// Imgui, we have to do this later since we need some information
|
||||||
const imgui_backends = if (step.target.isDarwin())
|
const imgui_backends = if (step.target.isDarwin())
|
||||||
@ -379,12 +464,15 @@ fn addDeps(
|
|||||||
// Other dependencies, we may dynamically link
|
// Other dependencies, we may dynamically link
|
||||||
if (static) {
|
if (static) {
|
||||||
const zlib_step = try zlib.link(b, step);
|
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, .{
|
const libpng_step = try libpng.link(b, step, .{
|
||||||
.zlib = .{
|
.zlib = .{
|
||||||
.step = zlib_step,
|
.step = zlib_step,
|
||||||
.include = &zlib.include_paths,
|
.include = &zlib.include_paths,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
try static_libs.append(.{ .generated = &libpng_step.output_path_source });
|
||||||
|
|
||||||
// Freetype
|
// Freetype
|
||||||
const freetype_step = try freetype.link(b, step, .{
|
const freetype_step = try freetype.link(b, step, .{
|
||||||
@ -400,6 +488,7 @@ fn addDeps(
|
|||||||
.include = &zlib.include_paths,
|
.include = &zlib.include_paths,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
try static_libs.append(.{ .generated = &freetype_step.output_path_source });
|
||||||
|
|
||||||
// Harfbuzz
|
// Harfbuzz
|
||||||
const harfbuzz_step = try harfbuzz.link(b, step, .{
|
const harfbuzz_step = try harfbuzz.link(b, step, .{
|
||||||
@ -414,10 +503,11 @@ fn addDeps(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
system_sdk.include(b, harfbuzz_step, .{});
|
system_sdk.include(b, harfbuzz_step, .{});
|
||||||
|
try static_libs.append(.{ .generated = &harfbuzz_step.output_path_source });
|
||||||
|
|
||||||
// Pixman
|
// Pixman
|
||||||
const pixman_step = try pixman.link(b, step, .{});
|
const pixman_step = try pixman.link(b, step, .{});
|
||||||
_ = pixman_step;
|
try static_libs.append(.{ .generated = &pixman_step.output_path_source });
|
||||||
|
|
||||||
// Only Linux gets fontconfig
|
// Only Linux gets fontconfig
|
||||||
if (enable_fontconfig) {
|
if (enable_fontconfig) {
|
||||||
@ -448,9 +538,26 @@ fn addDeps(
|
|||||||
imgui_opts.freetype.include = &freetype.include_paths;
|
imgui_opts.freetype.include = &freetype.include_paths;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Imgui
|
if (!lib) {
|
||||||
const imgui_step = try imgui.link(b, step, imgui_opts);
|
step.addModule("glfw", glfw.module(b));
|
||||||
try glfw.link(b, imgui_step, glfw_opts);
|
|
||||||
|
// 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(
|
fn benchSteps(
|
||||||
@ -490,7 +597,7 @@ fn benchSteps(
|
|||||||
});
|
});
|
||||||
c_exe.setMainPkgPath("./src");
|
c_exe.setMainPkgPath("./src");
|
||||||
c_exe.install();
|
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-region16.c",
|
||||||
root ++ "pixman/pixman-region32.c",
|
root ++ "pixman/pixman-region32.c",
|
||||||
root ++ "pixman/pixman-solid-fill.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-trap.c",
|
||||||
root ++ "pixman/pixman-utils.c",
|
root ++ "pixman/pixman-utils.c",
|
||||||
};
|
};
|
||||||
|
246
src/App.zig
@ -5,10 +5,13 @@ const App = @This();
|
|||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
|
const assert = std.debug.assert;
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
const build_config = @import("build_config.zig");
|
||||||
const apprt = @import("apprt.zig");
|
const apprt = @import("apprt.zig");
|
||||||
const Window = @import("Window.zig");
|
const Window = @import("Window.zig");
|
||||||
const tracy = @import("tracy");
|
const tracy = @import("tracy");
|
||||||
|
const input = @import("input.zig");
|
||||||
const Config = @import("config.zig").Config;
|
const Config = @import("config.zig").Config;
|
||||||
const BlockingQueue = @import("./blocking_queue.zig").BlockingQueue;
|
const BlockingQueue = @import("./blocking_queue.zig").BlockingQueue;
|
||||||
const renderer = @import("renderer.zig");
|
const renderer = @import("renderer.zig");
|
||||||
@ -46,9 +49,11 @@ quit: bool,
|
|||||||
/// Mac settings
|
/// Mac settings
|
||||||
darwin: if (Darwin.enabled) Darwin else void,
|
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 Darwin = struct {
|
||||||
pub const enabled = builtin.target.isDarwin();
|
pub const enabled = builtin.target.isDarwin() and build_config.artifact == .exe;
|
||||||
|
|
||||||
tabbing_id: *macos.foundation.String,
|
tabbing_id: *macos.foundation.String,
|
||||||
|
|
||||||
@ -61,9 +66,13 @@ pub const Darwin = struct {
|
|||||||
/// Initialize the main app instance. This creates the main window, sets
|
/// Initialize the main app instance. This creates the main window, sets
|
||||||
/// up the renderer state, compiles the shaders, etc. This is the primary
|
/// up the renderer state, compiles the shaders, etc. This is the primary
|
||||||
/// "startup" logic.
|
/// "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
|
// 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();
|
errdefer app_backend.terminate();
|
||||||
|
|
||||||
// The mailbox for messaging this thread
|
// The mailbox for messaging this thread
|
||||||
@ -86,8 +95,9 @@ pub fn create(alloc: Allocator, config: *const Config) !*App {
|
|||||||
};
|
};
|
||||||
errdefer app.windows.deinit(alloc);
|
errdefer app.windows.deinit(alloc);
|
||||||
|
|
||||||
// On Mac, we enable window tabbing
|
// On Mac, we enable window tabbing. We only do this if we're building
|
||||||
if (comptime builtin.target.isDarwin()) {
|
// a standalone exe. In embedded mode the host app handles this for us.
|
||||||
|
if (Darwin.enabled) {
|
||||||
const NSWindow = objc.Class.getClass("NSWindow").?;
|
const NSWindow = objc.Class.getClass("NSWindow").?;
|
||||||
NSWindow.msgSend(void, objc.sel("setAllowsAutomaticWindowTabbing:"), .{true});
|
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();
|
errdefer if (comptime builtin.target.isDarwin()) app.darwin.deinit();
|
||||||
|
|
||||||
// Create the first window
|
|
||||||
_ = try app.newWindow(.{});
|
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,7 +123,7 @@ pub fn destroy(self: *App) void {
|
|||||||
// Clean up all our windows
|
// Clean up all our windows
|
||||||
for (self.windows.items) |window| window.destroy();
|
for (self.windows.items) |window| window.destroy();
|
||||||
self.windows.deinit(self.alloc);
|
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.mailbox.destroy(self.alloc);
|
||||||
self.alloc.destroy(self);
|
self.alloc.destroy(self);
|
||||||
|
|
||||||
@ -134,24 +141,61 @@ pub fn wakeup(self: App) void {
|
|||||||
/// application quits or every window is closed.
|
/// application quits or every window is closed.
|
||||||
pub fn run(self: *App) !void {
|
pub fn run(self: *App) !void {
|
||||||
while (!self.quit and self.windows.items.len > 0) {
|
while (!self.quit and self.windows.items.len > 0) {
|
||||||
// Block for any events.
|
try self.tick();
|
||||||
try self.runtime.wait();
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If any windows are closing, destroy them
|
/// Tick ticks the app loop. This will drain our mailbox and process those
|
||||||
var i: usize = 0;
|
/// events.
|
||||||
while (i < self.windows.items.len) {
|
pub fn tick(self: *App) !void {
|
||||||
const window = self.windows.items[i];
|
// Block for any events.
|
||||||
if (window.shouldClose()) {
|
try self.runtime.wait();
|
||||||
window.destroy();
|
|
||||||
_ = self.windows.swapRemove(i);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
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.
|
i += 1;
|
||||||
if (!self.quit) try self.drainMailbox();
|
}
|
||||||
|
|
||||||
|
// 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
|
/// Create a new tab in the parent window
|
||||||
fn newTab(self: *App, msg: Message.NewWindow) !void {
|
fn newTab(self: *App, msg: Message.NewWindow) !void {
|
||||||
if (comptime !builtin.target.isDarwin()) {
|
if (comptime !builtin.target.isDarwin()) {
|
||||||
@ -188,6 +219,13 @@ fn newTab(self: *App, msg: Message.NewWindow) !void {
|
|||||||
return;
|
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 {
|
const parent = msg.parent orelse {
|
||||||
log.warn("parent must be set in new_tab message", .{});
|
log.warn("parent must be set in new_tab message", .{});
|
||||||
return;
|
return;
|
||||||
@ -258,6 +296,9 @@ pub const Message = union(enum) {
|
|||||||
},
|
},
|
||||||
|
|
||||||
const NewWindow = struct {
|
const NewWindow = struct {
|
||||||
|
/// Runtime-specific window options.
|
||||||
|
runtime: apprt.runtime.Window.Options = .{},
|
||||||
|
|
||||||
/// The parent window, only used for new tabs.
|
/// The parent window, only used for new tabs.
|
||||||
parent: ?*Window = null,
|
parent: ?*Window = null,
|
||||||
|
|
||||||
@ -273,8 +314,7 @@ pub const Wasm = if (!builtin.target.isWasm()) struct {} else struct {
|
|||||||
const alloc = wasm.alloc;
|
const alloc = wasm.alloc;
|
||||||
|
|
||||||
// export fn app_new(config: *Config) ?*App {
|
// export fn app_new(config: *Config) ?*App {
|
||||||
// return app_new_(config) catch |err| {
|
// return app_new_(config) catch |err| { log.err("error initializing app err={}", .{err});
|
||||||
// log.err("error initializing app err={}", .{err});
|
|
||||||
// return null;
|
// 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 std = @import("std");
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
|
const build_config = @import("build_config.zig");
|
||||||
const imgui = @import("imgui");
|
const imgui = @import("imgui");
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const assert = std.debug.assert;
|
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
|
/// If this is false, the rest of the terminal will be compiled without
|
||||||
/// dev mode support at all.
|
/// 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
|
/// The global DevMode instance that can be used app-wide. Assume all functions
|
||||||
/// are NOT thread-safe unless otherwise noted.
|
/// 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
|
// https://github.com/ziglang/zig/issues/13277
|
||||||
// Once above is fixed, use `c.TIOCSCTTY`
|
// Once above is fixed, use `c.TIOCSCTTY`
|
||||||
const TIOCSCTTY = if (builtin.os.tag == .macos) 536900705 else 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;
|
if (setsid() < 0) return error.ProcessGroupFailed;
|
||||||
|
|
||||||
// Set controlling terminal
|
// Set controlling terminal
|
||||||
if (c.ioctl(self.slave, TIOCSCTTY, @as(c_ulong, 0)) < 0)
|
switch (std.os.system.getErrno(c.ioctl(self.slave, TIOCSCTTY, @as(c_ulong, 0)))) {
|
||||||
return error.SetControllingTerminalFailed;
|
.SUCCESS => {},
|
||||||
|
else => |err| {
|
||||||
|
log.err("error setting controlling terminal errno={}", .{err});
|
||||||
|
return error.SetControllingTerminalFailed;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
// Can close master/slave pair now
|
// Can close master/slave pair now
|
||||||
std.os.close(self.slave);
|
std.os.close(self.slave);
|
||||||
|
@ -128,12 +128,17 @@ const Mouse = struct {
|
|||||||
/// Create a new window. This allocates and returns a pointer because we
|
/// Create a new window. This allocates and returns a pointer because we
|
||||||
/// need a stable pointer for user data callbacks. Therefore, a stack-only
|
/// need a stable pointer for user data callbacks. Therefore, a stack-only
|
||||||
/// initialization is not currently possible.
|
/// 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);
|
var self = try alloc.create(Window);
|
||||||
errdefer alloc.destroy(self);
|
errdefer alloc.destroy(self);
|
||||||
|
|
||||||
// Create the windowing system
|
// 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();
|
errdefer window.deinit();
|
||||||
|
|
||||||
// Initialize our renderer with our initialized windowing system.
|
// Initialize our renderer with our initialized windowing system.
|
||||||
@ -1287,7 +1292,7 @@ pub fn mouseButtonCallback(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Always record our latest mouse state
|
// 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.mouse.mods = @bitCast(input.Mods, mods);
|
||||||
|
|
||||||
self.renderer_state.mutex.lock();
|
self.renderer_state.mutex.lock();
|
||||||
|
@ -9,20 +9,22 @@
|
|||||||
//! logic as possible, and to only reach out to platform-specific implementation
|
//! logic as possible, and to only reach out to platform-specific implementation
|
||||||
//! code when absolutely necessary.
|
//! code when absolutely necessary.
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
|
const build_config = @import("build_config.zig");
|
||||||
|
|
||||||
pub usingnamespace @import("apprt/structs.zig");
|
pub usingnamespace @import("apprt/structs.zig");
|
||||||
pub const glfw = @import("apprt/glfw.zig");
|
pub const glfw = @import("apprt/glfw.zig");
|
||||||
pub const browser = @import("apprt/browser.zig");
|
pub const browser = @import("apprt/browser.zig");
|
||||||
|
pub const embedded = @import("apprt/embedded.zig");
|
||||||
pub const Window = @import("apprt/Window.zig");
|
pub const Window = @import("apprt/Window.zig");
|
||||||
|
|
||||||
/// The implementation to use for the app runtime. This is comptime chosen
|
/// The implementation to use for the app runtime. This is comptime chosen
|
||||||
/// so that every build has exactly one application runtime implementation.
|
/// so that every build has exactly one application runtime implementation.
|
||||||
/// Note: it is very rare to use Runtime directly; most usage will use
|
/// Note: it is very rare to use Runtime directly; most usage will use
|
||||||
/// Window or something.
|
/// Window or something.
|
||||||
pub const runtime = if (builtin.target.isWasm())
|
pub const runtime = switch (build_config.artifact) {
|
||||||
browser
|
.exe => glfw,
|
||||||
else switch (builtin.os.tag) {
|
.lib => embedded,
|
||||||
else => glfw,
|
.wasm_module => browser,
|
||||||
};
|
};
|
||||||
|
|
||||||
test {
|
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);
|
const log = std.log.scoped(.glfw);
|
||||||
|
|
||||||
pub const App = struct {
|
pub const App = struct {
|
||||||
pub fn init() !App {
|
pub const Options = struct {};
|
||||||
|
|
||||||
|
pub fn init(_: Options) !App {
|
||||||
if (!glfw.init(.{})) return error.GlfwInitFailed;
|
if (!glfw.init(.{})) return error.GlfwInitFailed;
|
||||||
return .{};
|
return .{};
|
||||||
}
|
}
|
||||||
@ -56,7 +58,11 @@ pub const Window = struct {
|
|||||||
/// The glfw mouse cursor handle.
|
/// The glfw mouse cursor handle.
|
||||||
cursor: glfw.Cursor,
|
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
|
// Create our window
|
||||||
const win = glfw.Window.create(
|
const win = glfw.Window.create(
|
||||||
640,
|
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 {
|
test {
|
||||||
std.testing.refAllDecls(@This());
|
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
|
/// A bitmask for all key modifiers. This is taken directly from the
|
||||||
/// GLFW representation, but we use this generically.
|
/// 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,
|
shift: bool = false,
|
||||||
ctrl: bool = false,
|
ctrl: bool = false,
|
||||||
alt: bool = false,
|
alt: bool = false,
|
||||||
@ -11,10 +13,23 @@ pub const Mods = packed struct {
|
|||||||
caps_lock: bool = false,
|
caps_lock: bool = false,
|
||||||
num_lock: bool = false,
|
num_lock: bool = false,
|
||||||
_padding: u2 = 0,
|
_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.
|
/// The action associated with an input event. This is backed by a c_int
|
||||||
pub const Action = enum {
|
/// 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,
|
release,
|
||||||
press,
|
press,
|
||||||
repeat,
|
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
|
/// 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
|
/// to anything and the key can be mapped to a printable character, then that
|
||||||
/// unicode character is sent directly to the pty.
|
/// 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,
|
invalid,
|
||||||
|
|
||||||
// a-z
|
// a-z
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
/// The state of a mouse button.
|
/// The state of a mouse button.
|
||||||
pub const MouseButtonState = enum(u1) {
|
///
|
||||||
release = 0,
|
/// This is backed by a c_int so we can use this as-is for our embedding API.
|
||||||
press = 1,
|
///
|
||||||
|
/// 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
|
/// 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
|
/// Its a bit silly to name numbers like this but given its a restricted
|
||||||
/// set, it feels better than passing around raw numeric literals.
|
/// 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();
|
const Self = @This();
|
||||||
|
|
||||||
/// The maximum value in this enum. This can be used to create a densely
|
/// 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 builtin = @import("builtin");
|
||||||
const options = @import("build_options");
|
const options = @import("build_options");
|
||||||
const glfw = @import("glfw");
|
const glfw = @import("glfw");
|
||||||
const fontconfig = @import("fontconfig");
|
|
||||||
const freetype = @import("freetype");
|
|
||||||
const harfbuzz = @import("harfbuzz");
|
|
||||||
const macos = @import("macos");
|
const macos = @import("macos");
|
||||||
const tracy = @import("tracy");
|
const tracy = @import("tracy");
|
||||||
|
const internal_os = @import("os/main.zig");
|
||||||
const xev = @import("xev");
|
const xev = @import("xev");
|
||||||
|
const fontconfig = @import("fontconfig");
|
||||||
|
const harfbuzz = @import("harfbuzz");
|
||||||
const renderer = @import("renderer.zig");
|
const renderer = @import("renderer.zig");
|
||||||
const xdg = @import("xdg.zig");
|
const xdg = @import("xdg.zig");
|
||||||
const internal_os = @import("os/main.zig");
|
|
||||||
|
|
||||||
const App = @import("App.zig");
|
const App = @import("App.zig");
|
||||||
const cli_args = @import("cli_args.zig");
|
const cli_args = @import("cli_args.zig");
|
||||||
const Config = @import("config.zig").Config;
|
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 {
|
pub fn main() !void {
|
||||||
// Output some debug information right away
|
state.init();
|
||||||
std.log.info("dependency harfbuzz={s}", .{harfbuzz.versionString()});
|
defer state.deinit();
|
||||||
if (options.fontconfig) {
|
const alloc = state.alloc;
|
||||||
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();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Try reading our config
|
// Try reading our config
|
||||||
var config = try Config.default(alloc);
|
var config = try Config.default(alloc);
|
||||||
@ -133,9 +91,10 @@ pub fn main() !void {
|
|||||||
// We want to log all our errors
|
// We want to log all our errors
|
||||||
glfw.setErrorCallback(glfwErrorCallback);
|
glfw.setErrorCallback(glfwErrorCallback);
|
||||||
|
|
||||||
// Run our app
|
// Run our app with a single initial window to start.
|
||||||
var app = try App.create(alloc, &config);
|
var app = try App.create(alloc, .{}, &config);
|
||||||
defer app.destroy();
|
defer app.destroy();
|
||||||
|
_ = try app.newWindow(.{});
|
||||||
try app.run();
|
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 {
|
test {
|
||||||
_ = @import("Pty.zig");
|
_ = @import("Pty.zig");
|
||||||
_ = @import("Command.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
|
/// This is called just prior to spinning up the renderer thread for
|
||||||
/// final main thread setup requirements.
|
/// final main thread setup requirements.
|
||||||
pub fn finalizeWindowInit(self: *const Metal, win: apprt.runtime.Window) !void {
|
pub fn finalizeWindowInit(self: *const Metal, win: apprt.runtime.Window) !void {
|
||||||
// Set our window backing layer to be our swapchain
|
const Info = struct {
|
||||||
const nswindow = switch (apprt.runtime) {
|
view: objc.Object,
|
||||||
apprt.glfw => objc.Object.fromId(glfwNative.getCocoaWindow(win.window).?),
|
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"),
|
else => @compileError("unsupported apprt for metal"),
|
||||||
};
|
};
|
||||||
const contentView = objc.Object.fromId(nswindow.getProperty(?*anyopaque, "contentView").?);
|
|
||||||
contentView.setProperty("layer", self.swapchain.value);
|
// Make our view layer-backed with our Metal layer
|
||||||
contentView.setProperty("wantsLayer", true);
|
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
|
// Ensure that our metal layer has a content scale set to match the
|
||||||
// scale factor of the window. This avoids magnification issues leading
|
// scale factor of the window. This avoids magnification issues leading
|
||||||
// to blurry rendering.
|
// to blurry rendering.
|
||||||
const layer = contentView.getProperty(objc.Object, "layer");
|
const layer = info.view.getProperty(objc.Object, "layer");
|
||||||
const scaleFactor = nswindow.getProperty(macos.graphics.c.CGFloat, "backingScaleFactor");
|
layer.setProperty("contentsScale", info.scaleFactor);
|
||||||
layer.setProperty("contentsScale", scaleFactor);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This is called if this renderer runs DevMode.
|
/// This is called if this renderer runs DevMode.
|
||||||
@ -547,9 +568,14 @@ pub fn render(
|
|||||||
.{@as(c_ulong, 0)},
|
.{@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("loadAction", @enumToInt(MTLLoadAction.clear));
|
||||||
attachment.setProperty("storeAction", @enumToInt(MTLStoreAction.store));
|
attachment.setProperty("storeAction", @enumToInt(MTLStoreAction.store));
|
||||||
attachment.setProperty("texture", surface.getProperty(objc.c.id, "texture").?);
|
attachment.setProperty("texture", texture);
|
||||||
attachment.setProperty("clearColor", MTLClearColor{
|
attachment.setProperty("clearColor", MTLClearColor{
|
||||||
.red = @intToFloat(f32, critical.bg.r) / 255,
|
.red = @intToFloat(f32, critical.bg.r) / 255,
|
||||||
.green = @intToFloat(f32, critical.bg.g) / 255,
|
.green = @intToFloat(f32, critical.bg.g) / 255,
|
||||||
@ -613,16 +639,18 @@ pub fn render(
|
|||||||
state.mutex.lock();
|
state.mutex.lock();
|
||||||
defer state.mutex.unlock();
|
defer state.mutex.unlock();
|
||||||
|
|
||||||
if (state.devmode) |dm| {
|
if (DevMode.enabled) {
|
||||||
if (dm.visible) {
|
if (state.devmode) |dm| {
|
||||||
imgui.ImplMetal.newFrame(desc.value);
|
if (dm.visible) {
|
||||||
imgui.ImplGlfw.newFrame();
|
imgui.ImplMetal.newFrame(desc.value);
|
||||||
try dm.update();
|
imgui.ImplGlfw.newFrame();
|
||||||
imgui.ImplMetal.renderDrawData(
|
try dm.update();
|
||||||
try dm.render(),
|
imgui.ImplMetal.renderDrawData(
|
||||||
buffer.value,
|
try dm.render(),
|
||||||
encoder.value,
|
buffer.value,
|
||||||
);
|
encoder.value,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -650,18 +678,20 @@ fn drawCells(
|
|||||||
.{ buf.value, @as(c_ulong, 0), @as(c_ulong, 0) },
|
.{ buf.value, @as(c_ulong, 0), @as(c_ulong, 0) },
|
||||||
);
|
);
|
||||||
|
|
||||||
encoder.msgSend(
|
if (cells.items.len > 0) {
|
||||||
void,
|
encoder.msgSend(
|
||||||
objc.sel("drawIndexedPrimitives:indexCount:indexType:indexBuffer:indexBufferOffset:instanceCount:"),
|
void,
|
||||||
.{
|
objc.sel("drawIndexedPrimitives:indexCount:indexType:indexBuffer:indexBufferOffset:instanceCount:"),
|
||||||
@enumToInt(MTLPrimitiveType.triangle),
|
.{
|
||||||
@as(c_ulong, 6),
|
@enumToInt(MTLPrimitiveType.triangle),
|
||||||
@enumToInt(MTLIndexType.uint16),
|
@as(c_ulong, 6),
|
||||||
self.buf_instance.value,
|
@enumToInt(MTLIndexType.uint16),
|
||||||
@as(c_ulong, 0),
|
self.buf_instance.value,
|
||||||
@as(c_ulong, cells.items.len),
|
@as(c_ulong, 0),
|
||||||
},
|
@as(c_ulong, cells.items.len),
|
||||||
);
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resize the screen.
|
/// Resize the screen.
|
||||||
@ -690,7 +720,8 @@ pub fn setScreenSize(self: *Metal, _: renderer.ScreenSize) !void {
|
|||||||
// and we split them equal across all boundaries.
|
// and we split them equal across all boundaries.
|
||||||
const padding = self.padding.explicit.add(if (self.padding.balance)
|
const padding = self.padding.explicit.add(if (self.padding.balance)
|
||||||
renderer.Padding.balanced(dim, grid_size, self.cell_size)
|
renderer.Padding.balanced(dim, grid_size, self.cell_size)
|
||||||
else .{});
|
else
|
||||||
|
.{});
|
||||||
const padded_dim = dim.subPadding(padding);
|
const padded_dim = dim.subPadding(padding);
|
||||||
|
|
||||||
// Update our shaper
|
// Update our shaper
|
||||||
|
@ -106,10 +106,10 @@ pub const Padding = struct {
|
|||||||
const padding_bot = space_bot - padding_top;
|
const padding_bot = space_bot - padding_top;
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.top = padding_top,
|
.top = @max(0, padding_top),
|
||||||
.bottom = padding_bot,
|
.bottom = @max(0, padding_bot),
|
||||||
.right = padding_right,
|
.right = @max(0, padding_right),
|
||||||
.left = padding_left,
|
.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" {
|
test "GridSize update exact" {
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ pub const Exec = @This();
|
|||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
|
const build_config = @import("../build_config.zig");
|
||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const termio = @import("../termio.zig");
|
const termio = @import("../termio.zig");
|
||||||
@ -361,6 +362,21 @@ const Subprocess = struct {
|
|||||||
try env.put("TERM", "xterm-256color");
|
try env.put("TERM", "xterm-256color");
|
||||||
try env.put("COLORTERM", "truecolor");
|
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 .{
|
return .{
|
||||||
.env = env,
|
.env = env,
|
||||||
.cwd = opts.config.@"working-directory",
|
.cwd = opts.config.@"working-directory",
|
||||||
|