Merge pull request #1298 from mitchellh/ios

build: add iOS-compatible libghostty build to xcframework
This commit is contained in:
Mitchell Hashimoto
2024-01-14 20:56:42 -08:00
committed by GitHub
53 changed files with 1162 additions and 487 deletions

View File

@ -88,7 +88,7 @@ jobs:
# codesigning. IMPORTANT: this must NOT run in a Nix environment.
# Nix breaks xcodebuild so this has to be run outside.
- name: Build Ghostty.app
run: cd macos && xcodebuild -configuration Release
run: cd macos && xcodebuild -target Ghostty -configuration Release
# We inject the "build number" as simply the number of commits since HEAD.
# This will be a monotonically always increasing build number that we use.

View File

@ -88,7 +88,13 @@ jobs:
# codesigning. IMPORTANT: this must NOT run in a Nix environment.
# Nix breaks xcodebuild so this has to be run outside.
- name: Build Ghostty.app
run: cd macos && xcodebuild
run: cd macos && xcodebuild -target Ghostty
# Build the iOS target without code signing just to verify it works.
- name: Build Ghostty iOS
run: |
cd macos
xcodebuild -target Ghostty-iOS "CODE_SIGNING_ALLOWED=NO"
build-windows:
runs-on: windows-2019

342
build.zig
View File

