初始提交:terminalX 可运行态(M0/M1/M1.5 已真机验证)
- M0: libghostty SSH 终端(渲染/输入/连接)+ 白屏修复(OutputGate) + 会话状态机/自动重连 - M1: tsnet 用户态组网 + SSH-over-tsnet(fd 桥),shell 级真机验证;R5(Go+gvisor+C+++Swift 同进程) retire - M1.5: tmux -CC 原生 tab(MVP) - 结构: packages/(TXCore·TXTransport), apps/TerminalX, vendor/(libghostty-spm/libssh2/mbedtls/tsnet-bridge), artifacts/ - 文档: CLAUDE.md + docs/HANDOFF.md(新会话入口) - 环境: 认证代理→依赖 vendor 本地化;Go 在 ~/.local/go;仅模拟器/未签名 - 待续: M2 mosh, tmux 多 pane, M4 安全(host key/SE) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
61
vendor/libghostty-spm/Patches/ghostty/0001-darwin-libghostty-install.sh
vendored
Executable file
61
vendor/libghostty-spm/Patches/ghostty/0001-darwin-libghostty-install.sh
vendored
Executable file
@@ -0,0 +1,61 @@
|
||||
#!/bin/zsh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SOURCE_DIR=${1:-}
|
||||
|
||||
if [ -z "$SOURCE_DIR" ]; then
|
||||
echo "[-] missing source_dir"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BUILD_ZIG="$SOURCE_DIR/build.zig"
|
||||
MARKER="libghostty static install for Darwin"
|
||||
|
||||
if [ ! -f "$BUILD_ZIG" ]; then
|
||||
echo "[-] build.zig not found: $BUILD_ZIG"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if grep -Fq "$MARKER" "$BUILD_ZIG"; then
|
||||
echo "[+] patch already applied: 0001-darwin-libghostty-install"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if grep -Fq "const lib_shared = try buildpkg.GhosttyLib.initShared" "$BUILD_ZIG"; then
|
||||
sed -i '' \
|
||||
'/We shouldn'\''t have this guard but we don'\''t currently/,/^ }$/c\
|
||||
// libghostty static install for Darwin:\
|
||||
// upstream only wires this for non-Darwin today, but we need the\
|
||||
// static archive for our own XCFramework assembly pipeline.\
|
||||
lib_shared.installHeader();\
|
||||
if (config.target.result.os.tag.isDarwin()) {\
|
||||
lib_static.install("libghostty.a");\
|
||||
} else if (config.target.result.os.tag == .windows) {\
|
||||
lib_shared.install("ghostty-internal.dll");\
|
||||
lib_static.install("ghostty-internal-static.lib");\
|
||||
} else {\
|
||||
lib_shared.install("ghostty-internal.so");\
|
||||
lib_static.install("ghostty-internal.a");\
|
||||
}' \
|
||||
"$BUILD_ZIG"
|
||||
else
|
||||
sed -i '' \
|
||||
'/We shouldn'\''t have this guard but we don'\''t currently/,/^ }$/c\
|
||||
// libghostty static install for Darwin:\
|
||||
// upstream only wires this for non-Darwin today, but we need the\
|
||||
// static archive for our own XCFramework assembly pipeline.\
|
||||
libghostty_shared.installHeader(); // Only need one header\
|
||||
if (!config.target.result.os.tag.isDarwin()) {\
|
||||
libghostty_shared.install("libghostty.so");\
|
||||
}\
|
||||
libghostty_static.install("libghostty.a");' \
|
||||
"$BUILD_ZIG"
|
||||
fi
|
||||
|
||||
if ! grep -Fq "$MARKER" "$BUILD_ZIG"; then
|
||||
echo "[-] failed to apply patch: 0001-darwin-libghostty-install"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[+] applied patch: 0001-darwin-libghostty-install"
|
||||
536
vendor/libghostty-spm/Patches/ghostty/0002-host-managed-io-modern.patch
vendored
Normal file
536
vendor/libghostty-spm/Patches/ghostty/0002-host-managed-io-modern.patch
vendored
Normal file
@@ -0,0 +1,536 @@
|
||||
diff --git a/include/ghostty.h b/include/ghostty.h
|
||||
index 72bbb57a8..d9e399220 100644
|
||||
--- a/include/ghostty.h
|
||||
+++ b/include/ghostty.h
|
||||
@@ -464,10 +464,26 @@ typedef enum {
|
||||
GHOSTTY_SURFACE_CONTEXT_SPLIT = 2,
|
||||
} ghostty_surface_context_e;
|
||||
|
||||
+typedef enum {
|
||||
+ GHOSTTY_SURFACE_IO_BACKEND_EXEC = 0,
|
||||
+ GHOSTTY_SURFACE_IO_BACKEND_HOST_MANAGED = 1,
|
||||
+} ghostty_surface_io_backend_e;
|
||||
+
|
||||
+typedef void (*ghostty_surface_receive_buffer_cb)(void*, const uint8_t*, size_t);
|
||||
+typedef void (*ghostty_surface_receive_resize_cb)(void*,
|
||||
+ uint16_t,
|
||||
+ uint16_t,
|
||||
+ uint32_t,
|
||||
+ uint32_t);
|
||||
+
|
||||
typedef struct {
|
||||
ghostty_platform_e platform_tag;
|
||||
ghostty_platform_u platform;
|
||||
void* userdata;
|
||||
+ ghostty_surface_io_backend_e backend;
|
||||
+ void* receive_userdata;
|
||||
+ ghostty_surface_receive_buffer_cb receive_buffer;
|
||||
+ ghostty_surface_receive_resize_cb receive_resize;
|
||||
double scale_factor;
|
||||
float font_size;
|
||||
const char* working_directory;
|
||||
@@ -1109,6 +1125,8 @@ GHOSTTY_API ghostty_surface_config_s ghostty_surface_inherited_config(ghostty_su
|
||||
GHOSTTY_API void ghostty_surface_update_config(ghostty_surface_t, ghostty_config_t);
|
||||
GHOSTTY_API bool ghostty_surface_needs_confirm_quit(ghostty_surface_t);
|
||||
GHOSTTY_API bool ghostty_surface_process_exited(ghostty_surface_t);
|
||||
+GHOSTTY_API void ghostty_surface_write_buffer(ghostty_surface_t, const uint8_t*, uintptr_t);
|
||||
+GHOSTTY_API void ghostty_surface_process_exit(ghostty_surface_t, uint32_t, uint64_t);
|
||||
GHOSTTY_API void ghostty_surface_refresh(ghostty_surface_t);
|
||||
GHOSTTY_API void ghostty_surface_draw(ghostty_surface_t);
|
||||
GHOSTTY_API void ghostty_surface_set_content_scale(ghostty_surface_t, double, double);
|
||||
diff --git a/src/Surface.zig b/src/Surface.zig
|
||||
index 2c4edc497..b35c4465e 100644
|
||||
--- a/src/Surface.zig
|
||||
+++ b/src/Surface.zig
|
||||
@@ -628,40 +628,48 @@ pub fn init(
|
||||
// This separate block ({}) is important because our errdefers must
|
||||
// be scoped here to be valid.
|
||||
{
|
||||
- var env = rt_surface.defaultTermioEnv() catch |err| env: {
|
||||
- // If an error occurs, we don't want to block surface startup.
|
||||
- log.warn("error getting env map for surface err={}", .{err});
|
||||
- break :env internal_os.getEnvMap(alloc) catch
|
||||
- std.process.EnvMap.init(alloc);
|
||||
- };
|
||||
- errdefer env.deinit();
|
||||
+ const backend: termio.Backend = switch (rt_surface.termioBackend()) {
|
||||
+ .exec => backend: {
|
||||
+ var env = rt_surface.defaultTermioEnv() catch |err| env: {
|
||||
+ log.warn("error getting env map for surface err={}", .{err});
|
||||
+ break :env internal_os.getEnvMap(alloc) catch
|
||||
+ std.process.EnvMap.init(alloc);
|
||||
+ };
|
||||
+ errdefer env.deinit();
|
||||
|
||||
- // don't leak GHOSTTY_LOG to any subprocesses
|
||||
- env.remove("GHOSTTY_LOG");
|
||||
+ env.remove("GHOSTTY_LOG");
|
||||
|
||||
- var buf: [18]u8 = undefined;
|
||||
- try env.put(
|
||||
- "GHOSTTY_SURFACE_ID",
|
||||
- std.fmt.bufPrint(&buf, "0x{x:0>16}", .{self.id}) catch unreachable,
|
||||
- );
|
||||
+ var buf: [18]u8 = undefined;
|
||||
+ try env.put(
|
||||
+ "GHOSTTY_SURFACE_ID",
|
||||
+ std.fmt.bufPrint(&buf, "0x{x:0>16}", .{self.id}) catch unreachable,
|
||||
+ );
|
||||
|
||||
- // Initialize our IO backend
|
||||
- var io_exec = try termio.Exec.init(alloc, .{
|
||||
- .command = command,
|
||||
- .env = env,
|
||||
- .env_override = config.env,
|
||||
- .shell_integration = config.@"shell-integration",
|
||||
- .shell_integration_features = config.@"shell-integration-features",
|
||||
- .cursor_blink = config.@"cursor-style-blink",
|
||||
- .working_directory = if (config.@"working-directory") |wd| wd.value() else null,
|
||||
- .resources_dir = global_state.resources_dir.host(),
|
||||
- .term = config.term,
|
||||
- .rt_pre_exec_info = .init(config),
|
||||
- .rt_post_fork_info = .init(config),
|
||||
- });
|
||||
- errdefer io_exec.deinit();
|
||||
+ var io_exec = try termio.Exec.init(alloc, .{
|
||||
+ .command = command,
|
||||
+ .env = env,
|
||||
+ .env_override = config.env,
|
||||
+ .shell_integration = config.@"shell-integration",
|
||||
+ .shell_integration_features = config.@"shell-integration-features",
|
||||
+ .cursor_blink = config.@"cursor-style-blink",
|
||||
+ .working_directory = if (config.@"working-directory") |wd| wd.value() else null,
|
||||
+ .resources_dir = global_state.resources_dir.host(),
|
||||
+ .term = config.term,
|
||||
+ .rt_pre_exec_info = .init(config),
|
||||
+ .rt_post_fork_info = .init(config),
|
||||
+ });
|
||||
+ errdefer io_exec.deinit();
|
||||
+
|
||||
+ break :backend .{ .exec = io_exec };
|
||||
+ },
|
||||
+
|
||||
+ .host_managed => .{
|
||||
+ .host_managed = termio.HostManaged.init(
|
||||
+ try rt_surface.hostManagedConfig(),
|
||||
+ ),
|
||||
+ },
|
||||
+ };
|
||||
|
||||
- // Initialize our IO mailbox
|
||||
var io_mailbox = try termio.Mailbox.initSPSC(alloc);
|
||||
errdefer io_mailbox.deinit(alloc);
|
||||
|
||||
@@ -669,7 +677,7 @@ pub fn init(
|
||||
.size = size,
|
||||
.full_config = config,
|
||||
.config = try termio.Termio.DerivedConfig.init(alloc, config),
|
||||
- .backend = .{ .exec = io_exec },
|
||||
+ .backend = backend,
|
||||
.mailbox = io_mailbox,
|
||||
.renderer_state = &self.renderer_state,
|
||||
.renderer_wakeup = render_thread.wakeup,
|
||||
@@ -1311,9 +1319,11 @@ fn childExitedAbnormally(
|
||||
const alloc = arena.allocator();
|
||||
|
||||
// Build up our command for the error message
|
||||
- const command = try std.mem.join(alloc, " ", switch (self.io.backend) {
|
||||
- .exec => |*exec| exec.subprocess.args,
|
||||
- });
|
||||
+ const command = switch (self.io.backend) {
|
||||
+ .exec => |*exec| try std.mem.join(alloc, " ", exec.subprocess.args),
|
||||
+ .host_managed => try alloc.dupe(u8, "host-managed session"),
|
||||
+ };
|
||||
+ defer alloc.free(command);
|
||||
const runtime_str = try std.fmt.allocPrint(alloc, "{d} ms", .{info.runtime_ms});
|
||||
|
||||
self.renderer_state.mutex.lock();
|
||||
diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig
|
||||
index 7310159cc..0180884fc 100644
|
||||
--- a/src/apprt/embedded.zig
|
||||
+++ b/src/apprt/embedded.zig
|
||||
@@ -15,6 +15,7 @@ const input = @import("../input.zig");
|
||||
const internal_os = @import("../os/main.zig");
|
||||
const renderer = @import("../renderer.zig");
|
||||
const terminal = @import("../terminal/main.zig");
|
||||
+const termio = @import("../termio.zig");
|
||||
const CoreApp = @import("../App.zig");
|
||||
const CoreInspector = @import("../inspector/main.zig").Inspector;
|
||||
const CoreSurface = @import("../Surface.zig");
|
||||
@@ -411,6 +412,8 @@ pub const Surface = struct {
|
||||
app: *App,
|
||||
platform: Platform,
|
||||
userdata: ?*anyopaque = null,
|
||||
+ termio_backend: TermioBackend = .exec,
|
||||
+ host_io: HostIO = .{},
|
||||
core_surface: CoreSurface,
|
||||
content_scale: apprt.ContentScale,
|
||||
size: apprt.SurfaceSize,
|
||||
@@ -421,6 +424,17 @@ pub const Surface = struct {
|
||||
/// that getTitle works without the implementer needing to save it.
|
||||
title: ?[:0]const u8 = null,
|
||||
|
||||
+ pub const TermioBackend = enum(c_int) {
|
||||
+ exec = 0,
|
||||
+ host_managed = 1,
|
||||
+ };
|
||||
+
|
||||
+ pub const HostIO = struct {
|
||||
+ userdata: ?*anyopaque = null,
|
||||
+ receive_buffer: ?termio.HostManaged.Config.Write = null,
|
||||
+ receive_resize: ?termio.HostManaged.Config.Resize = null,
|
||||
+ };
|
||||
+
|
||||
/// Surface initialization options.
|
||||
pub const Options = extern struct {
|
||||
/// The platform that this surface is being initialized for and
|
||||
@@ -431,6 +445,20 @@ pub const Surface = struct {
|
||||
/// Userdata passed to some of the callbacks.
|
||||
userdata: ?*anyopaque = null,
|
||||
|
||||
+ /// The backend used to satisfy terminal IO for this surface.
|
||||
+ backend: TermioBackend = .exec,
|
||||
+
|
||||
+ /// Userdata passed to host-managed IO callbacks.
|
||||
+ receive_userdata: ?*anyopaque = null,
|
||||
+
|
||||
+ /// Called when Ghostty emits bytes that should be sent to the host
|
||||
+ /// transport.
|
||||
+ receive_buffer: ?termio.HostManaged.Config.Write = null,
|
||||
+
|
||||
+ /// Called when the terminal size changes and the host transport
|
||||
+ /// should be updated.
|
||||
+ receive_resize: ?termio.HostManaged.Config.Resize = null,
|
||||
+
|
||||
/// The scale factor of the screen.
|
||||
scale_factor: f64 = 1,
|
||||
|
||||
@@ -469,6 +497,12 @@ pub const Surface = struct {
|
||||
.app = app,
|
||||
.platform = try .init(opts.platform_tag, opts.platform),
|
||||
.userdata = opts.userdata,
|
||||
+ .termio_backend = opts.backend,
|
||||
+ .host_io = .{
|
||||
+ .userdata = opts.receive_userdata orelse opts.userdata,
|
||||
+ .receive_buffer = opts.receive_buffer,
|
||||
+ .receive_resize = opts.receive_resize,
|
||||
+ },
|
||||
.core_surface = undefined,
|
||||
.content_scale = .{
|
||||
.x = @floatCast(opts.scale_factor),
|
||||
@@ -634,6 +668,19 @@ pub const Surface = struct {
|
||||
return &self.core_surface;
|
||||
}
|
||||
|
||||
+ pub fn termioBackend(self: *const Surface) TermioBackend {
|
||||
+ return self.termio_backend;
|
||||
+ }
|
||||
+
|
||||
+ pub fn hostManagedConfig(self: *const Surface) !termio.HostManaged.Config {
|
||||
+ const write = self.host_io.receive_buffer orelse return error.HostManagedWriteRequired;
|
||||
+ return .{
|
||||
+ .userdata = self.host_io.userdata,
|
||||
+ .write = write,
|
||||
+ .resize = self.host_io.receive_resize,
|
||||
+ };
|
||||
+ }
|
||||
+
|
||||
pub fn rtApp(self: *const Surface) *App {
|
||||
return self.app;
|
||||
}
|
||||
@@ -1600,6 +1647,28 @@ pub const CAPI = struct {
|
||||
return surface.core_surface.child_exited;
|
||||
}
|
||||
|
||||
+ export fn ghostty_surface_write_buffer(
|
||||
+ surface: *Surface,
|
||||
+ ptr: [*]const u8,
|
||||
+ len: usize,
|
||||
+ ) void {
|
||||
+ if (len == 0) return;
|
||||
+ surface.core_surface.io.processOutput(ptr[0..len]);
|
||||
+ }
|
||||
+
|
||||
+ export fn ghostty_surface_process_exit(
|
||||
+ surface: *Surface,
|
||||
+ exit_code: u32,
|
||||
+ runtime_ms: u64,
|
||||
+ ) void {
|
||||
+ _ = surface.core_surface.io.surface_mailbox.push(.{
|
||||
+ .child_exited = .{
|
||||
+ .exit_code = exit_code,
|
||||
+ .runtime_ms = runtime_ms,
|
||||
+ },
|
||||
+ }, .{ .forever = {} });
|
||||
+ }
|
||||
+
|
||||
/// Returns true if the surface has a selection.
|
||||
export fn ghostty_surface_has_selection(surface: *Surface) bool {
|
||||
return surface.core_surface.hasSelection();
|
||||
diff --git a/src/termio.zig b/src/termio.zig
|
||||
index b16885109..6931fcb58 100644
|
||||
--- a/src/termio.zig
|
||||
+++ b/src/termio.zig
|
||||
@@ -23,6 +23,7 @@ const message = @import("termio/message.zig");
|
||||
pub const backend = @import("termio/backend.zig");
|
||||
pub const mailbox = @import("termio/mailbox.zig");
|
||||
pub const Exec = @import("termio/Exec.zig");
|
||||
+pub const HostManaged = @import("termio/HostManaged.zig");
|
||||
pub const Options = @import("termio/Options.zig");
|
||||
pub const Termio = @import("termio/Termio.zig");
|
||||
pub const Thread = @import("termio/Thread.zig");
|
||||
diff --git a/src/termio/HostManaged.zig b/src/termio/HostManaged.zig
|
||||
new file mode 100644
|
||||
index 000000000..0d1821350
|
||||
--- /dev/null
|
||||
+++ b/src/termio/HostManaged.zig
|
||||
@@ -0,0 +1,136 @@
|
||||
+const HostManaged = @This();
|
||||
+
|
||||
+const std = @import("std");
|
||||
+const Allocator = std.mem.Allocator;
|
||||
+const renderer = @import("../renderer.zig");
|
||||
+const terminal = @import("../terminal/main.zig");
|
||||
+const termio = @import("../termio.zig");
|
||||
+
|
||||
+userdata: ?*anyopaque = null,
|
||||
+write: Config.Write,
|
||||
+resize_cb: ?Config.Resize = null,
|
||||
+grid_size: renderer.GridSize = .{
|
||||
+ .columns = 0,
|
||||
+ .rows = 0,
|
||||
+},
|
||||
+screen_size: renderer.ScreenSize = .{
|
||||
+ .width = 0,
|
||||
+ .height = 0,
|
||||
+},
|
||||
+
|
||||
+pub const Config = struct {
|
||||
+ pub const Write = *const fn (?*anyopaque, [*]const u8, usize) callconv(.c) void;
|
||||
+ pub const Resize = *const fn (?*anyopaque, u16, u16, u32, u32) callconv(.c) void;
|
||||
+
|
||||
+ userdata: ?*anyopaque = null,
|
||||
+ write: Write,
|
||||
+ resize: ?Resize = null,
|
||||
+};
|
||||
+
|
||||
+pub fn init(cfg: Config) HostManaged {
|
||||
+ return .{
|
||||
+ .userdata = cfg.userdata,
|
||||
+ .write = cfg.write,
|
||||
+ .resize_cb = cfg.resize,
|
||||
+ };
|
||||
+}
|
||||
+
|
||||
+pub fn deinit(self: *HostManaged) void {
|
||||
+ _ = self;
|
||||
+}
|
||||
+
|
||||
+pub fn initTerminal(self: *HostManaged, term: *terminal.Terminal) void {
|
||||
+ self.grid_size = .{
|
||||
+ .columns = term.cols,
|
||||
+ .rows = term.rows,
|
||||
+ };
|
||||
+ self.screen_size = .{
|
||||
+ .width = term.width_px,
|
||||
+ .height = term.height_px,
|
||||
+ };
|
||||
+}
|
||||
+
|
||||
+pub fn threadEnter(
|
||||
+ self: *HostManaged,
|
||||
+ alloc: Allocator,
|
||||
+ io: *termio.Termio,
|
||||
+ td: *termio.Termio.ThreadData,
|
||||
+) !void {
|
||||
+ _ = alloc;
|
||||
+ _ = io;
|
||||
+
|
||||
+ td.backend = .{ .host_managed = .{} };
|
||||
+ self.notifyResize();
|
||||
+}
|
||||
+
|
||||
+pub fn threadExit(self: *HostManaged, td: *termio.Termio.ThreadData) void {
|
||||
+ _ = self;
|
||||
+ _ = td;
|
||||
+}
|
||||
+
|
||||
+pub fn focusGained(
|
||||
+ self: *HostManaged,
|
||||
+ td: *termio.Termio.ThreadData,
|
||||
+ focused: bool,
|
||||
+) !void {
|
||||
+ _ = self;
|
||||
+ _ = td;
|
||||
+ _ = focused;
|
||||
+}
|
||||
+
|
||||
+pub fn resize(
|
||||
+ self: *HostManaged,
|
||||
+ grid_size: renderer.GridSize,
|
||||
+ screen_size: renderer.ScreenSize,
|
||||
+) !void {
|
||||
+ self.grid_size = grid_size;
|
||||
+ self.screen_size = screen_size;
|
||||
+ self.notifyResize();
|
||||
+}
|
||||
+
|
||||
+pub fn queueWrite(
|
||||
+ self: *HostManaged,
|
||||
+ alloc: Allocator,
|
||||
+ td: *termio.Termio.ThreadData,
|
||||
+ data: []const u8,
|
||||
+ linefeed: bool,
|
||||
+) !void {
|
||||
+ _ = alloc;
|
||||
+ _ = td;
|
||||
+ _ = linefeed;
|
||||
+
|
||||
+ if (data.len == 0) return;
|
||||
+ self.write(self.userdata, data.ptr, data.len);
|
||||
+}
|
||||
+
|
||||
+pub fn childExitedAbnormally(
|
||||
+ self: *HostManaged,
|
||||
+ gpa: Allocator,
|
||||
+ t: *terminal.Terminal,
|
||||
+ exit_code: u32,
|
||||
+ runtime_ms: u64,
|
||||
+) !void {
|
||||
+ _ = self;
|
||||
+ _ = gpa;
|
||||
+ _ = t;
|
||||
+ _ = exit_code;
|
||||
+ _ = runtime_ms;
|
||||
+}
|
||||
+
|
||||
+fn notifyResize(self: *HostManaged) void {
|
||||
+ const resize_cb = self.resize_cb orelse return;
|
||||
+ resize_cb(
|
||||
+ self.userdata,
|
||||
+ self.grid_size.columns,
|
||||
+ self.grid_size.rows,
|
||||
+ self.screen_size.width,
|
||||
+ self.screen_size.height,
|
||||
+ );
|
||||
+}
|
||||
+
|
||||
+pub const ThreadData = struct {
|
||||
+ pub fn deinit(self: *ThreadData, alloc: Allocator) void {
|
||||
+ _ = self;
|
||||
+ _ = alloc;
|
||||
+ }
|
||||
+};
|
||||
diff --git a/src/termio/backend.zig b/src/termio/backend.zig
|
||||
index c29009acb..9857cbd7a 100644
|
||||
--- a/src/termio/backend.zig
|
||||
+++ b/src/termio/backend.zig
|
||||
@@ -11,28 +11,34 @@ const ProcessInfo = @import("../pty.zig").ProcessInfo;
|
||||
const WRITE_REQ_PREALLOC = std.math.pow(usize, 2, 5);
|
||||
|
||||
/// The kinds of backends.
|
||||
-pub const Kind = enum { exec };
|
||||
+pub const Kind = enum { exec, host_managed };
|
||||
|
||||
/// Configuration for the various backend types.
|
||||
pub const Config = union(Kind) {
|
||||
/// Exec uses posix exec to run a command with a pty.
|
||||
exec: termio.Exec.Config,
|
||||
+
|
||||
+ /// Host managed forwards terminal IO through embedder callbacks.
|
||||
+ host_managed: termio.HostManaged.Config,
|
||||
};
|
||||
|
||||
/// Backend implementations. A backend is responsible for owning the pty
|
||||
/// behavior and providing read/write capabilities.
|
||||
pub const Backend = union(Kind) {
|
||||
exec: termio.Exec,
|
||||
+ host_managed: termio.HostManaged,
|
||||
|
||||
pub fn deinit(self: *Backend) void {
|
||||
switch (self.*) {
|
||||
.exec => |*exec| exec.deinit(),
|
||||
+ .host_managed => |*host_managed| host_managed.deinit(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn initTerminal(self: *Backend, t: *terminal.Terminal) void {
|
||||
switch (self.*) {
|
||||
.exec => |*exec| exec.initTerminal(t),
|
||||
+ .host_managed => |*host_managed| host_managed.initTerminal(t),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,12 +50,14 @@ pub const Backend = union(Kind) {
|
||||
) !void {
|
||||
switch (self.*) {
|
||||
.exec => |*exec| try exec.threadEnter(alloc, io, td),
|
||||
+ .host_managed => |*host_managed| try host_managed.threadEnter(alloc, io, td),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn threadExit(self: *Backend, td: *termio.Termio.ThreadData) void {
|
||||
switch (self.*) {
|
||||
.exec => |*exec| exec.threadExit(td),
|
||||
+ .host_managed => |*host_managed| host_managed.threadExit(td),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +68,7 @@ pub const Backend = union(Kind) {
|
||||
) !void {
|
||||
switch (self.*) {
|
||||
.exec => |*exec| try exec.focusGained(td, focused),
|
||||
+ .host_managed => |*host_managed| try host_managed.focusGained(td, focused),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +79,7 @@ pub const Backend = union(Kind) {
|
||||
) !void {
|
||||
switch (self.*) {
|
||||
.exec => |*exec| try exec.resize(grid_size, screen_size),
|
||||
+ .host_managed => |*host_managed| try host_managed.resize(grid_size, screen_size),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +92,7 @@ pub const Backend = union(Kind) {
|
||||
) !void {
|
||||
switch (self.*) {
|
||||
.exec => |*exec| try exec.queueWrite(alloc, td, data, linefeed),
|
||||
+ .host_managed => |*host_managed| try host_managed.queueWrite(alloc, td, data, linefeed),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,6 +110,12 @@ pub const Backend = union(Kind) {
|
||||
exit_code,
|
||||
runtime_ms,
|
||||
),
|
||||
+ .host_managed => |*host_managed| try host_managed.childExitedAbnormally(
|
||||
+ gpa,
|
||||
+ t,
|
||||
+ exit_code,
|
||||
+ runtime_ms,
|
||||
+ ),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,6 +125,7 @@ pub const Backend = union(Kind) {
|
||||
pub fn getProcessInfo(self: *Backend, comptime info: ProcessInfo) ?ProcessInfo.Type(info) {
|
||||
return switch (self.*) {
|
||||
.exec => |*exec| exec.getProcessInfo(info),
|
||||
+ .host_managed => null,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -115,10 +133,12 @@ pub const Backend = union(Kind) {
|
||||
/// Termio thread data. See termio.ThreadData for docs.
|
||||
pub const ThreadData = union(Kind) {
|
||||
exec: termio.Exec.ThreadData,
|
||||
+ host_managed: termio.HostManaged.ThreadData,
|
||||
|
||||
pub fn deinit(self: *ThreadData, alloc: Allocator) void {
|
||||
switch (self.*) {
|
||||
.exec => |*exec| exec.deinit(alloc),
|
||||
+ .host_managed => |*host_managed| host_managed.deinit(alloc),
|
||||
}
|
||||
}
|
||||
|
||||
518
vendor/libghostty-spm/Patches/ghostty/0002-host-managed-io.patch
vendored
Normal file
518
vendor/libghostty-spm/Patches/ghostty/0002-host-managed-io.patch
vendored
Normal file
@@ -0,0 +1,518 @@
|
||||
diff --git a/include/ghostty.h b/include/ghostty.h
|
||||
index 40ff55c..4c4f01b 100644
|
||||
--- a/include/ghostty.h
|
||||
+++ b/include/ghostty.h
|
||||
@@ -437,10 +437,26 @@ typedef enum {
|
||||
GHOSTTY_SURFACE_CONTEXT_SPLIT = 2,
|
||||
} ghostty_surface_context_e;
|
||||
|
||||
+typedef enum {
|
||||
+ GHOSTTY_SURFACE_IO_BACKEND_EXEC = 0,
|
||||
+ GHOSTTY_SURFACE_IO_BACKEND_HOST_MANAGED = 1,
|
||||
+} ghostty_surface_io_backend_e;
|
||||
+
|
||||
+typedef void (*ghostty_surface_receive_buffer_cb)(void*, const uint8_t*, size_t);
|
||||
+typedef void (*ghostty_surface_receive_resize_cb)(void*,
|
||||
+ uint16_t,
|
||||
+ uint16_t,
|
||||
+ uint32_t,
|
||||
+ uint32_t);
|
||||
+
|
||||
typedef struct {
|
||||
ghostty_platform_e platform_tag;
|
||||
ghostty_platform_u platform;
|
||||
void* userdata;
|
||||
+ ghostty_surface_io_backend_e backend;
|
||||
+ void* receive_userdata;
|
||||
+ ghostty_surface_receive_buffer_cb receive_buffer;
|
||||
+ ghostty_surface_receive_resize_cb receive_resize;
|
||||
double scale_factor;
|
||||
float font_size;
|
||||
const char* working_directory;
|
||||
@@ -1080,6 +1096,8 @@ ghostty_surface_config_s ghostty_surface_inherited_config(ghostty_surface_t, gho
|
||||
void ghostty_surface_update_config(ghostty_surface_t, ghostty_config_t);
|
||||
bool ghostty_surface_needs_confirm_quit(ghostty_surface_t);
|
||||
bool ghostty_surface_process_exited(ghostty_surface_t);
|
||||
+void ghostty_surface_write_buffer(ghostty_surface_t, const uint8_t*, uintptr_t);
|
||||
+void ghostty_surface_process_exit(ghostty_surface_t, uint32_t, uint64_t);
|
||||
void ghostty_surface_refresh(ghostty_surface_t);
|
||||
void ghostty_surface_draw(ghostty_surface_t);
|
||||
void ghostty_surface_set_content_scale(ghostty_surface_t, double, double);
|
||||
diff --git a/src/Surface.zig b/src/Surface.zig
|
||||
index 4d66622..145d113 100644
|
||||
--- a/src/Surface.zig
|
||||
+++ b/src/Surface.zig
|
||||
@@ -620,34 +620,42 @@ pub fn init(
|
||||
// This separate block ({}) is important because our errdefers must
|
||||
// be scoped here to be valid.
|
||||
{
|
||||
- var env = rt_surface.defaultTermioEnv() catch |err| env: {
|
||||
- // If an error occurs, we don't want to block surface startup.
|
||||
- log.warn("error getting env map for surface err={}", .{err});
|
||||
- break :env internal_os.getEnvMap(alloc) catch
|
||||
- std.process.EnvMap.init(alloc);
|
||||
+ const backend: termio.Backend = switch (rt_surface.termioBackend()) {
|
||||
+ .exec => backend: {
|
||||
+ var env = rt_surface.defaultTermioEnv() catch |err| env: {
|
||||
+ log.warn("error getting env map for surface err={}", .{err});
|
||||
+ break :env internal_os.getEnvMap(alloc) catch
|
||||
+ std.process.EnvMap.init(alloc);
|
||||
+ };
|
||||
+ errdefer env.deinit();
|
||||
+
|
||||
+ env.remove("GHOSTTY_LOG");
|
||||
+
|
||||
+ var io_exec = try termio.Exec.init(alloc, .{
|
||||
+ .command = command,
|
||||
+ .env = env,
|
||||
+ .env_override = config.env,
|
||||
+ .shell_integration = config.@"shell-integration",
|
||||
+ .shell_integration_features = config.@"shell-integration-features",
|
||||
+ .cursor_blink = config.@"cursor-style-blink",
|
||||
+ .working_directory = if (config.@"working-directory") |wd| wd.value() else null,
|
||||
+ .resources_dir = global_state.resources_dir.host(),
|
||||
+ .term = config.term,
|
||||
+ .rt_pre_exec_info = .init(config),
|
||||
+ .rt_post_fork_info = .init(config),
|
||||
+ });
|
||||
+ errdefer io_exec.deinit();
|
||||
+
|
||||
+ break :backend .{ .exec = io_exec };
|
||||
+ },
|
||||
+
|
||||
+ .host_managed => .{
|
||||
+ .host_managed = termio.HostManaged.init(
|
||||
+ try rt_surface.hostManagedConfig(),
|
||||
+ ),
|
||||
+ },
|
||||
};
|
||||
- errdefer env.deinit();
|
||||
-
|
||||
- // don't leak GHOSTTY_LOG to any subprocesses
|
||||
- env.remove("GHOSTTY_LOG");
|
||||
-
|
||||
- // Initialize our IO backend
|
||||
- var io_exec = try termio.Exec.init(alloc, .{
|
||||
- .command = command,
|
||||
- .env = env,
|
||||
- .env_override = config.env,
|
||||
- .shell_integration = config.@"shell-integration",
|
||||
- .shell_integration_features = config.@"shell-integration-features",
|
||||
- .cursor_blink = config.@"cursor-style-blink",
|
||||
- .working_directory = if (config.@"working-directory") |wd| wd.value() else null,
|
||||
- .resources_dir = global_state.resources_dir.host(),
|
||||
- .term = config.term,
|
||||
- .rt_pre_exec_info = .init(config),
|
||||
- .rt_post_fork_info = .init(config),
|
||||
- });
|
||||
- errdefer io_exec.deinit();
|
||||
|
||||
- // Initialize our IO mailbox
|
||||
var io_mailbox = try termio.Mailbox.initSPSC(alloc);
|
||||
errdefer io_mailbox.deinit(alloc);
|
||||
|
||||
@@ -655,7 +663,7 @@ pub fn init(
|
||||
.size = size,
|
||||
.full_config = config,
|
||||
.config = try termio.Termio.DerivedConfig.init(alloc, config),
|
||||
- .backend = .{ .exec = io_exec },
|
||||
+ .backend = backend,
|
||||
.mailbox = io_mailbox,
|
||||
.renderer_state = &self.renderer_state,
|
||||
.renderer_wakeup = render_thread.wakeup,
|
||||
@@ -1289,9 +1297,11 @@ fn childExitedAbnormally(
|
||||
const alloc = arena.allocator();
|
||||
|
||||
// Build up our command for the error message
|
||||
- const command = try std.mem.join(alloc, " ", switch (self.io.backend) {
|
||||
- .exec => |*exec| exec.subprocess.args,
|
||||
- });
|
||||
+ const command = switch (self.io.backend) {
|
||||
+ .exec => |*exec| try std.mem.join(alloc, " ", exec.subprocess.args),
|
||||
+ .host_managed => try alloc.dupe(u8, "host-managed session"),
|
||||
+ };
|
||||
+ defer alloc.free(command);
|
||||
const runtime_str = try std.fmt.allocPrint(alloc, "{d} ms", .{info.runtime_ms});
|
||||
|
||||
self.renderer_state.mutex.lock();
|
||||
diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig
|
||||
index 0d5a4f8..b44d4d3 100644
|
||||
--- a/src/apprt/embedded.zig
|
||||
+++ b/src/apprt/embedded.zig
|
||||
@@ -15,6 +15,7 @@ const input = @import("../input.zig");
|
||||
const internal_os = @import("../os/main.zig");
|
||||
const renderer = @import("../renderer.zig");
|
||||
const terminal = @import("../terminal/main.zig");
|
||||
+const termio = @import("../termio.zig");
|
||||
const CoreApp = @import("../App.zig");
|
||||
const CoreInspector = @import("../inspector/main.zig").Inspector;
|
||||
const CoreSurface = @import("../Surface.zig");
|
||||
@@ -409,6 +410,8 @@ pub const Surface = struct {
|
||||
app: *App,
|
||||
platform: Platform,
|
||||
userdata: ?*anyopaque = null,
|
||||
+ termio_backend: TermioBackend = .exec,
|
||||
+ host_io: HostIO = .{},
|
||||
core_surface: CoreSurface,
|
||||
content_scale: apprt.ContentScale,
|
||||
size: apprt.SurfaceSize,
|
||||
@@ -419,6 +422,17 @@ pub const Surface = struct {
|
||||
/// that getTitle works without the implementer needing to save it.
|
||||
title: ?[:0]const u8 = null,
|
||||
|
||||
+ pub const TermioBackend = enum(c_int) {
|
||||
+ exec = 0,
|
||||
+ host_managed = 1,
|
||||
+ };
|
||||
+
|
||||
+ pub const HostIO = struct {
|
||||
+ userdata: ?*anyopaque = null,
|
||||
+ receive_buffer: ?termio.HostManaged.Config.Write = null,
|
||||
+ receive_resize: ?termio.HostManaged.Config.Resize = null,
|
||||
+ };
|
||||
+
|
||||
/// Surface initialization options.
|
||||
pub const Options = extern struct {
|
||||
/// The platform that this surface is being initialized for and
|
||||
@@ -429,6 +443,20 @@ pub const Surface = struct {
|
||||
/// Userdata passed to some of the callbacks.
|
||||
userdata: ?*anyopaque = null,
|
||||
|
||||
+ /// The backend used to satisfy terminal IO for this surface.
|
||||
+ backend: TermioBackend = .exec,
|
||||
+
|
||||
+ /// Userdata passed to host-managed IO callbacks.
|
||||
+ receive_userdata: ?*anyopaque = null,
|
||||
+
|
||||
+ /// Called when Ghostty emits bytes that should be sent to the host
|
||||
+ /// transport.
|
||||
+ receive_buffer: ?termio.HostManaged.Config.Write = null,
|
||||
+
|
||||
+ /// Called when the terminal size changes and the host transport
|
||||
+ /// should be updated.
|
||||
+ receive_resize: ?termio.HostManaged.Config.Resize = null,
|
||||
+
|
||||
/// The scale factor of the screen.
|
||||
scale_factor: f64 = 1,
|
||||
|
||||
@@ -467,6 +495,12 @@ pub const Surface = struct {
|
||||
.app = app,
|
||||
.platform = try .init(opts.platform_tag, opts.platform),
|
||||
.userdata = opts.userdata,
|
||||
+ .termio_backend = opts.backend,
|
||||
+ .host_io = .{
|
||||
+ .userdata = opts.receive_userdata orelse opts.userdata,
|
||||
+ .receive_buffer = opts.receive_buffer,
|
||||
+ .receive_resize = opts.receive_resize,
|
||||
+ },
|
||||
.core_surface = undefined,
|
||||
.content_scale = .{
|
||||
.x = @floatCast(opts.scale_factor),
|
||||
@@ -632,6 +666,19 @@ pub const Surface = struct {
|
||||
return &self.core_surface;
|
||||
}
|
||||
|
||||
+ pub fn termioBackend(self: *const Surface) TermioBackend {
|
||||
+ return self.termio_backend;
|
||||
+ }
|
||||
+
|
||||
+ pub fn hostManagedConfig(self: *const Surface) !termio.HostManaged.Config {
|
||||
+ const write = self.host_io.receive_buffer orelse return error.HostManagedWriteRequired;
|
||||
+ return .{
|
||||
+ .userdata = self.host_io.userdata,
|
||||
+ .write = write,
|
||||
+ .resize = self.host_io.receive_resize,
|
||||
+ };
|
||||
+ }
|
||||
+
|
||||
pub fn rtApp(self: *const Surface) *App {
|
||||
return self.app;
|
||||
}
|
||||
@@ -1598,6 +1645,28 @@ pub const CAPI = struct {
|
||||
return surface.core_surface.child_exited;
|
||||
}
|
||||
|
||||
+ export fn ghostty_surface_write_buffer(
|
||||
+ surface: *Surface,
|
||||
+ ptr: [*]const u8,
|
||||
+ len: usize,
|
||||
+ ) void {
|
||||
+ if (len == 0) return;
|
||||
+ surface.core_surface.io.processOutput(ptr[0..len]);
|
||||
+ }
|
||||
+
|
||||
+ export fn ghostty_surface_process_exit(
|
||||
+ surface: *Surface,
|
||||
+ exit_code: u32,
|
||||
+ runtime_ms: u64,
|
||||
+ ) void {
|
||||
+ _ = surface.core_surface.io.surface_mailbox.push(.{
|
||||
+ .child_exited = .{
|
||||
+ .exit_code = exit_code,
|
||||
+ .runtime_ms = runtime_ms,
|
||||
+ },
|
||||
+ }, .{ .forever = {} });
|
||||
+ }
|
||||
+
|
||||
/// Returns true if the surface has a selection.
|
||||
export fn ghostty_surface_has_selection(surface: *Surface) bool {
|
||||
return surface.core_surface.hasSelection();
|
||||
diff --git a/src/termio.zig b/src/termio.zig
|
||||
index b168851..6931fcb 100644
|
||||
--- a/src/termio.zig
|
||||
+++ b/src/termio.zig
|
||||
@@ -23,6 +23,7 @@ const message = @import("termio/message.zig");
|
||||
pub const backend = @import("termio/backend.zig");
|
||||
pub const mailbox = @import("termio/mailbox.zig");
|
||||
pub const Exec = @import("termio/Exec.zig");
|
||||
+pub const HostManaged = @import("termio/HostManaged.zig");
|
||||
pub const Options = @import("termio/Options.zig");
|
||||
pub const Termio = @import("termio/Termio.zig");
|
||||
pub const Thread = @import("termio/Thread.zig");
|
||||
diff --git a/src/termio/HostManaged.zig b/src/termio/HostManaged.zig
|
||||
new file mode 100644
|
||||
index 0000000..0d18213
|
||||
--- /dev/null
|
||||
+++ b/src/termio/HostManaged.zig
|
||||
@@ -0,0 +1,136 @@
|
||||
+const HostManaged = @This();
|
||||
+
|
||||
+const std = @import("std");
|
||||
+const Allocator = std.mem.Allocator;
|
||||
+const renderer = @import("../renderer.zig");
|
||||
+const terminal = @import("../terminal/main.zig");
|
||||
+const termio = @import("../termio.zig");
|
||||
+
|
||||
+userdata: ?*anyopaque = null,
|
||||
+write: Config.Write,
|
||||
+resize_cb: ?Config.Resize = null,
|
||||
+grid_size: renderer.GridSize = .{
|
||||
+ .columns = 0,
|
||||
+ .rows = 0,
|
||||
+},
|
||||
+screen_size: renderer.ScreenSize = .{
|
||||
+ .width = 0,
|
||||
+ .height = 0,
|
||||
+},
|
||||
+
|
||||
+pub const Config = struct {
|
||||
+ pub const Write = *const fn (?*anyopaque, [*]const u8, usize) callconv(.c) void;
|
||||
+ pub const Resize = *const fn (?*anyopaque, u16, u16, u32, u32) callconv(.c) void;
|
||||
+
|
||||
+ userdata: ?*anyopaque = null,
|
||||
+ write: Write,
|
||||
+ resize: ?Resize = null,
|
||||
+};
|
||||
+
|
||||
+pub fn init(cfg: Config) HostManaged {
|
||||
+ return .{
|
||||
+ .userdata = cfg.userdata,
|
||||
+ .write = cfg.write,
|
||||
+ .resize_cb = cfg.resize,
|
||||
+ };
|
||||
+}
|
||||
+
|
||||
+pub fn deinit(self: *HostManaged) void {
|
||||
+ _ = self;
|
||||
+}
|
||||
+
|
||||
+pub fn initTerminal(self: *HostManaged, term: *terminal.Terminal) void {
|
||||
+ self.grid_size = .{
|
||||
+ .columns = term.cols,
|
||||
+ .rows = term.rows,
|
||||
+ };
|
||||
+ self.screen_size = .{
|
||||
+ .width = term.width_px,
|
||||
+ .height = term.height_px,
|
||||
+ };
|
||||
+}
|
||||
+
|
||||
+pub fn threadEnter(
|
||||
+ self: *HostManaged,
|
||||
+ alloc: Allocator,
|
||||
+ io: *termio.Termio,
|
||||
+ td: *termio.Termio.ThreadData,
|
||||
+) !void {
|
||||
+ _ = alloc;
|
||||
+ _ = io;
|
||||
+
|
||||
+ td.backend = .{ .host_managed = .{} };
|
||||
+ self.notifyResize();
|
||||
+}
|
||||
+
|
||||
+pub fn threadExit(self: *HostManaged, td: *termio.Termio.ThreadData) void {
|
||||
+ _ = self;
|
||||
+ _ = td;
|
||||
+}
|
||||
+
|
||||
+pub fn focusGained(
|
||||
+ self: *HostManaged,
|
||||
+ td: *termio.Termio.ThreadData,
|
||||
+ focused: bool,
|
||||
+) !void {
|
||||
+ _ = self;
|
||||
+ _ = td;
|
||||
+ _ = focused;
|
||||
+}
|
||||
+
|
||||
+pub fn resize(
|
||||
+ self: *HostManaged,
|
||||
+ grid_size: renderer.GridSize,
|
||||
+ screen_size: renderer.ScreenSize,
|
||||
+) !void {
|
||||
+ self.grid_size = grid_size;
|
||||
+ self.screen_size = screen_size;
|
||||
+ self.notifyResize();
|
||||
+}
|
||||
+
|
||||
+pub fn queueWrite(
|
||||
+ self: *HostManaged,
|
||||
+ alloc: Allocator,
|
||||
+ td: *termio.Termio.ThreadData,
|
||||
+ data: []const u8,
|
||||
+ linefeed: bool,
|
||||
+) !void {
|
||||
+ _ = alloc;
|
||||
+ _ = td;
|
||||
+ _ = linefeed;
|
||||
+
|
||||
+ if (data.len == 0) return;
|
||||
+ self.write(self.userdata, data.ptr, data.len);
|
||||
+}
|
||||
+
|
||||
+pub fn childExitedAbnormally(
|
||||
+ self: *HostManaged,
|
||||
+ gpa: Allocator,
|
||||
+ t: *terminal.Terminal,
|
||||
+ exit_code: u32,
|
||||
+ runtime_ms: u64,
|
||||
+) !void {
|
||||
+ _ = self;
|
||||
+ _ = gpa;
|
||||
+ _ = t;
|
||||
+ _ = exit_code;
|
||||
+ _ = runtime_ms;
|
||||
+}
|
||||
+
|
||||
+fn notifyResize(self: *HostManaged) void {
|
||||
+ const resize_cb = self.resize_cb orelse return;
|
||||
+ resize_cb(
|
||||
+ self.userdata,
|
||||
+ self.grid_size.columns,
|
||||
+ self.grid_size.rows,
|
||||
+ self.screen_size.width,
|
||||
+ self.screen_size.height,
|
||||
+ );
|
||||
+}
|
||||
+
|
||||
+pub const ThreadData = struct {
|
||||
+ pub fn deinit(self: *ThreadData, alloc: Allocator) void {
|
||||
+ _ = self;
|
||||
+ _ = alloc;
|
||||
+ }
|
||||
+};
|
||||
diff --git a/src/termio/backend.zig b/src/termio/backend.zig
|
||||
index ae0e200..f1d35d2 100644
|
||||
--- a/src/termio/backend.zig
|
||||
+++ b/src/termio/backend.zig
|
||||
@@ -10,28 +10,34 @@ const termio = @import("../termio.zig");
|
||||
const WRITE_REQ_PREALLOC = std.math.pow(usize, 2, 5);
|
||||
|
||||
/// The kinds of backends.
|
||||
-pub const Kind = enum { exec };
|
||||
+pub const Kind = enum { exec, host_managed };
|
||||
|
||||
/// Configuration for the various backend types.
|
||||
pub const Config = union(Kind) {
|
||||
/// Exec uses posix exec to run a command with a pty.
|
||||
exec: termio.Exec.Config,
|
||||
+
|
||||
+ /// Host managed forwards terminal IO through embedder callbacks.
|
||||
+ host_managed: termio.HostManaged.Config,
|
||||
};
|
||||
|
||||
/// Backend implementations. A backend is responsible for owning the pty
|
||||
/// behavior and providing read/write capabilities.
|
||||
pub const Backend = union(Kind) {
|
||||
exec: termio.Exec,
|
||||
+ host_managed: termio.HostManaged,
|
||||
|
||||
pub fn deinit(self: *Backend) void {
|
||||
switch (self.*) {
|
||||
.exec => |*exec| exec.deinit(),
|
||||
+ .host_managed => |*host_managed| host_managed.deinit(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn initTerminal(self: *Backend, t: *terminal.Terminal) void {
|
||||
switch (self.*) {
|
||||
.exec => |*exec| exec.initTerminal(t),
|
||||
+ .host_managed => |*host_managed| host_managed.initTerminal(t),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,12 +49,14 @@ pub const Backend = union(Kind) {
|
||||
) !void {
|
||||
switch (self.*) {
|
||||
.exec => |*exec| try exec.threadEnter(alloc, io, td),
|
||||
+ .host_managed => |*host_managed| try host_managed.threadEnter(alloc, io, td),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn threadExit(self: *Backend, td: *termio.Termio.ThreadData) void {
|
||||
switch (self.*) {
|
||||
.exec => |*exec| exec.threadExit(td),
|
||||
+ .host_managed => |*host_managed| host_managed.threadExit(td),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +67,7 @@ pub const Backend = union(Kind) {
|
||||
) !void {
|
||||
switch (self.*) {
|
||||
.exec => |*exec| try exec.focusGained(td, focused),
|
||||
+ .host_managed => |*host_managed| try host_managed.focusGained(td, focused),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +78,7 @@ pub const Backend = union(Kind) {
|
||||
) !void {
|
||||
switch (self.*) {
|
||||
.exec => |*exec| try exec.resize(grid_size, screen_size),
|
||||
+ .host_managed => |*host_managed| try host_managed.resize(grid_size, screen_size),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +91,7 @@ pub const Backend = union(Kind) {
|
||||
) !void {
|
||||
switch (self.*) {
|
||||
.exec => |*exec| try exec.queueWrite(alloc, td, data, linefeed),
|
||||
+ .host_managed => |*host_managed| try host_managed.queueWrite(alloc, td, data, linefeed),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +109,12 @@ pub const Backend = union(Kind) {
|
||||
exit_code,
|
||||
runtime_ms,
|
||||
),
|
||||
+ .host_managed => |*host_managed| try host_managed.childExitedAbnormally(
|
||||
+ gpa,
|
||||
+ t,
|
||||
+ exit_code,
|
||||
+ runtime_ms,
|
||||
+ ),
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -105,10 +122,12 @@ pub const Backend = union(Kind) {
|
||||
/// Termio thread data. See termio.ThreadData for docs.
|
||||
pub const ThreadData = union(Kind) {
|
||||
exec: termio.Exec.ThreadData,
|
||||
+ host_managed: termio.HostManaged.ThreadData,
|
||||
|
||||
pub fn deinit(self: *ThreadData, alloc: Allocator) void {
|
||||
switch (self.*) {
|
||||
.exec => |*exec| exec.deinit(alloc),
|
||||
+ .host_managed => |*host_managed| host_managed.deinit(alloc),
|
||||
}
|
||||
}
|
||||
|
||||
1720
vendor/libghostty-spm/Patches/ghostty/0003-prebuilt-framedata.patch
vendored
Normal file
1720
vendor/libghostty-spm/Patches/ghostty/0003-prebuilt-framedata.patch
vendored
Normal file
File diff suppressed because it is too large
Load Diff
97
vendor/libghostty-spm/Patches/ghostty/0004-ios-fixes.sh
vendored
Executable file
97
vendor/libghostty-spm/Patches/ghostty/0004-ios-fixes.sh
vendored
Executable file
@@ -0,0 +1,97 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SOURCE_DIR="${1:?Usage: $0 <ghostty-source-dir>}"
|
||||
|
||||
# Patch 1: cf_release_thread — ignore loop.run errors on iOS
|
||||
# The kqueue-based event loop panics on iOS simulator due to mach port issues
|
||||
CF_RELEASE="${SOURCE_DIR}/src/os/cf_release_thread.zig"
|
||||
if [ -f "$CF_RELEASE" ]; then
|
||||
if grep -q 'try self.loop.run(.until_done);' "$CF_RELEASE"; then
|
||||
sed -i '' 's/try self\.loop\.run(\.until_done);/self.loop.run(.until_done) catch |err| { log.warn("cf release loop failed err={}", .{err}); return; };/' "$CF_RELEASE"
|
||||
echo "[+] patched cf_release_thread to ignore loop errors"
|
||||
else
|
||||
echo "[+] cf_release_thread already patched"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Patch 2: Disable private window blur API (App Store compliance)
|
||||
EMBEDDED="${SOURCE_DIR}/src/apprt/embedded.zig"
|
||||
if [ -f "$EMBEDDED" ]; then
|
||||
if grep -q 'CGSSetWindowBackgroundBlurRadius' "$EMBEDDED"; then
|
||||
python3 - "$EMBEDDED" <<'PY'
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
path = Path(sys.argv[1])
|
||||
text = path.read_text()
|
||||
|
||||
old_fn = """ export fn ghostty_set_window_background_blur(
|
||||
app: *App,
|
||||
window: *anyopaque,
|
||||
) void {
|
||||
// This is only supported on macOS
|
||||
if (comptime builtin.target.os.tag != .macos) return;
|
||||
|
||||
const config = &app.config;
|
||||
|
||||
// Do nothing if we don't have background transparency enabled
|
||||
if (config.@"background-opacity" >= 1.0) return;
|
||||
|
||||
const nswindow = objc.Object.fromId(window);
|
||||
_ = CGSSetWindowBackgroundBlurRadius(
|
||||
CGSDefaultConnectionForThread(),
|
||||
nswindow.msgSend(usize, objc.sel("windowNumber"), .{}),
|
||||
@intCast(config.@"background-blur".cval()),
|
||||
);
|
||||
}
|
||||
|
||||
/// See ghostty_set_window_background_blur
|
||||
extern "c" fn CGSSetWindowBackgroundBlurRadius(*anyopaque, usize, c_int) i32;
|
||||
extern "c" fn CGSDefaultConnectionForThread() *anyopaque;"""
|
||||
|
||||
new_fn = """ export fn ghostty_set_window_background_blur(
|
||||
app: *App,
|
||||
window: *anyopaque,
|
||||
) void {
|
||||
_ = app;
|
||||
_ = window;
|
||||
return;
|
||||
}"""
|
||||
|
||||
if old_fn not in text:
|
||||
print("[+] blur patch already applied or source changed")
|
||||
else:
|
||||
path.write_text(text.replace(old_fn, new_fn))
|
||||
print("[+] patched: disabled private blur API")
|
||||
PY
|
||||
else
|
||||
echo "[+] blur patch already applied"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Patch 3: Link Metal frameworks
|
||||
BUILD_ZIG="${SOURCE_DIR}/pkg/macos/build.zig"
|
||||
if [ -f "$BUILD_ZIG" ]; then
|
||||
if ! grep -q 'lib.linkFramework("Metal")' "$BUILD_ZIG"; then
|
||||
perl -0pi -e 's/lib\.linkFramework\("IOSurface"\);/lib.linkFramework("IOSurface");\n lib.linkFramework("Metal");\n lib.linkFramework("MetalKit");/g' "$BUILD_ZIG"
|
||||
perl -0pi -e 's/module\.linkFramework\("IOSurface", \.\{\}\);/module.linkFramework("IOSurface", .{});\n module.linkFramework("Metal", .{});\n module.linkFramework("MetalKit", .{});/g' "$BUILD_ZIG"
|
||||
echo "[+] patched: linked Metal frameworks"
|
||||
else
|
||||
echo "[+] Metal frameworks already linked"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Patch 4: Lower iOS deployment target to 15.0
|
||||
CONFIG_ZIG="${SOURCE_DIR}/src/build/Config.zig"
|
||||
if [ -f "$CONFIG_ZIG" ]; then
|
||||
if grep -q '\.ios => \.{ \.semver = \.{' "$CONFIG_ZIG"; then
|
||||
perl -0pi -e 's/\.ios => \.{ \.semver = \.{\n\s*\.major = \d+,\n\s*\.minor = \d+,\n\s*\.patch = \d+,/.ios => .{ .semver = .{\n .major = 15,\n .minor = 0,\n .patch = 0,/s' "$CONFIG_ZIG"
|
||||
echo "[+] patched: iOS deployment target -> 15.0"
|
||||
else
|
||||
echo "[+] iOS deployment target already patched"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "[+] all ios-fixes patches applied"
|
||||
473
vendor/libghostty-spm/Patches/ghostty/0005-ios-metal-rendering.sh
vendored
Executable file
473
vendor/libghostty-spm/Patches/ghostty/0005-ios-metal-rendering.sh
vendored
Executable file
@@ -0,0 +1,473 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SOURCE_DIR="${1:?Usage: $0 <ghostty-source-dir>}"
|
||||
|
||||
# =============================================================================
|
||||
# Patch 1: IOSurfaceLayer — iOS rendering compatibility
|
||||
#
|
||||
# Problem: On iOS, IOSurface dimensions may differ by ±1 pixel from the
|
||||
# CALayer bounds due to rounding differences between UIKit point-to-pixel
|
||||
# conversion and Metal's integer pixel sizes. The upstream code does an
|
||||
# exact match and silently discards the surface, causing a blank screen.
|
||||
#
|
||||
# Additionally, iOS doesn't have a native CALayer subclass optimized for
|
||||
# IOSurface content display. Using the private CAIOSurfaceLayer class
|
||||
# (available since iOS 11) provides hardware-accelerated compositing.
|
||||
#
|
||||
# Fix:
|
||||
# - Allow ±1px tolerance on iOS when comparing surface vs layer dimensions
|
||||
# - Dynamically adjust contentsScale when dimensions don't match exactly
|
||||
# - Use CAIOSurfaceLayer as base class on iOS for native IOSurface compositing
|
||||
# - Mark layer as opaque since terminal content fills the entire bounds
|
||||
# =============================================================================
|
||||
IOSURFACE_LAYER="${SOURCE_DIR}/src/renderer/metal/IOSurfaceLayer.zig"
|
||||
if [ -f "$IOSURFACE_LAYER" ]; then
|
||||
if grep -q 'const log = std.log.scoped(.IOSurfaceLayer);' "$IOSURFACE_LAYER"; then
|
||||
python3 - "$IOSURFACE_LAYER" <<'PY'
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
path = Path(sys.argv[1])
|
||||
src = path.read_text()
|
||||
|
||||
# Need builtin for comptime os.tag checks
|
||||
src = src.replace(
|
||||
'const std = @import("std");\nconst Allocator = std.mem.Allocator;',
|
||||
'const std = @import("std");\nconst builtin = @import("builtin");\nconst Allocator = std.mem.Allocator;'
|
||||
)
|
||||
|
||||
# The scoped log is only used in the size check we're replacing; drop it
|
||||
src = src.replace('\nconst log = std.log.scoped(.IOSurfaceLayer);\n', '\n')
|
||||
|
||||
# Terminal surface is always fully opaque — tell the compositor
|
||||
src = src.replace(
|
||||
'layer.setProperty("contentsGravity", macos.animation.kCAGravityTopLeft);\n\n layer.setInstanceVariable',
|
||||
'layer.setProperty("contentsGravity", macos.animation.kCAGravityTopLeft);\n layer.setProperty("opaque", true);\n\n layer.setInstanceVariable'
|
||||
)
|
||||
|
||||
# Replace the strict size equality check with a platform-aware version.
|
||||
# On iOS, UIKit's point→pixel rounding can produce a 1px discrepancy.
|
||||
# Rather than dropping the frame entirely (→ blank screen), we accept it
|
||||
# and recalculate contentsScale so CoreAnimation stretches correctly.
|
||||
old_block = """ if (width != surface.getWidth() or height != surface.getHeight()) {
|
||||
log.debug(
|
||||
"setSurfaceCallback(): surface is wrong size for layer, discarding. surface = {d}x{d}, layer = {d}x{d}",
|
||||
.{ surface.getWidth(), surface.getHeight(), width, height },
|
||||
);
|
||||
return;
|
||||
}"""
|
||||
|
||||
new_block = """ const sw = surface.getWidth();
|
||||
const sh = surface.getHeight();
|
||||
const dw: usize = if (width > sw) width - sw else sw - width;
|
||||
const dh: usize = if (height > sh) height - sh else sh - height;
|
||||
// iOS UIKit rounding can produce ±1px discrepancy; macOS must match exactly
|
||||
const max_drift: usize = if (comptime builtin.os.tag == .ios) 1 else 0;
|
||||
if (dw > max_drift or dh > max_drift) {
|
||||
if (comptime builtin.os.tag == .ios) {
|
||||
// Recalculate contentsScale so CA maps surface pixels to layer points
|
||||
const pw = bounds.size.width;
|
||||
const ph = bounds.size.height;
|
||||
if (pw > 0 and ph > 0) {
|
||||
const cs_x: f64 = @as(f64, @floatFromInt(sw)) / pw;
|
||||
const cs_y: f64 = @as(f64, @floatFromInt(sh)) / ph;
|
||||
const cs: f64 = @max(cs_x, cs_y);
|
||||
if (@abs(cs - scale) > 0.01) {
|
||||
layer.setProperty("contentsScale", cs);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}"""
|
||||
|
||||
if old_block not in src:
|
||||
print("[!] IOSurfaceLayer size check block not found — source may have changed")
|
||||
sys.exit(1)
|
||||
src = src.replace(old_block, new_block)
|
||||
|
||||
# Use the system-provided CAIOSurfaceLayer on iOS; it handles
|
||||
# IOSurface display natively with zero-copy compositing.
|
||||
old_cls = """ const CALayer =
|
||||
objc.getClass("CALayer") orelse return error.ObjCFailed;
|
||||
|
||||
var subclass =
|
||||
objc.allocateClassPair(CALayer, "IOSurfaceLayer") orelse return error.ObjCFailed;"""
|
||||
|
||||
new_cls = """ const parent_cls = if (comptime builtin.os.tag == .ios)
|
||||
// CAIOSurfaceLayer provides native zero-copy IOSurface compositing
|
||||
objc.getClass("CAIOSurfaceLayer") orelse
|
||||
objc.getClass("CALayer") orelse return error.ObjCFailed
|
||||
else
|
||||
objc.getClass("CALayer") orelse return error.ObjCFailed;
|
||||
|
||||
var subclass =
|
||||
objc.allocateClassPair(parent_cls, "IOSurfaceLayer") orelse return error.ObjCFailed;"""
|
||||
|
||||
src = src.replace(old_cls, new_cls)
|
||||
|
||||
path.write_text(src)
|
||||
print("[+] patched IOSurfaceLayer: iOS size tolerance + CAIOSurfaceLayer")
|
||||
PY
|
||||
else
|
||||
echo "[+] IOSurfaceLayer already patched"
|
||||
fi
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# Patch 2: Metal.zig — iOS first-frame display + synchronous present
|
||||
#
|
||||
# Problem 1: On iOS the IOSurfaceLayer is added as a sublayer of the UIView's
|
||||
# backing layer. By the time the renderer registers its display callback the
|
||||
# sublayer already has its bounds set, so no "display" message is generated.
|
||||
# The first frame never renders.
|
||||
#
|
||||
# Problem 2: The async present path dispatches to the main thread via GCD.
|
||||
# On iOS the render loop already runs on the main thread, so the async
|
||||
# dispatch adds an unnecessary runloop turn of latency and can cause ordering
|
||||
# issues with UIKit layout.
|
||||
#
|
||||
# Fix:
|
||||
# - Call setNeedsDisplay after registering the display callback on iOS
|
||||
# - On iOS, always use the synchronous present path (setSurface checks
|
||||
# isMainThread internally and runs inline when true)
|
||||
# =============================================================================
|
||||
METAL_ZIG="${SOURCE_DIR}/src/renderer/Metal.zig"
|
||||
if [ -f "$METAL_ZIG" ]; then
|
||||
if ! grep -q 'setNeedsDisplay' "$METAL_ZIG"; then
|
||||
python3 - "$METAL_ZIG" <<'PY'
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
path = Path(sys.argv[1])
|
||||
src = path.read_text()
|
||||
|
||||
# Kick the first display cycle after callback registration on iOS
|
||||
old_cb = """ @ptrCast(&displayCallback),
|
||||
@ptrCast(renderer),
|
||||
);
|
||||
}"""
|
||||
|
||||
new_cb = """ @ptrCast(&displayCallback),
|
||||
@ptrCast(renderer),
|
||||
);
|
||||
|
||||
// iOS: sublayer bounds are already set before the callback is wired up,
|
||||
// so no display message fires automatically. Kick the first frame.
|
||||
if (comptime builtin.os.tag == .ios) {
|
||||
self.layer.layer.msgSend(void, objc.sel("setNeedsDisplay"), .{});
|
||||
}
|
||||
}"""
|
||||
|
||||
if old_cb not in src:
|
||||
print("[!] Metal loopEnter callback not found")
|
||||
sys.exit(1)
|
||||
src = src.replace(old_cb, new_cb)
|
||||
|
||||
# iOS render loop is main-thread; skip the async dispatch path entirely.
|
||||
old_present = """pub inline fn present(self: *Metal, target: Target, sync: bool) !void {
|
||||
if (sync) {
|
||||
self.layer.setSurfaceSync(target.surface);
|
||||
} else {
|
||||
try self.layer.setSurface(target.surface);
|
||||
}
|
||||
}"""
|
||||
|
||||
new_present = """pub inline fn present(self: *Metal, target: Target, sync: bool) !void {
|
||||
// iOS: always present synchronously — the render loop already runs on
|
||||
// the main thread, so the async GCD hop is unnecessary overhead.
|
||||
if (comptime builtin.os.tag == .ios) {
|
||||
try self.layer.setSurface(target.surface);
|
||||
return;
|
||||
}
|
||||
if (sync) {
|
||||
self.layer.setSurfaceSync(target.surface);
|
||||
} else {
|
||||
try self.layer.setSurface(target.surface);
|
||||
}
|
||||
}"""
|
||||
|
||||
if old_present not in src:
|
||||
print("[!] Metal present function not found")
|
||||
sys.exit(1)
|
||||
src = src.replace(old_present, new_present)
|
||||
|
||||
path.write_text(src)
|
||||
print("[+] patched Metal.zig: iOS first-frame trigger + sync present")
|
||||
PY
|
||||
else
|
||||
echo "[+] Metal.zig already patched"
|
||||
fi
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# Patch 3: coretext.zig — Skip CF release thread on iOS
|
||||
#
|
||||
# Problem: The CoreText font shaper spawns a background thread that uses
|
||||
# libxev's kqueue event loop to asynchronously release CoreFoundation
|
||||
# objects. On iOS, kqueue's Mach port allocation fails (sandbox restrictions
|
||||
# + simulator incompatibility), crashing the thread and potentially stalling
|
||||
# font shaping operations.
|
||||
#
|
||||
# Fix: Make the CF release thread optional. On iOS, skip thread creation
|
||||
# entirely and release CF objects synchronously in endFrame(). This is
|
||||
# acceptable because iOS devices have fast enough CF release performance
|
||||
# and the terminal doesn't produce the same volume of shaped text as a
|
||||
# desktop compositor.
|
||||
# =============================================================================
|
||||
CORETEXT="${SOURCE_DIR}/src/font/shaper/coretext.zig"
|
||||
if [ -f "$CORETEXT" ]; then
|
||||
if grep -q 'cf_release_thread: \*CFReleaseThread,' "$CORETEXT"; then
|
||||
python3 - "$CORETEXT" <<'PY'
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
path = Path(sys.argv[1])
|
||||
src = path.read_text()
|
||||
|
||||
# Make the struct fields optional so nil can represent "no thread"
|
||||
src = src.replace(
|
||||
'cf_release_thread: *CFReleaseThread,\n cf_release_thr: std.Thread,',
|
||||
'cf_release_thread: ?*CFReleaseThread,\n cf_release_thr: ?std.Thread,'
|
||||
)
|
||||
|
||||
# Guard thread creation behind a comptime platform check
|
||||
old_create = """ // Create the CF release thread.
|
||||
var cf_release_thread = try alloc.create(CFReleaseThread);
|
||||
errdefer alloc.destroy(cf_release_thread);
|
||||
cf_release_thread.* = try .init(alloc);
|
||||
errdefer cf_release_thread.deinit();
|
||||
|
||||
// Start the CF release thread.
|
||||
var cf_release_thr = try std.Thread.spawn(
|
||||
.{},
|
||||
CFReleaseThread.threadMain,
|
||||
.{cf_release_thread},
|
||||
);
|
||||
cf_release_thr.setName("cf_release") catch {};
|
||||
|
||||
return .{"""
|
||||
|
||||
new_create = """ // On iOS the kqueue-based event loop used by the release thread
|
||||
// crashes due to Mach port sandbox restrictions. Skip it entirely
|
||||
// and fall through to synchronous release in endFrame.
|
||||
var cf_release_thread: ?*CFReleaseThread = null;
|
||||
var cf_release_thr: ?std.Thread = null;
|
||||
if (comptime builtin.os.tag != .ios) {
|
||||
const thr_obj = try alloc.create(CFReleaseThread);
|
||||
errdefer alloc.destroy(thr_obj);
|
||||
thr_obj.* = try .init(alloc);
|
||||
errdefer thr_obj.deinit();
|
||||
const thr = try std.Thread.spawn(.{}, CFReleaseThread.threadMain, .{thr_obj});
|
||||
thr.setName("cf_release") catch {};
|
||||
cf_release_thread = thr_obj;
|
||||
cf_release_thr = thr;
|
||||
}
|
||||
|
||||
return .{"""
|
||||
|
||||
if old_create not in src:
|
||||
print("[!] coretext CF release thread creation block not found")
|
||||
sys.exit(1)
|
||||
src = src.replace(old_create, new_create)
|
||||
|
||||
# Deinit: only join/stop the thread if it was created
|
||||
old_deinit = """ // Stop the CF release thread
|
||||
{
|
||||
self.cf_release_thread.stop.notify() catch |err|
|
||||
log.err("error notifying cf release thread to stop, may stall err={}", .{err});
|
||||
self.cf_release_thr.join();
|
||||
}
|
||||
self.cf_release_thread.deinit();
|
||||
self.alloc.destroy(self.cf_release_thread);"""
|
||||
|
||||
new_deinit = """ // Stop the CF release thread (nil on iOS)
|
||||
if (self.cf_release_thread) |thr_obj| {
|
||||
thr_obj.stop.notify() catch |err|
|
||||
log.err("error notifying cf release thread to stop, may stall err={}", .{err});
|
||||
self.cf_release_thr.?.join();
|
||||
thr_obj.deinit();
|
||||
self.alloc.destroy(thr_obj);
|
||||
}"""
|
||||
|
||||
if old_deinit not in src:
|
||||
print("[!] coretext CF release thread deinit block not found")
|
||||
sys.exit(1)
|
||||
src = src.replace(old_deinit, new_deinit)
|
||||
|
||||
# endFrame: guard the mailbox push behind an optional check.
|
||||
# When nil (iOS), fall through to the synchronous release below.
|
||||
old_end = """ // Send the items. If the send succeeds then we wake up the
|
||||
// thread to process the items. If the send fails then do a manual
|
||||
// cleanup.
|
||||
if (self.cf_release_thread.mailbox.push(.{ .release = .{
|
||||
.refs = items,
|
||||
.alloc = self.alloc,
|
||||
} }, .{ .forever = {} }) != 0) {
|
||||
self.cf_release_thread.wakeup.notify() catch |err| {
|
||||
log.warn(
|
||||
"error notifying cf release thread to wake up, may stall err={}",
|
||||
.{err},
|
||||
);
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
for (items) |ref| macos.foundation.CFRelease(ref);"""
|
||||
|
||||
new_end = """ // Offload to the background release thread when available.
|
||||
// On iOS cf_release_thread is nil, so we fall through to sync release.
|
||||
if (self.cf_release_thread) |thr_obj| {
|
||||
if (thr_obj.mailbox.push(.{ .release = .{
|
||||
.refs = items,
|
||||
.alloc = self.alloc,
|
||||
} }, .{ .forever = {} }) != 0) {
|
||||
thr_obj.wakeup.notify() catch |err| {
|
||||
log.warn(
|
||||
"error notifying cf release thread to wake up, may stall err={}",
|
||||
.{err},
|
||||
);
|
||||
};
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (items) |ref| macos.foundation.CFRelease(ref);"""
|
||||
|
||||
if old_end not in src:
|
||||
print("[!] coretext endFrame mailbox block not found")
|
||||
sys.exit(1)
|
||||
src = src.replace(old_end, new_end)
|
||||
|
||||
path.write_text(src)
|
||||
print("[+] patched coretext.zig: CF release thread disabled on iOS")
|
||||
PY
|
||||
else
|
||||
echo "[+] coretext.zig already patched"
|
||||
fi
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# Patch 4: iosurface.zig — Explicit row byte alignment
|
||||
#
|
||||
# Problem: When creating an IOSurface without specifying bytesPerRow, the
|
||||
# system picks whatever alignment it wants. Metal textures created from
|
||||
# these surfaces may have mismatched row strides, causing corrupted or
|
||||
# shifted glyph rendering (especially visible on font atlas textures).
|
||||
#
|
||||
# Fix: Calculate 64-byte-aligned row bytes and pass kIOSurfaceBytesPerRow
|
||||
# when creating the IOSurface. Also suppress unused return value warnings
|
||||
# from IOSurfaceLock/Unlock.
|
||||
# =============================================================================
|
||||
IOSURFACE="${SOURCE_DIR}/pkg/macos/iosurface/iosurface.zig"
|
||||
if [ -f "$IOSURFACE" ]; then
|
||||
if ! grep -q 'kIOSurfaceBytesPerRow' "$IOSURFACE"; then
|
||||
python3 - "$IOSURFACE" <<'PY'
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
path = Path(sys.argv[1])
|
||||
src = path.read_text()
|
||||
|
||||
# Compute aligned row stride before creating the Number objects
|
||||
old_start = """ pub fn init(properties: Properties) Allocator.Error!*IOSurface {
|
||||
var w = try foundation.Number.create(.int, &properties.width);"""
|
||||
|
||||
new_start = """ pub fn init(properties: Properties) Allocator.Error!*IOSurface {
|
||||
// Ensure row stride is 64-byte aligned for Metal texture compatibility.
|
||||
const aligned_stride: c_int = @intCast(
|
||||
(properties.width * properties.bytes_per_element + 63) & ~@as(c_int, 63),
|
||||
);
|
||||
var w = try foundation.Number.create(.int, &properties.width);"""
|
||||
|
||||
if old_start not in src:
|
||||
print("[!] iosurface init start not found")
|
||||
sys.exit(1)
|
||||
src = src.replace(old_start, new_start)
|
||||
|
||||
# Create a Number for the stride and include it in the dictionary
|
||||
old_dict_setup = """ var bpe = try foundation.Number.create(.int, &properties.bytes_per_element);
|
||||
defer bpe.release();
|
||||
|
||||
var properties_dict = try foundation.Dictionary.create("""
|
||||
|
||||
new_dict_setup = """ var bpe = try foundation.Number.create(.int, &properties.bytes_per_element);
|
||||
defer bpe.release();
|
||||
|
||||
var stride_num = try foundation.Number.create(.int, &aligned_stride);
|
||||
defer stride_num.release();
|
||||
|
||||
var properties_dict = try foundation.Dictionary.create("""
|
||||
|
||||
if old_dict_setup not in src:
|
||||
print("[!] iosurface bpe block not found")
|
||||
sys.exit(1)
|
||||
src = src.replace(old_dict_setup, new_dict_setup)
|
||||
|
||||
# Extend the dictionary keys/values arrays
|
||||
old_dict = """ &[_]?*const anyopaque{
|
||||
c.kIOSurfaceWidth,
|
||||
c.kIOSurfaceHeight,
|
||||
c.kIOSurfacePixelFormat,
|
||||
c.kIOSurfaceBytesPerElement,
|
||||
},
|
||||
&[_]?*const anyopaque{ w, h, pf, bpe },"""
|
||||
|
||||
new_dict = """ &[_]?*const anyopaque{
|
||||
c.kIOSurfaceWidth,
|
||||
c.kIOSurfaceHeight,
|
||||
c.kIOSurfacePixelFormat,
|
||||
c.kIOSurfaceBytesPerElement,
|
||||
c.kIOSurfaceBytesPerRow,
|
||||
},
|
||||
&[_]?*const anyopaque{ w, h, pf, bpe, stride_num },"""
|
||||
|
||||
if old_dict not in src:
|
||||
print("[!] iosurface dictionary keys not found")
|
||||
sys.exit(1)
|
||||
src = src.replace(old_dict, new_dict)
|
||||
|
||||
# Silence unused return value from IOSurfaceLock/Unlock
|
||||
src = src.replace(
|
||||
' c.IOSurfaceLock(\n @ptrCast(self),\n 0,\n null,\n );',
|
||||
' _ = c.IOSurfaceLock(\n @ptrCast(self),\n 0,\n null,\n );'
|
||||
)
|
||||
src = src.replace(
|
||||
' c.IOSurfaceUnlock(\n @ptrCast(self),\n 0,\n null,\n );',
|
||||
' _ = c.IOSurfaceUnlock(\n @ptrCast(self),\n 0,\n null,\n );'
|
||||
)
|
||||
|
||||
path.write_text(src)
|
||||
print("[+] patched iosurface.zig: 64-byte stride alignment for Metal")
|
||||
PY
|
||||
else
|
||||
echo "[+] iosurface.zig already patched"
|
||||
fi
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# Patch 5: build.zig.zon — Update libxev to fix iOS kqueue mach port panic
|
||||
#
|
||||
# Problem: The bundled libxev uses mach ports for async wakeup on Darwin.
|
||||
# Its kqueue backend checks `os.tag != .macos` and returns null for mach port
|
||||
# kevents on non-macOS Darwin (iOS). The caller then unwraps null with `.?`
|
||||
# causing a panic. A newer libxev version fixes this by properly supporting
|
||||
# iOS as a Darwin target.
|
||||
#
|
||||
# Fix: Update the libxev dependency URL and hash to a version that handles
|
||||
# iOS mach ports correctly.
|
||||
# =============================================================================
|
||||
BUILD_ZON="${SOURCE_DIR}/build.zig.zon"
|
||||
if [ -f "$BUILD_ZON" ]; then
|
||||
if ! grep -q '7e7d2f2ab4700544657f8ec268715c8ef320d839' "$BUILD_ZON"; then
|
||||
sed -i '' 's|"https://deps.files.ghostty.org/libxev-34fa50878aec6e5fa8f532867001ab3c36fae23e.tar.gz"|"https://github.com/mitchellh/libxev/archive/7e7d2f2ab4700544657f8ec268715c8ef320d839.tar.gz"|' "$BUILD_ZON"
|
||||
sed -i '' 's|"libxev-0.0.0-86vtc4IcEwCqEYxEYoN_3KXmc6A9VLcm22aVImfvecYs"|"libxev-0.0.0-86vtcwE9EwB942iWRnaNMXHv3n0BeLAs_tVhrs5cT8cQ"|' "$BUILD_ZON"
|
||||
echo "[+] patched build.zig.zon: updated libxev for iOS mach port fix"
|
||||
else
|
||||
echo "[+] libxev already updated"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "[+] all ios metal rendering patches applied"
|
||||
197
vendor/libghostty-spm/Patches/ghostty/0006-disable-custom-shaders.sh
vendored
Executable file
197
vendor/libghostty-spm/Patches/ghostty/0006-disable-custom-shaders.sh
vendored
Executable file
@@ -0,0 +1,197 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SOURCE_DIR="${1:?Usage: $0 <ghostty-source-dir>}"
|
||||
|
||||
MARKER="LIBGHOSTTY_SPM_TRIM_PATCH"
|
||||
|
||||
# Skip if already applied
|
||||
if grep -q "$MARKER" "$SOURCE_DIR/src/build/Config.zig" 2>/dev/null; then
|
||||
echo "[+] trim patch already applied"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
python3 - "$SOURCE_DIR" "$MARKER" <<'PYEOF'
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
source_dir = Path(sys.argv[1])
|
||||
marker = sys.argv[2]
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# 1. Config.zig — add custom_shaders feature flag
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
config_path = source_dir / "src/build/Config.zig"
|
||||
text = config_path.read_text()
|
||||
|
||||
# Add field
|
||||
text = text.replace(
|
||||
"sentry: bool = true,",
|
||||
f"sentry: bool = true,\ncustom_shaders: bool = true, // {marker}",
|
||||
)
|
||||
|
||||
# Add option parsing after sentry block
|
||||
sentry_end = """ ) orelse sentry: {
|
||||
switch (target.result.os.tag) {
|
||||
.macos, .ios => break :sentry true,
|
||||
|
||||
// Note its false for linux because the crash reports on Linux
|
||||
// don't have much useful information.
|
||||
else => break :sentry false,
|
||||
}
|
||||
};"""
|
||||
|
||||
new_options = sentry_end + """
|
||||
|
||||
config.custom_shaders = b.option(
|
||||
bool,
|
||||
"custom-shaders",
|
||||
"Build with custom shader (glslang/spirv-cross) support.",
|
||||
) orelse true;"""
|
||||
|
||||
text = text.replace(sentry_end, new_options)
|
||||
|
||||
# Add to addOptions
|
||||
text = text.replace(
|
||||
'step.addOption(bool, "sentry", self.sentry);',
|
||||
'step.addOption(bool, "sentry", self.sentry);\n'
|
||||
' step.addOption(bool, "custom_shaders", self.custom_shaders);',
|
||||
)
|
||||
|
||||
config_path.write_text(text)
|
||||
print("[+] patched Config.zig")
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# 2. SharedDeps.zig — gate glslang + spirv-cross on custom_shaders
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
shared_path = source_dir / "src/build/SharedDeps.zig"
|
||||
text = shared_path.read_text()
|
||||
|
||||
# Gate glslang — wrap with custom_shaders check
|
||||
text = text.replace(
|
||||
' // Glslang\n if (b.lazyDependency("glslang", .{',
|
||||
' // Glslang — only needed for custom shaders\n if (self.config.custom_shaders) if (b.lazyDependency("glslang", .{',
|
||||
)
|
||||
# Close the extra if at end of glslang block
|
||||
text = text.replace(
|
||||
""" step.linkLibrary(glslang_dep.artifact("glslang"));
|
||||
try static_libs.append(
|
||||
b.allocator,
|
||||
glslang_dep.artifact("glslang").getEmittedBin(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Spirv-cross""",
|
||||
""" step.linkLibrary(glslang_dep.artifact("glslang"));
|
||||
try static_libs.append(
|
||||
b.allocator,
|
||||
glslang_dep.artifact("glslang").getEmittedBin(),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Spirv-cross""",
|
||||
)
|
||||
|
||||
# Gate spirv-cross — wrap with custom_shaders check
|
||||
text = text.replace(
|
||||
' // Spirv-cross\n if (b.lazyDependency("spirv_cross", .{',
|
||||
' // Spirv-cross — only needed for custom shaders\n if (self.config.custom_shaders) if (b.lazyDependency("spirv_cross", .{',
|
||||
)
|
||||
text = text.replace(
|
||||
""" step.linkLibrary(spirv_cross_dep.artifact("spirv_cross"));
|
||||
try static_libs.append(
|
||||
b.allocator,
|
||||
spirv_cross_dep.artifact("spirv_cross").getEmittedBin(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Sentry""",
|
||||
""" step.linkLibrary(spirv_cross_dep.artifact("spirv_cross"));
|
||||
try static_libs.append(
|
||||
b.allocator,
|
||||
spirv_cross_dep.artifact("spirv_cross").getEmittedBin(),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Sentry""",
|
||||
)
|
||||
|
||||
shared_path.write_text(text)
|
||||
print("[+] patched SharedDeps.zig")
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# 3. build_config.zig — re-export custom_shaders flag
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
bc_path = source_dir / "src/build_config.zig"
|
||||
text = bc_path.read_text()
|
||||
|
||||
if "custom_shaders" not in text:
|
||||
text = text.replace(
|
||||
'const options = @import("build_options");',
|
||||
'const options = @import("build_options");\npub const custom_shaders = options.custom_shaders;',
|
||||
)
|
||||
bc_path.write_text(text)
|
||||
print("[+] patched build_config.zig")
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# 4. global.zig — conditional glslang init
|
||||
# (global.zig already imports build_config)
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
global_path = source_dir / "src/global.zig"
|
||||
text = global_path.read_text()
|
||||
|
||||
text = text.replace(
|
||||
'const glslang = @import("glslang");',
|
||||
'const glslang = if (build_config.custom_shaders) @import("glslang") else struct {\n'
|
||||
' pub fn init() !void {}\n'
|
||||
'};',
|
||||
)
|
||||
|
||||
global_path.write_text(text)
|
||||
print("[+] patched global.zig")
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# 5. renderer/shadertoy.zig — gate shader imports and loadFromFile
|
||||
# When custom_shaders is disabled, loadFromFile is unreachable
|
||||
# so glslang/spirv_cross are never semantically analyzed
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
shader_path = source_dir / "src/renderer/shadertoy.zig"
|
||||
text = shader_path.read_text()
|
||||
|
||||
# Make imports conditional — these won't be analyzed if never reached
|
||||
text = text.replace(
|
||||
'const glslang = @import("glslang");',
|
||||
'const build_config = @import("../build_config.zig");\n'
|
||||
'const glslang = @import("glslang");',
|
||||
)
|
||||
|
||||
# Add early return in loadFromFiles when custom_shaders is disabled
|
||||
# This prevents loadFromFile (and thus spirvFromGlsl etc) from being analyzed
|
||||
text = text.replace(
|
||||
"""pub fn loadFromFiles(
|
||||
alloc_gpa: Allocator,
|
||||
paths: configpkg.RepeatablePath,
|
||||
target: Target,
|
||||
) ![]const [:0]const u8 {
|
||||
var list: std.ArrayList([:0]const u8) = .empty;""",
|
||||
"""pub fn loadFromFiles(
|
||||
alloc_gpa: Allocator,
|
||||
paths: configpkg.RepeatablePath,
|
||||
target: Target,
|
||||
) ![]const [:0]const u8 {
|
||||
if (comptime !build_config.custom_shaders) return &.{};
|
||||
var list: std.ArrayList([:0]const u8) = .empty;""",
|
||||
)
|
||||
|
||||
shader_path.write_text(text)
|
||||
print("[+] patched renderer/shadertoy.zig")
|
||||
|
||||
print(f"[+] all trim patches complete ({marker})")
|
||||
PYEOF
|
||||
|
||||
echo "[+] trim patch applied"
|
||||
408
vendor/libghostty-spm/Patches/ghostty/0007-disable-inspector.sh
vendored
Executable file
408
vendor/libghostty-spm/Patches/ghostty/0007-disable-inspector.sh
vendored
Executable file
@@ -0,0 +1,408 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SOURCE_DIR="${1:?Usage: $0 <ghostty-source-dir>}"
|
||||
|
||||
MARKER="LIBGHOSTTY_SPM_INSPECTOR_DISABLE"
|
||||
|
||||
if grep -q "$MARKER" "$SOURCE_DIR/src/build/Config.zig" 2>/dev/null; then
|
||||
echo "[+] inspector disable patch already applied"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
python3 - "$SOURCE_DIR" "$MARKER" <<'PYEOF'
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
source_dir = Path(sys.argv[1])
|
||||
marker = sys.argv[2]
|
||||
|
||||
def patch_file(rel_path, replacements):
|
||||
path = source_dir / rel_path
|
||||
text = path.read_text()
|
||||
for old, new in replacements:
|
||||
if old not in text:
|
||||
print(f"[-] pattern not found in {rel_path}:")
|
||||
print(f" {old[:80]}...")
|
||||
sys.exit(1)
|
||||
count = text.count(old)
|
||||
if count > 1:
|
||||
print(f"[-] pattern matched {count} times in {rel_path} (expected 1):")
|
||||
print(f" {old[:80]}...")
|
||||
sys.exit(1)
|
||||
text = text.replace(old, new, 1)
|
||||
path.write_text(text)
|
||||
print(f"[+] patched {rel_path}")
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# 1. Config.zig — add inspector feature flag
|
||||
# (runs AFTER 0006, so custom_shaders line already exists)
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
patch_file("src/build/Config.zig", [
|
||||
# Add field after custom_shaders (added by 0006)
|
||||
(
|
||||
f"custom_shaders: bool = true, // LIBGHOSTTY_SPM_TRIM_PATCH",
|
||||
f"custom_shaders: bool = true, // LIBGHOSTTY_SPM_TRIM_PATCH\n"
|
||||
f"inspector: bool = true, // {marker}",
|
||||
),
|
||||
# Add option parsing after the custom_shaders option block (added by 0006)
|
||||
(
|
||||
' "custom-shaders",\n'
|
||||
' "Build with custom shader (glslang/spirv-cross) support.",\n'
|
||||
' ) orelse true;',
|
||||
' "custom-shaders",\n'
|
||||
' "Build with custom shader (glslang/spirv-cross) support.",\n'
|
||||
' ) orelse true;\n'
|
||||
'\n'
|
||||
' config.inspector = b.option(\n'
|
||||
' bool,\n'
|
||||
' "inspector",\n'
|
||||
' "Build with terminal inspector (dcimgui) support.",\n'
|
||||
' ) orelse true;',
|
||||
),
|
||||
# Add to addOptions after custom_shaders (added by 0006)
|
||||
(
|
||||
' step.addOption(bool, "custom_shaders", self.custom_shaders);',
|
||||
' step.addOption(bool, "custom_shaders", self.custom_shaders);\n'
|
||||
' step.addOption(bool, "inspector", self.inspector);',
|
||||
),
|
||||
])
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# 2. SharedDeps.zig — gate dcimgui linking on inspector flag
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
patch_file("src/build/SharedDeps.zig", [
|
||||
(
|
||||
' // cimgui\n'
|
||||
' if (b.lazyDependency("dcimgui", .{',
|
||||
' // cimgui — only needed for inspector\n'
|
||||
' if (self.config.inspector) if (b.lazyDependency("dcimgui", .{',
|
||||
),
|
||||
# Close the extra if — find the end of the dcimgui block
|
||||
(
|
||||
' );\n'
|
||||
' }\n'
|
||||
'\n'
|
||||
' // Fonts',
|
||||
' );\n'
|
||||
' };\n'
|
||||
'\n'
|
||||
' // Fonts',
|
||||
),
|
||||
])
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# 3. build_config.zig — re-export inspector flag
|
||||
# (runs AFTER 0006, so custom_shaders line already exists)
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
patch_file("src/build_config.zig", [
|
||||
(
|
||||
'pub const custom_shaders = options.custom_shaders;',
|
||||
'pub const custom_shaders = options.custom_shaders;\n'
|
||||
'pub const inspector = options.inspector;',
|
||||
),
|
||||
])
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# 4. inspector/main.zig — COMPLETE FILE REPLACEMENT with stub module
|
||||
#
|
||||
# This is the key stability improvement. Instead of pattern-matching
|
||||
# downstream files, we replace the single chokepoint module with
|
||||
# stubs that satisfy all downstream type/method requirements.
|
||||
# When inspector=true, everything re-exports as before.
|
||||
# When inspector=false, stub types with no-op methods are provided
|
||||
# so Surface.zig, renderer, termio all compile without modification.
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
inspector_main = source_dir / "src/inspector/main.zig"
|
||||
inspector_main.write_text('''\
|
||||
const build_config = @import("../build_config.zig");
|
||||
const std = @import("std");
|
||||
const terminal = @import("../terminal/main.zig");
|
||||
const input = @import("../input.zig");
|
||||
const renderer = @import("../renderer.zig");
|
||||
|
||||
pub const widgets = if (build_config.inspector) @import("widgets.zig") else struct {
|
||||
pub const key = struct {
|
||||
pub const Event = StubKeyEvent;
|
||||
};
|
||||
pub const renderer = struct {
|
||||
pub const Info = StubRendererInfo;
|
||||
};
|
||||
pub const surface = struct {
|
||||
pub const Mouse = StubMouse;
|
||||
};
|
||||
};
|
||||
|
||||
pub const Inspector = if (build_config.inspector) @import("Inspector.zig") else StubInspector;
|
||||
pub const KeyEvent = widgets.key.Event;
|
||||
|
||||
/// Stub types — these satisfy all downstream method/field accesses
|
||||
/// when inspector is disabled, so no other files need patching.
|
||||
|
||||
const StubMouse = struct {
|
||||
last_xpos: f64 = 0,
|
||||
last_ypos: f64 = 0,
|
||||
last_point: ?terminal.Pin = null,
|
||||
};
|
||||
|
||||
const StubRendererInfo = struct {
|
||||
pub const empty: @This() = .{};
|
||||
|
||||
pub fn deinit(_: *@This(), _: std.mem.Allocator) void {}
|
||||
|
||||
pub fn overlayFeatures(
|
||||
_: *const @This(),
|
||||
_: std.mem.Allocator,
|
||||
) std.mem.Allocator.Error![]renderer.Overlay.Feature {
|
||||
return &.{};
|
||||
}
|
||||
};
|
||||
|
||||
const StubKeyEvent = struct {
|
||||
event: input.KeyEvent = undefined,
|
||||
binding: []const input.Binding.Action = &.{},
|
||||
pty: []const u8 = "",
|
||||
|
||||
pub fn deinit(_: *const @This(), _: std.mem.Allocator) void {}
|
||||
};
|
||||
|
||||
const StubInspector = struct {
|
||||
mouse: StubMouse = .{},
|
||||
|
||||
pub fn setup() void {}
|
||||
pub fn init(_: std.mem.Allocator) !@This() { return .{}; }
|
||||
pub fn deinit(_: *@This(), _: std.mem.Allocator) void {}
|
||||
pub fn render(_: *@This(), _: anytype) void {}
|
||||
|
||||
pub fn rendererInfo(_: *@This()) *StubRendererInfo {
|
||||
return &stub_renderer_info;
|
||||
}
|
||||
|
||||
pub fn recordKeyEvent(
|
||||
_: *@This(),
|
||||
_: std.mem.Allocator,
|
||||
_: StubKeyEvent,
|
||||
) std.mem.Allocator.Error!void {}
|
||||
|
||||
pub fn recordPtyRead(
|
||||
_: *@This(),
|
||||
_: std.mem.Allocator,
|
||||
_: *terminal.Terminal,
|
||||
_: []const u8,
|
||||
) !void {}
|
||||
|
||||
var stub_renderer_info: StubRendererInfo = .{};
|
||||
};
|
||||
|
||||
test {
|
||||
if (build_config.inspector) {
|
||||
@import("std").testing.refAllDecls(@This());
|
||||
}
|
||||
}
|
||||
''')
|
||||
print("[+] replaced src/inspector/main.zig with stub module")
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# 5. input/key.zig — gate dcimgui import + imguiKey method
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
patch_file("src/input/key.zig", [
|
||||
(
|
||||
'const cimgui = @import("dcimgui");',
|
||||
'const _build_config = @import("../build_config.zig");\n'
|
||||
'const cimgui = if (_build_config.inspector) @import("dcimgui") else struct {};',
|
||||
),
|
||||
(
|
||||
' pub fn imguiKey(self: Key) ?c_int {',
|
||||
' pub fn imguiKey(self: Key) ?c_int {\n'
|
||||
' if (comptime !_build_config.inspector) return null;',
|
||||
),
|
||||
])
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# 6. apprt/embedded.zig — gate Inspector struct + CoreInspector import
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
embedded_path = source_dir / "src/apprt/embedded.zig"
|
||||
text = embedded_path.read_text()
|
||||
|
||||
# 6a. Gate CoreInspector import
|
||||
old = 'const CoreInspector = @import("../inspector/main.zig").Inspector;'
|
||||
new = 'const CoreInspector = if (@import("../build_config.zig").inspector) @import("../inspector/main.zig").Inspector else struct {};'
|
||||
assert old in text, f"pattern not found: {old}"
|
||||
text = text.replace(old, new, 1)
|
||||
|
||||
# 6b. Gate the Inspector struct definition using brace-depth counting
|
||||
# Find the struct opening
|
||||
struct_marker = 'pub const Inspector = struct {\n const cimgui = @import("dcimgui");'
|
||||
assert struct_marker in text, f"Inspector struct marker not found"
|
||||
|
||||
struct_start_idx = text.index('pub const Inspector = struct {\n const cimgui = @import("dcimgui");')
|
||||
# Find the opening brace
|
||||
brace_start = text.index('{', struct_start_idx)
|
||||
# Count braces to find the matching close
|
||||
depth = 0
|
||||
i = brace_start
|
||||
while i < len(text):
|
||||
if text[i] == '{':
|
||||
depth += 1
|
||||
elif text[i] == '}':
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
break
|
||||
i += 1
|
||||
# i now points to the closing }
|
||||
# The struct ends with `};` — we want to capture up to and including `}`
|
||||
# but NOT the `;` since the semicolon will come after the else branch
|
||||
struct_end_brace = i + 1 # position after closing }
|
||||
# Check for trailing semicolon to know where to resume after replacement
|
||||
struct_end = struct_end_brace
|
||||
if struct_end < len(text) and text[struct_end] == ';':
|
||||
struct_end += 1 # skip the original ; in the replacement range
|
||||
|
||||
# Extract the original struct content (up to } but not ;)
|
||||
original_struct = text[struct_start_idx:struct_end_brace]
|
||||
|
||||
# Wrap: replace opening with comptime conditional
|
||||
new_struct = original_struct.replace(
|
||||
'pub const Inspector = struct {',
|
||||
'pub const Inspector = if (@import("../build_config.zig").inspector) struct {',
|
||||
1,
|
||||
)
|
||||
|
||||
# Add else branch after closing };
|
||||
stub = """ else struct {
|
||||
surface: *Surface = undefined,
|
||||
pub fn init(_: *Surface) !@This() { return .{}; }
|
||||
pub fn deinit(_: *@This()) void {}
|
||||
};"""
|
||||
new_struct = new_struct + stub
|
||||
|
||||
text = text[:struct_start_idx] + new_struct + text[struct_end:]
|
||||
|
||||
# 6c. Gate initInspector body
|
||||
old_init = """ pub fn initInspector(self: *Surface) !*Inspector {
|
||||
if (self.inspector) |v| return v;
|
||||
|
||||
const alloc = self.app.core_app.alloc;
|
||||
const inspector = try alloc.create(Inspector);
|
||||
errdefer alloc.destroy(inspector);
|
||||
inspector.* = try .init(self);
|
||||
self.inspector = inspector;
|
||||
return inspector;
|
||||
}"""
|
||||
new_init = """ pub fn initInspector(self: *Surface) !*Inspector {
|
||||
if (comptime !@import("../build_config.zig").inspector) return error.InspectorUnavailable;
|
||||
if (self.inspector) |v| return v;
|
||||
|
||||
const alloc = self.app.core_app.alloc;
|
||||
const inspector = try alloc.create(Inspector);
|
||||
errdefer alloc.destroy(inspector);
|
||||
inspector.* = try .init(self);
|
||||
self.inspector = inspector;
|
||||
return inspector;
|
||||
}"""
|
||||
assert old_init in text, "initInspector pattern not found"
|
||||
text = text.replace(old_init, new_init, 1)
|
||||
|
||||
# 6d. Gate freeInspector body
|
||||
old_free = """ pub fn freeInspector(self: *Surface) void {
|
||||
if (self.inspector) |v| {
|
||||
v.deinit();
|
||||
self.app.core_app.alloc.destroy(v);
|
||||
self.inspector = null;
|
||||
}
|
||||
}"""
|
||||
new_free = """ pub fn freeInspector(self: *Surface) void {
|
||||
if (comptime !@import("../build_config.zig").inspector) return;
|
||||
if (self.inspector) |v| {
|
||||
v.deinit();
|
||||
self.app.core_app.alloc.destroy(v);
|
||||
self.inspector = null;
|
||||
}
|
||||
}"""
|
||||
assert old_free in text, "freeInspector pattern not found"
|
||||
text = text.replace(old_free, new_free, 1)
|
||||
|
||||
# 6e. Gate CAPI inspector functions that call methods on Inspector
|
||||
# These functions take *Inspector and call methods that don't exist on the stub struct.
|
||||
# We add comptime early-returns so the method calls are never analyzed.
|
||||
capi_guards = [
|
||||
(' export fn ghostty_inspector_set_size(ptr: *Inspector, w: u32, h: u32) void {\n'
|
||||
' ptr.updateSize(w, h);',
|
||||
' export fn ghostty_inspector_set_size(ptr: *Inspector, w: u32, h: u32) void {\n'
|
||||
' if (comptime !@import("../build_config.zig").inspector) return;\n'
|
||||
' ptr.updateSize(w, h);'),
|
||||
(' export fn ghostty_inspector_set_content_scale(ptr: *Inspector, x: f64, y: f64) void {\n'
|
||||
' ptr.updateContentScale(x, y);',
|
||||
' export fn ghostty_inspector_set_content_scale(ptr: *Inspector, x: f64, y: f64) void {\n'
|
||||
' if (comptime !@import("../build_config.zig").inspector) return;\n'
|
||||
' ptr.updateContentScale(x, y);'),
|
||||
(' ptr.mouseButtonCallback(\n'
|
||||
' action,\n'
|
||||
' button,',
|
||||
' if (comptime !@import("../build_config.zig").inspector) return;\n'
|
||||
' ptr.mouseButtonCallback(\n'
|
||||
' action,\n'
|
||||
' button,'),
|
||||
(' export fn ghostty_inspector_mouse_pos(ptr: *Inspector, x: f64, y: f64) void {\n'
|
||||
' ptr.cursorPosCallback(x, y);',
|
||||
' export fn ghostty_inspector_mouse_pos(ptr: *Inspector, x: f64, y: f64) void {\n'
|
||||
' if (comptime !@import("../build_config.zig").inspector) return;\n'
|
||||
' ptr.cursorPosCallback(x, y);'),
|
||||
(' ptr.scrollCallback(\n'
|
||||
' x,\n'
|
||||
' y,',
|
||||
' if (comptime !@import("../build_config.zig").inspector) return;\n'
|
||||
' ptr.scrollCallback(\n'
|
||||
' x,\n'
|
||||
' y,'),
|
||||
(' ptr.keyCallback(\n'
|
||||
' action,\n'
|
||||
' key,',
|
||||
' if (comptime !@import("../build_config.zig").inspector) return;\n'
|
||||
' ptr.keyCallback(\n'
|
||||
' action,\n'
|
||||
' key,'),
|
||||
(' export fn ghostty_inspector_text(\n'
|
||||
' ptr: *Inspector,\n'
|
||||
' str: [*:0]const u8,\n'
|
||||
' ) void {\n'
|
||||
' ptr.textCallback(std.mem.sliceTo(str, 0));',
|
||||
' export fn ghostty_inspector_text(\n'
|
||||
' ptr: *Inspector,\n'
|
||||
' str: [*:0]const u8,\n'
|
||||
' ) void {\n'
|
||||
' if (comptime !@import("../build_config.zig").inspector) return;\n'
|
||||
' ptr.textCallback(std.mem.sliceTo(str, 0));'),
|
||||
(' export fn ghostty_inspector_set_focus(ptr: *Inspector, focused: bool) void {\n'
|
||||
' ptr.focusCallback(focused);',
|
||||
' export fn ghostty_inspector_set_focus(ptr: *Inspector, focused: bool) void {\n'
|
||||
' if (comptime !@import("../build_config.zig").inspector) return;\n'
|
||||
' ptr.focusCallback(focused);'),
|
||||
(' export fn ghostty_inspector_metal_init(ptr: *Inspector, device: objc.c.id) bool {\n'
|
||||
' return ptr.initMetal(.fromId(device));',
|
||||
' export fn ghostty_inspector_metal_init(ptr: *Inspector, device: objc.c.id) bool {\n'
|
||||
' if (comptime !@import("../build_config.zig").inspector) return false;\n'
|
||||
' return ptr.initMetal(.fromId(device));'),
|
||||
(' return ptr.renderMetal(\n'
|
||||
' .fromId(command_buffer),',
|
||||
' if (comptime !@import("../build_config.zig").inspector) return;\n'
|
||||
' return ptr.renderMetal(\n'
|
||||
' .fromId(command_buffer),'),
|
||||
(' export fn ghostty_inspector_metal_shutdown(ptr: *Inspector) void {\n'
|
||||
' if (ptr.backend) |v| {',
|
||||
' export fn ghostty_inspector_metal_shutdown(ptr: *Inspector) void {\n'
|
||||
' if (comptime !@import("../build_config.zig").inspector) return;\n'
|
||||
' if (ptr.backend) |v| {'),
|
||||
]
|
||||
for old_capi, new_capi in capi_guards:
|
||||
assert old_capi in text, f"CAPI pattern not found: {old_capi[:60]}..."
|
||||
text = text.replace(old_capi, new_capi, 1)
|
||||
|
||||
embedded_path.write_text(text)
|
||||
print("[+] patched apprt/embedded.zig")
|
||||
|
||||
print(f"[+] all inspector patches complete ({marker})")
|
||||
PYEOF
|
||||
|
||||
echo "[+] inspector disable patch applied"
|
||||
111
vendor/libghostty-spm/Patches/ghostty/0008-macos-metal-texture-storage.sh
vendored
Executable file
111
vendor/libghostty-spm/Patches/ghostty/0008-macos-metal-texture-storage.sh
vendored
Executable file
@@ -0,0 +1,111 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SOURCE_DIR="${1:?Usage: $0 <ghostty-source-dir>}"
|
||||
METAL_ZIG="$SOURCE_DIR/src/renderer/Metal.zig"
|
||||
|
||||
if [ ! -f "$METAL_ZIG" ]; then
|
||||
echo "[-] Metal.zig not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if grep -q 'LIBGHOSTTY_SPM_TEXTURE_STORAGE_PATCH' "$METAL_ZIG"; then
|
||||
echo "[+] Metal texture storage already patched"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
python3 - "$METAL_ZIG" <<'PY'
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
path = Path(sys.argv[1])
|
||||
src = path.read_text()
|
||||
|
||||
replacements = [
|
||||
(
|
||||
"""default_storage_mode: mtl.MTLResourceOptions.StorageMode,
|
||||
|
||||
/// The maximum 2D texture width and height supported by the device.
|
||||
""",
|
||||
"""default_storage_mode: mtl.MTLResourceOptions.StorageMode,
|
||||
|
||||
/// The default storage mode to use for MTLTexture resources.
|
||||
default_texture_storage_mode: mtl.MTLResourceOptions.StorageMode,
|
||||
|
||||
/// The maximum 2D texture width and height supported by the device.
|
||||
""",
|
||||
),
|
||||
(
|
||||
""" const max_texture_size = queryMaxTextureSize(device);
|
||||
log.debug(
|
||||
"device properties default_storage_mode={} max_texture_size={}",
|
||||
.{ default_storage_mode, max_texture_size },
|
||||
);
|
||||
""",
|
||||
""" // LIBGHOSTTY_SPM_TEXTURE_STORAGE_PATCH
|
||||
// MTLStorageModeShared is valid for textures on Apple GPUs, while Intel
|
||||
// and AMD macOS GPUs require managed textures even when hasUnifiedMemory is
|
||||
// true. Keep buffer storage unchanged, but choose texture storage from the
|
||||
// Metal GPU family as Apple recommends for CPU-updated textures.
|
||||
const default_texture_storage_mode: mtl.MTLResourceOptions.StorageMode = switch (comptime builtin.os.tag) {
|
||||
.ios => .shared,
|
||||
.macos => if (device.msgSend(
|
||||
bool,
|
||||
objc.sel("supportsFamily:"),
|
||||
.{mtl.MTLGPUFamily.apple1},
|
||||
)) .shared else .managed,
|
||||
else => default_storage_mode,
|
||||
};
|
||||
const max_texture_size = queryMaxTextureSize(device);
|
||||
log.debug(
|
||||
"device properties default_storage_mode={} default_texture_storage_mode={} max_texture_size={}",
|
||||
.{ default_storage_mode, default_texture_storage_mode, max_texture_size },
|
||||
);
|
||||
""",
|
||||
),
|
||||
(
|
||||
""" .default_storage_mode = default_storage_mode,
|
||||
.max_texture_size = max_texture_size,
|
||||
""",
|
||||
""" .default_storage_mode = default_storage_mode,
|
||||
.default_texture_storage_mode = default_texture_storage_mode,
|
||||
.max_texture_size = max_texture_size,
|
||||
""",
|
||||
),
|
||||
]
|
||||
|
||||
for old, new in replacements:
|
||||
if old not in src:
|
||||
print("[-] Metal.zig structure block not found")
|
||||
sys.exit(1)
|
||||
src = src.replace(old, new, 1)
|
||||
|
||||
old_count = src.count(".storage_mode = self.default_storage_mode")
|
||||
if old_count < 4:
|
||||
print("[-] expected texture storage call sites not found")
|
||||
sys.exit(1)
|
||||
|
||||
src = src.replace(
|
||||
".storage_mode = self.default_storage_mode",
|
||||
".storage_mode = self.default_texture_storage_mode",
|
||||
)
|
||||
|
||||
buffer_marker = """pub inline fn bufferOptions(self: Metal) bufferpkg.Options {"""
|
||||
buffer_start = src.find(buffer_marker)
|
||||
if buffer_start == -1:
|
||||
print("[-] bufferOptions not found")
|
||||
sys.exit(1)
|
||||
|
||||
src = (
|
||||
src[:buffer_start]
|
||||
+ src[buffer_start:].replace(
|
||||
".storage_mode = self.default_texture_storage_mode",
|
||||
".storage_mode = self.default_storage_mode",
|
||||
1,
|
||||
)
|
||||
)
|
||||
|
||||
path.write_text(src)
|
||||
print("[+] patched Metal.zig: split buffer and texture storage modes")
|
||||
PY
|
||||
95
vendor/libghostty-spm/Patches/ghostty/0009-libcxx-apple-availability.sh
vendored
Executable file
95
vendor/libghostty-spm/Patches/ghostty/0009-libcxx-apple-availability.sh
vendored
Executable file
@@ -0,0 +1,95 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SOURCE_DIR="${1:?Usage: $0 <ghostty-source-dir>}"
|
||||
|
||||
# Zig's bundled libc++ headers ship with Apple vendor availability annotations
|
||||
# disabled, so C++ code references runtime symbols that the Apple system
|
||||
# libc++.1.dylib may not export at our deployment floors (e.g.
|
||||
# std::__1::__libcpp_verbose_abort, exported only since iOS 16.3 /
|
||||
# macOS 13.3 / tvOS 16.3). Forcing the annotations on makes libc++ headers
|
||||
# degrade gracefully below those floors (exactly like Apple SDK clang) and
|
||||
# turns any hard dependency on a too-new symbol into a compile error instead
|
||||
# of a dyld crash at app launch.
|
||||
#
|
||||
# -Wno-macro-redefined: zig predefines the macro to 0 on its own command
|
||||
# line; our -D override redefines it.
|
||||
|
||||
# Patch 1: highway flags (hwy/abort.cc et al reference __libcpp_verbose_abort
|
||||
# through the -fno-exceptions throw helpers)
|
||||
HIGHWAY_BUILD="${SOURCE_DIR}/pkg/highway/build.zig"
|
||||
if [ ! -f "$HIGHWAY_BUILD" ]; then
|
||||
echo "[-] missing: $HIGHWAY_BUILD; upstream changed, update this patch"
|
||||
exit 1
|
||||
fi
|
||||
if ! grep -q '_LIBCPP_HAS_VENDOR_AVAILABILITY_ANNOTATIONS' "$HIGHWAY_BUILD"; then
|
||||
perl -0pi -e 's/try flags\.appendSlice\(b\.allocator, &\.\{\n/try flags.appendSlice(b.allocator, &.{\n "-D_LIBCPP_HAS_VENDOR_AVAILABILITY_ANNOTATIONS=1",\n "-Wno-macro-redefined",\n/' "$HIGHWAY_BUILD"
|
||||
grep -q '_LIBCPP_HAS_VENDOR_AVAILABILITY_ANNOTATIONS' "$HIGHWAY_BUILD" || {
|
||||
echo "[-] highway flags block not found; upstream changed, update this patch"
|
||||
exit 1
|
||||
}
|
||||
echo "[+] patched: highway libc++ availability annotations"
|
||||
else
|
||||
echo "[+] highway libc++ availability already patched"
|
||||
fi
|
||||
|
||||
# Patch 2: simdutf flags
|
||||
SIMDUTF_BUILD="${SOURCE_DIR}/pkg/simdutf/build.zig"
|
||||
if [ ! -f "$SIMDUTF_BUILD" ]; then
|
||||
echo "[-] missing: $SIMDUTF_BUILD; upstream changed, update this patch"
|
||||
exit 1
|
||||
fi
|
||||
if ! grep -q '_LIBCPP_HAS_VENDOR_AVAILABILITY_ANNOTATIONS' "$SIMDUTF_BUILD"; then
|
||||
perl -0pi -e 's/try flags\.appendSlice\(b\.allocator, &\.\{\n/try flags.appendSlice(b.allocator, &.{\n "-D_LIBCPP_HAS_VENDOR_AVAILABILITY_ANNOTATIONS=1",\n "-Wno-macro-redefined",\n/' "$SIMDUTF_BUILD"
|
||||
grep -q '_LIBCPP_HAS_VENDOR_AVAILABILITY_ANNOTATIONS' "$SIMDUTF_BUILD" || {
|
||||
echo "[-] simdutf flags block not found; upstream changed, update this patch"
|
||||
exit 1
|
||||
}
|
||||
echo "[+] patched: simdutf libc++ availability annotations"
|
||||
else
|
||||
echo "[+] simdutf libc++ availability already patched"
|
||||
fi
|
||||
|
||||
# Patch 3: ghostty's own C++ SIMD sources (src/simd/*.cpp)
|
||||
SHARED_DEPS="${SOURCE_DIR}/src/build/SharedDeps.zig"
|
||||
if [ ! -f "$SHARED_DEPS" ]; then
|
||||
echo "[-] missing: $SHARED_DEPS; upstream changed, update this patch"
|
||||
exit 1
|
||||
fi
|
||||
if grep -q 'HWY_NO_LIBCXX' "$SHARED_DEPS" &&
|
||||
grep -q 'SIMDUTF_NO_LIBCXX' "$SHARED_DEPS"; then
|
||||
echo "[+] src/simd already builds without libc++"
|
||||
elif ! grep -q '_LIBCPP_HAS_VENDOR_AVAILABILITY_ANNOTATIONS' "$SHARED_DEPS"; then
|
||||
python3 - "$SHARED_DEPS" <<'PY'
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
path = Path(sys.argv[1])
|
||||
text = path.read_text()
|
||||
|
||||
old_flags = """ .flags = if (target.result.cpu.arch == .x86_64) &.{
|
||||
b.fmt("-DHWY_DISABLED_TARGETS={}", .{HWY_DISABLED_TARGETS}),
|
||||
} else &.{},"""
|
||||
|
||||
new_flags = """ .flags = if (target.result.cpu.arch == .x86_64) &.{
|
||||
"-D_LIBCPP_HAS_VENDOR_AVAILABILITY_ANNOTATIONS=1",
|
||||
"-Wno-macro-redefined",
|
||||
b.fmt("-DHWY_DISABLED_TARGETS={}", .{HWY_DISABLED_TARGETS}),
|
||||
} else &.{
|
||||
"-D_LIBCPP_HAS_VENDOR_AVAILABILITY_ANNOTATIONS=1",
|
||||
"-Wno-macro-redefined",
|
||||
},"""
|
||||
|
||||
if old_flags not in text:
|
||||
print("[-] src/simd flags block not found; upstream changed, update this patch")
|
||||
sys.exit(1)
|
||||
|
||||
path.write_text(text.replace(old_flags, new_flags))
|
||||
print("[+] patched: src/simd libc++ availability annotations")
|
||||
PY
|
||||
else
|
||||
echo "[+] src/simd libc++ availability already patched"
|
||||
fi
|
||||
|
||||
echo "[+] all libcxx-apple-availability patches applied"
|
||||
22
vendor/libghostty-spm/Patches/ghostty/0010-fix-scroll-remainder-zeroing.patch
vendored
Normal file
22
vendor/libghostty-spm/Patches/ghostty/0010-fix-scroll-remainder-zeroing.patch
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
diff --git a/src/Surface.zig b/src/Surface.zig
|
||||
index 4d66622..c0fe19e 100644
|
||||
--- a/src/Surface.zig
|
||||
+++ b/src/Surface.zig
|
||||
@@ -3430,7 +3430,7 @@ pub fn scrollCallback(
|
||||
}
|
||||
|
||||
// We scroll by the number of rows in the offset and save the remainder
|
||||
- const amount = poff / cell_size;
|
||||
+ const amount = @trunc(poff / cell_size);
|
||||
assert(@abs(amount) >= 1);
|
||||
self.mouse.pending_scroll_y = poff - (amount * cell_size);
|
||||
|
||||
@@ -3455,7 +3455,7 @@ pub fn scrollCallback(
|
||||
break :x .{};
|
||||
}
|
||||
|
||||
- const amount = poff / cell_size;
|
||||
+ const amount = @trunc(poff / cell_size);
|
||||
assert(@abs(amount) >= 1);
|
||||
self.mouse.pending_scroll_x = poff - (amount * cell_size);
|
||||
const delta: isize = @intFromFloat(@trunc(amount));
|
||||
25
vendor/libghostty-spm/Patches/ghostty/README.md
vendored
Normal file
25
vendor/libghostty-spm/Patches/ghostty/README.md
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
# Ghostty Patches
|
||||
|
||||
This directory is the single place for local upstream Ghostty patches used by
|
||||
the `libghostty-spm` build pipeline.
|
||||
|
||||
## Rules
|
||||
|
||||
- Keep patches numbered so they apply in a stable order.
|
||||
- Prefer standard unified diff files (`.patch`) when the upstream context is
|
||||
stable.
|
||||
- Use executable patch scripts (`.sh`) only when upstream context is too
|
||||
unstable for a reliable diff.
|
||||
- Keep version-specific variants beside the original patch and select them in
|
||||
`Script/apply-patches.sh` using an upstream API marker.
|
||||
- Preserve newer Ghostty's renamed internal-library outputs when extending its
|
||||
Darwin static-library build path.
|
||||
- Every patch in this directory must be safe to re-run.
|
||||
- Patches here are applied automatically by `Script/build-ghostty.sh`, so they
|
||||
affect macOS, iOS, and Mac Catalyst builds equally.
|
||||
|
||||
## Current goal
|
||||
|
||||
This patch workflow exists so we can carry host-managed IO work required for
|
||||
sandboxed iOS, macOS, and Mac Catalyst integration without hiding upstream
|
||||
modifications inside ad-hoc build script edits.
|
||||
Reference in New Issue
Block a user