@ -37,25 +37,7 @@ const app_version = std.SemanticVersion{ .major = 0, .minor = 1, .patch = 0 };
pub fn build(b: *std.Build) !void {
const optimize = b.standardOptimizeOption(.{});
const target = target: {
var result = b.standardTargetOptions(.{});
// On macOS, we specify a minimum supported version. This is important
// to set since header files will use this to determine the availability
// of certain APIs and I believe it is also encoded in the Mach-O
// binaries.
if (result.result.os.tag == .macos and
result.query.os_version_min == null)
{
result.query.os_version_min = .{ .semver = .{
.major = 12,
.minor = 0,
.patch = 0,
} };
}
break :target result;
};
const target = b.standardTargetOptions(.{});
const wasm_target: WasmTarget = .browser;
@ -438,92 +420,55 @@ pub fn build(b: *std.Build) !void {
// On Mac we can build the embedding library. This only handles the macOS lib.
if (builtin.target.isDarwin() and target.result.os.tag == .macos) {
// Modify our build configuration for macOS builds.
const macos_config: BuildConfig = config: {
var copy = config;
// Always static for the macOS app because we want all of our
// dependencies in a fat static library.
copy.static = true;
break :config copy;
};
const static_lib_aarch64 = lib: {
const lib = b.addStaticLibrary(.{
.name = "ghostty",
.root_source_file = .{ .path = "src/main_c.zig" },
.target = b.resolveTargetQuery(.{
.cpu_arch = .aarch64,
.os_tag = .macos,
.os_version_min = target.query.os_version_min,
}),
.optimize = optimize,
});
lib.bundle_compiler_rt = true;
lib.linkLibC();
lib.root_module.addOptions("build_options", exe_options);
// Create a single static lib with all our dependencies merged
var lib_list = try addDeps(b, lib, macos_config);
try lib_list.append(lib.getEmittedBin());
const libtool = LibtoolStep.create(b, .{
.name = "ghostty",
.out_name = "libghostty-aarch64-fat.a",
.sources = lib_list.items,
});
libtool.step.dependOn(&lib.step);
b.default_step.dependOn(libtool.step);
break :lib libtool;
};
const static_lib_x86_64 = lib: {
const lib = b.addStaticLibrary(.{
.name = "ghostty",
.root_source_file = .{ .path = "src/main_c.zig" },
.target = b.resolveTargetQuery(.{
.cpu_arch = .x86_64,
.os_tag = .macos,
.os_version_min = target.query.os_version_min,
}),
.optimize = optimize,
});
lib.bundle_compiler_rt = true;
lib.linkLibC();
lib.root_module.addOptions("build_options", exe_options);
// Create a single static lib with all our dependencies merged
var lib_list = try addDeps(b, lib, macos_config);
try lib_list.append(lib.getEmittedBin());
const libtool = LibtoolStep.create(b, .{
.name = "ghostty",
.out_name = "libghostty-x86_64-fat.a",
.sources = lib_list.items,
});
libtool.step.dependOn(&lib.step);
b.default_step.dependOn(libtool.step);
break :lib libtool;
};
const static_lib_universal = LipoStep.create(b, .{
.name = "ghostty",
.out_name = "libghostty.a",
.input_a = static_lib_aarch64.output,
.input_b = static_lib_x86_64.output,
});
static_lib_universal.step.dependOn(static_lib_aarch64.step);
static_lib_universal.step.dependOn(static_lib_x86_64.step);
// Create the universal macOS lib.
const macos_lib_step, const macos_lib_path = try createMacOSLib(
b,
optimize,
config,
exe_options,
);
// Add our library to zig-out
const lib_install = b.addInstallLibFile(
static_lib_universal.output,
"libghostty.a",
macos_lib_path,
"libghostty-macos.a",
);
b.getInstallStep().dependOn(&lib_install.step);
// Copy our ghostty.h to include
// Create the universal iOS lib.
const ios_lib_step, const ios_lib_path = try createIOSLib(
b,
null,
optimize,
config,
exe_options,
);
// Add our library to zig-out
const ios_lib_install = b.addInstallLibFile(
ios_lib_path,
"libghostty-ios.a",
);
b.getInstallStep().dependOn(&ios_lib_install.step);
// Create the iOS simulator lib.
const ios_sim_lib_step, const ios_sim_lib_path = try createIOSLib(
b,
.simulator,
optimize,
config,
exe_options,
);
// Add our library to zig-out
const ios_sim_lib_install = b.addInstallLibFile(
ios_sim_lib_path,
"libghostty-ios-simulator.a",
);
b.getInstallStep().dependOn(&ios_sim_lib_install.step);
// Copy our ghostty.h to include. The header file is shared by
// all embedded targets.
const header_install = b.addInstallHeaderFile(
"include/ghostty.h",
"ghostty.h",
@ -535,10 +480,25 @@ pub fn build(b: *std.Build) !void {
const xcframework = XCFrameworkStep.create(b, .{
.name = "GhosttyKit",
.out_path = "macos/GhosttyKit.xcframework",
.library = static_lib_universal.output,
.headers = .{ .path = "include" },
.libraries = &.{
.{
.library = macos_lib_path,
.headers = .{ .path = "include" },
},
.{
.library = ios_lib_path,
.headers = .{ .path = "include" },
},
.{
.library = ios_sim_lib_path,
.headers = .{ .path = "include" },
},
},
});
xcframework.step.dependOn(static_lib_universal.step);
xcframework.step.dependOn(ios_lib_step);
xcframework.step.dependOn(ios_sim_lib_step);
xcframework.step.dependOn(macos_lib_step);
xcframework.step.dependOn(&header_install.step);
b.default_step.dependOn(xcframework.step);
}
@ -672,6 +632,175 @@ pub fn build(b: *std.Build) !void {
}
}
/// Returns the minimum OS version for the given OS tag. This shouldn't
/// be used generally, it should only be used for Darwin-based OS currently.
fn osVersionMin(tag: std.Target.Os.Tag) ?std.Target.Query.OsVersion {
return switch (tag) {
// The lowest supported version of macOS is 12.x because
// this is the first version to support Apple Silicon so it is
// the earliest version we can virtualize to test (I only have
// an Apple Silicon machine for macOS).
.macos => .{ .semver = .{
.major = 12,
.minor = 0,
.patch = 0,
} },
// iOS 17 picked arbitrarily
.ios => .{ .semver = .{
.major = 17,
.minor = 0,
.patch = 0,
} },
// This should never happen currently. If we add a new target then
// we should add a new case here.
else => @panic("unhandled os version min os tag"),
};
}
/// Creates a universal macOS libghostty library and returns the path
/// to the final library.
///
/// The library is always a fat static library currently because this is
/// expected to be used directly with Xcode and Swift. In the future, we
/// probably want to change this because it makes it harder to use the
/// library in other contexts.
fn createMacOSLib(
b: *std.Build,
optimize: std.builtin.OptimizeMode,
config: BuildConfig,
exe_options: *std.Build.Step.Options,
) !struct { *std.Build.Step, std.Build.LazyPath } {
// Modify our build configuration for macOS builds.
const macos_config: BuildConfig = config: {
var copy = config;
// Always static for the macOS app because we want all of our
// dependencies in a fat static library.
copy.static = true;
break :config copy;
};
const static_lib_aarch64 = lib: {
const lib = b.addStaticLibrary(.{
.name = "ghostty",
.root_source_file = .{ .path = "src/main_c.zig" },
.target = b.resolveTargetQuery(.{
.cpu_arch = .aarch64,
.os_tag = .macos,
.os_version_min = osVersionMin(.macos),
}),
.optimize = optimize,
});
lib.bundle_compiler_rt = true;
lib.linkLibC();
lib.root_module.addOptions("build_options", exe_options);
// Create a single static lib with all our dependencies merged
var lib_list = try addDeps(b, lib, macos_config);
try lib_list.append(lib.getEmittedBin());
const libtool = LibtoolStep.create(b, .{
.name = "ghostty",
.out_name = "libghostty-aarch64-fat.a",
.sources = lib_list.items,
});
libtool.step.dependOn(&lib.step);
b.default_step.dependOn(libtool.step);
break :lib libtool;
};
const static_lib_x86_64 = lib: {
const lib = b.addStaticLibrary(.{
.name = "ghostty",
.root_source_file = .{ .path = "src/main_c.zig" },
.target = b.resolveTargetQuery(.{
.cpu_arch = .x86_64,
.os_tag = .macos,
.os_version_min = osVersionMin(.macos),
}),
.optimize = optimize,
});
lib.bundle_compiler_rt = true;
lib.linkLibC();
lib.root_module.addOptions("build_options", exe_options);
// Create a single static lib with all our dependencies merged
var lib_list = try addDeps(b, lib, macos_config);
try lib_list.append(lib.getEmittedBin());
const libtool = LibtoolStep.create(b, .{
.name = "ghostty",
.out_name = "libghostty-x86_64-fat.a",
.sources = lib_list.items,
});
libtool.step.dependOn(&lib.step);
b.default_step.dependOn(libtool.step);
break :lib libtool;
};
const static_lib_universal = LipoStep.create(b, .{
.name = "ghostty",
.out_name = "libghostty.a",
.input_a = static_lib_aarch64.output,
.input_b = static_lib_x86_64.output,
});
static_lib_universal.step.dependOn(static_lib_aarch64.step);
static_lib_universal.step.dependOn(static_lib_x86_64.step);
return .{
static_lib_universal.step,
static_lib_universal.output,
};
}
/// Create an Apple iOS/iPadOS build.
fn createIOSLib(
b: *std.Build,
abi: ?std.Target.Abi,
optimize: std.builtin.OptimizeMode,
config: BuildConfig,
exe_options: *std.Build.Step.Options,
) !struct { *std.Build.Step, std.Build.LazyPath } {
const ios_config: BuildConfig = config: {
var copy = config;
copy.static = true;
break :config copy;
};
const lib = b.addStaticLibrary(.{
.name = "ghostty",
.root_source_file = .{ .path = "src/main_c.zig" },
.optimize = optimize,
.target = b.resolveTargetQuery(.{
.cpu_arch = .aarch64,
.os_tag = .ios,
.os_version_min = osVersionMin(.ios),
.abi = abi,
}),
});
lib.bundle_compiler_rt = true;
lib.linkLibC();
lib.root_module.addOptions("build_options", exe_options);
// Create a single static lib with all our dependencies merged
var lib_list = try addDeps(b, lib, ios_config);
try lib_list.append(lib.getEmittedBin());
const libtool = LibtoolStep.create(b, .{
.name = "ghostty",
.out_name = "libghostty-ios-fat.a",
.sources = lib_list.items,
});
libtool.step.dependOn(&lib.step);
return .{
libtool.step,
libtool.output,
};
}
/// Used to keep track of a list of file sources.
const LazyPathList = std.ArrayList(std.Build.LazyPath);
@ -830,7 +959,14 @@ fn addDeps(
// Mac Stuff
if (step.rootModuleTarget().isDarwin()) {
step.root_module.addImport("objc", objc_dep.module("objc"));
// This is a bit of a hack that should probably be fixed upstream
// in zig-objc, but we need to add the apple SDK paths to the
// zig-objc module so that it can find the objc runtime headers.
const module = objc_dep.module("objc");
module.resolved_target = step.root_module.resolved_target;
try @import("apple_sdk").addPaths(b, module);
step.root_module.addImport("objc", module);
step.root_module.addImport("macos", macos_dep.module("macos"));
step.linkLibrary(macos_dep.artifact("macos"));
try static_libs.append(macos_dep.artifact("macos").getEmittedBin());

View File

@ -5,8 +5,8 @@
.dependencies = .{
// Zig libs
.libxev = .{
.url = "https://github.com/mitchellh/libxev/archive/74bc7aea4a8f88210f0ad4215108613ab7e7af1a.tar.gz",
.hash = "122029743e5d96aa1b57a1b99ff58bf13ff9ed6d8f624ac3ae8074062feb91c5bd8d",
.url = "https://github.com/mitchellh/libxev/archive/4e6781895e4e6c477597c8c2713d36cd82b57d07.tar.gz",
.hash = "12203f87e00caa6c07c02a748f234a5c0ee2ca5c334ec464e88810d93e7b5495a56f",
},
.mach_glfw = .{
.url = "https://github.com/der-teufel-programming/mach-glfw/archive/a9aae000cdc104dabe75d829ff9dab6809e47604.tar.gz",

View File

@ -1,5 +1,11 @@
{
"images" : [
{
"filename" : "icon_512x512@2x@2x 1.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"filename" : "icon_16x16.png",
"idiom" : "mac",

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

View File

@ -11,6 +11,9 @@
552964E62B34A9B400030505 /* vim in Resources */ = {isa = PBXBuildFile; fileRef = 552964E52B34A9B400030505 /* vim */; };
8503D7C72A549C66006CFF3D /* FullScreenHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8503D7C62A549C66006CFF3D /* FullScreenHandler.swift */; };
857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 857F63802A5E64F200CA4815 /* MainMenu.xib */; };
A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = A514C8D52B54A16400493A16 /* Ghostty.Config.swift */; };
A514C8D72B54A16400493A16 /* Ghostty.Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = A514C8D52B54A16400493A16 /* Ghostty.Config.swift */; };
A514C8D82B54DC6800493A16 /* Ghostty.App.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */; };
A51B78472AF4B58B00F3EDB9 /* TerminalWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51B78462AF4B58B00F3EDB9 /* TerminalWindow.swift */; };
A51BFC1E2B2FB5CE00E92F16 /* About.xib in Resources */ = {isa = PBXBuildFile; fileRef = A51BFC1D2B2FB5CE00E92F16 /* About.xib */; };
A51BFC202B2FB64F00E92F16 /* AboutController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51BFC1F2B2FB64F00E92F16 /* AboutController.swift */; };
@ -20,8 +23,12 @@
A5278A9B2AA05B2600CD3039 /* Ghostty.Input.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5278A9A2AA05B2600CD3039 /* Ghostty.Input.swift */; };
A53426352A7DA53D00EBB7A2 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53426342A7DA53D00EBB7A2 /* AppDelegate.swift */; };
A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A535B9D9299C569B0017E2E4 /* ErrorView.swift */; };
A53D0C8E2B53B0EA00305CE6 /* GhosttyKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */; };
A53D0C942B53B43700305CE6 /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53D0C932B53B43700305CE6 /* iOSApp.swift */; };
A53D0C952B53B4D800305CE6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; };
A53D0C9B2B543F3B00305CE6 /* Ghostty.App.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */; };
A53D0C9C2B543F7B00305CE6 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BB729B6F53A0055DE60 /* Package.swift */; };
A55685E029A03A9F004303CE /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55685DF29A03A9F004303CE /* AppError.swift */; };
A55B7BB629B6F47F0055DE60 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BB529B6F47F0055DE60 /* AppState.swift */; };
A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BB729B6F53A0055DE60 /* Package.swift */; };
A55B7BBC29B6FC330055DE60 /* SurfaceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BBB29B6FC330055DE60 /* SurfaceView.swift */; };
A56B880B2A840447007A0E29 /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A56B880A2A840447007A0E29 /* Carbon.framework */; };
@ -60,6 +67,7 @@
552964E52B34A9B400030505 /* vim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = vim; path = "../zig-out/share/vim"; sourceTree = "<group>"; };
8503D7C62A549C66006CFF3D /* FullScreenHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenHandler.swift; sourceTree = "<group>"; };
857F63802A5E64F200CA4815 /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = "<group>"; };
A514C8D52B54A16400493A16 /* Ghostty.Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Config.swift; sourceTree = "<group>"; };
A51B78462AF4B58B00F3EDB9 /* TerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalWindow.swift; sourceTree = "<group>"; };
A51BFC1D2B2FB5CE00E92F16 /* About.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = About.xib; sourceTree = "<group>"; };
A51BFC1F2B2FB64F00E92F16 /* AboutController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutController.swift; sourceTree = "<group>"; };
@ -69,8 +77,9 @@
A5278A9A2AA05B2600CD3039 /* Ghostty.Input.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Input.swift; sourceTree = "<group>"; };
A53426342A7DA53D00EBB7A2 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
A535B9D9299C569B0017E2E4 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = "<group>"; };
A53D0C932B53B43700305CE6 /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = "<group>"; };
A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.App.swift; sourceTree = "<group>"; };
A55685DF29A03A9F004303CE /* AppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = "<group>"; };
A55B7BB529B6F47F0055DE60 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = "<group>"; };
A55B7BB729B6F53A0055DE60 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = "<group>"; };
A55B7BBB29B6FC330055DE60 /* SurfaceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurfaceView.swift; sourceTree = "<group>"; };
A56B880A2A840447007A0E29 /* Carbon.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Carbon.framework; path = System/Library/Frameworks/Carbon.framework; sourceTree = SDKROOT; };
@ -99,6 +108,7 @@
A5CEAFFE29C2410700646FDA /* Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backport.swift; sourceTree = "<group>"; };
A5D0AF3A2B36A1DE00D21823 /* TerminalRestorable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalRestorable.swift; sourceTree = "<group>"; };
A5D0AF3C2B37804400D21823 /* CodableBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodableBridge.swift; sourceTree = "<group>"; };
A5D4499D2B53AE7B000F5B83 /* Ghostty-iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Ghostty-iOS.app"; sourceTree = BUILT_PRODUCTS_DIR; };
A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = GhosttyKit.xcframework; sourceTree = "<group>"; };
A5E112922AF73E6E00C6E0C2 /* ClipboardConfirmation.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ClipboardConfirmation.xib; sourceTree = "<group>"; };
A5E112942AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardConfirmationController.swift; sourceTree = "<group>"; };
@ -117,6 +127,14 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
A5D4499A2B53AE7B000F5B83 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
A53D0C8E2B53B0EA00305CE6 /* GhosttyKit.xcframework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
@ -175,12 +193,37 @@
path = Settings;
sourceTree = "<group>";
};
A54CD6ED299BEB14008C95BB /* Sources */ = {
A53D0C912B53B41900305CE6 /* App */ = {
isa = PBXGroup;
children = (
A53D0C962B53B57D00305CE6 /* macOS */,
A53D0C922B53B42000305CE6 /* iOS */,
);
path = App;
sourceTree = "<group>";
};
A53D0C922B53B42000305CE6 /* iOS */ = {
isa = PBXGroup;
children = (
A53D0C932B53B43700305CE6 /* iOSApp.swift */,
);
path = iOS;
sourceTree = "<group>";
};
A53D0C962B53B57D00305CE6 /* macOS */ = {
isa = PBXGroup;
children = (
A5FEB2FF2ABB69450068369E /* main.swift */,
A53426342A7DA53D00EBB7A2 /* AppDelegate.swift */,
857F63802A5E64F200CA4815 /* MainMenu.xib */,
);
path = macOS;
sourceTree = "<group>";
};
A54CD6ED299BEB14008C95BB /* Sources */ = {
isa = PBXGroup;
children = (
A53D0C912B53B41900305CE6 /* App */,
A53426362A7DC53000EBB7A2 /* Features */,
A534263D2A7DCBB000EBB7A2 /* Helpers */,
A55B7BB429B6F4410055DE60 /* Ghostty */,
@ -192,9 +235,10 @@
isa = PBXGroup;
children = (
A55B7BB729B6F53A0055DE60 /* Package.swift */,
A55B7BB529B6F47F0055DE60 /* AppState.swift */,
A55B7BBB29B6FC330055DE60 /* SurfaceView.swift */,
A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */,
A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */,
A514C8D52B54A16400493A16 /* Ghostty.Config.swift */,
A5278A9A2AA05B2600CD3039 /* Ghostty.Input.swift */,
A56D58852ACDDB4100508D2C /* Ghostty.Shell.swift */,
A59630A32AF059BB00D64628 /* Ghostty.SplitNode.swift */,
@ -255,6 +299,7 @@
isa = PBXGroup;
children = (
A5B30531299BEAAA0047F10C /* Ghostty.app */,
A5D4499D2B53AE7B000F5B83 /* Ghostty-iOS.app */,
);
name = Products;
sourceTree = "<group>";
@ -310,6 +355,23 @@
productReference = A5B30531299BEAAA0047F10C /* Ghostty.app */;
productType = "com.apple.product-type.application";
};
A5D4499C2B53AE7B000F5B83 /* Ghostty-iOS */ = {
isa = PBXNativeTarget;
buildConfigurationList = A5D449AB2B53AE7B000F5B83 /* Build configuration list for PBXNativeTarget "Ghostty-iOS" */;
buildPhases = (
A5D449992B53AE7B000F5B83 /* Sources */,
A5D4499A2B53AE7B000F5B83 /* Frameworks */,
A5D4499B2B53AE7B000F5B83 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = "Ghostty-iOS";
productName = "Ghostty-iOS";
productReference = A5D4499D2B53AE7B000F5B83 /* Ghostty-iOS.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
@ -317,12 +379,15 @@
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1420;
LastSwiftUpdateCheck = 1520;
LastUpgradeCheck = 1420;
TargetAttributes = {
A5B30530299BEAAA0047F10C = {
CreatedOnToolsVersion = 14.2;
};
A5D4499C2B53AE7B000F5B83 = {
CreatedOnToolsVersion = 15.2;
};
};
};
buildConfigurationList = A5B3052C299BEAAA0047F10C /* Build configuration list for PBXProject "Ghostty" */;
@ -342,6 +407,7 @@
projectRoot = "";
targets = (
A5B30530299BEAAA0047F10C /* Ghostty */,
A5D4499C2B53AE7B000F5B83 /* Ghostty-iOS */,
);
};
/* End PBXProject section */
@ -363,6 +429,14 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
A5D4499B2B53AE7B000F5B83 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
A53D0C952B53B4D800305CE6 /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@ -371,6 +445,7 @@
buildActionMask = 2147483647;
files = (
A59630A42AF059BB00D64628 /* Ghostty.SplitNode.swift in Sources */,
A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */,
A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */,
A5D0AF3D2B37804400D21823 /* CodableBridge.swift in Sources */,
A5D0AF3B2B36A1DE00D21823 /* TerminalRestorable.swift in Sources */,
@ -390,7 +465,6 @@
A5FEB3002ABB69450068369E /* main.swift in Sources */,
A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */,
A51B78472AF4B58B00F3EDB9 /* TerminalWindow.swift in Sources */,
A55B7BB629B6F47F0055DE60 /* AppState.swift in Sources */,
A5CEAFDC29B8009000646FDA /* SplitView.swift in Sources */,
A5CDF1932AAF9E0800513312 /* ConfigurationErrorsController.swift in Sources */,
A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */,
@ -403,6 +477,18 @@
A596309E2AEE1D6C00D64628 /* TerminalView.swift in Sources */,
A5CEAFDE29B8058B00646FDA /* SplitView.Divider.swift in Sources */,
A5E112972AF7401B00C6E0C2 /* ClipboardConfirmationView.swift in Sources */,
A514C8D82B54DC6800493A16 /* Ghostty.App.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
A5D449992B53AE7B000F5B83 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
A53D0C942B53B43700305CE6 /* iOSApp.swift in Sources */,
A514C8D72B54A16400493A16 /* Ghostty.Config.swift in Sources */,
A53D0C9C2B543F7B00305CE6 /* Package.swift in Sources */,
A53D0C9B2B543F3B00305CE6 /* Ghostty.App.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -682,6 +768,123 @@
};
name = Release;
};
A5D449A82B53AE7B000F5B83 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = Ghostty;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 0.1;
"OTHER_LDFLAGS[arch=*]" = "-lstdc++";
PRODUCT_BUNDLE_IDENTIFIER = "com.mitchellh.ghostty-ios";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
A5D449A92B53AE7B000F5B83 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = Ghostty;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 0.1;
"OTHER_LDFLAGS[arch=*]" = "-lstdc++";
PRODUCT_BUNDLE_IDENTIFIER = "com.mitchellh.ghostty-ios";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
A5D449AA2B53AE7B000F5B83 /* ReleaseLocal */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = Ghostty;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 0.1;
"OTHER_LDFLAGS[arch=*]" = "-lstdc++";
PRODUCT_BUNDLE_IDENTIFIER = "com.mitchellh.ghostty-ios";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = ReleaseLocal;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
@ -705,6 +908,16 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = ReleaseLocal;
};
A5D449AB2B53AE7B000F5B83 /* Build configuration list for PBXNativeTarget "Ghostty-iOS" */ = {
isa = XCConfigurationList;
buildConfigurations = (
A5D449A82B53AE7B000F5B83 /* Debug */,
A5D449A92B53AE7B000F5B83 /* Release */,
A5D449AA2B53AE7B000F5B83 /* ReleaseLocal */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = ReleaseLocal;
};
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */

View File

@ -0,0 +1,33 @@
import SwiftUI
@main
struct Ghostty_iOSApp: App {
@StateObject private var ghostty_app = Ghostty.App()
var body: some Scene {
WindowGroup {
iOS_ContentView()
.environmentObject(ghostty_app)
}
}
}
struct iOS_ContentView: View {
@EnvironmentObject private var ghostty_app: Ghostty.App
var body: some View {
VStack {
Image("AppIconImage")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxHeight: 96)
Text("Ghostty")
Text("State: \(ghostty_app.readiness.rawValue)")
}
.padding()
}
}
#Preview {
iOS_ContentView()
}

View File

@ -8,7 +8,7 @@ class AppDelegate: NSObject,
ObservableObject,
NSApplicationDelegate,
UNUserNotificationCenterDelegate,
GhosttyAppStateDelegate
GhosttyAppDelegate
{
// The application logger. We should probably move this at some point to a dedicated
// class/struct but for now it lives here! 🤷
@ -62,7 +62,7 @@ class AppDelegate: NSObject,
private var applicationHasBecomeActive: Bool = false
/// The ghostty global state. Only one per process.
let ghostty: Ghostty.AppState = Ghostty.AppState()
let ghostty: Ghostty.App = Ghostty.App()
/// Manages our terminal windows.
let terminalManager: TerminalManager
@ -143,7 +143,7 @@ class AppDelegate: NSObject,
}
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return ghostty.shouldQuitAfterLastWindowClosed
return ghostty.config.shouldQuitAfterLastWindowClosed
}
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
@ -242,7 +242,7 @@ class AppDelegate: NSObject,
/// Sync all of our menu item keyboard shortcuts with the Ghostty configuration.
private func syncMenuShortcuts() {
guard ghostty.config != nil else { return }
guard ghostty.readiness == .ready else { return }
syncMenuShortcut(action: "open_config", menuItem: self.menuOpenConfig)
syncMenuShortcut(action: "reload_config", menuItem: self.menuReloadConfig)
@ -286,19 +286,16 @@ class AppDelegate: NSObject,
/// Syncs a single menu shortcut for the given action. The action string is the same
/// action string used for the Ghostty configuration.
private func syncMenuShortcut(action: String, menuItem: NSMenuItem?) {
guard let cfg = ghostty.config else { return }
guard let menu = menuItem else { return }
let trigger = ghostty_config_trigger(cfg, action, UInt(action.count))
guard let equiv = Ghostty.keyEquivalent(key: trigger.key) else {
guard let equiv = ghostty.config.keyEquivalent(for: action) else {
// No shortcut, clear the menu item
menu.keyEquivalent = ""
menu.keyEquivalentModifierMask = []
return
}
menu.keyEquivalent = equiv
menu.keyEquivalentModifierMask = Ghostty.eventModifierFlags(mods: trigger.mods)
menu.keyEquivalent = equiv.key
menu.keyEquivalentModifierMask = equiv.modifiers
}
private func focusedSurface() -> ghostty_surface_t? {
@ -341,7 +338,7 @@ class AppDelegate: NSObject,
withCompletionHandler(options)
}
//MARK: - GhosttyAppStateDelegate
//MARK: - GhosttyAppDelegate
func findSurface(forUUID uuid: UUID) -> Ghostty.SurfaceView? {
for c in terminalManager.windows {
@ -353,11 +350,11 @@ class AppDelegate: NSObject,
return nil
}
func configDidReload(_ state: Ghostty.AppState) {
func configDidReload(_ state: Ghostty.App) {
// Depending on the "window-save-state" setting we have to set the NSQuitAlwaysKeepsWindows
// configuration. This is the only way to carefully control whether macOS invokes the
// state restoration system.
switch (ghostty.windowSaveState) {
switch (ghostty.config.windowSaveState) {
case "never": UserDefaults.standard.setValue(false, forKey: "NSQuitAlwaysKeepsWindows")
case "always": UserDefaults.standard.setValue(true, forKey: "NSQuitAlwaysKeepsWindows")
case "default": fallthrough
@ -373,7 +370,7 @@ class AppDelegate: NSObject,
// If we have configuration errors, we need to show them.
let c = ConfigurationErrorsController.sharedInstance
c.errors = state.configErrors()
c.errors = state.config.errors
if (c.errors.count > 0) {
if (c.window == nil || !c.window!.isVisible) {
c.showWindow(self)
@ -383,7 +380,7 @@ class AppDelegate: NSObject,
/// Sync the appearance of our app with the theme specified in the config.
private func syncAppearance() {
guard let theme = ghostty.windowTheme else { return }
guard let theme = ghostty.config.windowTheme else { return }
switch (theme) {
case "dark":
let appearance = NSAppearance(named: .darkAqua)

View File

@ -11,7 +11,7 @@ class TerminalController: NSWindowController, NSWindowDelegate,
override var windowNibName: NSNib.Name? { "Terminal" }
/// The app instance that this terminal view will represent.
let ghostty: Ghostty.AppState
let ghostty: Ghostty.App
/// The currently focused surface.
var focusedSurface: Ghostty.SurfaceView? = nil
@ -46,7 +46,7 @@ class TerminalController: NSWindowController, NSWindowDelegate,
/// changes in the list.
private var tabWindowsHash: Int = 0
init(_ ghostty: Ghostty.AppState,
init(_ ghostty: Ghostty.App,
withBaseConfig base: Ghostty.SurfaceConfiguration? = nil,
withSurfaceTree tree: Ghostty.SplitNode? = nil
) {
@ -101,7 +101,6 @@ class TerminalController: NSWindowController, NSWindowDelegate,
tabListenForFrame = false
guard let windows = self.window?.tabbedWindows else { return }
guard let cfg = ghostty.config else { return }
// We only listen for frame changes if we have more than 1 window,
// otherwise the accessory view doesn't matter.
@ -109,8 +108,7 @@ class TerminalController: NSWindowController, NSWindowDelegate,
for (index, window) in windows.enumerated().prefix(9) {
let action = "goto_tab:\(index + 1)"
let trigger = ghostty_config_trigger(cfg, action, UInt(action.count))
guard let equiv = Ghostty.keyEquivalentLabel(key: trigger.key, mods: trigger.mods) else {
guard let equiv = ghostty.config.keyEquivalent(for: action) else {
continue
}
@ -157,13 +155,13 @@ class TerminalController: NSWindowController, NSWindowDelegate,
window.identifier = .init(String(describing: TerminalWindowRestoration.self))
// If window decorations are disabled, remove our title
if (!ghostty.windowDecorations) { window.styleMask.remove(.titled) }
if (!ghostty.config.windowDecorations) { window.styleMask.remove(.titled) }
// Terminals typically operate in sRGB color space and macOS defaults
// to "native" which is typically P3. There is a lot more resources
// covered in thie GitHub issue: https://github.com/mitchellh/ghostty/pull/376
// Ghostty defaults to sRGB but this can be overridden.
switch (ghostty.windowColorspace) {
switch (ghostty.config.windowColorspace) {
case "display-p3":
window.colorSpace = .displayP3
case "srgb":
@ -462,7 +460,7 @@ class TerminalController: NSWindowController, NSWindowDelegate,
}
func cellSizeDidChange(to: NSSize) {
guard ghostty.windowStepResize else { return }
guard ghostty.config.windowStepResize else { return }
self.window?.contentResizeIncrements = to
}
@ -504,7 +502,7 @@ class TerminalController: NSWindowController, NSWindowDelegate,
str = cc.contents
}
Ghostty.AppState.completeClipboardRequest(cc.surface, data: str, state: cc.state, confirmed: true)
Ghostty.App.completeClipboardRequest(cc.surface, data: str, state: cc.state, confirmed: true)
}
}
@ -591,7 +589,7 @@ class TerminalController: NSWindowController, NSWindowDelegate,
// If we already have a clipboard confirmation view up, we ignore this request.
// This shouldn't be possible...
guard self.clipboardConfirmation == nil else {
Ghostty.AppState.completeClipboardRequest(surface, data: "", state: state, confirmed: true)
Ghostty.App.completeClipboardRequest(surface, data: "", state: state, confirmed: true)
return
}

View File

@ -11,7 +11,7 @@ class TerminalManager {
let closePublisher: AnyCancellable
}
let ghostty: Ghostty.AppState
let ghostty: Ghostty.App
/// The currently focused surface of the main window.
var focusedSurface: Ghostty.SurfaceView? { mainWindow?.controller.focusedSurface }
@ -37,7 +37,7 @@ class TerminalManager {
return windows.last
}
init(_ ghostty: Ghostty.AppState) {
init(_ ghostty: Ghostty.App) {
self.ghostty = ghostty
let center = NotificationCenter.default
@ -66,7 +66,7 @@ class TerminalManager {
let window = c.window!
// We want to go fullscreen if we're configured for new windows to go fullscreen
var toggleFullScreen = ghostty.windowFullscreen
var toggleFullScreen = ghostty.config.windowFullscreen
// If the previous focused window prior to creating this window is fullscreen,
// then this window also becomes fullscreen.
@ -130,7 +130,7 @@ class TerminalManager {
controller.showWindow(self)
// Add the window to the tab group and show it.
switch ghostty.windowNewTabPosition {
switch ghostty.config.windowNewTabPosition {
case "end":
// If we already have a tab group and we want the new tab to open at the end,
// then we use the last window in the tab group as the parent.

View File

@ -66,7 +66,7 @@ class TerminalWindowRestoration: NSObject, NSWindowRestoration {
// If our configuration is "never" then we never restore the state
// no matter what.
if (appDelegate.terminalManager.ghostty.windowSaveState == "never") {
if (appDelegate.terminalManager.ghostty.config.windowSaveState == "never") {
completionHandler(nil, nil)
return
}

View File

@ -37,7 +37,7 @@ protocol TerminalViewModel: ObservableObject {
/// The main terminal view. This terminal view supports splits.
struct TerminalView<ViewModel: TerminalViewModel>: View {
@ObservedObject var ghostty: Ghostty.AppState
@ObservedObject var ghostty: Ghostty.App
// The required view model
@ObservedObject var viewModel: ViewModel
@ -83,7 +83,7 @@ struct TerminalView<ViewModel: TerminalViewModel>: View {
VStack(spacing: 0) {
// If we're running in debug mode we show a warning so that users
// know that performance will be degraded.
if (ghostty.info.mode == GHOSTTY_BUILD_MODE_DEBUG) {
if (Ghostty.info.mode == GHOSTTY_BUILD_MODE_DEBUG) {
DebugBuildWarningView()
}

View File

@ -2,51 +2,36 @@ import SwiftUI
import UserNotifications
import GhosttyKit
protocol GhosttyAppStateDelegate: AnyObject {
protocol GhosttyAppDelegate: AnyObject {
/// Called when the configuration did finish reloading.
func configDidReload(_ state: Ghostty.AppState)
func configDidReload(_ app: Ghostty.App)
#if os(macOS)
/// Called when a callback needs access to a specific surface. This should return nil
/// when the surface is no longer valid.
func findSurface(forUUID uuid: UUID) -> Ghostty.SurfaceView?
#endif
}
extension Ghostty {
enum AppReadiness {
case loading, error, ready
}
enum FontSizeModification {
case increase(Int)
case decrease(Int)
case reset
}
struct Info {
var mode: ghostty_build_mode_e
var version: String
}
/// The AppState is the global state that is associated with the Swift app. This handles initially
/// initializing Ghostty, loading the configuration, etc.
class AppState: ObservableObject {
/// The readiness value of the state.
@Published var readiness: AppReadiness = .loading
/// Optional delegate
weak var delegate: GhosttyAppStateDelegate?
/// The ghostty global configuration. This should only be changed when it is definitely
/// safe to change. It is definite safe to change only when the embedded app runtime
/// in Ghostty says so (usually, only in a reload configuration callback).
@Published var config: ghostty_config_t? = nil {
didSet {
// Free the old value whenever we change
guard let old = oldValue else { return }
ghostty_config_free(old)
}
// IMPORTANT: THIS IS NOT DONE.
// This is a refactor/redo of Ghostty.AppState so that it supports both macOS and iOS
class App: ObservableObject {
enum Readiness: String {
case loading, error, ready
}
/// Optional delegate
weak var delegate: GhosttyAppDelegate?
/// The readiness value of the state.
@Published var readiness: Readiness = .loading
/// The global app configuration. This defines the app level configuration plus any behavior
/// for new windows, tabs, etc. Note that when creating a new window, it may inherit some
/// configuration (i.e. font size) from the previously focused window. This would override this.
private(set) var config: Config
/// The ghostty app instance. We only have one of these for the entire app, although I guess
/// in theory you can have multiple... I don't know why you would...
@Published var app: ghostty_app_t? = nil {
@ -55,253 +40,118 @@ extension Ghostty {
ghostty_app_free(old)
}
}
/// True if we should quit when the last window is closed.
var shouldQuitAfterLastWindowClosed: Bool {
guard let config = self.config else { return true }
var v = false;
let key = "quit-after-last-window-closed"
_ = ghostty_config_get(config, &v, key, UInt(key.count))
return v
}
/// window-colorspace
var windowColorspace: String {
guard let config = self.config else { return "" }
var v: UnsafePointer<Int8>? = nil
let key = "window-colorspace"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return "" }
guard let ptr = v else { return "" }
return String(cString: ptr)
}
/// window-save-state
var windowSaveState: String {
guard let config = self.config else { return "" }
var v: UnsafePointer<Int8>? = nil
let key = "window-save-state"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return "" }
guard let ptr = v else { return "" }
return String(cString: ptr)
}
/// window-new-tab-position
var windowNewTabPosition: String {
guard let config = self.config else { return "" }
var v: UnsafePointer<Int8>? = nil
let key = "window-new-tab-position"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return "" }
guard let ptr = v else { return "" }
return String(cString: ptr)
}
/// True if we need to confirm before quitting.
var needsConfirmQuit: Bool {
guard let app = app else { return false }
return ghostty_app_needs_confirm_quit(app)
}
/// Build information
var info: Info {
let raw = ghostty_info()
let version = NSString(
bytes: raw.version,
length: Int(raw.version_len),
encoding: NSUTF8StringEncoding
) ?? "unknown"
return Info(mode: raw.build_mode, version: String(version))
}
/// True if we want to render window decorations
var windowDecorations: Bool {
guard let config = self.config else { return true }
var v = false;
let key = "window-decoration"
_ = ghostty_config_get(config, &v, key, UInt(key.count))
return v;
}
/// The window theme as a string.
var windowTheme: String? {
guard let config = self.config else { return nil }
var v: UnsafePointer<Int8>? = nil
let key = "window-theme"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return nil }
guard let ptr = v else { return nil }
return String(cString: ptr)
}
/// Whether to resize windows in discrete steps or use "fluid" resizing
var windowStepResize: Bool {
guard let config = self.config else { return true }
var v = false
let key = "window-step-resize"
_ = ghostty_config_get(config, &v, key, UInt(key.count))
return v
}
/// Whether to open new windows in fullscreen.
var windowFullscreen: Bool {
guard let config = self.config else { return true }
var v = false
let key = "fullscreen"
_ = ghostty_config_get(config, &v, key, UInt(key.count))
return v
}
/// The background opacity.
var backgroundOpacity: Double {
guard let config = self.config else { return 1 }
var v: Double = 1
let key = "background-opacity"
_ = ghostty_config_get(config, &v, key, UInt(key.count))
return v;
}
init() {
// Initialize ghostty global state. This happens once per process.
guard ghostty_init() == GHOSTTY_SUCCESS else {
AppDelegate.logger.critical("ghostty_init failed")
if ghostty_init() != GHOSTTY_SUCCESS {
logger.critical("ghostty_init failed, weird things may happen")
readiness = .error
return
}
// Initialize the global configuration.
guard let cfg = Self.loadConfig() else {
self.config = Config()
if self.config.config == nil {
readiness = .error
return
}
self.config = cfg;
// Create our "runtime" config. The "runtime" is the configuration that ghostty
// uses to interface with the application runtime environment.
var runtime_cfg = ghostty_runtime_config_s(
userdata: Unmanaged.passUnretained(self).toOpaque(),
supports_selection_clipboard: false,
wakeup_cb: { userdata in AppState.wakeup(userdata) },
reload_config_cb: { userdata in AppState.reloadConfig(userdata) },
open_config_cb: { userdata in AppState.openConfig(userdata) },
set_title_cb: { userdata, title in AppState.setTitle(userdata, title: title) },
set_mouse_shape_cb: { userdata, shape in AppState.setMouseShape(userdata, shape: shape) },
set_mouse_visibility_cb: { userdata, visible in AppState.setMouseVisibility(userdata, visible: visible) },
read_clipboard_cb: { userdata, loc, state in AppState.readClipboard(userdata, location: loc, state: state) },
confirm_read_clipboard_cb: { userdata, str, state, request in AppState.confirmReadClipboard(userdata, string: str, state: state, request: request ) },
write_clipboard_cb: { userdata, str, loc, confirm in AppState.writeClipboard(userdata, string: str, location: loc, confirm: confirm) },
new_split_cb: { userdata, direction, surfaceConfig in AppState.newSplit(userdata, direction: direction, config: surfaceConfig) },
new_tab_cb: { userdata, surfaceConfig in AppState.newTab(userdata, config: surfaceConfig) },
new_window_cb: { userdata, surfaceConfig in AppState.newWindow(userdata, config: surfaceConfig) },
control_inspector_cb: { userdata, mode in AppState.controlInspector(userdata, mode: mode) },
close_surface_cb: { userdata, processAlive in AppState.closeSurface(userdata, processAlive: processAlive) },
focus_split_cb: { userdata, direction in AppState.focusSplit(userdata, direction: direction) },
wakeup_cb: { userdata in App.wakeup(userdata) },
reload_config_cb: { userdata in App.reloadConfig(userdata) },
open_config_cb: { userdata in App.openConfig(userdata) },
set_title_cb: { userdata, title in App.setTitle(userdata, title: title) },
set_mouse_shape_cb: { userdata, shape in App.setMouseShape(userdata, shape: shape) },
set_mouse_visibility_cb: { userdata, visible in App.setMouseVisibility(userdata, visible: visible) },
read_clipboard_cb: { userdata, loc, state in App.readClipboard(userdata, location: loc, state: state) },
confirm_read_clipboard_cb: { userdata, str, state, request in App.confirmReadClipboard(userdata, string: str, state: state, request: request ) },
write_clipboard_cb: { userdata, str, loc, confirm in App.writeClipboard(userdata, string: str, location: loc, confirm: confirm) },
new_split_cb: { userdata, direction, surfaceConfig in App.newSplit(userdata, direction: direction, config: surfaceConfig) },
new_tab_cb: { userdata, surfaceConfig in App.newTab(userdata, config: surfaceConfig) },
new_window_cb: { userdata, surfaceConfig in App.newWindow(userdata, config: surfaceConfig) },
control_inspector_cb: { userdata, mode in App.controlInspector(userdata, mode: mode) },
close_surface_cb: { userdata, processAlive in App.closeSurface(userdata, processAlive: processAlive) },
focus_split_cb: { userdata, direction in App.focusSplit(userdata, direction: direction) },
resize_split_cb: { userdata, direction, amount in
AppState.resizeSplit(userdata, direction: direction, amount: amount) },
App.resizeSplit(userdata, direction: direction, amount: amount) },
equalize_splits_cb: { userdata in
AppState.equalizeSplits(userdata) },
toggle_split_zoom_cb: { userdata in AppState.toggleSplitZoom(userdata) },
goto_tab_cb: { userdata, n in AppState.gotoTab(userdata, n: n) },
toggle_fullscreen_cb: { userdata, nonNativeFullscreen in AppState.toggleFullscreen(userdata, nonNativeFullscreen: nonNativeFullscreen) },
set_initial_window_size_cb: { userdata, width, height in AppState.setInitialWindowSize(userdata, width: width, height: height) },
render_inspector_cb: { userdata in AppState.renderInspector(userdata) },
set_cell_size_cb: { userdata, width, height in AppState.setCellSize(userdata, width: width, height: height) },
App.equalizeSplits(userdata) },
toggle_split_zoom_cb: { userdata in App.toggleSplitZoom(userdata) },
goto_tab_cb: { userdata, n in App.gotoTab(userdata, n: n) },
toggle_fullscreen_cb: { userdata, nonNativeFullscreen in App.toggleFullscreen(userdata, nonNativeFullscreen: nonNativeFullscreen) },
set_initial_window_size_cb: { userdata, width, height in App.setInitialWindowSize(userdata, width: width, height: height) },
render_inspector_cb: { userdata in App.renderInspector(userdata) },
set_cell_size_cb: { userdata, width, height in App.setCellSize(userdata, width: width, height: height) },
show_desktop_notification_cb: { userdata, title, body in
AppState.showUserNotification(userdata, title: title, body: body)
App.showUserNotification(userdata, title: title, body: body)
}
)
// Create the ghostty app.
guard let app = ghostty_app_new(&runtime_cfg, cfg) else {
AppDelegate.logger.critical("ghostty_app_new failed")
guard let app = ghostty_app_new(&runtime_cfg, config.config) else {
logger.critical("ghostty_app_new failed")
readiness = .error
return
}
self.app = app
#if os(macOS)
// Subscribe to notifications for keyboard layout change so that we can update Ghostty.
NotificationCenter.default.addObserver(
self,
selector: #selector(self.keyboardSelectionDidChange(notification:)),
name: NSTextInputContext.keyboardSelectionDidChangeNotification,
object: nil)
#endif
self.readiness = .ready
}
deinit {
// This will force the didSet callbacks to run which free.
self.app = nil
self.config = nil
#if os(macOS)
// Remove our observer
NotificationCenter.default.removeObserver(
self,
name: NSTextInputContext.keyboardSelectionDidChangeNotification,
object: nil)
#endif
}
/// Initializes a new configuration and loads all the values.
static func loadConfig() -> ghostty_config_t? {
// Initialize the global configuration.
guard let cfg = ghostty_config_new() else {
AppDelegate.logger.critical("ghostty_config_new failed")
return nil
}
// Load our configuration files from the home directory.
ghostty_config_load_default_files(cfg);
ghostty_config_load_cli_args(cfg);
ghostty_config_load_recursive_files(cfg);
// TODO: we'd probably do some config loading here... for now we'd
// have to do this synchronously. When we support config updating we can do
// this async and update later.
// Finalize will make our defaults available.
ghostty_config_finalize(cfg)
// Log any configuration errors. These will be automatically shown in a
// pop-up window too.
let errCount = ghostty_config_errors_count(cfg)
if errCount > 0 {
AppDelegate.logger.warning("config error: \(errCount) configuration errors on reload")
var errors: [String] = [];
for i in 0..<errCount {
let err = ghostty_config_get_error(cfg, UInt32(i))
let message = String(cString: err.message)
errors.append(message)
AppDelegate.logger.warning("config error: \(message)")
}
}
return cfg
}
/// Returns the configuration errors (if any).
func configErrors() -> [String] {
guard let cfg = self.config else { return [] }
var errors: [String] = [];
let errCount = ghostty_config_errors_count(cfg)
for i in 0..<errCount {
let err = ghostty_config_get_error(cfg, UInt32(i))
let message = String(cString: err.message)
errors.append(message)
}
return errors
}
// MARK: App Operations
func appTick() {
guard let app = self.app else { return }
// Tick our app, which lets us know if we want to quit
let exit = ghostty_app_tick(app)
if (!exit) { return }
// On iOS, applications do not terminate programmatically like they do
// on macOS. On iOS, applications are only terminated when a user physically
// closes the application (i.e. going to the home screen). If we request
// exit on iOS we ignore it.
#if os(iOS)
logger.info("quit request received, ignoring on iOS")
#endif
#if os(macOS)
// We want to quit, start that process
NSApplication.shared.terminate(nil)
#endif
}
func openConfig() {
guard let app = self.app else { return }
ghostty_app_open_config(app)
@ -311,7 +161,7 @@ extension Ghostty {
guard let app = self.app else { return }
ghostty_app_reload_config(app)
}
/// Request that the given surface is closed. This will trigger the full normal surface close event
/// cycle which will call our close surface callback.
func requestClose(surface: ghostty_surface_t) {
@ -321,14 +171,14 @@ extension Ghostty {
func newTab(surface: ghostty_surface_t) {
let action = "new_tab"
if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) {
AppDelegate.logger.warning("action failed action=\(action)")
logger.warning("action failed action=\(action)")
}
}
func newWindow(surface: ghostty_surface_t) {
let action = "new_window"
if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) {
AppDelegate.logger.warning("action failed action=\(action)")
logger.warning("action failed action=\(action)")
}
}
@ -351,16 +201,22 @@ extension Ghostty {
func splitToggleZoom(surface: ghostty_surface_t) {
let action = "toggle_split_zoom"
if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) {
AppDelegate.logger.warning("action failed action=\(action)")
logger.warning("action failed action=\(action)")
}
}
func toggleFullscreen(surface: ghostty_surface_t) {
let action = "toggle_fullscreen"
if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) {
AppDelegate.logger.warning("action failed action=\(action)")
logger.warning("action failed action=\(action)")
}
}
enum FontSizeModification {
case increase(Int)
case decrease(Int)
case reset
}
func changeFontSize(surface: ghostty_surface_t, _ change: FontSizeModification) {
let action: String
@ -373,25 +229,80 @@ extension Ghostty {
action = "reset_font_size"
}
if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) {
AppDelegate.logger.warning("action failed action=\(action)")
logger.warning("action failed action=\(action)")
}
}
func toggleTerminalInspector(surface: ghostty_surface_t) {
let action = "inspector:toggle"
if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) {
AppDelegate.logger.warning("action failed action=\(action)")
logger.warning("action failed action=\(action)")
}
}
#if os(iOS)
// MARK: Ghostty Callbacks (iOS)
static func wakeup(_ userdata: UnsafeMutableRawPointer?) {}
static func reloadConfig(_ userdata: UnsafeMutableRawPointer?) -> ghostty_config_t? { return nil }
static func openConfig(_ userdata: UnsafeMutableRawPointer?) {}
static func setTitle(_ userdata: UnsafeMutableRawPointer?, title: UnsafePointer<CChar>?) {}
static func setMouseShape(_ userdata: UnsafeMutableRawPointer?, shape: ghostty_mouse_shape_e) {}
static func setMouseVisibility(_ userdata: UnsafeMutableRawPointer?, visible: Bool) {}
static func readClipboard(
_ userdata: UnsafeMutableRawPointer?,
location: ghostty_clipboard_e,
state: UnsafeMutableRawPointer?
) {}
static func confirmReadClipboard(
_ userdata: UnsafeMutableRawPointer?,
string: UnsafePointer<CChar>?,
state: UnsafeMutableRawPointer?,
request: ghostty_clipboard_request_e
) {}
static func writeClipboard(
_ userdata: UnsafeMutableRawPointer?,
string: UnsafePointer<CChar>?,
location: ghostty_clipboard_e,
confirm: Bool
) {}
static func newSplit(
_ userdata: UnsafeMutableRawPointer?,
direction: ghostty_split_direction_e,
config: ghostty_surface_config_s
) {}
static func newTab(_ userdata: UnsafeMutableRawPointer?, config: ghostty_surface_config_s) {}
static func newWindow(_ userdata: UnsafeMutableRawPointer?, config: ghostty_surface_config_s) {}
static func controlInspector(_ userdata: UnsafeMutableRawPointer?, mode: ghostty_inspector_mode_e) {}
static func closeSurface(_ userdata: UnsafeMutableRawPointer?, processAlive: Bool) {}
static func focusSplit(_ userdata: UnsafeMutableRawPointer?, direction: ghostty_split_focus_direction_e) {}
static func resizeSplit(_ userdata: UnsafeMutableRawPointer?, direction: ghostty_split_resize_direction_e, amount: UInt16) {}
static func equalizeSplits(_ userdata: UnsafeMutableRawPointer?) {}
static func toggleSplitZoom(_ userdata: UnsafeMutableRawPointer?) {}
static func gotoTab(_ userdata: UnsafeMutableRawPointer?, n: Int32) {}
static func toggleFullscreen(_ userdata: UnsafeMutableRawPointer?, nonNativeFullscreen: ghostty_non_native_fullscreen_e) {}
static func setInitialWindowSize(_ userdata: UnsafeMutableRawPointer?, width: UInt32, height: UInt32) {}
static func renderInspector(_ userdata: UnsafeMutableRawPointer?) {}
static func setCellSize(_ userdata: UnsafeMutableRawPointer?, width: UInt32, height: UInt32) {}
static func showUserNotification(_ userdata: UnsafeMutableRawPointer?, title: UnsafePointer<CChar>?, body: UnsafePointer<CChar>?) {}
#endif
#if os(macOS)
// MARK: Notifications
// Called when the selected keyboard changes. We have to notify Ghostty so that
// it can reload the keyboard mapping for input.
@objc private func keyboardSelectionDidChange(notification: NSNotification) {
guard let app = self.app else { return }
ghostty_app_keyboard_changed(app)
}
// MARK: Ghostty Callbacks
// MARK: Ghostty Callbacks (macOS)
static func newSplit(_ userdata: UnsafeMutableRawPointer?, direction: ghostty_split_direction_e, config: ghostty_surface_config_s) {
let surface = self.surfaceUserdata(from: userdata)
@ -534,14 +445,15 @@ extension Ghostty {
}
static func reloadConfig(_ userdata: UnsafeMutableRawPointer?) -> ghostty_config_t? {
guard let newConfig = Self.loadConfig() else {
let newConfig = Config()
guard newConfig.loaded else {
AppDelegate.logger.warning("failed to reload configuration")
return nil
}
// Assign the new config. This will automatically free the old config.
// It is safe to free the old config from within this function call.
let state = Unmanaged<AppState>.fromOpaque(userdata!).takeUnretainedValue()
let state = Unmanaged<Self>.fromOpaque(userdata!).takeUnretainedValue()
state.config = newConfig
// If we have a delegate, notify.
@ -549,11 +461,11 @@ extension Ghostty {
delegate.configDidReload(state)
}
return newConfig
return newConfig.config
}
static func wakeup(_ userdata: UnsafeMutableRawPointer?) {
let state = Unmanaged<AppState>.fromOpaque(userdata!).takeUnretainedValue()
let state = Unmanaged<App>.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
@ -662,7 +574,7 @@ extension Ghostty {
let surface = self.surfaceUserdata(from: userdata)
guard let appState = self.appState(fromView: surface) else { return }
guard appState.windowDecorations else {
guard appState.config.windowDecorations else {
let alert = NSAlert()
alert.messageText = "Tabs are disabled"
alert.informativeText = "Enable window decorations to use tabs"
@ -701,16 +613,18 @@ extension Ghostty {
}
/// Returns the GhosttyState from the given userdata value.
static private func appState(fromView view: SurfaceView) -> AppState? {
static private func appState(fromView view: SurfaceView) -> App? {
guard let surface = view.surface else { return nil }
guard let app = ghostty_surface_app(surface) else { return nil }
guard let app_ud = ghostty_app_userdata(app) else { return nil }
return Unmanaged<AppState>.fromOpaque(app_ud).takeUnretainedValue()
return Unmanaged<App>.fromOpaque(app_ud).takeUnretainedValue()
}
/// Returns the surface view from the userdata.
static private func surfaceUserdata(from userdata: UnsafeMutableRawPointer?) -> SurfaceView {
return Unmanaged<SurfaceView>.fromOpaque(userdata!).takeUnretainedValue()
}
#endif
}
}

View File

@ -0,0 +1,238 @@
import SwiftUI
import GhosttyKit
extension Ghostty {
/// Maps to a `ghostty_config_t` and the various operations on that.
class Config: ObservableObject {
// The underlying C pointer to the Ghostty config structure. This
// should never be accessed directly. Any operations on this should
// be called from the functions on this or another class.
private(set) var config: ghostty_config_t? = nil {
didSet {
// Free the old value whenever we change
guard let old = oldValue else { return }
ghostty_config_free(old)
}
}
/// True if the configuration is loaded
var loaded: Bool { config != nil }
/// Return the errors found while loading the configuration.
var errors: [String] {
guard let cfg = self.config else { return [] }
var errors: [String] = [];
let errCount = ghostty_config_errors_count(cfg)
for i in 0..<errCount {
let err = ghostty_config_get_error(cfg, UInt32(i))
let message = String(cString: err.message)
errors.append(message)
}
return errors
}
init() {
if let cfg = Self.loadConfig() {
self.config = cfg
}
}
deinit {
self.config = nil
}
/// Initializes a new configuration and loads all the values.
static private func loadConfig() -> ghostty_config_t? {
// Initialize the global configuration.
guard let cfg = ghostty_config_new() else {
logger.critical("ghostty_config_new failed")
return nil
}
// Load our configuration from files, CLI args, and then any referenced files.
// We only do this on macOS because other Apple platforms do not have the
// same filesystem concept.
#if os(macOS)
ghostty_config_load_default_files(cfg);
ghostty_config_load_cli_args(cfg);
ghostty_config_load_recursive_files(cfg);
#endif
// TODO: we'd probably do some config loading here... for now we'd
// have to do this synchronously. When we support config updating we can do
// this async and update later.
// Finalize will make our defaults available.
ghostty_config_finalize(cfg)
// Log any configuration errors. These will be automatically shown in a
// pop-up window too.
let errCount = ghostty_config_errors_count(cfg)
if errCount > 0 {
logger.warning("config error: \(errCount) configuration errors on reload")
var errors: [String] = [];
for i in 0..<errCount {
let err = ghostty_config_get_error(cfg, UInt32(i))
let message = String(cString: err.message)
errors.append(message)
logger.warning("config error: \(message)")
}
}
return cfg
}
#if os(macOS)
// MARK: - Keybindings
/// A convenience struct that has the key + modifiers for some keybinding.
struct KeyEquivalent: CustomStringConvertible {
let key: String
let modifiers: NSEvent.ModifierFlags
var description: String {
var key = self.key
// Note: the order below matters; it matches the ordering modifiers
// shown for macOS menu shortcut labels.
if modifiers.contains(.command) { key = "\(key)" }
if modifiers.contains(.shift) { key = "\(key)" }
if modifiers.contains(.option) { key = "\(key)" }
if modifiers.contains(.control) { key = "\(key)" }
return key
}
}
/// Return the key equivalent for the given action. The action is the name of the action
/// in the Ghostty configuration. For example `keybind = cmd+q=quit` in Ghostty
/// configuration would be "quit" action.
///
/// Returns nil if there is no key equivalent for the given action.
func keyEquivalent(for action: String) -> KeyEquivalent? {
guard let cfg = self.config else { return nil }
let trigger = ghostty_config_trigger(cfg, action, UInt(action.count))
guard let equiv = Ghostty.keyEquivalent(key: trigger.key) else { return nil }
return KeyEquivalent(
key: equiv,
modifiers: Ghostty.eventModifierFlags(mods: trigger.mods)
)
}
#endif
// MARK: - Configuration Values
/// For all of the configuration values below, see the associated Ghostty documentation for
/// details on what each means. We only add documentation if there is a strange conversion
/// due to the embedded library and Swift.
var shouldQuitAfterLastWindowClosed: Bool {
guard let config = self.config else { return true }
var v = false;
let key = "quit-after-last-window-closed"
_ = ghostty_config_get(config, &v, key, UInt(key.count))
return v
}
var windowColorspace: String {
guard let config = self.config else { return "" }
var v: UnsafePointer<Int8>? = nil
let key = "window-colorspace"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return "" }
guard let ptr = v else { return "" }
return String(cString: ptr)
}
var windowSaveState: String {
guard let config = self.config else { return "" }
var v: UnsafePointer<Int8>? = nil
let key = "window-save-state"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return "" }
guard let ptr = v else { return "" }
return String(cString: ptr)
}
var windowNewTabPosition: String {
guard let config = self.config else { return "" }
var v: UnsafePointer<Int8>? = nil
let key = "window-new-tab-position"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return "" }
guard let ptr = v else { return "" }
return String(cString: ptr)
}
var windowDecorations: Bool {
guard let config = self.config else { return true }
var v = false;
let key = "window-decoration"
_ = ghostty_config_get(config, &v, key, UInt(key.count))
return v;
}
var windowTheme: String? {
guard let config = self.config else { return nil }
var v: UnsafePointer<Int8>? = nil
let key = "window-theme"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return nil }
guard let ptr = v else { return nil }
return String(cString: ptr)
}
var windowStepResize: Bool {
guard let config = self.config else { return true }
var v = false
let key = "window-step-resize"
_ = ghostty_config_get(config, &v, key, UInt(key.count))
return v
}
var windowFullscreen: Bool {
guard let config = self.config else { return true }
var v = false
let key = "fullscreen"
_ = ghostty_config_get(config, &v, key, UInt(key.count))
return v
}
var backgroundOpacity: Double {
guard let config = self.config else { return 1 }
var v: Double = 1
let key = "background-opacity"
_ = ghostty_config_get(config, &v, key, UInt(key.count))
return v;
}
var unfocusedSplitOpacity: Double {
guard let config = self.config else { return 1 }
var opacity: Double = 0.85
let key = "unfocused-split-opacity"
_ = ghostty_config_get(config, &opacity, key, UInt(key.count))
return 1 - opacity
}
var unfocusedSplitFill: Color {
guard let config = self.config else { return .white }
var rgb: UInt32 = 16777215 // white default
let key = "unfocused-split-fill"
if (!ghostty_config_get(config, &rgb, key, UInt(key.count))) {
let bg_key = "background"
_ = ghostty_config_get(config, &rgb, bg_key, UInt(bg_key.count));
}
let red = Double(rgb & 0xff)
let green = Double((rgb >> 8) & 0xff)
let blue = Double((rgb >> 16) & 0xff)
return Color(
red: red / 255,
green: green / 255,
blue: blue / 255
)
}
}
}

View File

@ -7,21 +7,6 @@ extension Ghostty {
return Self.keyToEquivalent[key]
}
/// Returns the keyEquivalent label that includes the mods.
static func keyEquivalentLabel(key: ghostty_input_key_e, mods: ghostty_input_mods_e) -> String? {
guard var key = Self.keyEquivalent(key: key) else { return nil }
let flags = Self.eventModifierFlags(mods: mods)
// Note: the order below matters; it matches the ordering modifiers show for
// macOS menu shortcut labels.
if flags.contains(.command) { key = "\(key)" }
if flags.contains(.shift) { key = "\(key)" }
if flags.contains(.option) { key = "\(key)" }
if flags.contains(.control) { key = "\(key)" }
return key
}
/// Returns the event modifier flags set for the Ghostty mods enum.
static func eventModifierFlags(mods: ghostty_input_mods_e) -> NSEvent.ModifierFlags {
var flags = NSEvent.ModifierFlags(rawValue: 0);

View File

@ -1,7 +1,14 @@
import os
import SwiftUI
import GhosttyKit
struct Ghostty {
// The primary logger used by the GhosttyKit libraries.
static let logger = Logger(
subsystem: Bundle.main.bundleIdentifier!,
category: "ghostty"
)
// All the notifications that will be emitted will be put here.
struct Notification {}
@ -12,6 +19,26 @@ struct Ghostty {
static let userNotificationActionShow = "com.mitchellh.ghostty.userNotification.Show"
}
// MARK: Build Info
extension Ghostty {
struct Info {
var mode: ghostty_build_mode_e
var version: String
}
static var info: Info {
let raw = ghostty_info()
let version = NSString(
bytes: raw.version,
length: Int(raw.version_len),
encoding: NSUTF8StringEncoding
) ?? "unknown"
return Info(mode: raw.build_mode, version: String(version))
}
}
// MARK: Surface Notifications
extension Ghostty {

View File

@ -5,7 +5,7 @@ import GhosttyKit
extension Ghostty {
/// Render a terminal for the active app in the environment.
struct Terminal: View {
@EnvironmentObject private var ghostty: Ghostty.AppState
@EnvironmentObject private var ghostty: Ghostty.App
@FocusedValue(\.ghosttySurfaceTitle) private var surfaceTitle: String?
var body: some View {
@ -49,40 +49,12 @@ extension Ghostty {
// Maintain whether our window has focus (is key) or not
@State private var windowFocus: Bool = true
@EnvironmentObject private var ghostty: Ghostty.AppState
@EnvironmentObject private var ghostty: Ghostty.App
// This is true if the terminal is considered "focused". The terminal is focused if
// it is both individually focused and the containing window is key.
private var hasFocus: Bool { surfaceFocus && windowFocus }
// The opacity of the rectangle when unfocused.
private var unfocusedOpacity: Double {
var opacity: Double = 0.85
let key = "unfocused-split-opacity"
_ = ghostty_config_get(ghostty.config, &opacity, key, UInt(key.count))
return 1 - opacity
}
// The color for the rectangle overlay when unfocused.
private var unfocusedFill: Color {
var rgb: UInt32 = 16777215 // white default
let key = "unfocused-split-fill"
if (!ghostty_config_get(ghostty.config, &rgb, key, UInt(key.count))) {
let bg_key = "background"
_ = ghostty_config_get(ghostty.config, &rgb, bg_key, UInt(bg_key.count));
}
let red = Double(rgb & 0xff)
let green = Double((rgb >> 8) & 0xff)
let blue = Double((rgb >> 16) & 0xff)
return Color(
red: red / 255,
green: green / 255,
blue: blue / 255
)
}
var body: some View {
ZStack {
// We use a GeometryReader to get the frame bounds so that our metal surface
@ -175,10 +147,10 @@ extension Ghostty {
// because we want to keep our focused surface dark even if we don't have window
// focus.
if (isSplit && !surfaceFocus) {
let overlayOpacity = unfocusedOpacity;
let overlayOpacity = ghostty.config.unfocusedSplitOpacity;
if (overlayOpacity > 0) {
Rectangle()
.fill(unfocusedFill)
.fill(ghostty.config.unfocusedSplitFill)
.allowsHitTesting(false)
.opacity(overlayOpacity)
}

View File

@ -1,3 +1,3 @@
# This file is auto-generated! check build-support/check-zig-cache-hash.sh for
# more details.
"sha256-hE4MNVZx/kA90MPHEraJDayBtLw29HZfnFChLdXPS0g="
"sha256-to0V9rCefIs8KcWsx+nopgQO4i7O3gb06LGNc6NXN2M="

View File

@ -45,6 +45,7 @@ const SDK = struct {
pub fn fromTarget(target: std.Target) !SDK {
return switch (target.os.tag) {
.ios => .{ .platform = "iPhoneOS", .version = "" },
.macos => .{ .platform = "MacOSX", .version = "14" },
else => {
std.log.err("unsupported os={}", .{target.os.tag});

View File

@ -71,10 +71,12 @@ pub fn build(b: *std.Build) !void {
.file = imgui.path("backends/imgui_impl_metal.mm"),
.flags = flags.items,
});
lib.addCSourceFile(.{
.file = imgui.path("backends/imgui_impl_osx.mm"),
.flags = flags.items,
});
if (target.result.os.tag == .macos) {
lib.addCSourceFile(.{
.file = imgui.path("backends/imgui_impl_osx.mm"),
.flags = flags.items,
});
}
}
lib.installHeadersDirectoryOptions(.{

View File

@ -15,6 +15,11 @@ pub fn build(b: *std.Build) !void {
});
lib.linkLibC();
lib.addIncludePath(upstream.path("include"));
if (target.result.isDarwin()) {
const apple_sdk = @import("apple_sdk");
try apple_sdk.addPaths(b, &lib.root_module);
}
module.addIncludePath(upstream.path("include"));
module.addIncludePath(.{ .path = "" });
@ -86,7 +91,7 @@ pub fn build(b: *std.Build) !void {
b.installArtifact(lib);
{
if (target.query.isNative()) {
const test_exe = b.addTest(.{
.name = "test",
.root_source_file = .{ .path = "main.zig" },

View File

@ -1,12 +1,14 @@
.{
.name = "freetype",
.version = "2.13.2",
.paths = .{""},
.dependencies = .{
.freetype = .{
.url = "https://github.com/freetype/freetype/archive/refs/tags/VER-2-13-2.tar.gz",
.hash = "1220b81f6ecfb3fd222f76cf9106fecfa6554ab07ec7fdc4124b9bb063ae2adf969d",
},
.apple_sdk = .{ .path = "../apple-sdk" },
.libpng = .{ .path = "../libpng" },
.zlib = .{ .path = "../zlib" },
},

View File

@ -12,8 +12,15 @@ pub fn build(b: *std.Build) !void {
module.addIncludePath(upstream.path(""));
module.addIncludePath(.{ .path = "override" });
if (target.result.isDarwin()) {
// See pkg/harfbuzz/build.zig
module.resolved_target = target;
defer module.resolved_target = null;
const apple_sdk = @import("apple_sdk");
try apple_sdk.addPaths(b, module);
}
{
if (target.query.isNative()) {
const test_exe = b.addTest(.{
.name = "test",
.root_source_file = .{ .path = "main.zig" },
@ -45,6 +52,10 @@ fn buildGlslang(
lib.linkLibCpp();
lib.addIncludePath(upstream.path(""));
lib.addIncludePath(.{ .path = "override" });
if (target.result.isDarwin()) {
const apple_sdk = @import("apple_sdk");
try apple_sdk.addPaths(b, &lib.root_module);
}
var flags = std.ArrayList([]const u8).init(b.allocator);
defer flags.deinit();

View File

@ -7,5 +7,7 @@
.url = "https://github.com/KhronosGroup/glslang/archive/refs/tags/13.1.1.tar.gz",
.hash = "1220481fe19def1172cd0728743019c0f440181a6342b62d03e24d05c70141516799",
},
.apple_sdk = .{ .path = "../apple-sdk" },
},
}

View File

@ -34,6 +34,18 @@ pub fn build(b: *std.Build) !void {
lib.addIncludePath(upstream.path("src"));
module.addIncludePath(upstream.path("src"));
if (target.result.isDarwin()) {
// This is definitely super sketchy and not right but without this
// zig build test breaks on macOS. We have to look into what exactly
// is going on here but this getting comitted in the interest of
// unblocking zig build test.
module.resolved_target = target;
defer module.resolved_target = null;
try apple_sdk.addPaths(b, &lib.root_module);
try apple_sdk.addPaths(b, module);
}
const freetype_dep = b.dependency("freetype", .{ .target = target, .optimize = optimize });
lib.linkLibrary(freetype_dep.artifact("freetype"));
module.addIncludePath(freetype_dep.builder.dependency("freetype", .{}).path("include"));
@ -59,19 +71,10 @@ pub fn build(b: *std.Build) !void {
"-DHAVE_FT_DONE_MM_VAR=1",
"-DHAVE_FT_GET_TRANSFORM=1",
});
if (coretext_enabled and target.result.isDarwin()) {
// This is definitely super sketchy and not right but without this
// zig build test breaks on macOS. We have to look into what exactly
// is going on here but this getting comitted in the interest of
// unblocking zig build test.
module.resolved_target = target;
defer module.resolved_target = null;
if (coretext_enabled) {
try flags.appendSlice(&.{"-DHAVE_CORETEXT=1"});
try apple_sdk.addPaths(b, &lib.root_module);
try apple_sdk.addPaths(b, module);
lib.linkFramework("ApplicationServices");
module.linkFramework("ApplicationServices", .{});
lib.linkFramework("CoreText");
module.linkFramework("CoreText", .{});
}
lib.addCSourceFile(.{

View File

@ -1,6 +1,7 @@
.{
.name = "harfbuzz",
.version = "8.2.2",
.paths = .{""},
.dependencies = .{
.harfbuzz = .{
.url = "https://github.com/harfbuzz/harfbuzz/archive/refs/tags/8.2.2.tar.gz",

View File

@ -15,6 +15,10 @@ pub fn build(b: *std.Build) !void {
if (target.result.os.tag == .linux) {
lib.linkSystemLibrary("m");
}
if (target.result.isDarwin()) {
const apple_sdk = @import("apple_sdk");
try apple_sdk.addPaths(b, &lib.root_module);
}
const zlib_dep = b.dependency("zlib", .{ .target = target, .optimize = optimize });
lib.linkLibrary(zlib_dep.artifact("z"));

View File

@ -1,14 +1,14 @@
.{
.name = "libpng",
.version = "1.6.40",
.paths = .{""},
.dependencies = .{
.libpng = .{
.url = "https://github.com/glennrp/libpng/archive/refs/tags/v1.6.40.tar.gz",
.hash = "12203d2722e3af6f9556503b114c25fe3eead528a93f5f26eefcb187a460d1548e07",
},
.zlib = .{
.path = "../zlib",
},
.zlib = .{ .path = "../zlib" },
.apple_sdk = .{ .path = "../apple-sdk" },
},
}

View File

@ -28,14 +28,16 @@ pub fn build(b: *std.Build) !void {
.file = .{ .path = "text/ext.c" },
.flags = flags.items,
});
lib.linkFramework("Carbon");
lib.linkFramework("CoreFoundation");
lib.linkFramework("CoreGraphics");
lib.linkFramework("CoreText");
lib.linkFramework("CoreVideo");
if (target.result.os.tag == .macos) {
lib.linkFramework("Carbon");
module.linkFramework("Carbon", .{});
}
if (target.result.isDarwin()) {
module.linkFramework("Carbon", .{});
module.linkFramework("CoreFoundation", .{});
module.linkFramework("CoreGraphics", .{});
module.linkFramework("CoreText", .{});

View File

@ -12,7 +12,7 @@ pub fn build(b: *std.Build) !void {
module.addIncludePath(upstream.path("src"));
b.installArtifact(lib);
{
if (target.query.isNative()) {
const test_exe = b.addTest(.{
.name = "test",
.root_source_file = .{ .path = "main.zig" },
@ -44,6 +44,11 @@ fn buildOniguruma(
lib.linkLibC();
lib.addIncludePath(upstream.path("src"));
if (target.result.isDarwin()) {
const apple_sdk = @import("apple_sdk");
try apple_sdk.addPaths(b, &lib.root_module);
}
lib.addConfigHeader(b.addConfigHeader(.{
.style = .{ .cmake = upstream.path("src/config.h.cmake.in") },
}, .{

View File

@ -7,5 +7,7 @@
.url = "https://github.com/kkos/oniguruma/archive/refs/tags/v6.9.9.tar.gz",
.hash = "1220c15e72eadd0d9085a8af134904d9a0f5dfcbed5f606ad60edc60ebeccd9706bb",
},
.apple_sdk = .{ .path = "../apple-sdk" },
},
}

View File

@ -16,6 +16,10 @@ pub fn build(b: *std.Build) !void {
if (target.result.os.tag != .windows) {
lib.linkSystemLibrary("pthread");
}
if (target.result.isDarwin()) {
const apple_sdk = @import("apple_sdk");
try apple_sdk.addPaths(b, &lib.root_module);
}
lib.addIncludePath(upstream.path(""));
lib.addIncludePath(.{ .path = "" });
@ -68,7 +72,7 @@ pub fn build(b: *std.Build) !void {
b.installArtifact(lib);
{
if (target.query.isNative()) {
const test_exe = b.addTest(.{
.name = "test",
.root_source_file = .{ .path = "main.zig" },

View File

@ -1,10 +1,13 @@
.{
.name = "pixman",
.version = "0.42.2",
.paths = .{""},
.dependencies = .{
.pixman = .{
.url = "https://deps.files.ghostty.dev/pixman-pixman-0.42.2.tar.gz",
.hash = "12209b9206f9a5d31ccd9a2312cc72cb9dfc3e034aee1883c549dc1d753fae457230",
},
.apple_sdk = .{ .path = "../apple-sdk" },
},
}

View File

@ -12,7 +12,7 @@ pub fn build(b: *std.Build) !void {
const lib = try buildSpirvCross(b, upstream, target, optimize);
b.installArtifact(lib);
{
if (target.query.isNative()) {
const test_exe = b.addTest(.{
.name = "test",
.root_source_file = .{ .path = "main.zig" },
@ -42,8 +42,10 @@ fn buildSpirvCross(
});
lib.linkLibC();
lib.linkLibCpp();
//lib.addIncludePath(upstream.path(""));
//lib.addIncludePath(.{ .path = "override" });
if (target.result.isDarwin()) {
const apple_sdk = @import("apple_sdk");
try apple_sdk.addPaths(b, &lib.root_module);
}
var flags = std.ArrayList([]const u8).init(b.allocator);
defer flags.deinit();

View File

@ -7,5 +7,7 @@
.url = "https://github.com/KhronosGroup/SPIRV-Cross/archive/4818f7e7ef7b7078a3a7a5a52c4a338e0dda22f4.tar.gz",
.hash = "1220b2d8a6cff1926ef28a29e312a0a503b555ebc2f082230b882410f49e672ac9c6",
},
.apple_sdk = .{ .path = "../apple-sdk" },
},
}

View File

@ -13,6 +13,11 @@ pub fn build(b: *std.Build) !void {
});
lib.linkLibC();
lib.addIncludePath(upstream.path(""));
if (target.result.isDarwin()) {
const apple_sdk = @import("apple_sdk");
try apple_sdk.addPaths(b, &lib.root_module);
}
lib.installHeadersDirectoryOptions(.{
.source_dir = upstream.path(""),
.install_dir = .header,

View File

@ -1,10 +1,13 @@
.{
.name = "zlib",
.version = "1.3.0",
.paths = .{""},
.dependencies = .{
.zlib = .{
.url = "https://github.com/madler/zlib/archive/refs/tags/v1.3.tar.gz",
.hash = "12207d353609d95cee9da7891919e6d9582e97b7aa2831bd50f33bf523a582a08547",
},
.apple_sdk = .{ .path = "../apple-sdk" },
},
}

View File

@ -290,7 +290,7 @@ fn setupFd(src: File.Handle, target: i32) !void {
}
}
},
.macos => {
.ios, .macos => {
// Mac doesn't support dup3 so we use dup2. We purposely clear
// CLO_ON_EXEC for this fd.
const flags = try os.fcntl(src, os.F.GETFD, 0);

View File

@ -1664,6 +1664,9 @@ pub const CAPI = struct {
ptr: *Surface,
window: *anyopaque,
) void {
// This is only supported on macOS
if (comptime builtin.target.os.tag != .macos) return;
const config = ptr.app.config;
// Do nothing if we don't have background transparency enabled

View File

@ -15,6 +15,12 @@ pub const Options = struct {
/// The path to write the framework
out_path: []const u8,
/// The libraries to bundle
libraries: []const Library,
};
/// A single library to bundle into the xcframework.
pub const Library = struct {
/// Library file (dylib, a) to package.
library: LazyPath,
@ -41,10 +47,12 @@ pub fn create(b: *std.Build, opts: Options) *XCFrameworkStep {
const run = RunStep.create(b, b.fmt("xcframework {s}", .{opts.name}));
run.has_side_effects = true;
run.addArgs(&.{ "xcodebuild", "-create-xcframework" });
run.addArg("-library");
run.addFileArg(opts.library);
run.addArg("-headers");
run.addFileArg(opts.headers);
for (opts.libraries) |lib| {
run.addArg("-library");
run.addFileArg(lib.library);
run.addArg("-headers");
run.addFileArg(lib.headers);
}
run.addArg("-output");
run.addArg(opts.out_path);
break :run run;

View File

@ -18,7 +18,7 @@ pub const SplitResizeDirection = Binding.Action.SplitResizeDirection;
// in theory for XKB too on Linux but we don't need it right now.
pub const Keymap = switch (builtin.os.tag) {
.macos => @import("input/KeymapDarwin.zig"),
else => struct {},
else => @import("input/KeymapNoop.zig"),
};
test {

38
src/input/KeymapNoop.zig Normal file
View File

@ -0,0 +1,38 @@
//! A noop implementation of the keymap interface so that the embedded
//! library can compile on non-macOS platforms.
const KeymapNoop = @This();
const Mods = @import("key.zig").Mods;
pub const State = struct {};
pub const Translation = struct {
text: []const u8,
composing: bool,
};
pub fn init() !KeymapNoop {
return .{};
}
pub fn deinit(self: *const KeymapNoop) void {
_ = self;
}
pub fn reload(self: *KeymapNoop) !void {
_ = self;
}
pub fn translate(
self: *const KeymapNoop,
out: []u8,
state: *State,
code: u16,
mods: Mods,
) !Translation {
_ = self;
_ = out;
_ = state;
_ = code;
_ = mods;
return .{ .text = "", .composing = false };
}

View File

@ -9,7 +9,7 @@ const Key = @import("key.zig").Key;
/// The full list of entries for the current platform.
pub const entries: []const Entry = entries: {
const native_idx = switch (builtin.os.tag) {
.macos => 4, // mac
.ios, .macos => 4, // mac
.windows => 3, // win
.linux => 2, // xkb
else => @compileError("unsupported platform"),

View File

@ -1,3 +1,4 @@
const std = @import("std");
pub const cursor = @import("cursor.zig");
pub const key = @import("key.zig");
pub const termio = @import("termio.zig");

View File

@ -130,7 +130,7 @@ pub const std_options = struct {
//
// sudo log stream --level debug --predicate 'subsystem=="com.mitchellh.ghostty"'
//
if (builtin.os.tag == .macos) {
if (builtin.target.isDarwin()) {
// Convert our levels to Mac levels
const mac_level: macos.os.LogType = switch (level) {
.debug => .debug,

View File

@ -52,6 +52,9 @@ pub fn launchedFromDesktop() bool {
// TODO: This should have some logic to detect this. Perhaps std.builtin.subsystem
.windows => false,
// iPhone/iPad is always launched from the "desktop"
.ios => true,
else => @compileError("unsupported platform"),
};
}

View File

@ -14,6 +14,10 @@ pub inline fn home(buf: []u8) !?[]u8 {
return switch (builtin.os.tag) {
inline .linux, .macos => try homeUnix(buf),
.windows => try homeWindows(buf),
// iOS doesn't have a user-writable home directory
.ios => null,
else => @compileError("unimplemented"),
};
}

View File

@ -7,18 +7,20 @@ const log = std.log.scoped(.os);
/// The system-configured double-click interval if its available.
pub fn clickInterval() ?u32 {
// On macOS, we can ask the system.
if (comptime builtin.target.isDarwin()) {
const NSEvent = objc.getClass("NSEvent") orelse {
log.err("NSEvent class not found. Can't get click interval.", .{});
return null;
};
return switch (builtin.os.tag) {
// On macOS, we can ask the system.
.macos => macos: {
const NSEvent = objc.getClass("NSEvent") orelse {
log.err("NSEvent class not found. Can't get click interval.", .{});
return null;
};
// Get the interval and convert to ms
const interval = NSEvent.msgSend(f64, objc.sel("doubleClickInterval"), .{});
const ms = @as(u32, @intFromFloat(@ceil(interval * 1000)));
return ms;
}
// Get the interval and convert to ms
const interval = NSEvent.msgSend(f64, objc.sel("doubleClickInterval"), .{});
const ms = @as(u32, @intFromFloat(@ceil(interval * 1000)));
break :macos ms;
},
return null;
else => null,
};
}

View File

@ -8,6 +8,7 @@ pub fn open(alloc: Allocator, url: []const u8) !void {
.linux => &.{ "xdg-open", url },
.macos => &.{ "open", url },
.windows => &.{ "rundll32", "url.dll,FileProtocolHandler", url },
.ios => return error.Unimplemented,
else => @compileError("unsupported OS"),
};

View File

@ -14,10 +14,41 @@ pub const winsize = extern struct {
ws_ypixel: u16 = 600,
};
pub const Pty = if (builtin.os.tag == .windows)
WindowsPty
else
PosixPty;
pub const Pty = switch (builtin.os.tag) {
.windows => WindowsPty,
.ios => NullPty,
else => PosixPty,
};
// A pty implementation that does nothing.
//
// TODO: This should be removed. This is only temporary until we have
// a termio that doesn't use a pty. This isn't used in any user-facing
// artifacts, this is just a stopgap to get compilation to work on iOS.
const NullPty = struct {
pub const Fd = std.os.fd_t;
master: Fd,
slave: Fd,
pub fn open(size: winsize) !Pty {
_ = size;
return .{ .master = 0, .slave = 0 };
}
pub fn deinit(self: *Pty) void {
_ = self;
}
pub fn setSize(self: *Pty, size: winsize) !void {
_ = self;
_ = size;
}
pub fn childPreExec(self: Pty) !void {
_ = self;
}
};
/// Linux PTY creation and management. This is just a thin layer on top
/// of Linux syscalls. The caller is responsible for detail-oriented handling