From 9818543ebee1e74ea9f8c32d9e08beec72f54543 Mon Sep 17 00:00:00 2001 From: Hojin You Date: Wed, 9 Jul 2025 16:27:22 -0400 Subject: [PATCH 01/46] Update Korean Translations --- po/ko_KR.UTF-8.po | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/po/ko_KR.UTF-8.po b/po/ko_KR.UTF-8.po index 9aa4aad5e..3482453ed 100644 --- a/po/ko_KR.UTF-8.po +++ b/po/ko_KR.UTF-8.po @@ -8,8 +8,8 @@ msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" "POT-Creation-Date: 2025-07-08 15:13-0500\n" -"PO-Revision-Date: 2025-03-31 03:08+0200\n" -"Last-Translator: Ruben Engelbrecht \n" +"PO-Revision-Date: 2025-07-09 16:11-0400\n" +"Last-Translator: Hojin You \n" "Language-Team: Korean \n" "Language: ko\n" "MIME-Version: 1.0\n" @@ -87,7 +87,7 @@ msgstr "오른쪽으로 창 나누기" #: src/apprt/gtk/ui/1.5/command-palette.blp:16 msgid "Execute a command…" -msgstr "" +msgstr "명령을 실행하세요…" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 @@ -160,7 +160,7 @@ msgstr "설정 열기" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 msgid "Command Palette" -msgstr "" +msgstr "명령 팔레트" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 msgid "Terminal Inspector" @@ -208,12 +208,12 @@ msgstr "허용" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 msgid "Remember choice for this split" -msgstr "" +msgstr "이 분할에 대한 선택 기억하기" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 msgid "Reload configuration to show this prompt again" -msgstr "" +msgstr "이 프롬프트를 다시 표시하려면 구성을 다시 로드하십시오." #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 #: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 @@ -285,9 +285,8 @@ msgid "View Open Tabs" msgstr "열린 탭 보기" #: src/apprt/gtk/Window.zig:266 -#, fuzzy msgid "New Split" -msgstr "나누기" +msgstr "새 분할" #: src/apprt/gtk/Window.zig:329 msgid "" From f5f2a4dd20642d7ca1d3f380349eb83762f1eb7e Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Wed, 9 Jul 2025 17:25:34 -0400 Subject: [PATCH 02/46] shell-integration: use $GHOSTTY_BIN_DIR/ghostty Locate our ghostty binary using $GHOSTTY_BIN_DIR rather than searching the PATH. --- src/shell-integration/bash/ghostty.bash | 4 ++-- .../fish/vendor_conf.d/ghostty-shell-integration.fish | 10 +++++----- src/shell-integration/zsh/ghostty-integration | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/shell-integration/bash/ghostty.bash b/src/shell-integration/bash/ghostty.bash index 5b338b11e..aacf37c3a 100644 --- a/src/shell-integration/bash/ghostty.bash +++ b/src/shell-integration/bash/ghostty.bash @@ -124,7 +124,7 @@ if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-* ]]; then builtin local ssh_target="${ssh_user}@${ssh_hostname}" # Check if terminfo is already cached - if ghostty +ssh-cache --host="$ssh_target" >/dev/null 2>&1; then + if "$GHOSTTY_BIN_DIR/ghostty" +ssh-cache --host="$ssh_target" >/dev/null 2>&1; then ssh_term="xterm-ghostty" elif builtin command -v infocmp >/dev/null 2>&1; then builtin local ssh_terminfo ssh_cpath_dir ssh_cpath @@ -147,7 +147,7 @@ if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-* ]]; then ssh_opts+=(-o "ControlPath=$ssh_cpath") # Cache successful installation - ghostty +ssh-cache --add="$ssh_target" >/dev/null 2>&1 || true + "$GHOSTTY_BIN_DIR/ghostty" +ssh-cache --add="$ssh_target" >/dev/null 2>&1 || true else builtin echo "Warning: Failed to install terminfo." >&2 fi diff --git a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish index 5381f834b..834f0ef10 100644 --- a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish +++ b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish @@ -120,11 +120,11 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" end end - set -l ssh_target "$ssh_user@$ssh_hostname" - if test -n "$ssh_hostname" + set -l ssh_target "$ssh_user@$ssh_hostname" + # Check if terminfo is already cached - if command -q ghostty; and ghostty +ssh-cache --host="$ssh_target" >/dev/null 2>&1 + if test -x "$GHOSTTY_BIN_DIR/ghostty"; and "$GHOSTTY_BIN_DIR/ghostty" +ssh-cache --host="$ssh_target" >/dev/null 2>&1 set ssh_term "xterm-ghostty" else if command -q infocmp set -l ssh_terminfo @@ -149,8 +149,8 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" set -a ssh_opts -o "ControlPath=$ssh_cpath" # Cache successful installation - if command -q ghostty - ghostty +ssh-cache --add="$ssh_target" >/dev/null 2>&1; or true + if test -x "$GHOSTTY_BIN_DIR/ghostty" + "$GHOSTTY_BIN_DIR/ghostty" +ssh-cache --add="$ssh_target" >/dev/null 2>&1; or true end else echo "Warning: Failed to install terminfo." >&2 diff --git a/src/shell-integration/zsh/ghostty-integration b/src/shell-integration/zsh/ghostty-integration index f3fb46180..8607664a2 100644 --- a/src/shell-integration/zsh/ghostty-integration +++ b/src/shell-integration/zsh/ghostty-integration @@ -276,7 +276,7 @@ _ghostty_deferred_init() { local ssh_target="${ssh_user}@${ssh_hostname}" # Check if terminfo is already cached - if (( $+commands[ghostty] )) && ghostty +ssh-cache --host="$ssh_target" >/dev/null 2>&1; then + if "$GHOSTTY_BIN_DIR/ghostty" +ssh-cache --host="$ssh_target" >/dev/null 2>&1; then ssh_term="xterm-ghostty" elif (( $+commands[infocmp] )); then local ssh_terminfo ssh_cpath_dir ssh_cpath @@ -299,7 +299,7 @@ _ghostty_deferred_init() { ssh_opts+=(-o "ControlPath=$ssh_cpath") # Cache successful installation - ghostty +ssh-cache --add="$ssh_target" >/dev/null 2>&1 || true + "$GHOSTTY_BIN_DIR/ghostty" +ssh-cache --add="$ssh_target" >/dev/null 2>&1 || true else print "Warning: Failed to install terminfo." >&2 fi From 8506637ae614237a39f3a167e94bb998979ffb25 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 7 Jul 2025 14:14:28 -0700 Subject: [PATCH 03/46] macos: add signpost API --- pkg/macos/build.zig | 7 +- pkg/macos/main.zig | 1 + pkg/macos/os.zig | 1 + pkg/macos/os/signpost.zig | 161 ++++++++++++++++++++++++ pkg/macos/os/{zig_log.c => zig_macos.c} | 1 + 5 files changed, 166 insertions(+), 5 deletions(-) create mode 100644 pkg/macos/os/signpost.zig rename pkg/macos/os/{zig_log.c => zig_macos.c} (90%) diff --git a/pkg/macos/build.zig b/pkg/macos/build.zig index 3e0a97d1a..ddbeb82c9 100644 --- a/pkg/macos/build.zig +++ b/pkg/macos/build.zig @@ -18,15 +18,12 @@ pub fn build(b: *std.Build) !void { .optimize = optimize, }); - var flags = std.ArrayList([]const u8).init(b.allocator); - defer flags.deinit(); lib.addCSourceFile(.{ - .file = b.path("os/zig_log.c"), - .flags = flags.items, + .file = b.path("os/zig_macos.c"), + .flags = &.{"-std=c99"}, }); lib.addCSourceFile(.{ .file = b.path("text/ext.c"), - .flags = flags.items, }); lib.linkFramework("CoreFoundation"); lib.linkFramework("CoreGraphics"); diff --git a/pkg/macos/main.zig b/pkg/macos/main.zig index 42253ba48..e4f4b9504 100644 --- a/pkg/macos/main.zig +++ b/pkg/macos/main.zig @@ -23,6 +23,7 @@ pub const c = @cImport({ @cInclude("IOSurface/IOSurfaceRef.h"); @cInclude("dispatch/dispatch.h"); @cInclude("os/log.h"); + @cInclude("os/signpost.h"); if (builtin.os.tag == .macos) { @cInclude("Carbon/Carbon.h"); diff --git a/pkg/macos/os.zig b/pkg/macos/os.zig index 183913bac..9716a9abc 100644 --- a/pkg/macos/os.zig +++ b/pkg/macos/os.zig @@ -1,6 +1,7 @@ const log = @import("os/log.zig"); pub const c = @import("os/c.zig"); +pub const signpost = @import("os/signpost.zig"); pub const Log = log.Log; pub const LogType = log.LogType; diff --git a/pkg/macos/os/signpost.zig b/pkg/macos/os/signpost.zig new file mode 100644 index 000000000..9fef584e4 --- /dev/null +++ b/pkg/macos/os/signpost.zig @@ -0,0 +1,161 @@ +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const c = @import("c.zig").c; +const logpkg = @import("log.zig"); +const Log = logpkg.Log; + +/// Checks whether signpost logging is enabled for the given log handle. +/// Returns true if signposts will be recorded for this log, false otherwise. +/// This can be used to avoid expensive operations when signpost logging is disabled. +/// +/// https://developer.apple.com/documentation/os/os_signpost_enabled?language=objc +pub fn enabled(log: *Log) bool { + return c.os_signpost_enabled(@ptrCast(log)); +} + +/// Emits a signpost event - a single point in time marker. +/// Events are useful for marking when specific actions occur, such as +/// user interactions, state changes, or other discrete occurrences. +/// The event will appear as a vertical line in Instruments. +/// +/// https://developer.apple.com/documentation/os/os_signpost_event_emit?language=objc +pub fn emitEvent( + log: *Log, + id: Id, + comptime name: [:0]const u8, +) void { + emitWithName(log, id, .event, name); +} + +/// Marks the beginning of a time interval. +/// Use this with intervalEnd to measure the duration of operations. +/// The same ID must be used for both the begin and end calls. +/// Intervals appear as horizontal bars in Instruments timeline. +/// +/// https://developer.apple.com/documentation/os/os_signpost_interval_begin?language=objc +pub fn intervalBegin(log: *Log, id: Id, comptime name: [:0]const u8) void { + emitWithName(log, id, .interval_begin, name); +} + +/// Marks the end of a time interval. +/// Must be paired with a prior intervalBegin call using the same ID. +/// The name should match the name used in intervalBegin. +/// Instruments will calculate and display the duration between begin and end. +/// +/// https://developer.apple.com/documentation/os/os_signpost_interval_end?language=objc +pub fn intervalEnd(log: *Log, id: Id, comptime name: [:0]const u8) void { + emitWithName(log, id, .interval_end, name); +} + +extern var __dso_handle: usize; + +/// The internal function to emit a signpost with a specific name. +fn emitWithName( + log: *Log, + id: Id, + typ: Type, + comptime name: [:0]const u8, +) void { + var buf: [64]u8 = @splat(0); + c._os_signpost_emit_with_name_impl( + &__dso_handle, + @ptrCast(log), + @intFromEnum(typ), + @intFromEnum(id), + name.ptr, + null, + &buf, + buf.len, + ); +} + +/// https://developer.apple.com/documentation/os/os_signpost_id_t?language=objc +pub const Id = enum(u64) { + null = 0, // OS_SIGNPOST_ID_NULL + invalid = 0xFFFFFFFFFFFFFFFF, // OS_SIGNPOST_ID_INVALID + exclusive = 0xEEEEB0B5B2B2EEEE, // OS_SIGNPOST_ID_EXCLUSIVE + _, + + /// Generates a new signpost ID for use with signpost operations. + /// The ID is unique for the given log handle and can be used to track + /// asynchronous operations or mark specific points of interest in the code. + /// Returns a unique signpost ID that can be used with os_signpost functions. + /// + /// https://developer.apple.com/documentation/os/os_signpost_id_generate?language=objc + pub fn generate(log: *Log) Id { + return @enumFromInt(c.os_signpost_id_generate(@ptrCast(log))); + } + + /// Creates a signpost ID based on a pointer value. + /// This is useful for tracking operations associated with a specific object + /// or memory location. The same pointer will always generate the same ID + /// for a given log handle, allowing correlation of signpost events. + /// Pass null to get the null signpost ID. + /// + /// https://developer.apple.com/documentation/os/os_signpost_id_for_pointer?language=objc + pub fn forPointer(log: *Log, ptr: ?*anyopaque) Id { + return @enumFromInt(c.os_signpost_id_make_with_pointer( + @ptrCast(log), + @ptrCast(ptr), + )); + } + + test "generate ID" { + // We can't really test the return value because it may return null + // if signposts are disabled. + const id: Id = .generate(Log.create("com.mitchellh.ghostty", "test")); + try std.testing.expect(id != .invalid); + } + + test "generate ID for pointer" { + var foo: usize = 0x1234; + const id: Id = .forPointer(Log.create("com.mitchellh.ghostty", "test"), &foo); + try std.testing.expect(id != .null); + } +}; + +/// https://developer.apple.com/documentation/os/ossignposttype?language=objc +pub const Type = enum(u8) { + event = 0, // OS_SIGNPOST_EVENT + interval_begin = 1, // OS_SIGNPOST_INTERVAL_BEGIN + interval_end = 2, // OS_SIGNPOST_INTERVAL_END + + pub const mask: u8 = 0x03; // OS_SIGNPOST_TYPE_MASK +}; + +/// Special os_log category values that surface in Instruments and other +/// tooling. +pub const Category = struct { + /// Points of Interest appear as a dedicated track in Instruments. + /// Use this for high-level application events that help understand + /// the flow of your application. + pub const points_of_interest: [:0]const u8 = "PointsOfInterest"; + + /// Dynamic Tracing category enables runtime-configurable logging. + /// Signposts in this category can be enabled/disabled dynamically + /// without recompiling. + pub const dynamic_tracing: [:0]const u8 = "DynamicTracking"; + + /// Dynamic Stack Tracing category captures call stacks at signpost + /// events. This provides deeper debugging information but has higher + /// performance overhead. + pub const dynamic_stack_tracing: [:0]const u8 = "DynamicStackTracking"; +}; + +test { + _ = Id; +} + +test enabled { + _ = enabled(Log.create("com.mitchellh.ghostty", "test")); +} + +test "intervals" { + const log = Log.create("com.mitchellh.ghostty", "test"); + defer log.release(); + + // Test that we can begin and end an interval + const id = Id.generate(log); + intervalBegin(log, id, "Test Interval"); +} diff --git a/pkg/macos/os/zig_log.c b/pkg/macos/os/zig_macos.c similarity index 90% rename from pkg/macos/os/zig_log.c rename to pkg/macos/os/zig_macos.c index ef3f616d5..1c4f06982 100644 --- a/pkg/macos/os/zig_log.c +++ b/pkg/macos/os/zig_macos.c @@ -1,4 +1,5 @@ #include +#include // A wrapper so we can use the os_log_with_type macro. void zig_os_log_with_type( From 0e8ccc73529758e2bbdcebcbb0c9835ba1d41f6c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 7 Jul 2025 16:36:17 -0700 Subject: [PATCH 04/46] benchmark: a new package and framework for benchmarking --- src/benchmark/Benchmark.zig | 165 +++++++++++++++++++++++++++++++ src/benchmark/TerminalStream.zig | 97 ++++++++++++++++++ src/benchmark/main.zig | 6 ++ src/main_ghostty.zig | 1 + 4 files changed, 269 insertions(+) create mode 100644 src/benchmark/Benchmark.zig create mode 100644 src/benchmark/TerminalStream.zig create mode 100644 src/benchmark/main.zig diff --git a/src/benchmark/Benchmark.zig b/src/benchmark/Benchmark.zig new file mode 100644 index 000000000..b7d9b6ad3 --- /dev/null +++ b/src/benchmark/Benchmark.zig @@ -0,0 +1,165 @@ +//! A single benchmark case. +const Benchmark = @This(); + +const std = @import("std"); +const builtin = @import("builtin"); +const assert = std.debug.assert; +const macos = @import("macos"); +const build_config = @import("../build_config.zig"); + +ptr: *anyopaque, +vtable: VTable, + +/// Create a new benchmark from a pointer and a vtable. +/// +/// This usually is only called by benchmark implementations, not +/// benchmark users. +pub fn init( + pointer: anytype, + vtable: VTable, +) Benchmark { + const Ptr = @TypeOf(pointer); + assert(@typeInfo(Ptr) == .pointer); // Must be a pointer + assert(@typeInfo(Ptr).pointer.size == .one); // Must be a single-item pointer + assert(@typeInfo(@typeInfo(Ptr).pointer.child) == .@"struct"); // Must point to a struct + return .{ .ptr = pointer, .vtable = vtable }; +} + +/// Run the benchmark. +pub fn run( + self: Benchmark, + mode: RunMode, +) Error!RunResult { + // Run our setup function if it exists. We do this first because + // we don't want this part of our benchmark and we want to fail fast. + if (self.vtable.setupFn) |func| try func(self.ptr); + defer if (self.vtable.teardownFn) |func| func(self.ptr); + + // Our result accumulator. This will be returned at the end of the run. + var result: RunResult = .{}; + + // If we're on macOS, we setup signposts so its easier to find + // the results in Instruments. There's a lot of nasty comptime stuff + // here but its just to ensure this does nothing on other platforms. + const signpost_name = "Ghostty Benchmark"; + const signpost: if (builtin.target.os.tag.isDarwin()) struct { + log: *macos.os.Log, + id: macos.os.signpost.Id, + } else void = if (comptime builtin.os.tag == .macos) macos: { + const log = macos.os.Log.create( + build_config.bundle_id, + macos.os.signpost.Category.points_of_interest, + ); + const id = macos.os.signpost.Id.generate(log); + macos.os.signpost.intervalBegin(log, id, signpost_name); + break :macos .{ .log = log, .id = id }; + } else {}; + defer if (comptime builtin.os.tag == .macos) { + macos.os.signpost.intervalEnd( + signpost.log, + signpost.id, + signpost_name, + ); + signpost.log.release(); + }; + + const start = std.time.Instant.now() catch return error.BenchmarkFailed; + while (true) { + // Run our step function. If it fails, we return the error. + try self.vtable.stepFn(self.ptr); + result.iterations += 1; + + // Get our current monotonic time and check our exit conditions. + const now = std.time.Instant.now() catch return error.BenchmarkFailed; + const exit = switch (mode) { + .once => true, + .duration => |ns| now.since(start) >= ns, + }; + + if (exit) { + result.duration = now.since(start); + return result; + } + } + + // We exit within the loop body. + unreachable; +} + +/// The type of benchmark run. This is used to determine how the benchmark +/// is executed. +pub const RunMode = union(enum) { + /// Run the benchmark exactly once. + once, + + /// Run the benchmark for a fixed duration in nanoseconds. This + /// will not interrupt a running step so if the granularity of the + /// duration is too low, benchmark results may be inaccurate. + duration: u64, +}; + +/// The result of a benchmark run. +pub const RunResult = struct { + /// The total iterations that step was executed. For "once" run + /// modes this will always be 1. + iterations: u32 = 0, + + /// The total time taken for the run. For "duration" run modes + /// this will be relatively close to the requested duration. + /// The units are nanoseconds. + duration: u64 = 0, +}; + +/// The possible errors that can occur during various stages of the +/// benchmark. Right now its just "failure" which ends the benchmark. +pub const Error = error{BenchmarkFailed}; + +/// The vtable that must be provided to invoke the real implementation. +pub const VTable = struct { + /// A single step to execute the benchmark. This should do the work + /// that is under test. This may be called multiple times if we're + /// testing throughput. + stepFn: *const fn (ptr: *anyopaque) Error!void, + + /// Setup and teardown functions. These are called once before + /// the first step and once after the last step. They are not part + /// of the benchmark results (unless you're benchmarking the full + /// binary). + setupFn: ?*const fn (ptr: *anyopaque) Error!void = null, + teardownFn: ?*const fn (ptr: *anyopaque) void = null, +}; + +test Benchmark { + const testing = std.testing; + const Simple = struct { + const Self = @This(); + + setup_i: usize = 0, + step_i: usize = 0, + + pub fn benchmark(self: *Self) Benchmark { + return .init(self, .{ + .stepFn = step, + .setupFn = setup, + }); + } + + fn setup(ptr: *anyopaque) Error!void { + const self: *Self = @ptrCast(@alignCast(ptr)); + self.setup_i += 1; + } + + fn step(ptr: *anyopaque) Error!void { + const self: *Self = @ptrCast(@alignCast(ptr)); + self.step_i += 1; + } + }; + + var s: Simple = .{}; + const b = s.benchmark(); + const result = try b.run(.once); + try testing.expectEqual(1, s.setup_i); + try testing.expectEqual(1, s.step_i); + try testing.expectEqual(1, result.iterations); + try testing.expect(result.duration > 0); +} diff --git a/src/benchmark/TerminalStream.zig b/src/benchmark/TerminalStream.zig new file mode 100644 index 000000000..8580695f2 --- /dev/null +++ b/src/benchmark/TerminalStream.zig @@ -0,0 +1,97 @@ +//! This benchmark tests the performance of the terminal stream +//! handler from input to terminal state update. This is useful to +//! test general throughput of VT parsing and handling. +//! +//! Note that the handler used for this benchmark isn't the full +//! terminal handler, since that requires a significant amount of +//! state. This is a simplified version that only handles specific +//! terminal operations like printing characters. We should expand +//! this to include more operations to improve the accuracy of the +//! benchmark. +//! +//! It is a fairly broad benchmark that can be used to determine +//! if we need to optimize something more specific (e.g. the parser). +const TerminalStream = @This(); + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const terminalpkg = @import("../terminal/main.zig"); +const Benchmark = @import("Benchmark.zig"); +const Terminal = terminalpkg.Terminal; +const Stream = terminalpkg.Stream(*Handler); + +terminal: Terminal, +handler: Handler, +stream: Stream, + +pub const Options = struct { + /// The size of the terminal. This affects benchmarking when + /// dealing with soft line wrapping and the memory impact + /// of page sizes. + @"terminal-rows": u16 = 80, + @"terminal-cols": u16 = 120, +}; + +/// Create a new terminal stream handler for the given arguments. +pub fn create( + alloc: Allocator, + args: Options, +) !*TerminalStream { + const ptr = try alloc.create(TerminalStream); + errdefer alloc.destroy(ptr); + + ptr.* = .{ + .terminal = try .init(alloc, .{ + .rows = args.@"terminal-rows", + .cols = args.@"terminal-cols", + }), + .handler = .{ .t = &ptr.terminal }, + .stream = .{ .handler = &ptr.handler }, + }; + + return ptr; +} + +pub fn destroy(self: *TerminalStream, alloc: Allocator) void { + self.terminal.deinit(alloc); + alloc.destroy(self); +} + +pub fn benchmark(self: *TerminalStream) Benchmark { + return .init(self, .{ + .stepFn = step, + .setupFn = setup, + }); +} + +fn setup(ptr: *anyopaque) Benchmark.Error!void { + const self: *TerminalStream = @ptrCast(@alignCast(ptr)); + self.terminal.fullReset(); +} + +fn step(ptr: *anyopaque) Benchmark.Error!void { + const self: *TerminalStream = @ptrCast(@alignCast(ptr)); + _ = self; +} + +/// Implements the handler interface for the terminal.Stream. +/// We should expand this to include more operations to make +/// our benchmark more realistic. +const Handler = struct { + t: *Terminal, + + pub fn print(self: *Handler, cp: u21) !void { + try self.t.print(cp); + } +}; + +test TerminalStream { + const testing = std.testing; + const alloc = testing.allocator; + + const impl: *TerminalStream = try .create(alloc, .{}); + defer impl.destroy(alloc); + + const bench = impl.benchmark(); + _ = try bench.run(.once); +} diff --git a/src/benchmark/main.zig b/src/benchmark/main.zig new file mode 100644 index 000000000..802519e29 --- /dev/null +++ b/src/benchmark/main.zig @@ -0,0 +1,6 @@ +pub const Benchmark = @import("Benchmark.zig"); +pub const TerminalStream = @import("TerminalStream.zig"); + +test { + _ = @import("std").testing.refAllDecls(@This()); +} diff --git a/src/main_ghostty.zig b/src/main_ghostty.zig index b747fe6f0..fb29303f1 100644 --- a/src/main_ghostty.zig +++ b/src/main_ghostty.zig @@ -182,6 +182,7 @@ test { _ = @import("surface_mouse.zig"); // Libraries + _ = @import("benchmark/main.zig"); _ = @import("crash/main.zig"); _ = @import("datastruct/main.zig"); _ = @import("inspector/main.zig"); From 1739418f6f6fab3bb7df9c2c84eba91ddabe91b2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 8 Jul 2025 08:55:34 -0700 Subject: [PATCH 05/46] cli: make the action parser (+foo) generic and reusable --- src/build/bash_completions.zig | 2 +- src/build/fish_completions.zig | 2 +- src/build/mdgen/mdgen.zig | 2 +- src/build/zsh_completions.zig | 2 +- src/cli.zig | 3 +- src/cli/action.zig | 511 +++++++++++++++------------------ src/cli/boo.zig | 2 +- src/cli/crash_report.zig | 2 +- src/cli/edit_config.zig | 2 +- src/cli/ghostty.zig | 290 +++++++++++++++++++ src/cli/help.zig | 2 +- src/cli/list_actions.zig | 2 +- src/cli/list_colors.zig | 2 +- src/cli/list_fonts.zig | 2 +- src/cli/list_keybinds.zig | 2 +- src/cli/list_themes.zig | 2 +- src/cli/show_config.zig | 2 +- src/cli/show_face.zig | 2 +- src/cli/ssh_cache.zig | 2 +- src/cli/validate_config.zig | 2 +- src/global.zig | 7 +- src/helpgen.zig | 2 +- 22 files changed, 549 insertions(+), 298 deletions(-) create mode 100644 src/cli/ghostty.zig diff --git a/src/build/bash_completions.zig b/src/build/bash_completions.zig index ad62ff97d..536cadbc4 100644 --- a/src/build/bash_completions.zig +++ b/src/build/bash_completions.zig @@ -1,7 +1,7 @@ const std = @import("std"); const Config = @import("../config/Config.zig"); -const Action = @import("../cli/action.zig").Action; +const Action = @import("../cli.zig").ghostty.Action; /// A bash completions configuration that contains all the available commands /// and options. diff --git a/src/build/fish_completions.zig b/src/build/fish_completions.zig index 2b2563ee7..0b6c45e1f 100644 --- a/src/build/fish_completions.zig +++ b/src/build/fish_completions.zig @@ -1,7 +1,7 @@ const std = @import("std"); const Config = @import("../config/Config.zig"); -const Action = @import("../cli/action.zig").Action; +const Action = @import("../cli.zig").ghostty.Action; /// A fish completions configuration that contains all the available commands /// and options. diff --git a/src/build/mdgen/mdgen.zig b/src/build/mdgen/mdgen.zig index e7d966323..53ed02067 100644 --- a/src/build/mdgen/mdgen.zig +++ b/src/build/mdgen/mdgen.zig @@ -2,7 +2,7 @@ const std = @import("std"); const help_strings = @import("help_strings"); const build_config = @import("../../build_config.zig"); const Config = @import("../../config/Config.zig"); -const Action = @import("../../cli/action.zig").Action; +const Action = @import("../../cli/ghostty.zig").Action; const KeybindAction = @import("../../input/Binding.zig").Action; pub fn substitute(alloc: std.mem.Allocator, input: []const u8, writer: anytype) !void { diff --git a/src/build/zsh_completions.zig b/src/build/zsh_completions.zig index 2ded6d73c..6bddcd285 100644 --- a/src/build/zsh_completions.zig +++ b/src/build/zsh_completions.zig @@ -1,7 +1,7 @@ const std = @import("std"); const Config = @import("../config/Config.zig"); -const Action = @import("../cli/action.zig").Action; +const Action = @import("../cli.zig").ghostty.Action; /// A zsh completions configuration that contains all the available commands /// and options. diff --git a/src/cli.zig b/src/cli.zig index 151e6e648..008ff1ebf 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -1,7 +1,8 @@ const diags = @import("cli/diagnostics.zig"); pub const args = @import("cli/args.zig"); -pub const Action = @import("cli/action.zig").Action; +pub const action = @import("cli/action.zig"); +pub const ghostty = @import("cli/ghostty.zig"); pub const CompatibilityHandler = args.CompatibilityHandler; pub const compatibilityRenamed = args.compatibilityRenamed; pub const DiagnosticList = diags.DiagnosticList; diff --git a/src/cli/action.zig b/src/cli/action.zig index 728f36efe..41173a9f1 100644 --- a/src/cli/action.zig +++ b/src/cli/action.zig @@ -1,320 +1,277 @@ const std = @import("std"); const Allocator = std.mem.Allocator; -const help_strings = @import("help_strings"); -const list_fonts = @import("list_fonts.zig"); -const help = @import("help.zig"); -const version = @import("version.zig"); -const list_keybinds = @import("list_keybinds.zig"); -const list_themes = @import("list_themes.zig"); -const list_colors = @import("list_colors.zig"); -const list_actions = @import("list_actions.zig"); -const ssh_cache = @import("ssh_cache.zig"); -const edit_config = @import("edit_config.zig"); -const show_config = @import("show_config.zig"); -const validate_config = @import("validate_config.zig"); -const crash_report = @import("crash_report.zig"); -const show_face = @import("show_face.zig"); -const boo = @import("boo.zig"); +pub const DetectError = error{ + /// Multiple actions were detected. You can specify at most one + /// action on the CLI otherwise the behavior desired is ambiguous. + MultipleActions, -/// Special commands that can be invoked via CLI flags. These are all -/// invoked by using `+` as a CLI flag. The only exception is -/// "version" which can be invoked additionally with `--version`. -pub const Action = enum { - /// Output the version and exit - version, - - /// Output help information for the CLI or configuration - help, - - /// List available fonts - @"list-fonts", - - /// List available keybinds - @"list-keybinds", - - /// List available themes - @"list-themes", - - /// List named RGB colors - @"list-colors", - - /// List keybind actions - @"list-actions", - - /// Manage SSH terminfo cache for automatic remote host setup - @"ssh-cache", - - /// Edit the config file in the configured terminal editor. - @"edit-config", - - /// Dump the config to stdout - @"show-config", - - // Validate passed config file - @"validate-config", - - // Show which font face Ghostty loads a codepoint from. - @"show-face", - - // List, (eventually) view, and (eventually) send crash reports. - @"crash-report", - - // Boo! - boo, - - pub const Error = error{ - /// Multiple actions were detected. You can specify at most one - /// action on the CLI otherwise the behavior desired is ambiguous. - MultipleActions, - - /// An unknown action was specified. - InvalidAction, - }; - - /// This should be returned by actions that want to print the help text. - pub const help_error = error.ActionHelpRequested; - - /// Detect the action from CLI args. - pub fn detectCLI(alloc: Allocator) !?Action { - var iter = try std.process.argsWithAllocator(alloc); - defer iter.deinit(); - return try detectIter(&iter); - } - - /// Detect the action from any iterator, used primarily for tests. - pub fn detectIter(iter: anytype) Error!?Action { - var pending_help: bool = false; - var pending: ?Action = null; - while (iter.next()) |arg| { - // If we see a "-e" and we haven't seen a command yet, then - // we are done looking for commands. This special case enables - // `ghostty -e ghostty +command`. If we've seen a command we - // still want to keep looking because - // `ghostty +command -e +command` is invalid. - if (std.mem.eql(u8, arg, "-e") and pending == null) return null; - - // Special case, --version always outputs the version no - // matter what, no matter what other args exist. - if (std.mem.eql(u8, arg, "--version")) return .version; - - // --help matches "help" but if a subcommand is specified - // then we match the subcommand. - if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) { - pending_help = true; - continue; - } - - // Commands must start with "+" - if (arg.len == 0 or arg[0] != '+') continue; - if (pending != null) return Error.MultipleActions; - pending = std.meta.stringToEnum(Action, arg[1..]) orelse return Error.InvalidAction; - } - - // If we have an action, we always return that action, even if we've - // seen "--help" or "-h" because the action may have its own help text. - if (pending != null) return pending; - - // If we've seen "--help" or "-h" then we return the help action. - if (pending_help) return .help; - - return pending; - } - - /// Run the action. This returns the exit code to exit with. - pub fn run(self: Action, alloc: Allocator) !u8 { - return self.runMain(alloc) catch |err| switch (err) { - // If help is requested, then we use some comptime trickery - // to find this action in the help strings and output that. - help_error => err: { - inline for (@typeInfo(Action).@"enum".fields) |field| { - // Future note: for now we just output the help text directly - // to stdout. In the future we can style this much prettier - // for all commands by just changing this one place. - - if (std.mem.eql(u8, field.name, @tagName(self))) { - const stdout = std.io.getStdOut().writer(); - const text = @field(help_strings.Action, field.name) ++ "\n"; - stdout.writeAll(text) catch |write_err| { - std.log.warn("failed to write help text: {}\n", .{write_err}); - break :err 1; - }; - - break :err 0; - } - } - - break :err err; - }, - else => err, - }; - } - - fn runMain(self: Action, alloc: Allocator) !u8 { - return switch (self) { - .version => try version.run(alloc), - .help => try help.run(alloc), - .@"list-fonts" => try list_fonts.run(alloc), - .@"list-keybinds" => try list_keybinds.run(alloc), - .@"list-themes" => try list_themes.run(alloc), - .@"list-colors" => try list_colors.run(alloc), - .@"list-actions" => try list_actions.run(alloc), - .@"ssh-cache" => try ssh_cache.run(alloc), - .@"edit-config" => try edit_config.run(alloc), - .@"show-config" => try show_config.run(alloc), - .@"validate-config" => try validate_config.run(alloc), - .@"crash-report" => try crash_report.run(alloc), - .@"show-face" => try show_face.run(alloc), - .boo => try boo.run(alloc), - }; - } - - /// Returns the filename associated with an action. This is a relative - /// path from the root src/ directory. - pub fn file(comptime self: Action) []const u8 { - comptime { - const filename = filename: { - const tag = @tagName(self); - var filename: [tag.len]u8 = undefined; - _ = std.mem.replace(u8, tag, "-", "_", &filename); - break :filename &filename; - }; - - return "cli/" ++ filename ++ ".zig"; - } - } - - /// Returns the options of action. Supports generating shell completions - /// without duplicating the mapping from Action to relevant Option - /// @import(..) declaration. - pub fn options(comptime self: Action) type { - comptime { - return switch (self) { - .version => version.Options, - .help => help.Options, - .@"list-fonts" => list_fonts.Options, - .@"list-keybinds" => list_keybinds.Options, - .@"list-themes" => list_themes.Options, - .@"list-colors" => list_colors.Options, - .@"list-actions" => list_actions.Options, - .@"ssh-cache" => ssh_cache.Options, - .@"edit-config" => edit_config.Options, - .@"show-config" => show_config.Options, - .@"validate-config" => validate_config.Options, - .@"crash-report" => crash_report.Options, - .@"show-face" => show_face.Options, - .boo => boo.Options, - }; - } - } + /// An unknown action was specified. + InvalidAction, }; -test "parse action none" { +/// Detect the action from CLI args. +pub fn detectArgs(comptime E: type, alloc: Allocator) !?E { + var iter = try std.process.argsWithAllocator(alloc); + defer iter.deinit(); + return try detectIter(E, &iter); +} + +/// Detect the action from any iterator. Each iterator value should yield +/// a CLI argument such as "--foo". +/// +/// The comptime type E must be an enum with the available actions. +/// If the type E has a decl `detectSpecialCase`, then it will be called +/// for each argument to allow handling of special cases. The function +/// signature for `detectSpecialCase` should be: +/// +/// fn detectSpecialCase(arg: []const u8) ?SpecialCase(E) +/// +pub fn detectIter( + comptime E: type, + iter: anytype, +) DetectError!?E { + var fallback: ?E = null; + var pending: ?E = null; + while (iter.next()) |arg| { + // Allow handling of special cases. + if (@hasDecl(E, "detectSpecialCase")) special: { + const special = E.detectSpecialCase(arg) orelse break :special; + switch (special) { + .action => |a| return a, + .fallback => |a| fallback = a, + .abort_if_no_action => if (pending == null) return null, + } + } + + // Commands must start with "+" + if (arg.len == 0 or arg[0] != '+') continue; + if (pending != null) return DetectError.MultipleActions; + pending = std.meta.stringToEnum(E, arg[1..]) orelse + return DetectError.InvalidAction; + } + + // If we have an action, we always return that action, even if we've + // seen "--help" or "-h" because the action may have its own help text. + if (pending != null) return pending; + + // If we have no action but we have a fallback, then we return that. + if (fallback) |a| return a; + + return null; +} + +/// The action enum E can implement the decl `detectSpecialCase` to +/// return this enum in order to perform various special case actions. +pub fn SpecialCase(comptime E: type) type { + return union(enum) { + /// Immediately return this action. + action: E, + + /// Return this action if no other action is found. + fallback: E, + + /// If there is no pending action (we haven't seen an action yet) + /// then we should return no action. This is kind of weird but is + /// a special case to allow "-e" in Ghostty. + abort_if_no_action, + }; +} + +test "detect direct match" { const testing = std.testing; const alloc = testing.allocator; + const Enum = enum { foo, bar, baz }; var iter = try std.process.ArgIteratorGeneral(.{}).init( alloc, - "--a=42 --b --b-f=false", + "+foo", ); defer iter.deinit(); - const action = try Action.detectIter(&iter); - try testing.expect(action == null); + const result = try detectIter(Enum, &iter); + try testing.expectEqual(Enum.foo, result.?); } -test "parse action version" { +test "detect invalid match" { const testing = std.testing; const alloc = testing.allocator; + const Enum = enum { foo, bar, baz }; - { - var iter = try std.process.ArgIteratorGeneral(.{}).init( - alloc, - "--a=42 --b --b-f=false --version", - ); - defer iter.deinit(); - const action = try Action.detectIter(&iter); - try testing.expect(action.? == .version); - } - - { - var iter = try std.process.ArgIteratorGeneral(.{}).init( - alloc, - "--version --a=42 --b --b-f=false", - ); - defer iter.deinit(); - const action = try Action.detectIter(&iter); - try testing.expect(action.? == .version); - } - - { - var iter = try std.process.ArgIteratorGeneral(.{}).init( - alloc, - "--c=84 --d --version --a=42 --b --b-f=false", - ); - defer iter.deinit(); - const action = try Action.detectIter(&iter); - try testing.expect(action.? == .version); - } + var iter = try std.process.ArgIteratorGeneral(.{}).init( + alloc, + "+invalid", + ); + defer iter.deinit(); + try testing.expectError( + DetectError.InvalidAction, + detectIter(Enum, &iter), + ); } -test "parse action plus" { +test "detect multiple actions" { const testing = std.testing; const alloc = testing.allocator; + const Enum = enum { foo, bar, baz }; - { - var iter = try std.process.ArgIteratorGeneral(.{}).init( - alloc, - "--a=42 --b --b-f=false +version", - ); - defer iter.deinit(); - const action = try Action.detectIter(&iter); - try testing.expect(action.? == .version); - } - - { - var iter = try std.process.ArgIteratorGeneral(.{}).init( - alloc, - "+version --a=42 --b --b-f=false", - ); - defer iter.deinit(); - const action = try Action.detectIter(&iter); - try testing.expect(action.? == .version); - } - - { - var iter = try std.process.ArgIteratorGeneral(.{}).init( - alloc, - "--c=84 --d +version --a=42 --b --b-f=false", - ); - defer iter.deinit(); - const action = try Action.detectIter(&iter); - try testing.expect(action.? == .version); - } + var iter = try std.process.ArgIteratorGeneral(.{}).init( + alloc, + "+foo +bar", + ); + defer iter.deinit(); + try testing.expectError( + DetectError.MultipleActions, + detectIter(Enum, &iter), + ); } -test "parse action plus ignores -e" { +test "detect no match" { const testing = std.testing; const alloc = testing.allocator; + const Enum = enum { foo, bar, baz }; + + var iter = try std.process.ArgIteratorGeneral(.{}).init( + alloc, + "--some-flag", + ); + defer iter.deinit(); + const result = try detectIter(Enum, &iter); + try testing.expect(result == null); +} + +test "detect special case action" { + const testing = std.testing; + const alloc = testing.allocator; + const Enum = enum { + foo, + bar, + + fn detectSpecialCase(arg: []const u8) ?SpecialCase(@This()) { + return if (std.mem.eql(u8, arg, "--special")) + .{ .action = .foo } + else + null; + } + }; { var iter = try std.process.ArgIteratorGeneral(.{}).init( alloc, - "--a=42 -e +version", + "--special +bar", ); defer iter.deinit(); - const action = try Action.detectIter(&iter); - try testing.expect(action == null); + const result = try detectIter(Enum, &iter); + try testing.expectEqual(Enum.foo, result.?); } { var iter = try std.process.ArgIteratorGeneral(.{}).init( alloc, - "+list-fonts --a=42 -e +version", + "+bar --special", ); defer iter.deinit(); - try testing.expectError( - Action.Error.MultipleActions, - Action.detectIter(&iter), + const result = try detectIter(Enum, &iter); + try testing.expectEqual(Enum.foo, result.?); + } + + { + var iter = try std.process.ArgIteratorGeneral(.{}).init( + alloc, + "+bar", ); + defer iter.deinit(); + const result = try detectIter(Enum, &iter); + try testing.expectEqual(Enum.bar, result.?); + } +} + +test "detect special case fallback" { + const testing = std.testing; + const alloc = testing.allocator; + const Enum = enum { + foo, + bar, + + fn detectSpecialCase(arg: []const u8) ?SpecialCase(@This()) { + return if (std.mem.eql(u8, arg, "--special")) + .{ .fallback = .foo } + else + null; + } + }; + + { + var iter = try std.process.ArgIteratorGeneral(.{}).init( + alloc, + "--special", + ); + defer iter.deinit(); + const result = try detectIter(Enum, &iter); + try testing.expectEqual(Enum.foo, result.?); + } + + { + var iter = try std.process.ArgIteratorGeneral(.{}).init( + alloc, + "+bar --special", + ); + defer iter.deinit(); + const result = try detectIter(Enum, &iter); + try testing.expectEqual(Enum.bar, result.?); + } + + { + var iter = try std.process.ArgIteratorGeneral(.{}).init( + alloc, + "--special +bar", + ); + defer iter.deinit(); + const result = try detectIter(Enum, &iter); + try testing.expectEqual(Enum.bar, result.?); + } +} + +test "detect special case abort_if_no_action" { + const testing = std.testing; + const alloc = testing.allocator; + const Enum = enum { + foo, + bar, + + fn detectSpecialCase(arg: []const u8) ?SpecialCase(@This()) { + return if (std.mem.eql(u8, arg, "-e")) + .abort_if_no_action + else + null; + } + }; + + { + var iter = try std.process.ArgIteratorGeneral(.{}).init( + alloc, + "-e", + ); + defer iter.deinit(); + const result = try detectIter(Enum, &iter); + try testing.expect(result == null); + } + + { + var iter = try std.process.ArgIteratorGeneral(.{}).init( + alloc, + "+foo -e", + ); + defer iter.deinit(); + const result = try detectIter(Enum, &iter); + try testing.expectEqual(Enum.foo, result.?); + } + + { + var iter = try std.process.ArgIteratorGeneral(.{}).init( + alloc, + "-e +bar", + ); + defer iter.deinit(); + const result = try detectIter(Enum, &iter); + try testing.expect(result == null); } } diff --git a/src/cli/boo.zig b/src/cli/boo.zig index 47c8ab741..72b282ef6 100644 --- a/src/cli/boo.zig +++ b/src/cli/boo.zig @@ -1,7 +1,7 @@ const std = @import("std"); const builtin = @import("builtin"); const args = @import("args.zig"); -const Action = @import("action.zig").Action; +const Action = @import("ghostty.zig").Action; const Allocator = std.mem.Allocator; const help_strings = @import("help_strings"); const vaxis = @import("vaxis"); diff --git a/src/cli/crash_report.zig b/src/cli/crash_report.zig index ff8509797..c6a383563 100644 --- a/src/cli/crash_report.zig +++ b/src/cli/crash_report.zig @@ -1,7 +1,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const args = @import("args.zig"); -const Action = @import("action.zig").Action; +const Action = @import("ghostty.zig").Action; const Config = @import("../config.zig").Config; const crash = @import("../crash/main.zig"); diff --git a/src/cli/edit_config.zig b/src/cli/edit_config.zig index 3be88e090..dd09d7e2f 100644 --- a/src/cli/edit_config.zig +++ b/src/cli/edit_config.zig @@ -3,7 +3,7 @@ const builtin = @import("builtin"); const assert = std.debug.assert; const args = @import("args.zig"); const Allocator = std.mem.Allocator; -const Action = @import("action.zig").Action; +const Action = @import("ghostty.zig").Action; const configpkg = @import("../config.zig"); const internal_os = @import("../os/main.zig"); const Config = configpkg.Config; diff --git a/src/cli/ghostty.zig b/src/cli/ghostty.zig new file mode 100644 index 000000000..c1b661f70 --- /dev/null +++ b/src/cli/ghostty.zig @@ -0,0 +1,290 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const help_strings = @import("help_strings"); +const actionpkg = @import("action.zig"); +const SpecialCase = actionpkg.SpecialCase; + +const list_fonts = @import("list_fonts.zig"); +const help = @import("help.zig"); +const version = @import("version.zig"); +const list_keybinds = @import("list_keybinds.zig"); +const list_themes = @import("list_themes.zig"); +const list_colors = @import("list_colors.zig"); +const list_actions = @import("list_actions.zig"); +const ssh_cache = @import("ssh_cache.zig"); +const edit_config = @import("edit_config.zig"); +const show_config = @import("show_config.zig"); +const validate_config = @import("validate_config.zig"); +const crash_report = @import("crash_report.zig"); +const show_face = @import("show_face.zig"); +const boo = @import("boo.zig"); + +/// Special commands that can be invoked via CLI flags. These are all +/// invoked by using `+` as a CLI flag. The only exception is +/// "version" which can be invoked additionally with `--version`. +pub const Action = enum { + /// Output the version and exit + version, + + /// Output help information for the CLI or configuration + help, + + /// List available fonts + @"list-fonts", + + /// List available keybinds + @"list-keybinds", + + /// List available themes + @"list-themes", + + /// List named RGB colors + @"list-colors", + + /// List keybind actions + @"list-actions", + + /// Manage SSH terminfo cache for automatic remote host setup + @"ssh-cache", + + /// Edit the config file in the configured terminal editor. + @"edit-config", + + /// Dump the config to stdout + @"show-config", + + // Validate passed config file + @"validate-config", + + // Show which font face Ghostty loads a codepoint from. + @"show-face", + + // List, (eventually) view, and (eventually) send crash reports. + @"crash-report", + + // Boo! + boo, + + pub fn detectSpecialCase(arg: []const u8) ?SpecialCase(Action) { + // If we see a "-e" and we haven't seen a command yet, then + // we are done looking for commands. This special case enables + // `ghostty -e ghostty +command`. If we've seen a command we + // still want to keep looking because + // `ghostty +command -e +command` is invalid. + if (std.mem.eql(u8, arg, "-e")) return .abort_if_no_action; + + // Special case, --version always outputs the version no + // matter what, no matter what other args exist. + if (std.mem.eql(u8, arg, "--version")) { + return .{ .action = .version }; + } + + // --help matches "help" but if a subcommand is specified + // then we match the subcommand. + if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) { + return .{ .fallback = .help }; + } + + return null; + } + + /// This should be returned by actions that want to print the help text. + pub const help_error = error.ActionHelpRequested; + + /// Run the action. This returns the exit code to exit with. + pub fn run(self: Action, alloc: Allocator) !u8 { + return self.runMain(alloc) catch |err| switch (err) { + // If help is requested, then we use some comptime trickery + // to find this action in the help strings and output that. + help_error => err: { + inline for (@typeInfo(Action).@"enum".fields) |field| { + // Future note: for now we just output the help text directly + // to stdout. In the future we can style this much prettier + // for all commands by just changing this one place. + + if (std.mem.eql(u8, field.name, @tagName(self))) { + const stdout = std.io.getStdOut().writer(); + const text = @field(help_strings.Action, field.name) ++ "\n"; + stdout.writeAll(text) catch |write_err| { + std.log.warn("failed to write help text: {}\n", .{write_err}); + break :err 1; + }; + + break :err 0; + } + } + + break :err err; + }, + else => err, + }; + } + + fn runMain(self: Action, alloc: Allocator) !u8 { + return switch (self) { + .version => try version.run(alloc), + .help => try help.run(alloc), + .@"list-fonts" => try list_fonts.run(alloc), + .@"list-keybinds" => try list_keybinds.run(alloc), + .@"list-themes" => try list_themes.run(alloc), + .@"list-colors" => try list_colors.run(alloc), + .@"list-actions" => try list_actions.run(alloc), + .@"ssh-cache" => try ssh_cache.run(alloc), + .@"edit-config" => try edit_config.run(alloc), + .@"show-config" => try show_config.run(alloc), + .@"validate-config" => try validate_config.run(alloc), + .@"crash-report" => try crash_report.run(alloc), + .@"show-face" => try show_face.run(alloc), + .boo => try boo.run(alloc), + }; + } + + /// Returns the filename associated with an action. This is a relative + /// path from the root src/ directory. + pub fn file(comptime self: Action) []const u8 { + comptime { + const filename = filename: { + const tag = @tagName(self); + var filename: [tag.len]u8 = undefined; + _ = std.mem.replace(u8, tag, "-", "_", &filename); + break :filename &filename; + }; + + return "cli/" ++ filename ++ ".zig"; + } + } + + /// Returns the options of action. Supports generating shell completions + /// without duplicating the mapping from Action to relevant Option + /// @import(..) declaration. + pub fn options(comptime self: Action) type { + comptime { + return switch (self) { + .version => version.Options, + .help => help.Options, + .@"list-fonts" => list_fonts.Options, + .@"list-keybinds" => list_keybinds.Options, + .@"list-themes" => list_themes.Options, + .@"list-colors" => list_colors.Options, + .@"list-actions" => list_actions.Options, + .@"ssh-cache" => ssh_cache.Options, + .@"edit-config" => edit_config.Options, + .@"show-config" => show_config.Options, + .@"validate-config" => validate_config.Options, + .@"crash-report" => crash_report.Options, + .@"show-face" => show_face.Options, + .boo => boo.Options, + }; + } + } +}; + +test "parse action none" { + const testing = std.testing; + const alloc = testing.allocator; + + var iter = try std.process.ArgIteratorGeneral(.{}).init( + alloc, + "--a=42 --b --b-f=false", + ); + defer iter.deinit(); + const action = try actionpkg.detectIter(Action, &iter); + try testing.expect(action == null); +} + +test "parse action version" { + const testing = std.testing; + const alloc = testing.allocator; + + { + var iter = try std.process.ArgIteratorGeneral(.{}).init( + alloc, + "--a=42 --b --b-f=false --version", + ); + defer iter.deinit(); + const action = try actionpkg.detectIter(Action, &iter); + try testing.expect(action.? == .version); + } + + { + var iter = try std.process.ArgIteratorGeneral(.{}).init( + alloc, + "--version --a=42 --b --b-f=false", + ); + defer iter.deinit(); + const action = try actionpkg.detectIter(Action, &iter); + try testing.expect(action.? == .version); + } + + { + var iter = try std.process.ArgIteratorGeneral(.{}).init( + alloc, + "--c=84 --d --version --a=42 --b --b-f=false", + ); + defer iter.deinit(); + const action = try actionpkg.detectIter(Action, &iter); + try testing.expect(action.? == .version); + } +} + +test "parse action plus" { + const testing = std.testing; + const alloc = testing.allocator; + + { + var iter = try std.process.ArgIteratorGeneral(.{}).init( + alloc, + "--a=42 --b --b-f=false +version", + ); + defer iter.deinit(); + const action = try actionpkg.detectIter(Action, &iter); + try testing.expect(action.? == .version); + } + + { + var iter = try std.process.ArgIteratorGeneral(.{}).init( + alloc, + "+version --a=42 --b --b-f=false", + ); + defer iter.deinit(); + const action = try actionpkg.detectIter(Action, &iter); + try testing.expect(action.? == .version); + } + + { + var iter = try std.process.ArgIteratorGeneral(.{}).init( + alloc, + "--c=84 --d +version --a=42 --b --b-f=false", + ); + defer iter.deinit(); + const action = try actionpkg.detectIter(Action, &iter); + try testing.expect(action.? == .version); + } +} + +test "parse action plus ignores -e" { + const testing = std.testing; + const alloc = testing.allocator; + + { + var iter = try std.process.ArgIteratorGeneral(.{}).init( + alloc, + "--a=42 -e +version", + ); + defer iter.deinit(); + const action = try actionpkg.detectIter(Action, &iter); + try testing.expect(action == null); + } + + { + var iter = try std.process.ArgIteratorGeneral(.{}).init( + alloc, + "+list-fonts --a=42 -e +version", + ); + defer iter.deinit(); + try testing.expectError( + actionpkg.DetectError.MultipleActions, + actionpkg.detectIter(Action, &iter), + ); + } +} diff --git a/src/cli/help.zig b/src/cli/help.zig index 6c989fd0c..0528dc1c2 100644 --- a/src/cli/help.zig +++ b/src/cli/help.zig @@ -1,7 +1,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const args = @import("args.zig"); -const Action = @import("action.zig").Action; +const Action = @import("ghostty.zig").Action; // Note that this options struct doesn't implement the `help` decl like other // actions. That is because the help command is special and wants to handle its diff --git a/src/cli/list_actions.zig b/src/cli/list_actions.zig index 1d17873cc..6f5ce06a2 100644 --- a/src/cli/list_actions.zig +++ b/src/cli/list_actions.zig @@ -1,6 +1,6 @@ const std = @import("std"); const args = @import("args.zig"); -const Action = @import("action.zig").Action; +const Action = @import("ghostty.zig").Action; const Allocator = std.mem.Allocator; const helpgen_actions = @import("../input/helpgen_actions.zig"); diff --git a/src/cli/list_colors.zig b/src/cli/list_colors.zig index bfe17df7c..e43a43c86 100644 --- a/src/cli/list_colors.zig +++ b/src/cli/list_colors.zig @@ -1,5 +1,5 @@ const std = @import("std"); -const Action = @import("action.zig").Action; +const Action = @import("ghostty.zig").Action; const args = @import("args.zig"); const x11_color = @import("../terminal/main.zig").x11_color; diff --git a/src/cli/list_fonts.zig b/src/cli/list_fonts.zig index e8a010ecd..58246d3ad 100644 --- a/src/cli/list_fonts.zig +++ b/src/cli/list_fonts.zig @@ -1,7 +1,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; -const Action = @import("action.zig").Action; +const Action = @import("ghostty.zig").Action; const args = @import("args.zig"); const font = @import("../font/main.zig"); diff --git a/src/cli/list_keybinds.zig b/src/cli/list_keybinds.zig index f84d540c3..94f445eea 100644 --- a/src/cli/list_keybinds.zig +++ b/src/cli/list_keybinds.zig @@ -1,7 +1,7 @@ const std = @import("std"); const builtin = @import("builtin"); const args = @import("args.zig"); -const Action = @import("action.zig").Action; +const Action = @import("ghostty.zig").Action; const Arena = std.heap.ArenaAllocator; const Allocator = std.mem.Allocator; const configpkg = @import("../config.zig"); diff --git a/src/cli/list_themes.zig b/src/cli/list_themes.zig index e80a92286..b85f98445 100644 --- a/src/cli/list_themes.zig +++ b/src/cli/list_themes.zig @@ -1,7 +1,7 @@ const std = @import("std"); const inputpkg = @import("../input.zig"); const args = @import("args.zig"); -const Action = @import("action.zig").Action; +const Action = @import("ghostty.zig").Action; const Config = @import("../config/Config.zig"); const themepkg = @import("../config/theme.zig"); const tui = @import("tui.zig"); diff --git a/src/cli/show_config.zig b/src/cli/show_config.zig index cbcd2486d..3f22c75c2 100644 --- a/src/cli/show_config.zig +++ b/src/cli/show_config.zig @@ -1,7 +1,7 @@ const std = @import("std"); const args = @import("args.zig"); const Allocator = std.mem.Allocator; -const Action = @import("action.zig").Action; +const Action = @import("ghostty.zig").Action; const configpkg = @import("../config.zig"); const Config = configpkg.Config; diff --git a/src/cli/show_face.zig b/src/cli/show_face.zig index b7f039dc8..e3b596bcd 100644 --- a/src/cli/show_face.zig +++ b/src/cli/show_face.zig @@ -1,7 +1,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; -const Action = @import("action.zig").Action; +const Action = @import("ghostty.zig").Action; const args = @import("args.zig"); const diagnostics = @import("diagnostics.zig"); const font = @import("../font/main.zig"); diff --git a/src/cli/ssh_cache.zig b/src/cli/ssh_cache.zig index c8e2e1123..1099f0112 100644 --- a/src/cli/ssh_cache.zig +++ b/src/cli/ssh_cache.zig @@ -3,7 +3,7 @@ const fs = std.fs; const Allocator = std.mem.Allocator; const xdg = @import("../os/xdg.zig"); const args = @import("args.zig"); -const Action = @import("action.zig").Action; +const Action = @import("ghostty.zig").Action; pub const Entry = @import("ssh-cache/Entry.zig"); pub const DiskCache = @import("ssh-cache/DiskCache.zig"); diff --git a/src/cli/validate_config.zig b/src/cli/validate_config.zig index 5bc6ff406..114843e9a 100644 --- a/src/cli/validate_config.zig +++ b/src/cli/validate_config.zig @@ -1,7 +1,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const args = @import("args.zig"); -const Action = @import("action.zig").Action; +const Action = @import("ghostty.zig").Action; const Config = @import("../config.zig").Config; const cli = @import("../cli.zig"); diff --git a/src/global.zig b/src/global.zig index 668d2faec..e68ec7f74 100644 --- a/src/global.zig +++ b/src/global.zig @@ -30,7 +30,7 @@ pub const GlobalState = struct { gpa: ?GPA, alloc: std.mem.Allocator, - action: ?cli.Action, + action: ?cli.ghostty.Action, logging: Logging, rlimits: ResourceLimits = .{}, @@ -92,7 +92,10 @@ pub const GlobalState = struct { unreachable; // We first try to parse any action that we may be executing. - self.action = try cli.Action.detectCLI(self.alloc); + self.action = try cli.action.detectArgs( + cli.ghostty.Action, + self.alloc, + ); // If we have an action executing, we disable logging by default // since we write to stderr we don't want logs messing up our diff --git a/src/helpgen.zig b/src/helpgen.zig index 560e5ce29..e1628c218 100644 --- a/src/helpgen.zig +++ b/src/helpgen.zig @@ -4,7 +4,7 @@ const std = @import("std"); const Config = @import("config/Config.zig"); -const Action = @import("cli/action.zig").Action; +const Action = @import("cli.zig").ghostty.Action; const KeybindAction = @import("input/Binding.zig").Action; pub fn main() !void { From b8f5cf9d52506add7d30cacc2142a79949f76ea3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 8 Jul 2025 09:12:03 -0700 Subject: [PATCH 06/46] initial `ghostty-bench` program --- src/benchmark/TerminalStream.zig | 56 ++++++++++++++++++++++++++--- src/benchmark/cli.zig | 61 ++++++++++++++++++++++++++++++++ src/benchmark/main.zig | 1 + src/build/GhosttyBench.zig | 16 +++++++++ src/main_bench.zig | 3 ++ 5 files changed, 133 insertions(+), 4 deletions(-) create mode 100644 src/benchmark/cli.zig create mode 100644 src/main_bench.zig diff --git a/src/benchmark/TerminalStream.zig b/src/benchmark/TerminalStream.zig index 8580695f2..6300ba04c 100644 --- a/src/benchmark/TerminalStream.zig +++ b/src/benchmark/TerminalStream.zig @@ -14,36 +14,49 @@ const TerminalStream = @This(); const std = @import("std"); +const assert = std.debug.assert; const Allocator = std.mem.Allocator; const terminalpkg = @import("../terminal/main.zig"); const Benchmark = @import("Benchmark.zig"); const Terminal = terminalpkg.Terminal; const Stream = terminalpkg.Stream(*Handler); +opts: Options, terminal: Terminal, handler: Handler, stream: Stream, +/// The file, opened in the setup function. +data_f: ?std.fs.File = null, + pub const Options = struct { /// The size of the terminal. This affects benchmarking when /// dealing with soft line wrapping and the memory impact /// of page sizes. @"terminal-rows": u16 = 80, @"terminal-cols": u16 = 120, + + /// The data to read as a filepath. If this is "-" then + /// we will read stdin. If this is unset, then we will + /// do nothing (benchmark is a noop). It'd be more unixy to + /// use stdin by default but I find that a hanging CLI command + /// with no interaction is a bit annoying. + data: ?[]const u8 = null, }; /// Create a new terminal stream handler for the given arguments. pub fn create( alloc: Allocator, - args: Options, + opts: Options, ) !*TerminalStream { const ptr = try alloc.create(TerminalStream); errdefer alloc.destroy(ptr); ptr.* = .{ + .opts = opts, .terminal = try .init(alloc, .{ - .rows = args.@"terminal-rows", - .cols = args.@"terminal-cols", + .rows = opts.@"terminal-rows", + .cols = opts.@"terminal-cols", }), .handler = .{ .t = &ptr.terminal }, .stream = .{ .handler = &ptr.handler }, @@ -61,17 +74,52 @@ pub fn benchmark(self: *TerminalStream) Benchmark { return .init(self, .{ .stepFn = step, .setupFn = setup, + .teardownFn = teardown, }); } fn setup(ptr: *anyopaque) Benchmark.Error!void { const self: *TerminalStream = @ptrCast(@alignCast(ptr)); + + // Always reset our terminal state self.terminal.fullReset(); + + // Open our data file to prepare for reading. We can do more + // validation here eventually. + assert(self.data_f == null); + if (self.opts.data) |path| { + self.data_f = std.fs.cwd().openFile(path, .{}) catch + return error.BenchmarkFailed; + } +} + +fn teardown(ptr: *anyopaque) void { + const self: *TerminalStream = @ptrCast(@alignCast(ptr)); + if (self.data_f) |f| { + f.close(); + self.data_f = null; + } } fn step(ptr: *anyopaque) Benchmark.Error!void { const self: *TerminalStream = @ptrCast(@alignCast(ptr)); - _ = self; + + // Get our buffered reader so we're not predominantly + // waiting on file IO. It'd be better to move this fully into + // memory. If we're IO bound though that should show up on + // the benchmark results and... I know writing this that we + // aren't currently IO bound. + const f = self.data_f orelse return; + var r = std.io.bufferedReader(f.reader()); + + var buf: [4096]u8 = undefined; + while (true) { + const n = r.read(&buf) catch return error.BenchmarkFailed; + if (n == 0) break; // EOF reached + const chunk = buf[0..n]; + self.stream.nextSlice(chunk) catch + return error.BenchmarkFailed; + } } /// Implements the handler interface for the terminal.Stream. diff --git a/src/benchmark/cli.zig b/src/benchmark/cli.zig new file mode 100644 index 000000000..781eafd24 --- /dev/null +++ b/src/benchmark/cli.zig @@ -0,0 +1,61 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const cli = @import("../cli.zig"); + +/// The available actions for the CLI. This is the list of available +/// benchmarks. +const Action = enum { + @"terminal-stream", + + /// Returns the struct associated with the action. The struct + /// should have a few decls: + /// + /// - `const Options`: The CLI options for the action. + /// - `fn create`: Create a new instance of the action from options. + /// - `fn benchmark`: Returns a `Benchmark` instance for the action. + /// + /// See TerminalStream for an example. + pub fn Struct(comptime action: Action) type { + return switch (action) { + .@"terminal-stream" => @import("TerminalStream.zig"), + }; + } +}; + +/// An entrypoint for the benchmark CLI. +pub fn main() !void { + // TODO: Better terminal output throughout this, use libvaxis. + + const alloc = std.heap.c_allocator; + const action_ = try cli.action.detectArgs(Action, alloc); + const action = action_ orelse return error.NoAction; + + // We need a comptime action to get the struct type and do the + // rest. + return switch (action) { + inline else => |comptime_action| { + const BenchmarkImpl = Action.Struct(comptime_action); + try mainAction(BenchmarkImpl, alloc); + }, + }; +} + +fn mainAction(comptime BenchmarkImpl: type, alloc: Allocator) !void { + // First, parse our CLI options. + const Options = BenchmarkImpl.Options; + var opts: Options = .{}; + defer if (@hasDecl(Options, "deinit")) opts.deinit(); + { + var iter = try cli.args.argsIterator(alloc); + defer iter.deinit(); + try cli.args.parse(Options, alloc, &opts, &iter); + } + + // Create our implementation + const impl = try BenchmarkImpl.create(alloc, opts); + defer impl.destroy(alloc); + + // Initialize our benchmark + const b = impl.benchmark(); + _ = try b.run(.once); +} diff --git a/src/benchmark/main.zig b/src/benchmark/main.zig index 802519e29..93e489578 100644 --- a/src/benchmark/main.zig +++ b/src/benchmark/main.zig @@ -1,3 +1,4 @@ +pub const cli = @import("cli.zig"); pub const Benchmark = @import("Benchmark.zig"); pub const TerminalStream = @import("TerminalStream.zig"); diff --git a/src/build/GhosttyBench.zig b/src/build/GhosttyBench.zig index 9e93a3b85..0dc18aa4d 100644 --- a/src/build/GhosttyBench.zig +++ b/src/build/GhosttyBench.zig @@ -14,6 +14,22 @@ pub fn init( var steps = std.ArrayList(*std.Build.Step.Compile).init(b.allocator); errdefer steps.deinit(); + // Our new benchmarking application. + { + const exe = b.addExecutable(.{ + .name = "ghostty-bench", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main_bench.zig"), + .target = deps.config.target, + // We always want our benchmarks to be in release mode. + .optimize = .ReleaseFast, + }), + }); + exe.linkLibC(); + _ = try deps.add(exe); + try steps.append(exe); + } + // Open the directory ./src/bench const c_dir_path = b.pathFromRoot("src/bench"); var c_dir = try std.fs.cwd().openDir(c_dir_path, .{ .iterate = true }); diff --git a/src/main_bench.zig b/src/main_bench.zig new file mode 100644 index 000000000..9e4af1fc7 --- /dev/null +++ b/src/main_bench.zig @@ -0,0 +1,3 @@ +const std = @import("std"); +const benchmark = @import("benchmark/main.zig"); +pub const main = benchmark.cli.main; From d30771ecffb501512cec5ae43d085560e9d61478 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 8 Jul 2025 10:02:06 -0700 Subject: [PATCH 07/46] pkg/macos: use new @ptrcast for os.log --- pkg/macos/os/log.zig | 10 +++++++--- src/benchmark/Benchmark.zig | 2 +- src/main_bench.zig | 2 ++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/pkg/macos/os/log.zig b/pkg/macos/os/log.zig index 8a399b73e..32ecb3296 100644 --- a/pkg/macos/os/log.zig +++ b/pkg/macos/os/log.zig @@ -8,10 +8,10 @@ pub const Log = opaque { subsystem: [:0]const u8, category: [:0]const u8, ) *Log { - return @as(?*Log, @ptrFromInt(@intFromPtr(c.os_log_create( + return @ptrCast(c.os_log_create( subsystem.ptr, category.ptr, - )))).?; + ).?); } pub fn release(self: *Log) void { @@ -32,7 +32,11 @@ pub const Log = opaque { comptime format: []const u8, args: anytype, ) void { - const str = nosuspend std.fmt.allocPrintZ(alloc, format, args) catch return; + const str = nosuspend std.fmt.allocPrintZ( + alloc, + format, + args, + ) catch return; defer alloc.free(str); zig_os_log_with_type(self, typ, str.ptr); } diff --git a/src/benchmark/Benchmark.zig b/src/benchmark/Benchmark.zig index b7d9b6ad3..0bc7539a8 100644 --- a/src/benchmark/Benchmark.zig +++ b/src/benchmark/Benchmark.zig @@ -50,7 +50,7 @@ pub fn run( build_config.bundle_id, macos.os.signpost.Category.points_of_interest, ); - const id = macos.os.signpost.Id.generate(log); + const id = macos.os.signpost.Id.forPointer(log, self.ptr); macos.os.signpost.intervalBegin(log, id, signpost_name); break :macos .{ .log = log, .id = id }; } else {}; diff --git a/src/main_bench.zig b/src/main_bench.zig index 9e4af1fc7..2314dc2ed 100644 --- a/src/main_bench.zig +++ b/src/main_bench.zig @@ -1,3 +1,5 @@ const std = @import("std"); +const builtin = @import("builtin"); const benchmark = @import("benchmark/main.zig"); + pub const main = benchmark.cli.main; From 01b2545d1d97c7c9c9f89ac0f84a067f51a439f0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 8 Jul 2025 16:24:36 -0700 Subject: [PATCH 08/46] macos: fix signpost API to use proper mach header base addr --- pkg/macos/os/signpost.zig | 49 +++++++++++++++++++++++++++++++------ src/benchmark/Benchmark.zig | 3 ++- 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/pkg/macos/os/signpost.zig b/pkg/macos/os/signpost.zig index 9fef584e4..d7201aa6c 100644 --- a/pkg/macos/os/signpost.zig +++ b/pkg/macos/os/signpost.zig @@ -5,6 +5,38 @@ const c = @import("c.zig").c; const logpkg = @import("log.zig"); const Log = logpkg.Log; +/// This should be called once at the start of the program to intialize +/// some required state for signpost logging. +/// +/// This is all to workaround a Zig bug: +/// https://github.com/ziglang/zig/issues/24370 +pub fn init() void { + if (__dso_handle != null) return; + + // Since __dso_handle is not automatically populated by the linker, + // we populate it by looking up the main function's module address + // which should be a mach-o header. + var info: DlInfo = undefined; + const result = dladdr(@import("root").main, &info); + assert(result != 0); + __dso_handle = @ptrCast(@alignCast(info.dli_fbase)); +} + +/// This should REALLY be an extern var that is populated by the linker, +/// but there is a Zig bug: https://github.com/ziglang/zig/issues/24370 +var __dso_handle: ?*c.mach_header = null; + +// Import the necessary C functions and types +extern "c" fn dladdr(addr: ?*const anyopaque, info: *DlInfo) c_int; + +// Define the Dl_info structure +const DlInfo = extern struct { + dli_fname: [*:0]const u8, // Pathname of shared object + dli_fbase: ?*anyopaque, // Base address of shared object + dli_sname: [*:0]const u8, // Name of nearest symbol + dli_saddr: ?*anyopaque, // Address of nearest symbol +}; + /// Checks whether signpost logging is enabled for the given log handle. /// Returns true if signposts will be recorded for this log, false otherwise. /// This can be used to avoid expensive operations when signpost logging is disabled. @@ -48,8 +80,6 @@ pub fn intervalEnd(log: *Log, id: Id, comptime name: [:0]const u8) void { emitWithName(log, id, .interval_end, name); } -extern var __dso_handle: usize; - /// The internal function to emit a signpost with a specific name. fn emitWithName( log: *Log, @@ -57,14 +87,17 @@ fn emitWithName( typ: Type, comptime name: [:0]const u8, ) void { - var buf: [64]u8 = @splat(0); + // Init must be called by this point. + assert(__dso_handle != null); + + var buf: [2]u8 = @splat(0); c._os_signpost_emit_with_name_impl( - &__dso_handle, + __dso_handle, @ptrCast(log), @intFromEnum(typ), @intFromEnum(id), name.ptr, - null, + "".ptr, &buf, buf.len, ); @@ -135,12 +168,12 @@ pub const Category = struct { /// Dynamic Tracing category enables runtime-configurable logging. /// Signposts in this category can be enabled/disabled dynamically /// without recompiling. - pub const dynamic_tracing: [:0]const u8 = "DynamicTracking"; + pub const dynamic_tracing: [:0]const u8 = "DynamicTracing"; /// Dynamic Stack Tracing category captures call stacks at signpost /// events. This provides deeper debugging information but has higher /// performance overhead. - pub const dynamic_stack_tracing: [:0]const u8 = "DynamicStackTracking"; + pub const dynamic_stack_tracing: [:0]const u8 = "DynamicStackTracing"; }; test { @@ -152,6 +185,8 @@ test enabled { } test "intervals" { + init(); + const log = Log.create("com.mitchellh.ghostty", "test"); defer log.release(); diff --git a/src/benchmark/Benchmark.zig b/src/benchmark/Benchmark.zig index 0bc7539a8..523ea6f9a 100644 --- a/src/benchmark/Benchmark.zig +++ b/src/benchmark/Benchmark.zig @@ -41,11 +41,12 @@ pub fn run( // If we're on macOS, we setup signposts so its easier to find // the results in Instruments. There's a lot of nasty comptime stuff // here but its just to ensure this does nothing on other platforms. - const signpost_name = "Ghostty Benchmark"; + const signpost_name = "ghostty"; const signpost: if (builtin.target.os.tag.isDarwin()) struct { log: *macos.os.Log, id: macos.os.signpost.Id, } else void = if (comptime builtin.os.tag == .macos) macos: { + macos.os.signpost.init(); const log = macos.os.Log.create( build_config.bundle_id, macos.os.signpost.Category.points_of_interest, From 20bb71c627f2e0e5cb3b8b318f877cbb33d088f1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 9 Jul 2025 11:33:00 -0700 Subject: [PATCH 09/46] libghostty: export benchmark CLI API --- include/ghostty.h | 3 ++ pkg/macos/os/signpost.zig | 15 +++++++- src/benchmark/Benchmark.zig | 6 ++-- src/benchmark/CApi.zig | 34 ++++++++++++++++++ src/benchmark/cli.zig | 60 ++++++++++++++++++++++--------- src/benchmark/main.zig | 1 + src/config.zig | 2 +- src/config/{CAPI.zig => CApi.zig} | 0 src/main_c.zig | 24 ++++++++----- 9 files changed, 114 insertions(+), 31 deletions(-) create mode 100644 src/benchmark/CApi.zig rename src/config/{CAPI.zig => CApi.zig} (100%) diff --git a/include/ghostty.h b/include/ghostty.h index 312e6595a..bcd88251b 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -932,6 +932,9 @@ bool ghostty_inspector_metal_shutdown(ghostty_inspector_t); // Don't use these unless you know what you're doing. void ghostty_set_window_background_blur(ghostty_app_t, void*); +// Benchmark API, if available. +bool ghostty_benchmark_cli(const char*, const char*); + #ifdef __cplusplus } #endif diff --git a/pkg/macos/os/signpost.zig b/pkg/macos/os/signpost.zig index d7201aa6c..0be6ad4b1 100644 --- a/pkg/macos/os/signpost.zig +++ b/pkg/macos/os/signpost.zig @@ -13,11 +13,24 @@ const Log = logpkg.Log; pub fn init() void { if (__dso_handle != null) return; + const root = @import("root"); + const sym = if (@hasDecl(root, "main")) + root.main + else + comptime first: { + for (@typeInfo(root).@"struct".decls) |decl_info| { + const decl = @field(root, decl_info.name); + if (@typeInfo(@TypeOf(decl)) == .@"fn") break :first decl; + } + + @compileError("No functions found in root module"); + }; + // Since __dso_handle is not automatically populated by the linker, // we populate it by looking up the main function's module address // which should be a mach-o header. var info: DlInfo = undefined; - const result = dladdr(@import("root").main, &info); + const result = dladdr(sym, &info); assert(result != 0); __dso_handle = @ptrCast(@alignCast(info.dli_fbase)); } diff --git a/src/benchmark/Benchmark.zig b/src/benchmark/Benchmark.zig index 523ea6f9a..4128a7adc 100644 --- a/src/benchmark/Benchmark.zig +++ b/src/benchmark/Benchmark.zig @@ -45,7 +45,7 @@ pub fn run( const signpost: if (builtin.target.os.tag.isDarwin()) struct { log: *macos.os.Log, id: macos.os.signpost.Id, - } else void = if (comptime builtin.os.tag == .macos) macos: { + } else void = if (builtin.target.os.tag.isDarwin()) darwin: { macos.os.signpost.init(); const log = macos.os.Log.create( build_config.bundle_id, @@ -53,9 +53,9 @@ pub fn run( ); const id = macos.os.signpost.Id.forPointer(log, self.ptr); macos.os.signpost.intervalBegin(log, id, signpost_name); - break :macos .{ .log = log, .id = id }; + break :darwin .{ .log = log, .id = id }; } else {}; - defer if (comptime builtin.os.tag == .macos) { + defer if (comptime builtin.target.os.tag.isDarwin()) { macos.os.signpost.intervalEnd( signpost.log, signpost.id, diff --git a/src/benchmark/CApi.zig b/src/benchmark/CApi.zig new file mode 100644 index 000000000..3bef8b269 --- /dev/null +++ b/src/benchmark/CApi.zig @@ -0,0 +1,34 @@ +const std = @import("std"); +const cli = @import("cli.zig"); +const state = &@import("../global.zig").state; + +const log = std.log.scoped(.benchmark); + +/// Run the Ghostty benchmark CLI with the given action and arguments. +export fn ghostty_benchmark_cli( + action_name_: [*:0]const u8, + args: [*:0]const u8, +) bool { + const action_name = std.mem.sliceTo(action_name_, 0); + const action: cli.Action = std.meta.stringToEnum( + cli.Action, + action_name, + ) orelse { + log.warn("unknown action={s}", .{action_name}); + return false; + }; + + cli.mainAction( + state.alloc, + action, + .{ .string = std.mem.sliceTo(args, 0) }, + ) catch |err| { + log.warn("failed to run action={s} err={}", .{ + @tagName(action), + err, + }); + return false; + }; + + return true; +} diff --git a/src/benchmark/cli.zig b/src/benchmark/cli.zig index 781eafd24..c0b8dcea6 100644 --- a/src/benchmark/cli.zig +++ b/src/benchmark/cli.zig @@ -4,7 +4,7 @@ const cli = @import("../cli.zig"); /// The available actions for the CLI. This is the list of available /// benchmarks. -const Action = enum { +pub const Action = enum { @"terminal-stream", /// Returns the struct associated with the action. The struct @@ -24,31 +24,57 @@ const Action = enum { /// An entrypoint for the benchmark CLI. pub fn main() !void { - // TODO: Better terminal output throughout this, use libvaxis. - const alloc = std.heap.c_allocator; const action_ = try cli.action.detectArgs(Action, alloc); const action = action_ orelse return error.NoAction; - - // We need a comptime action to get the struct type and do the - // rest. - return switch (action) { - inline else => |comptime_action| { - const BenchmarkImpl = Action.Struct(comptime_action); - try mainAction(BenchmarkImpl, alloc); - }, - }; + try mainAction(alloc, action, .cli); } -fn mainAction(comptime BenchmarkImpl: type, alloc: Allocator) !void { +/// Arguments that can be passed to the benchmark. +pub const Args = union(enum) { + /// The arguments passed to the CLI via argc/argv. + cli, + + /// Simple string arguments, parsed via std.process.ArgIteratorGeneral. + string: []const u8, +}; + +pub fn mainAction( + alloc: Allocator, + action: Action, + args: Args, +) !void { + switch (action) { + inline else => |comptime_action| { + const BenchmarkImpl = Action.Struct(comptime_action); + try mainActionImpl(BenchmarkImpl, alloc, args); + }, + } +} + +fn mainActionImpl( + comptime BenchmarkImpl: type, + alloc: Allocator, + args: Args, +) !void { // First, parse our CLI options. const Options = BenchmarkImpl.Options; var opts: Options = .{}; defer if (@hasDecl(Options, "deinit")) opts.deinit(); - { - var iter = try cli.args.argsIterator(alloc); - defer iter.deinit(); - try cli.args.parse(Options, alloc, &opts, &iter); + switch (args) { + .cli => { + var iter = try cli.args.argsIterator(alloc); + defer iter.deinit(); + try cli.args.parse(Options, alloc, &opts, &iter); + }, + .string => |str| { + var iter = try std.process.ArgIteratorGeneral(.{}).init( + alloc, + str, + ); + defer iter.deinit(); + try cli.args.parse(Options, alloc, &opts, &iter); + }, } // Create our implementation diff --git a/src/benchmark/main.zig b/src/benchmark/main.zig index 93e489578..010f11805 100644 --- a/src/benchmark/main.zig +++ b/src/benchmark/main.zig @@ -1,5 +1,6 @@ pub const cli = @import("cli.zig"); pub const Benchmark = @import("Benchmark.zig"); +pub const CApi = @import("CApi.zig"); pub const TerminalStream = @import("TerminalStream.zig"); test { diff --git a/src/config.zig b/src/config.zig index efc9fd973..c5bab5877 100644 --- a/src/config.zig +++ b/src/config.zig @@ -41,7 +41,7 @@ pub const BackgroundImageFit = Config.BackgroundImageFit; pub const LinkPreviews = Config.LinkPreviews; // Alternate APIs -pub const CAPI = @import("config/CAPI.zig"); +pub const CApi = @import("config/CApi.zig"); pub const Wasm = if (!builtin.target.cpu.arch.isWasm()) struct {} else @import("config/Wasm.zig"); test { diff --git a/src/config/CAPI.zig b/src/config/CApi.zig similarity index 100% rename from src/config/CAPI.zig rename to src/config/CApi.zig diff --git a/src/main_c.zig b/src/main_c.zig index 2c266cfb5..9a9bcc6d2 100644 --- a/src/main_c.zig +++ b/src/main_c.zig @@ -33,10 +33,16 @@ pub const std_options = main.std_options; comptime { // These structs need to be referenced so the `export` functions // are truly exported by the C API lib. - _ = @import("config.zig").CAPI; - if (@hasDecl(apprt.runtime, "CAPI")) { - _ = apprt.runtime.CAPI; - } + + // Our config API + _ = @import("config.zig").CApi; + + // Any apprt-specific C API, mainly libghostty for apprt.embedded. + if (@hasDecl(apprt.runtime, "CAPI")) _ = apprt.runtime.CAPI; + + // Our benchmark API. We probably want to gate this on a build + // config in the future but for now we always just export it. + _ = @import("benchmark/main.zig").CApi; } /// ghostty_info_s @@ -72,7 +78,7 @@ pub const String = extern struct { }; /// Initialize ghostty global state. -export fn ghostty_init(argc: usize, argv: [*][*:0]u8) c_int { +pub export fn ghostty_init(argc: usize, argv: [*][*:0]u8) c_int { assert(builtin.link_libc); std.os.argv = argv[0..argc]; @@ -86,7 +92,7 @@ export fn ghostty_init(argc: usize, argv: [*][*:0]u8) c_int { /// Runs an action if it is specified. If there is no action this returns /// false. If there is an action then this doesn't return. -export fn ghostty_cli_try_action() void { +pub export fn ghostty_cli_try_action() void { const action = state.action orelse return; std.log.info("executing CLI action={}", .{action}); posix.exit(action.run(state.alloc) catch |err| { @@ -98,7 +104,7 @@ export fn ghostty_cli_try_action() void { } /// Return metadata about Ghostty, such as version, build mode, etc. -export fn ghostty_info() Info { +pub export fn ghostty_info() Info { return .{ .mode = switch (builtin.mode) { .Debug => .debug, @@ -117,11 +123,11 @@ export fn ghostty_info() Info { /// the function call. /// /// This should only be used for singular strings maintained by Ghostty. -export fn ghostty_translate(msgid: [*:0]const u8) [*:0]const u8 { +pub export fn ghostty_translate(msgid: [*:0]const u8) [*:0]const u8 { return internal_os.i18n._(msgid); } /// Free a string allocated by Ghostty. -export fn ghostty_string_free(str: String) void { +pub export fn ghostty_string_free(str: String) void { state.alloc.free(str.ptr.?[0..str.len]); } From c990d35d6dc7e23e1c72d172cde00e62c3c99389 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 9 Jul 2025 13:22:04 -0700 Subject: [PATCH 10/46] macos: add benchmark tests to our Xcode project --- macos/Ghostty.xcodeproj/project.pbxproj | 158 +++++++++++++++++- .../xcshareddata/xcschemes/Ghostty.xcscheme | 13 ++ macos/GhosttyTests/BenchmarkTests.swift | 32 ++++ pkg/macos/os/signpost.zig | 27 +-- src/benchmark/TerminalStream.zig | 15 +- 5 files changed, 229 insertions(+), 16 deletions(-) create mode 100644 macos/GhosttyTests/BenchmarkTests.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index f6eedd864..f7ae5f525 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 56; + objectVersion = 70; objects = { /* Begin PBXBuildFile section */ @@ -152,6 +152,16 @@ FC9ABA9C2D0F53F80020D4C8 /* bash-completion in Resources */ = {isa = PBXBuildFile; fileRef = FC9ABA9B2D0F538D0020D4C8 /* bash-completion */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + A54F45F72E1F047A0046BD5C /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = A5B30529299BEAAA0047F10C /* Project object */; + proxyType = 1; + remoteGlobalIDString = A5B30530299BEAAA0047F10C; + remoteInfo = Ghostty; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXFileReference section */ 29C15B1C2CDC3B2000520DD4 /* bat */ = {isa = PBXFileReference; lastKnownFileType = folder; name = bat; path = "../zig-out/share/bat"; sourceTree = ""; }; 3B39CAA42B33949B00DABEB8 /* GhosttyReleaseLocal.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GhosttyReleaseLocal.entitlements; sourceTree = ""; }; @@ -199,6 +209,7 @@ A54B0CEC2D0CFB7300CBEFF8 /* ColorizedGhosttyIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorizedGhosttyIcon.swift; sourceTree = ""; }; A54B0CEE2D0D2E2400CBEFF8 /* ColorizedGhosttyIconImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorizedGhosttyIconImage.swift; sourceTree = ""; }; A54D786B2CA79788001B19B1 /* BaseTerminalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseTerminalController.swift; sourceTree = ""; }; + A54F45F32E1F047A0046BD5C /* GhosttyTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GhosttyTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; A553F4122E06EB1600257779 /* Ghostty.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; name = Ghostty.icon; path = ../images/Ghostty.icon; sourceTree = SOURCE_ROOT; }; A5593FDE2DF8D57100B47B10 /* TerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalWindow.swift; sourceTree = ""; }; A5593FE02DF8D73400B47B10 /* HiddenTitlebarTerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HiddenTitlebarTerminalWindow.swift; sourceTree = ""; }; @@ -291,7 +302,18 @@ FC9ABA9B2D0F538D0020D4C8 /* bash-completion */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "bash-completion"; path = "../zig-out/share/bash-completion"; sourceTree = ""; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedRootGroup section */ + A54F45F42E1F047A0046BD5C /* GhosttyTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = GhosttyTests; sourceTree = ""; }; +/* End PBXFileSystemSynchronizedRootGroup section */ + /* Begin PBXFrameworksBuildPhase section */ + A54F45F02E1F047A0046BD5C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; A5B3052E299BEAAA0047F10C /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -590,6 +612,7 @@ A51BFC282B30F26D00E92F16 /* GhosttyDebug.entitlements */, 3B39CAA42B33949B00DABEB8 /* GhosttyReleaseLocal.entitlements */, A54CD6ED299BEB14008C95BB /* Sources */, + A54F45F42E1F047A0046BD5C /* GhosttyTests */, A5D495A3299BECBA00DD1313 /* Frameworks */, A5A1F8862A489D7400D1E8BC /* Resources */, A5B30532299BEAAA0047F10C /* Products */, @@ -601,6 +624,7 @@ children = ( A5B30531299BEAAA0047F10C /* Ghostty.app */, A5D4499D2B53AE7B000F5B83 /* Ghostty-iOS.app */, + A54F45F32E1F047A0046BD5C /* GhosttyTests.xctest */, ); name = Products; sourceTree = ""; @@ -674,6 +698,29 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + A54F45F22E1F047A0046BD5C /* GhosttyTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = A54F45FC2E1F047A0046BD5C /* Build configuration list for PBXNativeTarget "GhosttyTests" */; + buildPhases = ( + A54F45EF2E1F047A0046BD5C /* Sources */, + A54F45F02E1F047A0046BD5C /* Frameworks */, + A54F45F12E1F047A0046BD5C /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + A54F45F82E1F047A0046BD5C /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + A54F45F42E1F047A0046BD5C /* GhosttyTests */, + ); + name = GhosttyTests; + packageProductDependencies = ( + ); + productName = GhosttyTests; + productReference = A54F45F32E1F047A0046BD5C /* GhosttyTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; A5B30530299BEAAA0047F10C /* Ghostty */ = { isa = PBXNativeTarget; buildConfigurationList = A5B30540299BEAAB0047F10C /* Build configuration list for PBXNativeTarget "Ghostty" */; @@ -718,9 +765,13 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1520; + LastSwiftUpdateCheck = 2600; LastUpgradeCheck = 1610; TargetAttributes = { + A54F45F22E1F047A0046BD5C = { + CreatedOnToolsVersion = 26.0; + TestTargetID = A5B30530299BEAAA0047F10C; + }; A5B30530299BEAAA0047F10C = { CreatedOnToolsVersion = 14.2; LastSwiftMigration = 1510; @@ -748,11 +799,19 @@ targets = ( A5B30530299BEAAA0047F10C /* Ghostty */, A5D4499C2B53AE7B000F5B83 /* Ghostty-iOS */, + A54F45F22E1F047A0046BD5C /* GhosttyTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + A54F45F12E1F047A0046BD5C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; A5B3052F299BEAAA0047F10C /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -794,6 +853,13 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + A54F45EF2E1F047A0046BD5C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; A5B3052D299BEAAA0047F10C /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -925,6 +991,14 @@ }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + A54F45F82E1F047A0046BD5C /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = A5B30530299BEAAA0047F10C /* Ghostty */; + targetProxy = A54F45F72E1F047A0046BD5C /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin XCBuildConfiguration section */ 3B39CAA22B33946300DABEB8 /* ReleaseLocal */ = { isa = XCBuildConfiguration; @@ -1034,6 +1108,76 @@ }; name = ReleaseLocal; }; + A54F45F92E1F047A0046BD5C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 15.5; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.mitchellh.GhosttyTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Ghostty.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ghostty"; + }; + name = Debug; + }; + A54F45FA2E1F047A0046BD5C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 15.5; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.mitchellh.GhosttyTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Ghostty.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ghostty"; + }; + name = Release; + }; + A54F45FB2E1F047A0046BD5C /* ReleaseLocal */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 15.5; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.mitchellh.GhosttyTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Ghostty.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ghostty"; + }; + name = ReleaseLocal; + }; A5B3053E299BEAAB0047F10C /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -1378,6 +1522,16 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + A54F45FC2E1F047A0046BD5C /* Build configuration list for PBXNativeTarget "GhosttyTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A54F45F92E1F047A0046BD5C /* Debug */, + A54F45FA2E1F047A0046BD5C /* Release */, + A54F45FB2E1F047A0046BD5C /* ReleaseLocal */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseLocal; + }; A5B3052C299BEAAA0047F10C /* Build configuration list for PBXProject "Ghostty" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/macos/Ghostty.xcodeproj/xcshareddata/xcschemes/Ghostty.xcscheme b/macos/Ghostty.xcodeproj/xcshareddata/xcschemes/Ghostty.xcscheme index 5900042f2..0d8761c9e 100644 --- a/macos/Ghostty.xcodeproj/xcshareddata/xcschemes/Ghostty.xcscheme +++ b/macos/Ghostty.xcodeproj/xcshareddata/xcschemes/Ghostty.xcscheme @@ -28,6 +28,19 @@ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES" shouldAutocreateTestPlan = "YES"> + + + + + + Date: Wed, 9 Jul 2025 14:23:59 -0700 Subject: [PATCH 11/46] benchmark: add codepoint width benchmark --- src/benchmark/CodepointWidth.zig | 204 +++++++++++++++++++++++++++++++ src/benchmark/TerminalStream.zig | 11 +- src/benchmark/cli.zig | 2 + src/benchmark/main.zig | 1 + src/benchmark/options.zig | 20 +++ 5 files changed, 232 insertions(+), 6 deletions(-) create mode 100644 src/benchmark/CodepointWidth.zig create mode 100644 src/benchmark/options.zig diff --git a/src/benchmark/CodepointWidth.zig b/src/benchmark/CodepointWidth.zig new file mode 100644 index 000000000..e9207aed5 --- /dev/null +++ b/src/benchmark/CodepointWidth.zig @@ -0,0 +1,204 @@ +//! This benchmark tests the throughput of codepoint width calculation. +//! This is a common operation in terminal character printing and the +//! motivating factor to write this benchmark was discovering that our +//! codepoint width function was 30% of the runtime of every character +//! print. +const CodepointWidth = @This(); + +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const Benchmark = @import("Benchmark.zig"); +const options = @import("options.zig"); +const UTF8Decoder = @import("../terminal/UTF8Decoder.zig"); +const simd = @import("../simd/main.zig"); +const table = @import("../unicode/main.zig").table; + +const log = std.log.scoped(.@"terminal-stream-bench"); + +opts: Options, + +/// The file, opened in the setup function. +data_f: ?std.fs.File = null, + +pub const Options = struct { + /// The type of codepoint width calculation to use. + mode: Mode = .noop, + + /// The data to read as a filepath. If this is "-" then + /// we will read stdin. If this is unset, then we will + /// do nothing (benchmark is a noop). It'd be more unixy to + /// use stdin by default but I find that a hanging CLI command + /// with no interaction is a bit annoying. + data: ?[]const u8 = null, +}; + +pub const Mode = enum { + /// The baseline mode copies the data from the fd into a buffer. This + /// is used to show the minimal overhead of reading the fd into memory + /// and establishes a baseline for the other modes. + noop, + + /// libc wcwidth + wcwidth, + + /// Our SIMD implementation. + simd, + + /// Test our lookup table implementation. + table, +}; + +/// Create a new terminal stream handler for the given arguments. +pub fn create( + alloc: Allocator, + opts: Options, +) !*CodepointWidth { + const ptr = try alloc.create(CodepointWidth); + errdefer alloc.destroy(ptr); + ptr.* = .{ .opts = opts }; + return ptr; +} + +pub fn destroy(self: *CodepointWidth, alloc: Allocator) void { + alloc.destroy(self); +} + +pub fn benchmark(self: *CodepointWidth) Benchmark { + return .init(self, .{ + .stepFn = switch (self.opts.mode) { + .noop => stepNoop, + .wcwidth => stepWcwidth, + .table => stepTable, + .simd => stepSimd, + }, + .setupFn = setup, + .teardownFn = teardown, + }); +} + +fn setup(ptr: *anyopaque) Benchmark.Error!void { + const self: *CodepointWidth = @ptrCast(@alignCast(ptr)); + + // Open our data file to prepare for reading. We can do more + // validation here eventually. + assert(self.data_f == null); + self.data_f = options.dataFile(self.opts.data) catch |err| { + log.warn("error opening data file err={}", .{err}); + return error.BenchmarkFailed; + }; +} + +fn teardown(ptr: *anyopaque) void { + const self: *CodepointWidth = @ptrCast(@alignCast(ptr)); + if (self.data_f) |f| { + f.close(); + self.data_f = null; + } +} + +fn stepNoop(ptr: *anyopaque) Benchmark.Error!void { + _ = ptr; +} + +extern "c" fn wcwidth(c: u32) c_int; + +fn stepWcwidth(ptr: *anyopaque) Benchmark.Error!void { + const self: *CodepointWidth = @ptrCast(@alignCast(ptr)); + + const f = self.data_f orelse return; + var r = std.io.bufferedReader(f.reader()); + var d: UTF8Decoder = .{}; + var buf: [4096]u8 = undefined; + while (true) { + const n = r.read(&buf) catch |err| { + log.warn("error reading data file err={}", .{err}); + return error.BenchmarkFailed; + }; + if (n == 0) break; // EOF reached + + for (buf[0..n]) |c| { + const cp_, const consumed = d.next(c); + assert(consumed); + if (cp_) |cp| { + const width = wcwidth(cp); + + // Write the width to the buffer to avoid it being compiled + // away + buf[0] = @intCast(width); + } + } + } +} + +fn stepTable(ptr: *anyopaque) Benchmark.Error!void { + const self: *CodepointWidth = @ptrCast(@alignCast(ptr)); + + const f = self.data_f orelse return; + var r = std.io.bufferedReader(f.reader()); + var d: UTF8Decoder = .{}; + var buf: [4096]u8 = undefined; + while (true) { + const n = r.read(&buf) catch |err| { + log.warn("error reading data file err={}", .{err}); + return error.BenchmarkFailed; + }; + if (n == 0) break; // EOF reached + + for (buf[0..n]) |c| { + const cp_, const consumed = d.next(c); + assert(consumed); + if (cp_) |cp| { + // This is the same trick we do in terminal.zig so we + // keep it here. + const width = if (cp <= 0xFF) + 1 + else + table.get(@intCast(cp)).width; + + // Write the width to the buffer to avoid it being compiled + // away + buf[0] = @intCast(width); + } + } + } +} + +fn stepSimd(ptr: *anyopaque) Benchmark.Error!void { + const self: *CodepointWidth = @ptrCast(@alignCast(ptr)); + + const f = self.data_f orelse return; + var r = std.io.bufferedReader(f.reader()); + var d: UTF8Decoder = .{}; + var buf: [4096]u8 = undefined; + while (true) { + const n = r.read(&buf) catch |err| { + log.warn("error reading data file err={}", .{err}); + return error.BenchmarkFailed; + }; + if (n == 0) break; // EOF reached + + for (buf[0..n]) |c| { + const cp_, const consumed = d.next(c); + assert(consumed); + if (cp_) |cp| { + const width = simd.codepointWidth(cp); + + // Write the width to the buffer to avoid it being compiled + // away + buf[0] = @intCast(width); + } + } + } +} + +test CodepointWidth { + const testing = std.testing; + const alloc = testing.allocator; + + const impl: *CodepointWidth = try .create(alloc, .{}); + defer impl.destroy(alloc); + + const bench = impl.benchmark(); + _ = try bench.run(.once); +} diff --git a/src/benchmark/TerminalStream.zig b/src/benchmark/TerminalStream.zig index 3b47fe879..5d235c4ee 100644 --- a/src/benchmark/TerminalStream.zig +++ b/src/benchmark/TerminalStream.zig @@ -18,6 +18,7 @@ const assert = std.debug.assert; const Allocator = std.mem.Allocator; const terminalpkg = @import("../terminal/main.zig"); const Benchmark = @import("Benchmark.zig"); +const options = @import("options.zig"); const Terminal = terminalpkg.Terminal; const Stream = terminalpkg.Stream(*Handler); @@ -89,12 +90,10 @@ fn setup(ptr: *anyopaque) Benchmark.Error!void { // Open our data file to prepare for reading. We can do more // validation here eventually. assert(self.data_f == null); - if (self.opts.data) |path| { - self.data_f = std.fs.cwd().openFile(path, .{}) catch |err| { - log.warn("error opening data file err={}", .{err}); - return error.BenchmarkFailed; - }; - } + self.data_f = options.dataFile(self.opts.data) catch |err| { + log.warn("error opening data file err={}", .{err}); + return error.BenchmarkFailed; + }; } fn teardown(ptr: *anyopaque) void { diff --git a/src/benchmark/cli.zig b/src/benchmark/cli.zig index c0b8dcea6..b35159c6b 100644 --- a/src/benchmark/cli.zig +++ b/src/benchmark/cli.zig @@ -6,6 +6,7 @@ const cli = @import("../cli.zig"); /// benchmarks. pub const Action = enum { @"terminal-stream", + @"codepoint-width", /// Returns the struct associated with the action. The struct /// should have a few decls: @@ -18,6 +19,7 @@ pub const Action = enum { pub fn Struct(comptime action: Action) type { return switch (action) { .@"terminal-stream" => @import("TerminalStream.zig"), + .@"codepoint-width" => @import("CodepointWidth.zig"), }; } }; diff --git a/src/benchmark/main.zig b/src/benchmark/main.zig index 010f11805..dd00f72b5 100644 --- a/src/benchmark/main.zig +++ b/src/benchmark/main.zig @@ -2,6 +2,7 @@ pub const cli = @import("cli.zig"); pub const Benchmark = @import("Benchmark.zig"); pub const CApi = @import("CApi.zig"); pub const TerminalStream = @import("TerminalStream.zig"); +pub const CodepointWidth = @import("CodepointWidth.zig"); test { _ = @import("std").testing.refAllDecls(@This()); diff --git a/src/benchmark/options.zig b/src/benchmark/options.zig new file mode 100644 index 000000000..867be6afc --- /dev/null +++ b/src/benchmark/options.zig @@ -0,0 +1,20 @@ +//! This file contains helpers for CLI options. + +const std = @import("std"); + +/// Returns the data file for the given path in a way that is consistent +/// across our CLI. If the path is not set then no file is returned. +/// If the path is "-", then we will return stdin. If the path is +/// a file then we will open and return the handle. +pub fn dataFile(path_: ?[]const u8) !?std.fs.File { + const path = path_ orelse return null; + + // Stdin + if (std.mem.eql(u8, path, "-")) return std.io.getStdIn(); + + // Normal file + const file = try std.fs.cwd().openFile(path, .{}); + errdefer file.close(); + + return file; +} From 99ed984af2a12340a7b5b17326bc037044524ba4 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 9 Jul 2025 14:31:47 -0700 Subject: [PATCH 12/46] benchmark: add GraphemeBreak and TerminalParser benchmarks --- src/benchmark/GraphemeBreak.zig | 146 +++++++++++++++++++++++++++++++ src/benchmark/TerminalParser.zig | 106 ++++++++++++++++++++++ src/benchmark/cli.zig | 4 + src/benchmark/main.zig | 2 + 4 files changed, 258 insertions(+) create mode 100644 src/benchmark/GraphemeBreak.zig create mode 100644 src/benchmark/TerminalParser.zig diff --git a/src/benchmark/GraphemeBreak.zig b/src/benchmark/GraphemeBreak.zig new file mode 100644 index 000000000..57effebe4 --- /dev/null +++ b/src/benchmark/GraphemeBreak.zig @@ -0,0 +1,146 @@ +//! This benchmark tests the throughput of grapheme break calculation. +//! This is a common operation in terminal character printing for terminals +//! that support grapheme clustering. +const GraphemeBreak = @This(); + +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const Benchmark = @import("Benchmark.zig"); +const options = @import("options.zig"); +const UTF8Decoder = @import("../terminal/UTF8Decoder.zig"); +const unicode = @import("../unicode/main.zig"); + +const log = std.log.scoped(.@"terminal-stream-bench"); + +opts: Options, + +/// The file, opened in the setup function. +data_f: ?std.fs.File = null, + +pub const Options = struct { + /// The type of codepoint width calculation to use. + mode: Mode = .table, + + /// The data to read as a filepath. If this is "-" then + /// we will read stdin. If this is unset, then we will + /// do nothing (benchmark is a noop). It'd be more unixy to + /// use stdin by default but I find that a hanging CLI command + /// with no interaction is a bit annoying. + data: ?[]const u8 = null, +}; + +pub const Mode = enum { + /// The baseline mode copies the data from the fd into a buffer. This + /// is used to show the minimal overhead of reading the fd into memory + /// and establishes a baseline for the other modes. + noop, + + /// Ghostty's table-based approach. + table, +}; + +/// Create a new terminal stream handler for the given arguments. +pub fn create( + alloc: Allocator, + opts: Options, +) !*GraphemeBreak { + const ptr = try alloc.create(GraphemeBreak); + errdefer alloc.destroy(ptr); + ptr.* = .{ .opts = opts }; + return ptr; +} + +pub fn destroy(self: *GraphemeBreak, alloc: Allocator) void { + alloc.destroy(self); +} + +pub fn benchmark(self: *GraphemeBreak) Benchmark { + return .init(self, .{ + .stepFn = switch (self.opts.mode) { + .noop => stepNoop, + .table => stepTable, + }, + .setupFn = setup, + .teardownFn = teardown, + }); +} + +fn setup(ptr: *anyopaque) Benchmark.Error!void { + const self: *GraphemeBreak = @ptrCast(@alignCast(ptr)); + + // Open our data file to prepare for reading. We can do more + // validation here eventually. + assert(self.data_f == null); + self.data_f = options.dataFile(self.opts.data) catch |err| { + log.warn("error opening data file err={}", .{err}); + return error.BenchmarkFailed; + }; +} + +fn teardown(ptr: *anyopaque) void { + const self: *GraphemeBreak = @ptrCast(@alignCast(ptr)); + if (self.data_f) |f| { + f.close(); + self.data_f = null; + } +} + +fn stepNoop(ptr: *anyopaque) Benchmark.Error!void { + const self: *GraphemeBreak = @ptrCast(@alignCast(ptr)); + + const f = self.data_f orelse return; + var r = std.io.bufferedReader(f.reader()); + var d: UTF8Decoder = .{}; + var buf: [4096]u8 = undefined; + while (true) { + const n = r.read(&buf) catch |err| { + log.warn("error reading data file err={}", .{err}); + return error.BenchmarkFailed; + }; + if (n == 0) break; // EOF reached + + for (buf[0..n]) |c| { + _ = d.next(c); + } + } +} + +fn stepTable(ptr: *anyopaque) Benchmark.Error!void { + const self: *GraphemeBreak = @ptrCast(@alignCast(ptr)); + + const f = self.data_f orelse return; + var r = std.io.bufferedReader(f.reader()); + var d: UTF8Decoder = .{}; + var state: unicode.GraphemeBreakState = .{}; + var cp1: u21 = 0; + var buf: [4096]u8 = undefined; + while (true) { + const n = r.read(&buf) catch |err| { + log.warn("error reading data file err={}", .{err}); + return error.BenchmarkFailed; + }; + if (n == 0) break; // EOF reached + + for (buf[0..n]) |c| { + const cp_, const consumed = d.next(c); + assert(consumed); + if (cp_) |cp2| { + const v = unicode.graphemeBreak(cp1, @intCast(cp2), &state); + buf[0] = @intCast(@intFromBool(v)); + cp1 = cp2; + } + } + } +} + +test GraphemeBreak { + const testing = std.testing; + const alloc = testing.allocator; + + const impl: *GraphemeBreak = try .create(alloc, .{}); + defer impl.destroy(alloc); + + const bench = impl.benchmark(); + _ = try bench.run(.once); +} diff --git a/src/benchmark/TerminalParser.zig b/src/benchmark/TerminalParser.zig new file mode 100644 index 000000000..9107d4555 --- /dev/null +++ b/src/benchmark/TerminalParser.zig @@ -0,0 +1,106 @@ +//! This benchmark tests the throughput of the terminal escape code parser. +const TerminalParser = @This(); + +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const terminalpkg = @import("../terminal/main.zig"); +const Benchmark = @import("Benchmark.zig"); +const options = @import("options.zig"); + +const log = std.log.scoped(.@"terminal-stream-bench"); + +opts: Options, + +/// The file, opened in the setup function. +data_f: ?std.fs.File = null, + +pub const Options = struct { + /// The data to read as a filepath. If this is "-" then + /// we will read stdin. If this is unset, then we will + /// do nothing (benchmark is a noop). It'd be more unixy to + /// use stdin by default but I find that a hanging CLI command + /// with no interaction is a bit annoying. + data: ?[]const u8 = null, +}; + +pub fn create( + alloc: Allocator, + opts: Options, +) !*TerminalParser { + const ptr = try alloc.create(TerminalParser); + errdefer alloc.destroy(ptr); + ptr.* = .{ .opts = opts }; + return ptr; +} + +pub fn destroy(self: *TerminalParser, alloc: Allocator) void { + alloc.destroy(self); +} + +pub fn benchmark(self: *TerminalParser) Benchmark { + return .init(self, .{ + .stepFn = step, + .setupFn = setup, + .teardownFn = teardown, + }); +} + +fn setup(ptr: *anyopaque) Benchmark.Error!void { + const self: *TerminalParser = @ptrCast(@alignCast(ptr)); + + // Open our data file to prepare for reading. We can do more + // validation here eventually. + assert(self.data_f == null); + self.data_f = options.dataFile(self.opts.data) catch |err| { + log.warn("error opening data file err={}", .{err}); + return error.BenchmarkFailed; + }; +} + +fn teardown(ptr: *anyopaque) void { + const self: *TerminalParser = @ptrCast(@alignCast(ptr)); + if (self.data_f) |f| { + f.close(); + self.data_f = null; + } +} + +fn step(ptr: *anyopaque) Benchmark.Error!void { + const self: *TerminalParser = @ptrCast(@alignCast(ptr)); + + // Get our buffered reader so we're not predominantly + // waiting on file IO. It'd be better to move this fully into + // memory. If we're IO bound though that should show up on + // the benchmark results and... I know writing this that we + // aren't currently IO bound. + const f = self.data_f orelse return; + var r = std.io.bufferedReader(f.reader()); + + var p: terminalpkg.Parser = .{}; + + var buf: [4096]u8 = undefined; + while (true) { + const n = r.read(&buf) catch |err| { + log.warn("error reading data file err={}", .{err}); + return error.BenchmarkFailed; + }; + if (n == 0) break; // EOF reached + for (buf[0..n]) |c| { + const actions = p.next(c); + //std.log.warn("actions={any}", .{actions}); + _ = actions; + } + } +} + +test TerminalParser { + const testing = std.testing; + const alloc = testing.allocator; + + const impl: *TerminalParser = try .create(alloc, .{}); + defer impl.destroy(alloc); + + const bench = impl.benchmark(); + _ = try bench.run(.once); +} diff --git a/src/benchmark/cli.zig b/src/benchmark/cli.zig index b35159c6b..3f59b4a72 100644 --- a/src/benchmark/cli.zig +++ b/src/benchmark/cli.zig @@ -7,6 +7,8 @@ const cli = @import("../cli.zig"); pub const Action = enum { @"terminal-stream", @"codepoint-width", + @"grapheme-break", + @"terminal-parser", /// Returns the struct associated with the action. The struct /// should have a few decls: @@ -20,6 +22,8 @@ pub const Action = enum { return switch (action) { .@"terminal-stream" => @import("TerminalStream.zig"), .@"codepoint-width" => @import("CodepointWidth.zig"), + .@"grapheme-break" => @import("GraphemeBreak.zig"), + .@"terminal-parser" => @import("TerminalParser.zig"), }; } }; diff --git a/src/benchmark/main.zig b/src/benchmark/main.zig index dd00f72b5..56c515c9d 100644 --- a/src/benchmark/main.zig +++ b/src/benchmark/main.zig @@ -3,6 +3,8 @@ pub const Benchmark = @import("Benchmark.zig"); pub const CApi = @import("CApi.zig"); pub const TerminalStream = @import("TerminalStream.zig"); pub const CodepointWidth = @import("CodepointWidth.zig"); +pub const GraphemeBreak = @import("GraphemeBreak.zig"); +pub const TerminalParser = @import("TerminalParser.zig"); test { _ = @import("std").testing.refAllDecls(@This()); From b5ff0442d436aad2c230ce40e898dec8584fbc3a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 9 Jul 2025 14:32:34 -0700 Subject: [PATCH 13/46] bench: remove old benchmarks we converted --- src/bench/codepoint-width.sh | 34 ------ src/bench/codepoint-width.zig | 204 ---------------------------------- src/bench/grapheme-break.sh | 33 ------ src/bench/grapheme-break.zig | 144 ------------------------ src/bench/page-init.sh | 16 --- src/bench/page-init.zig | 78 ------------- src/bench/parser.zig | 71 ------------ src/build/Config.zig | 4 - src/main.zig | 4 - 9 files changed, 588 deletions(-) delete mode 100755 src/bench/codepoint-width.sh delete mode 100644 src/bench/codepoint-width.zig delete mode 100755 src/bench/grapheme-break.sh delete mode 100644 src/bench/grapheme-break.zig delete mode 100755 src/bench/page-init.sh delete mode 100644 src/bench/page-init.zig delete mode 100644 src/bench/parser.zig diff --git a/src/bench/codepoint-width.sh b/src/bench/codepoint-width.sh deleted file mode 100755 index 43304ec2e..000000000 --- a/src/bench/codepoint-width.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env bash -# -# This is a trivial helper script to help run the codepoint-width benchmark. -# You probably want to tweak this script depending on what you're -# trying to measure. - -# Options: -# - "ascii", uniform random ASCII bytes -# - "utf8", uniform random unicode characters, encoded as utf8 -# - "rand", pure random data, will contain many invalid code sequences. -DATA="utf8" -SIZE="25000000" - -# Add additional arguments -ARGS="" - -# Generate the benchmark input ahead of time so it's not included in the time. -./zig-out/bin/bench-stream --mode=gen-$DATA | head -c $SIZE > /tmp/ghostty_bench_data -#cat ~/Downloads/JAPANESEBIBLE.txt > /tmp/ghostty_bench_data - -# Uncomment to instead use the contents of `stream.txt` as input. -# yes $(cat ./stream.txt) | head -c $SIZE > /tmp/ghostty_bench_data - -hyperfine \ - --warmup 10 \ - -n noop \ - "./zig-out/bin/bench-codepoint-width --mode=noop${ARGS} try benchNoop(reader, buf), - .wcwidth => try benchWcwidth(reader, buf), - .ziglyph => try benchZiglyph(reader, buf), - .simd => try benchSimd(reader, buf), - .table => try benchTable(reader, buf), - } -} - -noinline fn benchNoop( - reader: anytype, - buf: []u8, -) !void { - var d: UTF8Decoder = .{}; - while (true) { - const n = try reader.read(buf); - if (n == 0) break; - - // Using stream.next directly with a for loop applies a naive - // scalar approach. - for (buf[0..n]) |c| { - _ = d.next(c); - } - } -} - -extern "c" fn wcwidth(c: u32) c_int; - -noinline fn benchWcwidth( - reader: anytype, - buf: []u8, -) !void { - var d: UTF8Decoder = .{}; - while (true) { - const n = try reader.read(buf); - if (n == 0) break; - - // Using stream.next directly with a for loop applies a naive - // scalar approach. - for (buf[0..n]) |c| { - const cp_, const consumed = d.next(c); - assert(consumed); - if (cp_) |cp| { - const width = wcwidth(cp); - - // Write the width to the buffer to avoid it being compiled away - buf[0] = @intCast(width); - } - } - } -} - -noinline fn benchTable( - reader: anytype, - buf: []u8, -) !void { - var d: UTF8Decoder = .{}; - while (true) { - const n = try reader.read(buf); - if (n == 0) break; - - // Using stream.next directly with a for loop applies a naive - // scalar approach. - for (buf[0..n]) |c| { - const cp_, const consumed = d.next(c); - assert(consumed); - if (cp_) |cp| { - // This is the same trick we do in terminal.zig so we - // keep it here. - const width = if (cp <= 0xFF) 1 else table.get(@intCast(cp)).width; - - // Write the width to the buffer to avoid it being compiled away - buf[0] = @intCast(width); - } - } - } -} - -noinline fn benchZiglyph( - reader: anytype, - buf: []u8, -) !void { - var d: UTF8Decoder = .{}; - while (true) { - const n = try reader.read(buf); - if (n == 0) break; - - // Using stream.next directly with a for loop applies a naive - // scalar approach. - for (buf[0..n]) |c| { - const cp_, const consumed = d.next(c); - assert(consumed); - if (cp_) |cp| { - const width = ziglyph.display_width.codePointWidth(cp, .half); - - // Write the width to the buffer to avoid it being compiled away - buf[0] = @intCast(width); - } - } - } -} - -noinline fn benchSimd( - reader: anytype, - buf: []u8, -) !void { - var d: UTF8Decoder = .{}; - while (true) { - const n = try reader.read(buf); - if (n == 0) break; - - // Using stream.next directly with a for loop applies a naive - // scalar approach. - for (buf[0..n]) |c| { - const cp_, const consumed = d.next(c); - assert(consumed); - if (cp_) |cp| { - const width = simd.codepointWidth(cp); - - // Write the width to the buffer to avoid it being compiled away - buf[0] = @intCast(width); - } - } - } -} diff --git a/src/bench/grapheme-break.sh b/src/bench/grapheme-break.sh deleted file mode 100755 index 24f475caa..000000000 --- a/src/bench/grapheme-break.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env bash -# -# This is a trivial helper script to help run the grapheme-break benchmark. -# You probably want to tweak this script depending on what you're -# trying to measure. - -# Options: -# - "ascii", uniform random ASCII bytes -# - "utf8", uniform random unicode characters, encoded as utf8 -# - "rand", pure random data, will contain many invalid code sequences. -DATA="utf8" -SIZE="25000000" - -# Add additional arguments -ARGS="" - -# Generate the benchmark input ahead of time so it's not included in the time. -./zig-out/bin/bench-stream --mode=gen-$DATA | head -c $SIZE > /tmp/ghostty_bench_data -#cat ~/Downloads/JAPANESEBIBLE.txt > /tmp/ghostty_bench_data - -# Uncomment to instead use the contents of `stream.txt` as input. -# yes $(cat ./stream.txt) | head -c $SIZE > /tmp/ghostty_bench_data - -hyperfine \ - --warmup 10 \ - -n noop \ - "./zig-out/bin/bench-grapheme-break --mode=noop${ARGS} try benchNoop(reader, buf), - .ziglyph => try benchZiglyph(reader, buf), - .table => try benchTable(reader, buf), - } -} - -noinline fn benchNoop( - reader: anytype, - buf: []u8, -) !void { - var d: UTF8Decoder = .{}; - while (true) { - const n = try reader.read(buf); - if (n == 0) break; - - // Using stream.next directly with a for loop applies a naive - // scalar approach. - for (buf[0..n]) |c| { - _ = d.next(c); - } - } -} - -noinline fn benchTable( - reader: anytype, - buf: []u8, -) !void { - var d: UTF8Decoder = .{}; - var state: unicode.GraphemeBreakState = .{}; - var cp1: u21 = 0; - while (true) { - const n = try reader.read(buf); - if (n == 0) break; - - // Using stream.next directly with a for loop applies a naive - // scalar approach. - for (buf[0..n]) |c| { - const cp_, const consumed = d.next(c); - assert(consumed); - if (cp_) |cp2| { - const v = unicode.graphemeBreak(cp1, @intCast(cp2), &state); - buf[0] = @intCast(@intFromBool(v)); - cp1 = cp2; - } - } - } -} - -noinline fn benchZiglyph( - reader: anytype, - buf: []u8, -) !void { - var d: UTF8Decoder = .{}; - var state: u3 = 0; - var cp1: u21 = 0; - while (true) { - const n = try reader.read(buf); - if (n == 0) break; - - // Using stream.next directly with a for loop applies a naive - // scalar approach. - for (buf[0..n]) |c| { - const cp_, const consumed = d.next(c); - assert(consumed); - if (cp_) |cp2| { - const v = ziglyph.graphemeBreak(cp1, @intCast(cp2), &state); - buf[0] = @intCast(@intFromBool(v)); - cp1 = cp2; - } - } - } -} diff --git a/src/bench/page-init.sh b/src/bench/page-init.sh deleted file mode 100755 index 54712250b..000000000 --- a/src/bench/page-init.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash -# -# This is a trivial helper script to help run the page init benchmark. -# You probably want to tweak this script depending on what you're -# trying to measure. - -# Uncomment to test with an active terminal state. -# ARGS=" --terminal" - -hyperfine \ - --warmup 10 \ - -n alloc \ - "./zig-out/bin/bench-page-init --mode=alloc${ARGS} try benchAlloc(args.count), - .pool => try benchPool(alloc, args.count), - } -} - -noinline fn benchAlloc(count: usize) !void { - for (0..count) |_| { - _ = try terminal_new.Page.init(terminal_new.page.std_capacity); - } -} - -noinline fn benchPool(alloc: Allocator, count: usize) !void { - var list = try terminal_new.PageList.init( - alloc, - terminal_new.page.std_capacity.cols, - terminal_new.page.std_capacity.rows, - 0, - ); - defer list.deinit(); - - for (0..count) |_| { - _ = try list.grow(); - } -} diff --git a/src/bench/parser.zig b/src/bench/parser.zig deleted file mode 100644 index 9245c06cb..000000000 --- a/src/bench/parser.zig +++ /dev/null @@ -1,71 +0,0 @@ -//! This benchmark tests the throughput of the terminal escape code parser. -//! -//! To benchmark, this takes an input stream (which is expected to come in -//! as fast as possible), runs it through the parser, and does nothing -//! with the parse result. This bottlenecks and tests the throughput of the -//! parser. -//! -//! Usage: -//! -//! "--f=" - A file to read to parse. If path is "-" then stdin -//! is read. Required. -//! - -const std = @import("std"); -const ArenaAllocator = std.heap.ArenaAllocator; -const cli = @import("../cli.zig"); -const terminal = @import("../terminal/main.zig"); - -pub fn main() !void { - // Just use a GPA - const GPA = std.heap.GeneralPurposeAllocator(.{}); - var gpa = GPA{}; - defer _ = gpa.deinit(); - const alloc = gpa.allocator(); - - // Parse our args - var args: Args = args: { - var args: Args = .{}; - errdefer args.deinit(); - var iter = try cli.args.argsIterator(alloc); - defer iter.deinit(); - try cli.args.parse(Args, alloc, &args, &iter); - break :args args; - }; - defer args.deinit(); - - // Read the file for our input - const file = file: { - if (std.mem.eql(u8, args.f, "-")) - break :file std.io.getStdIn(); - - @panic("file reading not implemented yet"); - }; - - // Read all into memory (TODO: support buffers one day) - const input = try file.reader().readAllAlloc( - alloc, - 1024 * 1024 * 1024 * 1024 * 16, // 16 GB - ); - defer alloc.free(input); - - // Run our parser - var p: terminal.Parser = .{}; - for (input) |c| { - const actions = p.next(c); - //std.log.warn("actions={any}", .{actions}); - _ = actions; - } -} - -const Args = struct { - f: []const u8 = "-", - - /// This is set by the CLI parser for deinit. - _arena: ?ArenaAllocator = null, - - pub fn deinit(self: *Args) void { - if (self._arena) |arena| arena.deinit(); - self.* = undefined; - } -}; diff --git a/src/build/Config.zig b/src/build/Config.zig index a9a79fb53..88ea8d6ac 100644 --- a/src/build/Config.zig +++ b/src/build/Config.zig @@ -528,11 +528,7 @@ pub const ExeEntrypoint = enum { webgen_config, webgen_actions, webgen_commands, - bench_parser, bench_stream, - bench_codepoint_width, - bench_grapheme_break, - bench_page_init, }; /// The release channel for the build. diff --git a/src/main.zig b/src/main.zig index 121a3b7d2..25e723c92 100644 --- a/src/main.zig +++ b/src/main.zig @@ -10,11 +10,7 @@ const entrypoint = switch (build_config.exe_entrypoint) { .webgen_config => @import("build/webgen/main_config.zig"), .webgen_actions => @import("build/webgen/main_actions.zig"), .webgen_commands => @import("build/webgen/main_commands.zig"), - .bench_parser => @import("bench/parser.zig"), .bench_stream => @import("bench/stream.zig"), - .bench_codepoint_width => @import("bench/codepoint-width.zig"), - .bench_grapheme_break => @import("bench/grapheme-break.zig"), - .bench_page_init => @import("bench/page-init.zig"), }; /// The main entrypoint for the program. From a28b7e9205b8d002119926e3251169167be595ac Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 9 Jul 2025 14:42:19 -0700 Subject: [PATCH 14/46] synthetic cli (ghostty-gen) --- src/benchmark/cli.zig | 5 +- src/benchmark/main.zig | 2 +- src/build/GhosttyBench.zig | 19 +++++++- src/main_gen.zig | 5 ++ src/synthetic/cli.zig | 95 +++++++++++++++++++++++++++++++++++++ src/synthetic/cli/Ascii.zig | 53 +++++++++++++++++++++ src/synthetic/main.zig | 2 + 7 files changed, 177 insertions(+), 4 deletions(-) create mode 100644 src/main_gen.zig create mode 100644 src/synthetic/cli.zig create mode 100644 src/synthetic/cli/Ascii.zig diff --git a/src/benchmark/cli.zig b/src/benchmark/cli.zig index 3f59b4a72..97bb9c683 100644 --- a/src/benchmark/cli.zig +++ b/src/benchmark/cli.zig @@ -3,12 +3,13 @@ const Allocator = std.mem.Allocator; const cli = @import("../cli.zig"); /// The available actions for the CLI. This is the list of available -/// benchmarks. +/// benchmarks. View docs for each individual one in the predictably +/// named files. pub const Action = enum { - @"terminal-stream", @"codepoint-width", @"grapheme-break", @"terminal-parser", + @"terminal-stream", /// Returns the struct associated with the action. The struct /// should have a few decls: diff --git a/src/benchmark/main.zig b/src/benchmark/main.zig index 56c515c9d..49bb17289 100644 --- a/src/benchmark/main.zig +++ b/src/benchmark/main.zig @@ -7,5 +7,5 @@ pub const GraphemeBreak = @import("GraphemeBreak.zig"); pub const TerminalParser = @import("TerminalParser.zig"); test { - _ = @import("std").testing.refAllDecls(@This()); + @import("std").testing.refAllDecls(@This()); } diff --git a/src/build/GhosttyBench.zig b/src/build/GhosttyBench.zig index 0dc18aa4d..0588bba3c 100644 --- a/src/build/GhosttyBench.zig +++ b/src/build/GhosttyBench.zig @@ -14,7 +14,24 @@ pub fn init( var steps = std.ArrayList(*std.Build.Step.Compile).init(b.allocator); errdefer steps.deinit(); - // Our new benchmarking application. + // Our synthetic data generator + { + const exe = b.addExecutable(.{ + .name = "ghostty-gen", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main_gen.zig"), + .target = deps.config.target, + // We always want our datagen to be fast because it + // takes awhile to run. + .optimize = .ReleaseFast, + }), + }); + exe.linkLibC(); + _ = try deps.add(exe); + try steps.append(exe); + } + + // Our benchmarking application. { const exe = b.addExecutable(.{ .name = "ghostty-bench", diff --git a/src/main_gen.zig b/src/main_gen.zig new file mode 100644 index 000000000..b988819f8 --- /dev/null +++ b/src/main_gen.zig @@ -0,0 +1,5 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const synthetic = @import("synthetic/main.zig"); + +pub const main = synthetic.cli.main; diff --git a/src/synthetic/cli.zig b/src/synthetic/cli.zig new file mode 100644 index 000000000..7cb2e68d2 --- /dev/null +++ b/src/synthetic/cli.zig @@ -0,0 +1,95 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const cli = @import("../cli.zig"); + +/// The available actions for the CLI. This is the list of available +/// synthetic generators. View docs for each individual one in the +/// predictably named files under `cli/`. +pub const Action = enum { + ascii, + + /// Returns the struct associated with the action. The struct + /// should have a few decls: + /// + /// - `const Options`: The CLI options for the action. + /// - `fn create`: Create a new instance of the action from options. + /// - `fn destroy`: Destroy the instance of the action. + /// + /// See TerminalStream for an example. + pub fn Struct(comptime action: Action) type { + return switch (action) { + .ascii => @import("cli/Ascii.zig"), + }; + } +}; + +/// An entrypoint for the synthetic generator CLI. +pub fn main() !void { + const alloc = std.heap.c_allocator; + const action_ = try cli.action.detectArgs(Action, alloc); + const action = action_ orelse return error.NoAction; + try mainAction(alloc, action, .cli); +} + +pub const Args = union(enum) { + /// The arguments passed to the CLI via argc/argv. + cli, + + /// Simple string arguments, parsed via std.process.ArgIteratorGeneral. + string: []const u8, +}; + +pub fn mainAction( + alloc: Allocator, + action: Action, + args: Args, +) !void { + switch (action) { + inline else => |comptime_action| { + const Impl = Action.Struct(comptime_action); + try mainActionImpl(Impl, alloc, args); + }, + } +} + +fn mainActionImpl( + comptime Impl: type, + alloc: Allocator, + args: Args, +) !void { + // First, parse our CLI options. + const Options = Impl.Options; + var opts: Options = .{}; + defer if (@hasDecl(Options, "deinit")) opts.deinit(); + switch (args) { + .cli => { + var iter = try cli.args.argsIterator(alloc); + defer iter.deinit(); + try cli.args.parse(Options, alloc, &opts, &iter); + }, + .string => |str| { + var iter = try std.process.ArgIteratorGeneral(.{}).init( + alloc, + str, + ); + defer iter.deinit(); + try cli.args.parse(Options, alloc, &opts, &iter); + }, + } + + // TODO: Make this a command line option. + const seed: u64 = @truncate(@as( + u128, + @bitCast(std.time.nanoTimestamp()), + )); + var prng = std.Random.DefaultPrng.init(seed); + const rand = prng.random(); + + // Our output always goes to stdout. + const writer = std.io.getStdOut().writer(); + + // Create our implementation + const impl = try Impl.create(alloc, opts); + defer impl.destroy(alloc); + try impl.run(writer, rand); +} diff --git a/src/synthetic/cli/Ascii.zig b/src/synthetic/cli/Ascii.zig new file mode 100644 index 000000000..f294be2e0 --- /dev/null +++ b/src/synthetic/cli/Ascii.zig @@ -0,0 +1,53 @@ +//! This benchmark tests the throughput of grapheme break calculation. +//! This is a common operation in terminal character printing for terminals +//! that support grapheme clustering. +const Ascii = @This(); + +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const synthetic = @import("../main.zig"); + +const log = std.log.scoped(.@"terminal-stream-bench"); + +pub const Options = struct {}; + +/// Create a new terminal stream handler for the given arguments. +pub fn create( + alloc: Allocator, + _: Options, +) !*Ascii { + const ptr = try alloc.create(Ascii); + errdefer alloc.destroy(ptr); + return ptr; +} + +pub fn destroy(self: *Ascii, alloc: Allocator) void { + alloc.destroy(self); +} + +pub fn run(self: *Ascii, writer: anytype, rand: std.Random) !void { + _ = self; + + var gen: synthetic.Bytes = .{ + .rand = rand, + .alphabet = synthetic.Bytes.Alphabet.ascii, + }; + + var buf: [1024]u8 = undefined; + while (true) { + const data = try gen.next(&buf); + writer.writeAll(data) catch |err| switch (err) { + error.BrokenPipe => return, // stdout closed + else => return err, + }; + } +} + +test Ascii { + const testing = std.testing; + const alloc = testing.allocator; + + const impl: *Ascii = try .create(alloc, .{}); + defer impl.destroy(alloc); +} diff --git a/src/synthetic/main.zig b/src/synthetic/main.zig index 67cd47054..85f9f7d35 100644 --- a/src/synthetic/main.zig +++ b/src/synthetic/main.zig @@ -13,6 +13,8 @@ //! is not limited to that and we may want to extract this to a //! standalone package one day. +pub const cli = @import("cli.zig"); + pub const Generator = @import("Generator.zig"); pub const Bytes = @import("Bytes.zig"); pub const Utf8 = @import("Utf8.zig"); From a09452bf1bfda1f719a3924a0b8cb9f9c4f87f8e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 9 Jul 2025 15:00:48 -0700 Subject: [PATCH 15/46] synthetic: add osc/utf8 generators --- src/synthetic/cli.zig | 13 +++++++ src/synthetic/cli/Ascii.zig | 22 ++++++++---- src/synthetic/cli/Osc.zig | 67 +++++++++++++++++++++++++++++++++++++ src/synthetic/cli/Utf8.zig | 62 ++++++++++++++++++++++++++++++++++ 4 files changed, 158 insertions(+), 6 deletions(-) create mode 100644 src/synthetic/cli/Osc.zig create mode 100644 src/synthetic/cli/Utf8.zig diff --git a/src/synthetic/cli.zig b/src/synthetic/cli.zig index 7cb2e68d2..36832587c 100644 --- a/src/synthetic/cli.zig +++ b/src/synthetic/cli.zig @@ -7,6 +7,8 @@ const cli = @import("../cli.zig"); /// predictably named files under `cli/`. pub const Action = enum { ascii, + osc, + utf8, /// Returns the struct associated with the action. The struct /// should have a few decls: @@ -19,6 +21,8 @@ pub const Action = enum { pub fn Struct(comptime action: Action) type { return switch (action) { .ascii => @import("cli/Ascii.zig"), + .osc => @import("cli/Osc.zig"), + .utf8 => @import("cli/Utf8.zig"), }; } }; @@ -93,3 +97,12 @@ fn mainActionImpl( defer impl.destroy(alloc); try impl.run(writer, rand); } + +test { + // Make sure we ref all our actions + inline for (@typeInfo(Action).@"enum".fields) |field| { + const action = @field(Action, field.name); + const Impl = Action.Struct(action); + _ = Impl; + } +} diff --git a/src/synthetic/cli/Ascii.zig b/src/synthetic/cli/Ascii.zig index f294be2e0..25e5bb00b 100644 --- a/src/synthetic/cli/Ascii.zig +++ b/src/synthetic/cli/Ascii.zig @@ -1,6 +1,3 @@ -//! This benchmark tests the throughput of grapheme break calculation. -//! This is a common operation in terminal character printing for terminals -//! that support grapheme clustering. const Ascii = @This(); const std = @import("std"); @@ -37,9 +34,13 @@ pub fn run(self: *Ascii, writer: anytype, rand: std.Random) !void { var buf: [1024]u8 = undefined; while (true) { const data = try gen.next(&buf); - writer.writeAll(data) catch |err| switch (err) { - error.BrokenPipe => return, // stdout closed - else => return err, + writer.writeAll(data) catch |err| { + const Error = error{ NoSpaceLeft, BrokenPipe } || @TypeOf(err); + switch (@as(Error, err)) { + error.BrokenPipe => return, // stdout closed + error.NoSpaceLeft => return, // fixed buffer full + else => return err, + } }; } } @@ -50,4 +51,13 @@ test Ascii { const impl: *Ascii = try .create(alloc, .{}); defer impl.destroy(alloc); + + var prng = std.Random.DefaultPrng.init(1); + const rand = prng.random(); + + var buf: [1024]u8 = undefined; + var fbs = std.io.fixedBufferStream(&buf); + const writer = fbs.writer(); + + try impl.run(writer, rand); } diff --git a/src/synthetic/cli/Osc.zig b/src/synthetic/cli/Osc.zig new file mode 100644 index 000000000..4792cda6b --- /dev/null +++ b/src/synthetic/cli/Osc.zig @@ -0,0 +1,67 @@ +const Osc = @This(); + +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const synthetic = @import("../main.zig"); + +const log = std.log.scoped(.@"terminal-stream-bench"); + +pub const Options = struct { + /// Probability of generating a valid value. + @"p-valid": f64 = 0.5, +}; + +opts: Options, + +/// Create a new terminal stream handler for the given arguments. +pub fn create( + alloc: Allocator, + opts: Options, +) !*Osc { + const ptr = try alloc.create(Osc); + errdefer alloc.destroy(ptr); + ptr.* = .{ .opts = opts }; + return ptr; +} + +pub fn destroy(self: *Osc, alloc: Allocator) void { + alloc.destroy(self); +} + +pub fn run(self: *Osc, writer: anytype, rand: std.Random) !void { + var gen: synthetic.Osc = .{ + .rand = rand, + .p_valid = self.opts.@"p-valid", + }; + + var buf: [1024]u8 = undefined; + while (true) { + const data = try gen.next(&buf); + writer.writeAll(data) catch |err| { + const Error = error{ NoSpaceLeft, BrokenPipe } || @TypeOf(err); + switch (@as(Error, err)) { + error.BrokenPipe => return, // stdout closed + error.NoSpaceLeft => return, // fixed buffer full + else => return err, + } + }; + } +} + +test Osc { + const testing = std.testing; + const alloc = testing.allocator; + + const impl: *Osc = try .create(alloc, .{}); + defer impl.destroy(alloc); + + var prng = std.Random.DefaultPrng.init(1); + const rand = prng.random(); + + var buf: [1024]u8 = undefined; + var fbs = std.io.fixedBufferStream(&buf); + const writer = fbs.writer(); + + try impl.run(writer, rand); +} diff --git a/src/synthetic/cli/Utf8.zig b/src/synthetic/cli/Utf8.zig new file mode 100644 index 000000000..28a11f891 --- /dev/null +++ b/src/synthetic/cli/Utf8.zig @@ -0,0 +1,62 @@ +const Utf8 = @This(); + +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const synthetic = @import("../main.zig"); + +const log = std.log.scoped(.@"terminal-stream-bench"); + +pub const Options = struct {}; + +/// Create a new terminal stream handler for the given arguments. +pub fn create( + alloc: Allocator, + _: Options, +) !*Utf8 { + const ptr = try alloc.create(Utf8); + errdefer alloc.destroy(ptr); + return ptr; +} + +pub fn destroy(self: *Utf8, alloc: Allocator) void { + alloc.destroy(self); +} + +pub fn run(self: *Utf8, writer: anytype, rand: std.Random) !void { + _ = self; + + var gen: synthetic.Utf8 = .{ + .rand = rand, + }; + + var buf: [1024]u8 = undefined; + while (true) { + const data = try gen.next(&buf); + writer.writeAll(data) catch |err| { + const Error = error{ NoSpaceLeft, BrokenPipe } || @TypeOf(err); + switch (@as(Error, err)) { + error.BrokenPipe => return, // stdout closed + error.NoSpaceLeft => return, // fixed buffer full + else => return err, + } + }; + } +} + +test Utf8 { + const testing = std.testing; + const alloc = testing.allocator; + + const impl: *Utf8 = try .create(alloc, .{}); + defer impl.destroy(alloc); + + var prng = std.Random.DefaultPrng.init(1); + const rand = prng.random(); + + var buf: [1024]u8 = undefined; + var fbs = std.io.fixedBufferStream(&buf); + const writer = fbs.writer(); + + try impl.run(writer, rand); +} From 74b94ef30a26b861468db2e226cdbe38ee22da64 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 9 Jul 2025 15:02:07 -0700 Subject: [PATCH 16/46] remove src/bench --- src/bench/stream.sh | 30 ----- src/bench/stream.zig | 253 ------------------------------------- src/build/Config.zig | 1 - src/build/GhosttyBench.zig | 48 ------- src/main.zig | 1 - 5 files changed, 333 deletions(-) delete mode 100755 src/bench/stream.sh delete mode 100644 src/bench/stream.zig diff --git a/src/bench/stream.sh b/src/bench/stream.sh deleted file mode 100755 index 38d4c37cd..000000000 --- a/src/bench/stream.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env bash -# -# This is a trivial helper script to help run the stream benchmark. -# You probably want to tweak this script depending on what you're -# trying to measure. - -# Options: -# - "ascii", uniform random ASCII bytes -# - "utf8", uniform random unicode characters, encoded as utf8 -# - "rand", pure random data, will contain many invalid code sequences. -DATA="ascii" -SIZE="25000000" - -# Uncomment to test with an active terminal state. -# ARGS=" --terminal" - -# Generate the benchmark input ahead of time so it's not included in the time. -./zig-out/bin/bench-stream --mode=gen-$DATA | head -c $SIZE > /tmp/ghostty_bench_data - -# Uncomment to instead use the contents of `stream.txt` as input. (Ignores SIZE) -# echo $(cat ./stream.txt) > /tmp/ghostty_bench_data - -hyperfine \ - --warmup 10 \ - -n memcpy \ - "./zig-out/bin/bench-stream --mode=noop${ARGS} = 0) @bitCast(args.seed) else @truncate(@as(u128, @bitCast(std.time.nanoTimestamp()))); - var prng = std.Random.DefaultPrng.init(seed); - const rand = prng.random(); - - // Handle the modes that do not depend on terminal state first. - switch (args.mode) { - .@"gen-ascii" => { - var gen: synthetic.Bytes = .{ - .rand = rand, - .alphabet = synthetic.Bytes.Alphabet.ascii, - }; - try generate(writer, gen.generator()); - }, - - .@"gen-utf8" => { - var gen: synthetic.Utf8 = .{ - .rand = rand, - }; - try generate(writer, gen.generator()); - }, - - .@"gen-rand" => { - var gen: synthetic.Bytes = .{ .rand = rand }; - try generate(writer, gen.generator()); - }, - - .@"gen-osc" => { - var gen: synthetic.Osc = .{ - .rand = rand, - .p_valid = 0.5, - }; - try generate(writer, gen.generator()); - }, - - .@"gen-osc-valid" => { - var gen: synthetic.Osc = .{ - .rand = rand, - .p_valid = 1.0, - }; - try generate(writer, gen.generator()); - }, - - .@"gen-osc-invalid" => { - var gen: synthetic.Osc = .{ - .rand = rand, - .p_valid = 0.0, - }; - try generate(writer, gen.generator()); - }, - - .noop => try benchNoop(reader, buf), - - // Handle the ones that depend on terminal state next - inline .scalar, - .simd, - => |tag| switch (args.terminal) { - .new => { - const TerminalStream = terminal.Stream(*TerminalHandler); - var t = try terminal.Terminal.init(alloc, .{ - .cols = @intCast(args.@"terminal-cols"), - .rows = @intCast(args.@"terminal-rows"), - }); - var handler: TerminalHandler = .{ .t = &t }; - var stream: TerminalStream = .{ .handler = &handler }; - switch (tag) { - .scalar => try benchScalar(reader, &stream, buf), - .simd => try benchSimd(reader, &stream, buf), - else => @compileError("missing case"), - } - }, - - .none => { - var stream: terminal.Stream(NoopHandler) = .{ .handler = .{} }; - switch (tag) { - .scalar => try benchScalar(reader, &stream, buf), - .simd => try benchSimd(reader, &stream, buf), - else => @compileError("missing case"), - } - }, - }, - } -} - -fn generate( - writer: anytype, - gen: synthetic.Generator, -) !void { - var buf: [1024]u8 = undefined; - while (true) { - const data = try gen.next(&buf); - writer.writeAll(data) catch |err| switch (err) { - error.BrokenPipe => return, // stdout closed - else => return err, - }; - } -} - -noinline fn benchNoop(reader: anytype, buf: []u8) !void { - var total: usize = 0; - while (true) { - const n = try reader.readAll(buf); - if (n == 0) break; - total += n; - } - - std.log.info("total bytes len={}", .{total}); -} - -noinline fn benchScalar( - reader: anytype, - stream: anytype, - buf: []u8, -) !void { - while (true) { - const n = try reader.read(buf); - if (n == 0) break; - - // Using stream.next directly with a for loop applies a naive - // scalar approach. - for (buf[0..n]) |c| try stream.next(c); - } -} - -noinline fn benchSimd( - reader: anytype, - stream: anytype, - buf: []u8, -) !void { - while (true) { - const n = try reader.read(buf); - if (n == 0) break; - try stream.nextSlice(buf[0..n]); - } -} - -const NoopHandler = struct { - pub fn print(self: NoopHandler, cp: u21) !void { - _ = self; - _ = cp; - } -}; - -const TerminalHandler = struct { - t: *terminal.Terminal, - - pub fn print(self: *TerminalHandler, cp: u21) !void { - try self.t.print(cp); - } -}; diff --git a/src/build/Config.zig b/src/build/Config.zig index 88ea8d6ac..69a9dd8a0 100644 --- a/src/build/Config.zig +++ b/src/build/Config.zig @@ -528,7 +528,6 @@ pub const ExeEntrypoint = enum { webgen_config, webgen_actions, webgen_commands, - bench_stream, }; /// The release channel for the build. diff --git a/src/build/GhosttyBench.zig b/src/build/GhosttyBench.zig index 0588bba3c..5859a8bcf 100644 --- a/src/build/GhosttyBench.zig +++ b/src/build/GhosttyBench.zig @@ -47,54 +47,6 @@ pub fn init( try steps.append(exe); } - // Open the directory ./src/bench - const c_dir_path = b.pathFromRoot("src/bench"); - var c_dir = try std.fs.cwd().openDir(c_dir_path, .{ .iterate = true }); - defer c_dir.close(); - - // Go through and add each as a step - var c_dir_it = c_dir.iterate(); - while (try c_dir_it.next()) |entry| { - // Get the index of the last '.' so we can strip the extension. - const index = std.mem.lastIndexOfScalar(u8, entry.name, '.') orelse continue; - if (index == 0) continue; - - // If it doesn't end in 'zig' then ignore - if (!std.mem.eql(u8, entry.name[index + 1 ..], "zig")) continue; - - // Name of the conformance app and full path to the entrypoint. - const name = entry.name[0..index]; - - // Executable builder. - const bin_name = try std.fmt.allocPrint(b.allocator, "bench-{s}", .{name}); - const c_exe = b.addExecutable(.{ - .name = bin_name, - .root_module = b.createModule(.{ - .root_source_file = b.path("src/main.zig"), - .target = deps.config.target, - - // We always want our benchmarks to be in release mode. - .optimize = .ReleaseFast, - }), - }); - c_exe.linkLibC(); - - // Update our entrypoint - var enum_name: [64]u8 = undefined; - @memcpy(enum_name[0..name.len], name); - std.mem.replaceScalar(u8, enum_name[0..name.len], '-', '_'); - - var buf: [64]u8 = undefined; - const new_deps = try deps.changeEntrypoint(b, std.meta.stringToEnum( - Config.ExeEntrypoint, - try std.fmt.bufPrint(&buf, "bench_{s}", .{enum_name[0..name.len]}), - ).?); - - _ = try new_deps.add(c_exe); - - try steps.append(c_exe); - } - return .{ .steps = steps.items }; } diff --git a/src/main.zig b/src/main.zig index 25e723c92..b08e63dd2 100644 --- a/src/main.zig +++ b/src/main.zig @@ -10,7 +10,6 @@ const entrypoint = switch (build_config.exe_entrypoint) { .webgen_config => @import("build/webgen/main_config.zig"), .webgen_actions => @import("build/webgen/main_actions.zig"), .webgen_commands => @import("build/webgen/main_commands.zig"), - .bench_stream => @import("bench/stream.zig"), }; /// The main entrypoint for the program. From e962e9b5173e1afcb688733ac8eedc71fe073b90 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 10 Jul 2025 00:22:27 +0000 Subject: [PATCH 17/46] build(deps): bump cachix/install-nix-action from 31.4.1 to 31.5.0 Bumps [cachix/install-nix-action](https://github.com/cachix/install-nix-action) from 31.4.1 to 31.5.0. - [Release notes](https://github.com/cachix/install-nix-action/releases) - [Changelog](https://github.com/cachix/install-nix-action/blob/master/RELEASE.md) - [Commits](https://github.com/cachix/install-nix-action/compare/f0fe604f8a612776892427721526b4c7cfb23aba...cebd211ec2008b83bda8fb0b21c3c072f004fe04) --- updated-dependencies: - dependency-name: cachix/install-nix-action dependency-version: 31.5.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/nix.yml | 2 +- .github/workflows/release-pr.yml | 4 +-- .github/workflows/release-tag.yml | 4 +-- .github/workflows/release-tip.yml | 8 ++--- .github/workflows/test.yml | 42 +++++++++++------------ .github/workflows/update-colorschemes.yml | 2 +- 6 files changed, 31 insertions(+), 31 deletions(-) diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index bf8fd7208..ab881809c 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -42,7 +42,7 @@ jobs: /nix /zig - name: Setup Nix - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 + uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index e260996bb..7f48d109f 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -57,7 +57,7 @@ jobs: fetch-depth: 0 # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 + - uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -211,7 +211,7 @@ jobs: fetch-depth: 0 # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 + - uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index 4cc364127..ee0b389c1 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -89,7 +89,7 @@ jobs: /nix /zig - - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 + - uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0 with: nix_path: nixpkgs=channel:nixos-unstable @@ -130,7 +130,7 @@ jobs: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 + - uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 diff --git a/.github/workflows/release-tip.yml b/.github/workflows/release-tip.yml index b7c4949a5..941aed6e5 100644 --- a/.github/workflows/release-tip.yml +++ b/.github/workflows/release-tip.yml @@ -112,7 +112,7 @@ jobs: path: | /nix /zig - - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 + - uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -164,7 +164,7 @@ jobs: fetch-depth: 0 # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 + - uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -381,7 +381,7 @@ jobs: fetch-depth: 0 # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 + - uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -558,7 +558,7 @@ jobs: fetch-depth: 0 # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 + - uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 834d49a5c..cf3d983c0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -75,7 +75,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 + - uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -106,7 +106,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 + - uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -142,7 +142,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 + - uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -171,7 +171,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 + - uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -204,7 +204,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 + - uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -248,7 +248,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 + - uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -277,7 +277,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 + - uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -357,7 +357,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 + - uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -511,7 +511,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 + - uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -553,7 +553,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 + - uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -601,7 +601,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 + - uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -621,7 +621,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 + - uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -654,7 +654,7 @@ jobs: path: | /nix /zig - - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 + - uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -682,7 +682,7 @@ jobs: path: | /nix /zig - - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 + - uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -709,7 +709,7 @@ jobs: path: | /nix /zig - - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 + - uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -736,7 +736,7 @@ jobs: path: | /nix /zig - - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 + - uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -763,7 +763,7 @@ jobs: path: | /nix /zig - - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 + - uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -790,7 +790,7 @@ jobs: path: | /nix /zig - - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 + - uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -817,7 +817,7 @@ jobs: path: | /nix /zig - - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 + - uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -854,7 +854,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 + - uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -911,7 +911,7 @@ jobs: /nix /zig - name: Setup Nix - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 + uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 diff --git a/.github/workflows/update-colorschemes.yml b/.github/workflows/update-colorschemes.yml index b9ded559e..5392a9e11 100644 --- a/.github/workflows/update-colorschemes.yml +++ b/.github/workflows/update-colorschemes.yml @@ -29,7 +29,7 @@ jobs: /zig - name: Setup Nix - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1 + uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 From 5cdfe3d70e40fb7f4b2f0ed5ed710ab7d887ae01 Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Wed, 9 Jul 2025 21:44:32 -0400 Subject: [PATCH 18/46] elvish: revise the ssh integration The previous implementation wasn't quite working. This revision reworks it in a few ways: - Fix various syntax issues - Redirect the `ssh` command to our 'ssh-integration' function - Locate the `ghostty` binary using $GHOSTTY_BIN_DIR - Use os:temp-dir to create our temporary directory Also, consistently use 2-space indents, which is the Elvish standard. --- .editorconfig | 2 +- .../elvish/lib/ghostty-integration.elv | 160 +++++++++--------- 2 files changed, 77 insertions(+), 85 deletions(-) diff --git a/.editorconfig b/.editorconfig index d305bd294..4e9bec6ce 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,6 +1,6 @@ root = true -[*.{sh,bash}] +[*.{sh,bash,elv}] indent_size = 2 indent_style = space diff --git a/src/shell-integration/elvish/lib/ghostty-integration.elv b/src/shell-integration/elvish/lib/ghostty-integration.elv index 4e95b251f..6d0d19f4f 100644 --- a/src/shell-integration/elvish/lib/ghostty-integration.elv +++ b/src/shell-integration/elvish/lib/ghostty-integration.elv @@ -38,6 +38,9 @@ { use str + # List of enabled shell integration features + var features = [(str:split ',' $E:GHOSTTY_SHELL_FEATURES)] + # helper used by `mark-*` functions fn set-prompt-state {|new| set-env __ghostty_prompt_state $new } @@ -98,93 +101,81 @@ (external sudo) $@args } - # SSH Integration - use str + fn ssh-integration {|@args| + var ssh-term = "xterm-256color" + var ssh-opts = [] - if (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-) { - fn ssh {|@args| - var ssh-term = "xterm-256color" - var ssh-opts = [] + # Configure environment variables for remote session + if (has-value $features ssh-env) { + set ssh-opts = (conj $ssh-opts ^ + -o "SetEnv COLORTERM=truecolor" ^ + -o "SendEnv TERM_PROGRAM TERM_PROGRAM_VERSION") + } - # Configure environment variables for remote session - if (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-env) { - set ssh-opts = (conj $ssh-opts - -o "SetEnv COLORTERM=truecolor" - -o "SendEnv TERM_PROGRAM TERM_PROGRAM_VERSION" - ) + if (has-value $features ssh-terminfo) { + var ssh-user = "" + var ssh-hostname = "" + + # Parse ssh config + for line [((external ssh) -G $@args)] { + var parts = [(str:fields $line)] + if (> (count $parts) 1) { + var ssh-key = $parts[0] + var ssh-value = $parts[1] + if (eq $ssh-key user) { + set ssh-user = $ssh-value + } elif (eq $ssh-key hostname) { + set ssh-hostname = $ssh-value } - - # Install terminfo on remote host if needed - if (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-terminfo) { - var ssh-user = "" - var ssh-hostname = "" - - # Parse ssh config - var ssh-config = (external ssh -G $@args 2>/dev/null | slurp) - for line (str:split "\n" $ssh-config) { - var parts = (str:split " " $line) - if (> (count $parts) 1) { - var ssh-key = $parts[0] - var ssh-value = $parts[1] - if (eq $ssh-key user) { - set ssh-user = $ssh-value - } elif (eq $ssh-key hostname) { - set ssh-hostname = $ssh-value - } - if (and (not-eq $ssh-user "") (not-eq $ssh-hostname "")) { - break - } - } - } - - if (not-eq $ssh-hostname "") { - var ssh-target = $ssh-user"@"$ssh-hostname - - # Check if terminfo is already cached - if (and (has-external ghostty) (bool ?(external ghostty +ssh-cache --host=$ssh-target >/dev/null 2>&1))) { - set ssh-term = "xterm-ghostty" - } elif (has-external infocmp) { - var ssh-terminfo = (external infocmp -0 -x xterm-ghostty 2>/dev/null | slurp) - - if (not-eq $ssh-terminfo "") { - echo "Setting up xterm-ghostty terminfo on "$ssh-hostname"..." >&2 - - var ssh-cpath-dir = "" - try { - set ssh-cpath-dir = (external mktemp -d "/tmp/ghostty-ssh-"$ssh-user".XXXXXX" 2>/dev/null | slurp) - } catch { - set ssh-cpath-dir = "/tmp/ghostty-ssh-"$ssh-user"."(randint 10000 99999) - } - var ssh-cpath = $ssh-cpath-dir"/socket" - - if (bool ?(echo $ssh-terminfo | external ssh $@ssh-opts -o ControlMaster=yes -o ControlPath=$ssh-cpath -o ControlPersist=60s $@args ' - infocmp xterm-ghostty >/dev/null 2>&1 && exit 0 - command -v tic >/dev/null 2>&1 || exit 1 - mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && exit 0 - exit 1 - ' 2>/dev/null)) { - set ssh-term = "xterm-ghostty" - set ssh-opts = (conj $ssh-opts -o ControlPath=$ssh-cpath) - - # Cache successful installation - if (has-external ghostty) { - external ghostty +ssh-cache --add=$ssh-target >/dev/null 2>&1 - } - } else { - echo "Warning: Failed to install terminfo." >&2 - } - } else { - echo "Warning: Could not generate terminfo data." >&2 - } - } else { - echo "Warning: ghostty command not available for cache management." >&2 - } - } + if (and (not-eq $ssh-user "") (not-eq $ssh-hostname "")) { + break } - - # Execute SSH with TERM environment variable - external E:TERM=$ssh-term ssh $@ssh-opts $@args + } } + + if (not-eq $ssh-hostname "") { + var ghostty = $E:GHOSTTY_BIN_DIR/"ghostty" + var ssh-target = $ssh-user"@"$ssh-hostname + + # Check if terminfo is already cached + if (bool ?($ghostty +ssh-cache --host=$ssh-target)) { + set ssh-term = "xterm-ghostty" + } elif (has-external infocmp) { + var ssh-terminfo = ((external infocmp) -0 -x xterm-ghostty 2>/dev/null | slurp) + + if (not-eq $ssh-terminfo "") { + echo "Setting up xterm-ghostty terminfo on "$ssh-hostname"..." >&2 + + use os + var ssh-cpath-dir = (os:temp-dir "ghostty-ssh-"$ssh-user".*") + var ssh-cpath = $ssh-cpath-dir"/socket" + + if (bool ?(echo $ssh-terminfo | (external ssh) $@ssh-opts -o ControlMaster=yes -o ControlPath=$ssh-cpath -o ControlPersist=60s $@args ' + infocmp xterm-ghostty >/dev/null 2>&1 && exit 0 + command -v tic >/dev/null 2>&1 || exit 1 + mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && exit 0 + exit 1 + ' 2>/dev/null)) { + set ssh-term = "xterm-ghostty" + set ssh-opts = (conj $ssh-opts -o ControlPath=$ssh-cpath) + + # Cache successful installation + $ghostty +ssh-cache --add=$ssh-target >/dev/null + } else { + echo "Warning: Failed to install terminfo." >&2 + } + } else { + echo "Warning: Could not generate terminfo data." >&2 + } + } else { + echo "Warning: ghostty command not available for cache management." >&2 + } + } + } + + with [E:TERM = $ssh-term] { + (external ssh) $@ssh-opts $@args + } } defer { @@ -196,8 +187,6 @@ set edit:after-readline = (conj $edit:after-readline $mark-output-start~) set edit:after-command = (conj $edit:after-command $mark-output-end~) - var features = [(str:split ',' $E:GHOSTTY_SHELL_FEATURES)] - if (has-value $features title) { set after-chdir = (conj $after-chdir {|_| report-pwd }) } @@ -210,4 +199,7 @@ if (and (has-value $features sudo) (not-eq "" $E:TERMINFO) (has-external sudo)) { edit:add-var sudo~ $sudo-with-terminfo~ } + if (and (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-) (has-external ssh)) { + edit:add-var ssh~ $ssh-integration~ + } } From 88736a2ddb6ab4b0f69d569201248728d6099f14 Mon Sep 17 00:00:00 2001 From: ClearAspect Date: Thu, 10 Jul 2025 00:08:37 -0400 Subject: [PATCH 19/46] Fix custom shader cursor uniforms not set for non-block cursors (#7893) Fixes #7893 Previously, custom shader cursor uniforms were only updated when the cursor glyph was in the front (block) cursor list. This caused non-block cursors (such as bar, underline, hollow block, and lock) to be missing from custom shader effects. This commit adds a helper to the cell contents struct to retrieve the current cursor glyph from either the front or back cursor lists, and updates the renderer to use this helper when setting custom shader uniforms. As a result, custom shaders now receive correct cursor information for all supported cursor styles. --- src/renderer/cell.zig | 11 +++++++++++ src/renderer/generic.zig | 5 +---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/renderer/cell.zig b/src/renderer/cell.zig index 43d744176..b8b218d99 100644 --- a/src/renderer/cell.zig +++ b/src/renderer/cell.zig @@ -156,6 +156,17 @@ pub const Contents = struct { } } + /// Returns the current cursor glyph if present, checking both cursor lists. + pub fn getCursorGlyph(self: *Contents) ?shaderpkg.CellText { + if (self.fg_rows.lists[0].items.len > 0) { + return self.fg_rows.lists[0].items[0]; + } + if (self.fg_rows.lists[self.size.rows + 1].items.len > 0) { + return self.fg_rows.lists[self.size.rows + 1].items[0]; + } + return null; + } + /// Access a background cell. Prefer this function over direct indexing /// of `bg_cells` in order to avoid integer size bugs causing overflows. pub inline fn bgCell( diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index 3965d302a..2374ec1b0 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -2218,10 +2218,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { }; // Update custom cursor uniforms, if we have a cursor. - if (self.cells.fg_rows.lists[0].items.len > 0) { - const cursor: shaderpkg.CellText = - self.cells.fg_rows.lists[0].items[0]; - + if (self.cells.getCursorGlyph()) |cursor| { const cursor_width: f32 = @floatFromInt(cursor.glyph_size[0]); const cursor_height: f32 = @floatFromInt(cursor.glyph_size[1]); From 6744e57c68589249cb9ef3a725b0391b42177d46 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Wed, 9 Jul 2025 22:27:50 -0600 Subject: [PATCH 20/46] fix(terminal/PageList): update viewport in row count resize Before, if the row count increase past the active area then we added new rows to make sure that we had enough for the active area, but we didn't make sure that the viewport pin wasn't below the active area pin, which meant that later on if someone tried to get the bottom right pin for the viewport it would overshoot and we'd use null. This resulted in either a memory corruption bug in ReleaseFast if you scaled down the font while scrolled up slightly, or in Debug mode it was just a crash. --- src/terminal/PageList.zig | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 9838bfb53..d13cd7fef 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -1401,6 +1401,15 @@ fn resizeWithoutReflow(self: *PageList, opts: Resize) !void { assert(count < rows); for (count..rows) |_| _ = try self.grow(); } + + // Make sure that the viewport pin isn't below the active + // area, since that will lead to all sorts of problems. + switch (self.viewport) { + .pin => if (self.pinIsActive(self.viewport_pin.*)) { + self.viewport = .{ .active = {} }; + }, + .active, .top => {}, + } }, } From cc0d7acaefa7495a04c287dee65d06304c204024 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 9 Jul 2025 21:05:54 -0700 Subject: [PATCH 21/46] build: make the xcframework step dsym aware, even though we don't use it This was in pursuit of trying to get line numbers in `zig build run` on macOS to work, but I wasn't able to figure that out and this wasn't the right path because static libs can't have dsyms. But, it may still be useful to make the xcframework step dsym aware for future use so I'm PRing this. --- src/build/GhosttyLib.zig | 48 +++++++++++++++++++++++++++----- src/build/GhosttyXCFramework.zig | 4 +++ src/build/XCFrameworkStep.zig | 7 +++++ 3 files changed, 52 insertions(+), 7 deletions(-) diff --git a/src/build/GhosttyLib.zig b/src/build/GhosttyLib.zig index 4e36c57c8..857fd1798 100644 --- a/src/build/GhosttyLib.zig +++ b/src/build/GhosttyLib.zig @@ -1,6 +1,7 @@ const GhosttyLib = @This(); const std = @import("std"); +const RunStep = std.Build.Step.Run; const Config = @import("Config.zig"); const SharedDeps = @import("SharedDeps.zig"); const LibtoolStep = @import("LibtoolStep.zig"); @@ -11,6 +12,7 @@ step: *std.Build.Step, /// The final static library file output: std.Build.LazyPath, +dsym: ?std.Build.LazyPath, pub fn initStatic( b: *std.Build, @@ -18,9 +20,14 @@ pub fn initStatic( ) !GhosttyLib { const lib = b.addStaticLibrary(.{ .name = "ghostty", - .root_source_file = b.path("src/main_c.zig"), - .target = deps.config.target, - .optimize = deps.config.optimize, + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main_c.zig"), + .target = deps.config.target, + .optimize = deps.config.optimize, + .strip = deps.config.strip, + .omit_frame_pointer = deps.config.strip, + .unwind_tables = if (deps.config.strip) .none else .sync, + }), }); lib.linkLibC(); @@ -37,6 +44,7 @@ pub fn initStatic( if (!deps.config.target.result.os.tag.isDarwin()) return .{ .step = &lib.step, .output = lib.getEmittedBin(), + .dsym = null, }; // Create a static lib that contains all our dependencies. @@ -50,6 +58,9 @@ pub fn initStatic( return .{ .step = libtool.step, .output = libtool.output, + + // Static libraries cannot have dSYMs because they aren't linked. + .dsym = null, }; } @@ -59,16 +70,35 @@ pub fn initShared( ) !GhosttyLib { const lib = b.addSharedLibrary(.{ .name = "ghostty", - .root_source_file = b.path("src/main_c.zig"), - .target = deps.config.target, - .optimize = deps.config.optimize, - .strip = deps.config.strip, + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main_c.zig"), + .target = deps.config.target, + .optimize = deps.config.optimize, + .strip = deps.config.strip, + .omit_frame_pointer = deps.config.strip, + .unwind_tables = if (deps.config.strip) .none else .sync, + }), }); _ = try deps.add(lib); + // Get our debug symbols + const dsymutil: ?std.Build.LazyPath = dsymutil: { + if (!deps.config.target.result.os.tag.isDarwin()) { + break :dsymutil null; + } + + const dsymutil = RunStep.create(b, "dsymutil"); + dsymutil.addArgs(&.{"dsymutil"}); + dsymutil.addFileArg(lib.getEmittedBin()); + dsymutil.addArgs(&.{"-o"}); + const output = dsymutil.addOutputFileArg("libghostty.dSYM"); + break :dsymutil output; + }; + return .{ .step = &lib.step, .output = lib.getEmittedBin(), + .dsym = dsymutil, }; } @@ -95,6 +125,10 @@ pub fn initMacOSUniversal( return .{ .step = universal.step, .output = universal.output, + + // You can't run dsymutil on a universal binary, you have to + // do it on the individual binaries. + .dsym = null, }; } diff --git a/src/build/GhosttyXCFramework.zig b/src/build/GhosttyXCFramework.zig index 7debd6906..d036e7020 100644 --- a/src/build/GhosttyXCFramework.zig +++ b/src/build/GhosttyXCFramework.zig @@ -64,20 +64,24 @@ pub fn init( .{ .library = macos_universal.output, .headers = b.path("include"), + .dsym = macos_universal.dsym, }, .{ .library = ios.output, .headers = b.path("include"), + .dsym = ios.dsym, }, .{ .library = ios_sim.output, .headers = b.path("include"), + .dsym = ios_sim.dsym, }, }, .native => &.{.{ .library = macos_native.output, .headers = b.path("include"), + .dsym = macos_native.dsym, }}, }, }); diff --git a/src/build/XCFrameworkStep.zig b/src/build/XCFrameworkStep.zig index 8a0d5dc67..39f0f9bac 100644 --- a/src/build/XCFrameworkStep.zig +++ b/src/build/XCFrameworkStep.zig @@ -26,6 +26,9 @@ pub const Library = struct { /// Path to a directory with the headers. headers: LazyPath, + + /// Path to a debug symbols file (.dSYM) if available. + dsym: ?LazyPath, }; step: *Step, @@ -52,6 +55,10 @@ pub fn create(b: *std.Build, opts: Options) *XCFrameworkStep { run.addFileArg(lib.library); run.addArg("-headers"); run.addFileArg(lib.headers); + if (lib.dsym) |dsym| { + run.addArg("-debug-symbols"); + run.addFileArg(dsym); + } } run.addArg("-output"); run.addArg(opts.out_path); From ea4a056d34783885b7299202c68e3f18cc4dd799 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Wed, 9 Jul 2025 22:47:01 -0600 Subject: [PATCH 22/46] test(terminal/PageList): resize keeps viewport <= active This tests for the bug fixed in the last commit. --- src/terminal/PageList.zig | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index d13cd7fef..660949c9c 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -5984,6 +5984,36 @@ test "PageList resize (no reflow) more rows extends blank lines" { } } +test "PageList resize (no reflow) more rows contains viewport" { + const testing = std.testing; + const alloc = testing.allocator; + + // When the rows are increased we need to make sure that the viewport + // doesn't end up below the active area if it's currently in pin mode. + + var s = try init(alloc, 5, 5, 1); + defer s.deinit(); + try testing.expect(s.pages.first == s.pages.last); + + // Make it so we have scrollback + _ = try s.grow(); + + try testing.expectEqual(@as(usize, 5), s.rows); + try testing.expectEqual(@as(usize, 6), s.totalRows()); + + // Set viewport above active by scrolling up one. + s.scroll(.{ .delta_row = -1 }); + // The viewport should be a pin now. + try testing.expectEqual(Viewport.pin, s.viewport); + + // Resize + try s.resize(.{ .rows = 7, .reflow = false }); + try testing.expectEqual(@as(usize, 7), s.rows); + try testing.expectEqual(@as(usize, 7), s.totalRows()); + // The viewport should now be active, not a pin. + try testing.expectEqual(Viewport.active, s.viewport); +} + test "PageList resize (no reflow) less cols" { const testing = std.testing; const alloc = testing.allocator; From 36a3a3ffa42f829629f19934be05b1c97d741c8b Mon Sep 17 00:00:00 2001 From: ClearAspect Date: Thu, 10 Jul 2025 01:48:44 -0400 Subject: [PATCH 23/46] Add tests for getCursorGlyph() helper function --- src/renderer/cell.zig | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/renderer/cell.zig b/src/renderer/cell.zig index b8b218d99..b1ce4523c 100644 --- a/src/renderer/cell.zig +++ b/src/renderer/cell.zig @@ -361,14 +361,17 @@ test Contents { }; c.setCursor(cursor_cell, .block); try testing.expectEqual(cursor_cell, c.fg_rows.lists[0].items[0]); + try testing.expectEqual(cursor_cell, c.getCursorGlyph().?); // And remove it. c.setCursor(null, null); try testing.expectEqual(0, c.fg_rows.lists[0].items.len); + try testing.expect(c.getCursorGlyph() == null); // Add a hollow cursor. c.setCursor(cursor_cell, .block_hollow); try testing.expectEqual(cursor_cell, c.fg_rows.lists[rows + 1].items[0]); + try testing.expectEqual(cursor_cell, c.getCursorGlyph().?); } test "Contents clear retains other content" { From c23e3f8586964c655744bb8d20d7377210bc4ecb Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 10 Jul 2025 07:06:53 -0700 Subject: [PATCH 24/46] ci: update sequoia builders to xcode 26 beta 3, output version in CI The `-edge` variant of these builders will always use the latest macOS images that may not be stable. We'll remove this once Xcode 26 is released. --- .github/workflows/release-pr.yml | 4 ++-- .github/workflows/release-tip.yml | 15 ++++++++++++--- .github/workflows/test.yml | 9 ++++++--- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index 7f48d109f..a4bf69b1c 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -47,7 +47,7 @@ jobs: sentry-cli dif upload --project ghostty --wait dsym.zip build-macos: - runs-on: namespace-profile-ghostty-macos-sequoia + runs-on: namespace-profile-ghostty-macos-sequoia-edge timeout-minutes: 90 steps: - name: Checkout code @@ -201,7 +201,7 @@ jobs: destination-dir: ./ build-macos-debug: - runs-on: namespace-profile-ghostty-macos-sequoia + runs-on: namespace-profile-ghostty-macos-sequoia-edge timeout-minutes: 90 steps: - name: Checkout code diff --git a/.github/workflows/release-tip.yml b/.github/workflows/release-tip.yml index 941aed6e5..a9cff9a2b 100644 --- a/.github/workflows/release-tip.yml +++ b/.github/workflows/release-tip.yml @@ -154,7 +154,7 @@ jobs: ) }} - runs-on: namespace-profile-ghostty-macos-sequoia + runs-on: namespace-profile-ghostty-macos-sequoia-edge timeout-minutes: 90 steps: - name: Checkout code @@ -175,6 +175,9 @@ jobs: - name: XCode Select run: sudo xcode-select -s /Applications/Xcode_26.0.app + - name: Xcode Version + run: xcodebuild -version + # Setup Sparkle - name: Setup Sparkle env: @@ -371,7 +374,7 @@ jobs: ) }} - runs-on: namespace-profile-ghostty-macos-sequoia + runs-on: namespace-profile-ghostty-macos-sequoia-edge timeout-minutes: 90 steps: - name: Checkout code @@ -392,6 +395,9 @@ jobs: - name: XCode Select run: sudo xcode-select -s /Applications/Xcode_26.0.app + - name: Xcode Version + run: xcodebuild -version + # Setup Sparkle - name: Setup Sparkle env: @@ -548,7 +554,7 @@ jobs: ) }} - runs-on: namespace-profile-ghostty-macos-sequoia + runs-on: namespace-profile-ghostty-macos-sequoia-edge timeout-minutes: 90 steps: - name: Checkout code @@ -569,6 +575,9 @@ jobs: - name: XCode Select run: sudo xcode-select -s /Applications/Xcode_26.0.app + - name: Xcode Version + run: xcodebuild -version + # Setup Sparkle - name: Setup Sparkle env: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cf3d983c0..93ae9734f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -270,7 +270,7 @@ jobs: ghostty-source.tar.gz build-macos: - runs-on: namespace-profile-ghostty-macos-sequoia + runs-on: namespace-profile-ghostty-macos-sequoia-edge needs: test steps: - name: Checkout code @@ -288,6 +288,9 @@ jobs: - name: Xcode Select run: sudo xcode-select -s /Applications/Xcode_26.0.app + - name: Xcode Version + run: xcodebuild -version + - name: get the Zig deps id: deps run: nix build -L .#deps && echo "deps=$(readlink ./result)" >> $GITHUB_OUTPUT @@ -350,7 +353,7 @@ jobs: xcodebuild -target Ghostty-iOS "CODE_SIGNING_ALLOWED=NO" build-macos-matrix: - runs-on: namespace-profile-ghostty-macos-sequoia + runs-on: namespace-profile-ghostty-macos-sequoia-edge needs: test steps: - name: Checkout code @@ -614,7 +617,7 @@ jobs: nix develop -c zig build -Dsentry=${{ matrix.sentry }} test-macos: - runs-on: namespace-profile-ghostty-macos-sequoia + runs-on: namespace-profile-ghostty-macos-sequoia-edge needs: test steps: - name: Checkout code From 4dc4911ece52da05687ce6d937df71433fc6f635 Mon Sep 17 00:00:00 2001 From: Hojin You Date: Thu, 10 Jul 2025 14:46:46 -0400 Subject: [PATCH 25/46] Update po/ko_KR.UTF-8.po --- po/ko_KR.UTF-8.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/po/ko_KR.UTF-8.po b/po/ko_KR.UTF-8.po index 3482453ed..c13fe8a61 100644 --- a/po/ko_KR.UTF-8.po +++ b/po/ko_KR.UTF-8.po @@ -213,7 +213,7 @@ msgstr "이 분할에 대한 선택 기억하기" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 msgid "Reload configuration to show this prompt again" -msgstr "이 프롬프트를 다시 표시하려면 구성을 다시 로드하십시오." +msgstr "이 창을 다시 보려면 설정을 다시 불러오세요" #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 #: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 From cbb3f6f64f78c5a572f430096663cd3c8de08c26 Mon Sep 17 00:00:00 2001 From: Bryan Lee <38807139+liby@users.noreply.github.com> Date: Thu, 10 Jul 2025 23:16:23 +0800 Subject: [PATCH 26/46] ci: add shellcheck linting for shell scripts Add shellcheck to CI pipeline to ensure shell scripts follow best practices and catch common errors. Fix existing shellcheck warnings in test scripts to pass the new linting requirements. --- .github/workflows/test.yml | 35 ++++++++++++++++++++++++++++++ nix/build-support/update-mirror.sh | 2 +- nix/devShell.nix | 2 ++ test/run-all.sh | 5 +---- test/run-host.sh | 2 +- test/run.sh | 3 ++- 6 files changed, 42 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 93ae9734f..86dff2ea1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,6 +31,7 @@ jobs: - prettier - alejandra - typos + - shellcheck - translations - blueprint-compiler - test-pkg-linux @@ -778,6 +779,40 @@ jobs: - name: typos check run: nix develop -c typos + shellcheck: + if: github.repository == 'ghostty-org/ghostty' + runs-on: namespace-profile-ghostty-xsm + timeout-minutes: 60 + env: + ZIG_LOCAL_CACHE_DIR: /zig/local-cache + ZIG_GLOBAL_CACHE_DIR: /zig/global-cache + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Setup Cache + uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8 + with: + path: | + /nix + /zig + - uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0 + with: + nix_path: nixpkgs=channel:nixos-unstable + - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 + with: + name: ghostty + authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" + skipPush: true + useDaemon: false # sometimes fails on short jobs + - name: shellcheck + run: | + nix develop -c shellcheck \ + --check-sourced \ + --color=always \ + --severity=warning \ + --shell=bash \ + --external-sources \ + $(find . \( -name "*.sh" -o -name "*.bash" \) -type f ! -path "./zig-out/*" ! -path "./macos/build/*" ! -path "./.git/*" | sort) + translations: if: github.repository == 'ghostty-org/ghostty' runs-on: namespace-profile-ghostty-xsm diff --git a/nix/build-support/update-mirror.sh b/nix/build-support/update-mirror.sh index 35fd841e2..f346572ed 100755 --- a/nix/build-support/update-mirror.sh +++ b/nix/build-support/update-mirror.sh @@ -6,7 +6,7 @@ set -e # Exit immediately if a command exits with a non-zero status -SCRIPT_PATH="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" +SCRIPT_PATH="$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)" INPUT_FILE="$SCRIPT_PATH/../../build.zig.zon2json-lock" OUTPUT_DIR="blob" diff --git a/nix/devShell.nix b/nix/devShell.nix index 8a8ab441f..653c0c0b0 100644 --- a/nix/devShell.nix +++ b/nix/devShell.nix @@ -61,6 +61,7 @@ pinact, hyperfine, typos, + shellcheck, uv, wayland, wayland-scanner, @@ -101,6 +102,7 @@ in alejandra pinact typos + shellcheck # Testing parallel diff --git a/test/run-all.sh b/test/run-all.sh index 77beb344a..d4a785a44 100755 --- a/test/run-all.sh +++ b/test/run-all.sh @@ -9,9 +9,6 @@ DIR=$(cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd) # We always copy the bin in case it was rebuilt cp ${DIR}/../zig-out/bin/ghostty ${DIR}/ -# Build our image once -IMAGE=$(docker build --file ${DIR}/Dockerfile -q ${DIR}) - # Unix shortcut to just execute ./run-host for each one. We can do # this less esoterically if we ever wanted. find ${DIR}/cases \ @@ -23,4 +20,4 @@ find ${DIR}/cases \ ${DIR}/run-host.sh \ --case '{}' \ --rewrite-abs-path \ - $@ + "$@" diff --git a/test/run-host.sh b/test/run-host.sh index 887f2cfc1..da9dbe2e5 100755 --- a/test/run-host.sh +++ b/test/run-host.sh @@ -13,4 +13,4 @@ docker run \ --entrypoint "xvfb-run" \ $IMAGE \ --server-args="-screen 0, 1600x900x24" \ - /entrypoint.sh $@ + /entrypoint.sh "$@" diff --git a/test/run.sh b/test/run.sh index 641dc6943..db05ede76 100755 --- a/test/run.sh +++ b/test/run.sh @@ -63,6 +63,7 @@ if [ $bad -ne 0 ]; then fi # Load our test case +# shellcheck disable=SC1090 source ${ARG_CASE} if ! has_func "test_do"; then echo "Test case is invalid." @@ -79,7 +80,7 @@ if [ "$ARG_EXEC" = "ghostty" ]; then # We build in Nix (maybe). To be sure, we replace the interpreter so # it doesn't point to a Nix path. If we don't build in Nix, this should # still be safe. - patchelf --set-interpreter /lib/ld-linux-$(uname -m).so.1 ${ARG_EXEC} + patchelf --set-interpreter /lib/ld-linux-"$(uname -m)".so.1 ${ARG_EXEC} fi #-------------------------------------------------------------------- From 01233a48d14cb9ae191c085a386bccfbafb91243 Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Thu, 10 Jul 2025 15:12:54 -0400 Subject: [PATCH 27/46] bash: preserve an existing ENV value Our bash shell integration code uses ENV (in POSIX mode) to bootstrap our shell integration script. This had the side effect of overwriting an existing ENV value. This change preserves ENV by storing it temporarily in GHOSTTY_BASH_ENV. Note that this doesn't enable --posix mode support for automatic shell integration. (--posix does work; we just skip shell integration when that flag is specified.) We can reconsider implementing full --posix support separately. --- src/shell-integration/bash/ghostty.bash | 6 ++++++ src/termio/shell_integration.zig | 21 +++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/src/shell-integration/bash/ghostty.bash b/src/shell-integration/bash/ghostty.bash index aacf37c3a..ca5a012c6 100644 --- a/src/shell-integration/bash/ghostty.bash +++ b/src/shell-integration/bash/ghostty.bash @@ -26,6 +26,12 @@ if [ -n "$GHOSTTY_BASH_INJECT" ]; then builtin declare __ghostty_bash_flags="$GHOSTTY_BASH_INJECT" builtin unset ENV GHOSTTY_BASH_INJECT + # Restore an existing ENV that was replaced by the shell integration code. + if [[ -n "$GHOSTTY_BASH_ENV" ]]; then + builtin export ENV=$GHOSTTY_BASH_ENV + builtin unset GHOSTTY_BASH_ENV + fi + # Restore bash's default 'posix' behavior. Also reset 'inherit_errexit', # which doesn't happen as part of the 'posix' reset. builtin set +o posix diff --git a/src/termio/shell_integration.zig b/src/termio/shell_integration.zig index 469ff2859..438c2a0ea 100644 --- a/src/termio/shell_integration.zig +++ b/src/termio/shell_integration.zig @@ -340,6 +340,11 @@ fn setupBash( } } + // Preserve an existing ENV value. We're about to overwrite it. + if (env.get("ENV")) |v| { + try env.put("GHOSTTY_BASH_ENV", v); + } + // Set our new ENV to point to our integration script. var path_buf: [std.fs.max_path_bytes]u8 = undefined; const integ_dir = try std.fmt.bufPrint( @@ -502,6 +507,22 @@ test "bash: HISTFILE" { } } +test "bash: ENV" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var env = EnvMap.init(alloc); + defer env.deinit(); + + try env.put("ENV", "env.sh"); + + _ = try setupBash(alloc, .{ .shell = "bash" }, ".", &env); + try testing.expectEqualStrings("./shell-integration/bash/ghostty.bash", env.get("ENV").?); + try testing.expectEqualStrings("env.sh", env.get("GHOSTTY_BASH_ENV").?); +} + test "bash: additional arguments" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); From 099a2d7f03a3062462c96fa7e8d5c03dee3be5b3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 11 Jul 2025 00:13:59 +0000 Subject: [PATCH 28/46] build(deps): bump namespacelabs/nscloud-cache-action from 1.2.8 to 1.2.9 Bumps [namespacelabs/nscloud-cache-action](https://github.com/namespacelabs/nscloud-cache-action) from 1.2.8 to 1.2.9. - [Release notes](https://github.com/namespacelabs/nscloud-cache-action/releases) - [Commits](https://github.com/namespacelabs/nscloud-cache-action/compare/449c929cd5138e6607e7e78458e88cc476e76f89...0ac1550c04676e19d39872be6216ccbf9c6bab43) --- updated-dependencies: - dependency-name: namespacelabs/nscloud-cache-action dependency-version: 1.2.9 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/nix.yml | 2 +- .github/workflows/release-tag.yml | 2 +- .github/workflows/release-tip.yml | 2 +- .github/workflows/test.yml | 40 +++++++++++------------ .github/workflows/update-colorschemes.yml | 2 +- 5 files changed, 24 insertions(+), 24 deletions(-) diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index ab881809c..beeaa76a4 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -36,7 +36,7 @@ jobs: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8 + uses: namespacelabs/nscloud-cache-action@0ac1550c04676e19d39872be6216ccbf9c6bab43 # v1.2.9 with: path: | /nix diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index ee0b389c1..cc81b1a79 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -83,7 +83,7 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8 + uses: namespacelabs/nscloud-cache-action@0ac1550c04676e19d39872be6216ccbf9c6bab43 # v1.2.9 with: path: | /nix diff --git a/.github/workflows/release-tip.yml b/.github/workflows/release-tip.yml index a9cff9a2b..efc697f01 100644 --- a/.github/workflows/release-tip.yml +++ b/.github/workflows/release-tip.yml @@ -107,7 +107,7 @@ jobs: steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8 + uses: namespacelabs/nscloud-cache-action@0ac1550c04676e19d39872be6216ccbf9c6bab43 # v1.2.9 with: path: | /nix diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 86dff2ea1..e40a9f571 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -69,7 +69,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8 + uses: namespacelabs/nscloud-cache-action@0ac1550c04676e19d39872be6216ccbf9c6bab43 # v1.2.9 with: path: | /nix @@ -100,7 +100,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8 + uses: namespacelabs/nscloud-cache-action@0ac1550c04676e19d39872be6216ccbf9c6bab43 # v1.2.9 with: path: | /nix @@ -136,7 +136,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8 + uses: namespacelabs/nscloud-cache-action@0ac1550c04676e19d39872be6216ccbf9c6bab43 # v1.2.9 with: path: | /nix @@ -165,7 +165,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8 + uses: namespacelabs/nscloud-cache-action@0ac1550c04676e19d39872be6216ccbf9c6bab43 # v1.2.9 with: path: | /nix @@ -198,7 +198,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8 + uses: namespacelabs/nscloud-cache-action@0ac1550c04676e19d39872be6216ccbf9c6bab43 # v1.2.9 with: path: | /nix @@ -242,7 +242,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8 + uses: namespacelabs/nscloud-cache-action@0ac1550c04676e19d39872be6216ccbf9c6bab43 # v1.2.9 with: path: | /nix @@ -413,7 +413,7 @@ jobs: mkdir dist tar --verbose --extract --strip-components 1 --directory dist --file ghostty-source.tar.gz - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8 + uses: namespacelabs/nscloud-cache-action@0ac1550c04676e19d39872be6216ccbf9c6bab43 # v1.2.9 with: path: | /nix @@ -508,7 +508,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8 + uses: namespacelabs/nscloud-cache-action@0ac1550c04676e19d39872be6216ccbf9c6bab43 # v1.2.9 with: path: | /nix @@ -550,7 +550,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8 + uses: namespacelabs/nscloud-cache-action@0ac1550c04676e19d39872be6216ccbf9c6bab43 # v1.2.9 with: path: | /nix @@ -598,7 +598,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8 + uses: namespacelabs/nscloud-cache-action@0ac1550c04676e19d39872be6216ccbf9c6bab43 # v1.2.9 with: path: | /nix @@ -653,7 +653,7 @@ jobs: steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8 + uses: namespacelabs/nscloud-cache-action@0ac1550c04676e19d39872be6216ccbf9c6bab43 # v1.2.9 with: path: | /nix @@ -681,7 +681,7 @@ jobs: steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8 + uses: namespacelabs/nscloud-cache-action@0ac1550c04676e19d39872be6216ccbf9c6bab43 # v1.2.9 with: path: | /nix @@ -708,7 +708,7 @@ jobs: steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8 + uses: namespacelabs/nscloud-cache-action@0ac1550c04676e19d39872be6216ccbf9c6bab43 # v1.2.9 with: path: | /nix @@ -735,7 +735,7 @@ jobs: steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8 + uses: namespacelabs/nscloud-cache-action@0ac1550c04676e19d39872be6216ccbf9c6bab43 # v1.2.9 with: path: | /nix @@ -762,7 +762,7 @@ jobs: steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8 + uses: namespacelabs/nscloud-cache-action@0ac1550c04676e19d39872be6216ccbf9c6bab43 # v1.2.9 with: path: | /nix @@ -789,7 +789,7 @@ jobs: steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8 + uses: namespacelabs/nscloud-cache-action@0ac1550c04676e19d39872be6216ccbf9c6bab43 # v1.2.9 with: path: | /nix @@ -823,7 +823,7 @@ jobs: steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8 + uses: namespacelabs/nscloud-cache-action@0ac1550c04676e19d39872be6216ccbf9c6bab43 # v1.2.9 with: path: | /nix @@ -850,7 +850,7 @@ jobs: steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8 + uses: namespacelabs/nscloud-cache-action@0ac1550c04676e19d39872be6216ccbf9c6bab43 # v1.2.9 with: path: | /nix @@ -885,7 +885,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8 + uses: namespacelabs/nscloud-cache-action@0ac1550c04676e19d39872be6216ccbf9c6bab43 # v1.2.9 with: path: | /nix @@ -943,7 +943,7 @@ jobs: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8 + uses: namespacelabs/nscloud-cache-action@0ac1550c04676e19d39872be6216ccbf9c6bab43 # v1.2.9 with: path: | /nix diff --git a/.github/workflows/update-colorschemes.yml b/.github/workflows/update-colorschemes.yml index 5392a9e11..20cda12c9 100644 --- a/.github/workflows/update-colorschemes.yml +++ b/.github/workflows/update-colorschemes.yml @@ -22,7 +22,7 @@ jobs: fetch-depth: 0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8 + uses: namespacelabs/nscloud-cache-action@0ac1550c04676e19d39872be6216ccbf9c6bab43 # v1.2.9 with: path: | /nix From 9fa26387ef3dc4311594a87b7640afe75c5a3d11 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 10 Jul 2025 07:10:50 -0700 Subject: [PATCH 29/46] build: `zig build test` runs Xcode tests on macOS Related to #7879 This commit updates `zig build test` to run Xcode tests, too. These run in parallel to the Zig tests, so they don't add any time to the test. The Xcode tests will _not_ run when: (1) the target is not macOS, or (2) the `-Dtest-filter` option is non-empty. This makes it so that this change doesn't affect non-macOS and doesn't affect the general dev cycle because you usually will run `-Dtest-filter` when developing a core feature. I didn't add a step to only run Xcode tests because I find that when I'm working in Xcode I'm probably going to run the tests from there anyways. The integration with `zig build test` is just a convenience, especially around CI. Speaking of CI, this change also makes it so this will run in CI. --- build.zig | 41 +++++--- macos/Ghostty.xcodeproj/project.pbxproj | 6 +- .../BenchmarkTests.swift | 0 pkg/harfbuzz/build.zig | 26 +++-- pkg/harfbuzz/c.zig | 5 +- src/build/GhosttyXcodebuild.zig | 98 ++++++++++++++----- src/build/SharedDeps.zig | 2 +- 7 files changed, 121 insertions(+), 57 deletions(-) rename macos/{GhosttyTests => Tests}/BenchmarkTests.swift (100%) diff --git a/build.zig b/build.zig index 024e2db61..1c98b2fa5 100644 --- a/build.zig +++ b/build.zig @@ -8,7 +8,22 @@ comptime { } pub fn build(b: *std.Build) !void { + // This defines all the available build options (e.g. `-D`). const config = try buildpkg.Config.init(b); + const test_filter = b.option( + []const u8, + "test-filter", + "Filter for test. Only applies to Zig tests.", + ); + + // All our steps which we'll hook up later. The steps are shown + // up here just so that they are more self-documenting. + const run_step = b.step("run", "Run the app"); + const test_step = b.step("test", "Run all tests"); + const translations_step = b.step( + "update-translations", + "Update translation files", + ); // Ghostty resources like terminfo, shell integration, themes, etc. const resources = try buildpkg.GhosttyResources.init(b, &config); @@ -131,7 +146,6 @@ pub fn build(b: *std.Build) !void { b.getInstallPath(.prefix, "share/ghostty"), ); - const run_step = b.step("run", "Run the app"); run_step.dependOn(&run_cmd.step); break :run; } @@ -157,16 +171,18 @@ pub fn build(b: *std.Build) !void { }, ); - const run_step = b.step("run", "Run the app"); + // Run uses the native macOS app run_step.dependOn(&macos_app_native_only.open.step); + + // If we have no test filters, install the tests too + if (test_filter == null) { + macos_app_native_only.addTestStepDependencies(test_step); + } } } // Tests { - const test_step = b.step("test", "Run all tests"); - const test_filter = b.option([]const u8, "test-filter", "Filter for test"); - const test_exe = b.addTest(.{ .name = "ghostty-test", .filters = if (test_filter) |v| &.{v} else &.{}, @@ -180,18 +196,13 @@ pub fn build(b: *std.Build) !void { }), }); - { - if (config.emit_test_exe) b.installArtifact(test_exe); - _ = try deps.add(test_exe); - const test_run = b.addRunArtifact(test_exe); - test_step.dependOn(&test_run.step); - } + if (config.emit_test_exe) b.installArtifact(test_exe); + _ = try deps.add(test_exe); + const test_run = b.addRunArtifact(test_exe); + test_step.dependOn(&test_run.step); } // update-translations does what it sounds like and updates the "pot" // files. These should be committed to the repo. - { - const step = b.step("update-translations", "Update translation files"); - step.dependOn(i18n.update_step); - } + translations_step.dependOn(i18n.update_step); } diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index f7ae5f525..0c54ba693 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -303,7 +303,7 @@ /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ - A54F45F42E1F047A0046BD5C /* GhosttyTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = GhosttyTests; sourceTree = ""; }; + A54F45F42E1F047A0046BD5C /* Tests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Tests; sourceTree = ""; }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ @@ -612,7 +612,7 @@ A51BFC282B30F26D00E92F16 /* GhosttyDebug.entitlements */, 3B39CAA42B33949B00DABEB8 /* GhosttyReleaseLocal.entitlements */, A54CD6ED299BEB14008C95BB /* Sources */, - A54F45F42E1F047A0046BD5C /* GhosttyTests */, + A54F45F42E1F047A0046BD5C /* Tests */, A5D495A3299BECBA00DD1313 /* Frameworks */, A5A1F8862A489D7400D1E8BC /* Resources */, A5B30532299BEAAA0047F10C /* Products */, @@ -712,7 +712,7 @@ A54F45F82E1F047A0046BD5C /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( - A54F45F42E1F047A0046BD5C /* GhosttyTests */, + A54F45F42E1F047A0046BD5C /* Tests */, ); name = GhosttyTests; packageProductDependencies = ( diff --git a/macos/GhosttyTests/BenchmarkTests.swift b/macos/Tests/BenchmarkTests.swift similarity index 100% rename from macos/GhosttyTests/BenchmarkTests.swift rename to macos/Tests/BenchmarkTests.swift diff --git a/pkg/harfbuzz/build.zig b/pkg/harfbuzz/build.zig index 3bdc30a32..424f2afed 100644 --- a/pkg/harfbuzz/build.zig +++ b/pkg/harfbuzz/build.zig @@ -15,15 +15,23 @@ pub fn build(b: *std.Build) !void { }); const macos = b.dependency("macos", .{ .target = target, .optimize = optimize }); - const module = b.addModule("harfbuzz", .{ - .root_source_file = b.path("main.zig"), - .target = target, - .optimize = optimize, - .imports = &.{ - .{ .name = "freetype", .module = freetype.module("freetype") }, - .{ .name = "macos", .module = macos.module("macos") }, - }, - }); + const module = harfbuzz: { + const module = b.addModule("harfbuzz", .{ + .root_source_file = b.path("main.zig"), + .target = target, + .optimize = optimize, + .imports = &.{ + .{ .name = "freetype", .module = freetype.module("freetype") }, + .{ .name = "macos", .module = macos.module("macos") }, + }, + }); + + const options = b.addOptions(); + options.addOption(bool, "coretext", coretext_enabled); + options.addOption(bool, "freetype", freetype_enabled); + module.addOptions("build_options", options); + break :harfbuzz module; + }; // For dynamic linking, we prefer dynamic linking and to search by // mode first. Mode first will search all paths for a dynamic library diff --git a/pkg/harfbuzz/c.zig b/pkg/harfbuzz/c.zig index 51e477ebf..49e87dce7 100644 --- a/pkg/harfbuzz/c.zig +++ b/pkg/harfbuzz/c.zig @@ -1,7 +1,8 @@ const builtin = @import("builtin"); +const build_options = @import("build_options"); pub const c = @cImport({ @cInclude("hb.h"); - @cInclude("hb-ft.h"); - if (builtin.os.tag == .macos) @cInclude("hb-coretext.h"); + if (build_options.freetype) @cInclude("hb-ft.h"); + if (build_options.coretext) @cInclude("hb-coretext.h"); }); diff --git a/src/build/GhosttyXcodebuild.zig b/src/build/GhosttyXcodebuild.zig index 7fa2d2f95..d3bda032d 100644 --- a/src/build/GhosttyXcodebuild.zig +++ b/src/build/GhosttyXcodebuild.zig @@ -12,6 +12,7 @@ const XCFramework = @import("GhosttyXCFramework.zig"); build: *std.Build.Step.Run, open: *std.Build.Step.Run, copy: *std.Build.Step.Run, +xctest: *std.Build.Step.Run, pub const Deps = struct { xcframework: *const XCFramework, @@ -33,6 +34,21 @@ pub fn init( => "Release", }; + const xc_arch: ?[]const u8 = switch (deps.xcframework.target) { + // Universal is our default target, so we don't have to + // add anything. + .universal => null, + + // Native we need to override the architecture in the Xcode + // project with the -arch flag. + .native => switch (builtin.cpu.arch) { + .aarch64 => "arm64", + .x86_64 => "x86_64", + else => @panic("unsupported macOS arch"), + }, + }; + + const env = try std.process.getEnvMap(b.allocator); const app_path = b.fmt("macos/build/{s}/Ghostty.app", .{xc_config}); // Our step to build the Ghostty macOS app. @@ -41,12 +57,13 @@ pub fn init( // we create a new empty environment. const env_map = try b.allocator.create(std.process.EnvMap); env_map.* = .init(b.allocator); + if (env.get("PATH")) |v| try env_map.put("PATH", v); - const build = RunStep.create(b, "xcodebuild"); - build.has_side_effects = true; - build.cwd = b.path("macos"); - build.env_map = env_map; - build.addArgs(&.{ + const step = RunStep.create(b, "xcodebuild"); + step.has_side_effects = true; + step.cwd = b.path("macos"); + step.env_map = env_map; + step.addArgs(&.{ "xcodebuild", "-target", "Ghostty", @@ -54,36 +71,55 @@ pub fn init( xc_config, }); - switch (deps.xcframework.target) { - // Universal is our default target, so we don't have to - // add anything. - .universal => {}, - - // Native we need to override the architecture in the Xcode - // project with the -arch flag. - .native => build.addArgs(&.{ - "-arch", - switch (builtin.cpu.arch) { - .aarch64 => "arm64", - .x86_64 => "x86_64", - else => @panic("unsupported macOS arch"), - }, - }), - } + // If we have a specific architecture, we need to pass it + // to xcodebuild. + if (xc_arch) |arch| step.addArgs(&.{ "-arch", arch }); // We need the xcframework - deps.xcframework.addStepDependencies(&build.step); + deps.xcframework.addStepDependencies(&step.step); // We also need all these resources because the xcode project // references them via symlinks. - deps.resources.addStepDependencies(&build.step); - deps.i18n.addStepDependencies(&build.step); - deps.docs.installDummy(&build.step); + deps.resources.addStepDependencies(&step.step); + deps.i18n.addStepDependencies(&step.step); + deps.docs.installDummy(&step.step); // Expect success - build.expectExitCode(0); + step.expectExitCode(0); - break :build build; + break :build step; + }; + + const xctest = xctest: { + const env_map = try b.allocator.create(std.process.EnvMap); + env_map.* = .init(b.allocator); + if (env.get("PATH")) |v| try env_map.put("PATH", v); + + const step = RunStep.create(b, "xcodebuild test"); + step.has_side_effects = true; + step.cwd = b.path("macos"); + step.env_map = env_map; + step.addArgs(&.{ + "xcodebuild", + "test", + "-scheme", + "Ghostty", + }); + if (xc_arch) |arch| step.addArgs(&.{ "-arch", arch }); + + // We need the xcframework + deps.xcframework.addStepDependencies(&step.step); + + // We also need all these resources because the xcode project + // references them via symlinks. + deps.resources.addStepDependencies(&step.step); + deps.i18n.addStepDependencies(&step.step); + deps.docs.installDummy(&step.step); + + // Expect success + step.expectExitCode(0); + + break :xctest step; }; // Our step to open the resulting Ghostty app. @@ -143,6 +179,7 @@ pub fn init( .build = build, .open = open, .copy = copy, + .xctest = xctest, }; } @@ -155,3 +192,10 @@ pub fn installXcframework(self: *const Ghostty) void { const b = self.build.step.owner; b.getInstallStep().dependOn(&self.build.step); } + +pub fn addTestStepDependencies( + self: *const Ghostty, + other_step: *std.Build.Step, +) void { + other_step.dependOn(&self.xctest.step); +} diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index b6e9900e2..ea7e696ef 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -139,7 +139,7 @@ pub fn add( if (b.lazyDependency("harfbuzz", .{ .target = target, .optimize = optimize, - .@"enable-freetype" = true, + .@"enable-freetype" = self.config.font_backend.hasFreetype(), .@"enable-coretext" = self.config.font_backend.hasCoretext(), })) |harfbuzz_dep| { step.root_module.addImport( From cfad2e817b1f4992715a80c915e08402e52d2f45 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 10 Jul 2025 21:11:55 -0700 Subject: [PATCH 30/46] ci: downgrade back to Xcode 26 beta 1, the icon is broken in beta 3 --- .github/workflows/release-pr.yml | 4 ++-- .github/workflows/release-tip.yml | 6 +++--- .github/workflows/test.yml | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index a4bf69b1c..7f48d109f 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -47,7 +47,7 @@ jobs: sentry-cli dif upload --project ghostty --wait dsym.zip build-macos: - runs-on: namespace-profile-ghostty-macos-sequoia-edge + runs-on: namespace-profile-ghostty-macos-sequoia timeout-minutes: 90 steps: - name: Checkout code @@ -201,7 +201,7 @@ jobs: destination-dir: ./ build-macos-debug: - runs-on: namespace-profile-ghostty-macos-sequoia-edge + runs-on: namespace-profile-ghostty-macos-sequoia timeout-minutes: 90 steps: - name: Checkout code diff --git a/.github/workflows/release-tip.yml b/.github/workflows/release-tip.yml index a9cff9a2b..d784301b5 100644 --- a/.github/workflows/release-tip.yml +++ b/.github/workflows/release-tip.yml @@ -154,7 +154,7 @@ jobs: ) }} - runs-on: namespace-profile-ghostty-macos-sequoia-edge + runs-on: namespace-profile-ghostty-macos-sequoia timeout-minutes: 90 steps: - name: Checkout code @@ -374,7 +374,7 @@ jobs: ) }} - runs-on: namespace-profile-ghostty-macos-sequoia-edge + runs-on: namespace-profile-ghostty-macos-sequoia timeout-minutes: 90 steps: - name: Checkout code @@ -554,7 +554,7 @@ jobs: ) }} - runs-on: namespace-profile-ghostty-macos-sequoia-edge + runs-on: namespace-profile-ghostty-macos-sequoia timeout-minutes: 90 steps: - name: Checkout code diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 86dff2ea1..7e2b4e68b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -271,7 +271,7 @@ jobs: ghostty-source.tar.gz build-macos: - runs-on: namespace-profile-ghostty-macos-sequoia-edge + runs-on: namespace-profile-ghostty-macos-sequoia needs: test steps: - name: Checkout code @@ -354,7 +354,7 @@ jobs: xcodebuild -target Ghostty-iOS "CODE_SIGNING_ALLOWED=NO" build-macos-matrix: - runs-on: namespace-profile-ghostty-macos-sequoia-edge + runs-on: namespace-profile-ghostty-macos-sequoia needs: test steps: - name: Checkout code @@ -618,7 +618,7 @@ jobs: nix develop -c zig build -Dsentry=${{ matrix.sentry }} test-macos: - runs-on: namespace-profile-ghostty-macos-sequoia-edge + runs-on: namespace-profile-ghostty-macos-sequoia needs: test steps: - name: Checkout code From 991426e84e069008e36d6fddeef52dbc697df04f Mon Sep 17 00:00:00 2001 From: nferhat Date: Fri, 11 Jul 2025 12:47:28 +0100 Subject: [PATCH 31/46] renderer: Allow the renderer to draw transparent cells Co-authored-by: Kat <65649991+00-kat@users.noreply.github.com> --- src/config/Config.zig | 16 ++++++++++++++++ src/renderer/generic.zig | 9 +++++++++ 2 files changed, 25 insertions(+) diff --git a/src/config/Config.zig b/src/config/Config.zig index 5ebb5561b..1e2086876 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -814,6 +814,22 @@ palette: Palette = .{}, /// On macOS, changing this configuration requires restarting Ghostty completely. @"background-opacity": f64 = 1.0, +/// Applies background opacity to cells with an explicit background color +/// set. +/// +/// Normally, `background-opacity` is only applied to the window background. +/// If a cell has an explicit background color set, such as red, then that +/// background color will be fully opaque. An effect of this is that some +/// terminal applications that repaint the background color of the terminal +/// such as a Neovim and Tmux may not respect the `background-opacity` +/// (by design). +/// +/// Setting this to `true` will apply the `background-opacity` to all cells +/// regardless of whether they have an explicit background color set or not. +/// +/// Available since: 1.2.0 +@"background-opacity-cells": bool = false, + /// Whether to blur the background when `background-opacity` is less than 1. /// /// Valid values are: diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index 2374ec1b0..1517ec662 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -516,6 +516,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { cursor_text: ?configpkg.Config.TerminalColor, background: terminal.color.RGB, background_opacity: f64, + background_opacity_cells: bool, foreground: terminal.color.RGB, selection_background: ?configpkg.Config.TerminalColor, selection_foreground: ?configpkg.Config.TerminalColor, @@ -568,6 +569,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { return .{ .background_opacity = @max(0, @min(1, config.@"background-opacity")), + .background_opacity_cells = config.@"background-opacity-cells", .font_thicken = config.@"font-thicken", .font_thicken_strength = config.@"font-thicken-strength", .font_features = font_features.list, @@ -2628,6 +2630,13 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // Cells that are reversed should be fully opaque. if (style.flags.inverse) break :bg_alpha default; + // If the user requested to have opacity on all cells, apply it. + if (self.config.background_opacity_cells and bg_style != null) { + var opacity: f64 = @floatFromInt(default); + opacity *= self.config.background_opacity; + break :bg_alpha @intFromFloat(opacity); + } + // Cells that have an explicit bg color should be fully opaque. if (bg_style != null) break :bg_alpha default; From c4e10a1ac1f808c53d036e273973fd717ffdbd09 Mon Sep 17 00:00:00 2001 From: Hojin You Date: Fri, 11 Jul 2025 10:06:34 -0400 Subject: [PATCH 32/46] Update po/ko_KR.UTF-8.po remove extra space Co-authored-by: trag1c --- po/ko_KR.UTF-8.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/po/ko_KR.UTF-8.po b/po/ko_KR.UTF-8.po index c13fe8a61..875e7a1a5 100644 --- a/po/ko_KR.UTF-8.po +++ b/po/ko_KR.UTF-8.po @@ -213,7 +213,7 @@ msgstr "이 분할에 대한 선택 기억하기" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 msgid "Reload configuration to show this prompt again" -msgstr "이 창을 다시 보려면 설정을 다시 불러오세요" +msgstr "이 창을 다시 보려면 설정을 다시 불러오세요" #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 #: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 From 033d8c3099708504cff6746659424ecb29ece309 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sun, 6 Jul 2025 15:06:00 -0500 Subject: [PATCH 33/46] core/gtk: add apprt action to show native GUI warning when child exits Addresses #7649 for the core and GTK. macOS support will need to be added later. This adds an apprt action to show a native GUI warning of some kind when the child process of a terminal exits. Also adds a basic GTK implementation of this. In GTK it overlays an Adwaita banner at the bottom of the window (similar to the banner that shows up in at the top of windows in debug builds). --- include/ghostty.h | 8 ++++++++ macos/Sources/Ghostty/Ghostty.App.swift | 2 ++ src/Surface.zig | 16 ++++++++++++++++ src/apprt/action.zig | 4 ++++ src/apprt/gtk/App.zig | 8 ++++++++ src/apprt/gtk/Surface.zig | 17 +++++++++++++++++ src/apprt/surface.zig | 2 +- 7 files changed, 56 insertions(+), 1 deletion(-) diff --git a/include/ghostty.h b/include/ghostty.h index bcd88251b..0c9b840e7 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -680,6 +680,12 @@ typedef struct { uintptr_t len; } ghostty_action_open_url_s; +// apprt.surface.Message.ChildExited +typedef struct { + uint32_t exit_code; + uint64_t timetime_ms; +} ghostty_surface_message_childexited_s; + // apprt.Action.Key typedef enum { GHOSTTY_ACTION_QUIT, @@ -731,6 +737,7 @@ typedef enum { GHOSTTY_ACTION_REDO, GHOSTTY_ACTION_CHECK_FOR_UPDATES, GHOSTTY_ACTION_OPEN_URL, + GHOSTTY_ACTION_SHOW_CHILD_EXITED } ghostty_action_tag_e; typedef union { @@ -759,6 +766,7 @@ typedef union { ghostty_action_reload_config_s reload_config; ghostty_action_config_change_s config_change; ghostty_action_open_url_s open_url; + ghostty_surface_message_childexited_s child_exited; } ghostty_action_u; typedef struct { diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index 0fdea1760..f78585c9a 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -579,6 +579,8 @@ extension Ghostty { case GHOSTTY_ACTION_SIZE_LIMIT: fallthrough case GHOSTTY_ACTION_QUIT_TIMER: + fallthrough + case GHOSTTY_SHOW_CHILD_EXITED: Ghostty.logger.info("known but unimplemented action action=\(action.tag.rawValue)") return false default: diff --git a/src/Surface.zig b/src/Surface.zig index a4a8d46df..6e58ab5a5 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1018,6 +1018,14 @@ fn childExited(self: *Surface, info: apprt.surface.Message.ChildExited) void { return; }; + _ = self.rt_app.performAction( + .{ .surface = self }, + .show_child_exited, + info, + ) catch |err| { + log.err("error trying to show native child exited GUI err={}", .{err}); + }; + return; } @@ -1044,6 +1052,14 @@ fn childExited(self: *Surface, info: apprt.surface.Message.ChildExited) void { t.screen.kitty_keyboard.set(.set, .{}); } + _ = self.rt_app.performAction( + .{ .surface = self }, + .show_child_exited, + info, + ) catch |err| { + log.err("error trying to show native child exited GUI err={}", .{err}); + }; + // Waiting after command we stop here. The terminal is updated, our // state is updated, and now its up to the user to decide what to do. if (self.config.wait_after_command) return; diff --git a/src/apprt/action.zig b/src/apprt/action.zig index 1c3c7c72c..201d27e31 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -272,6 +272,9 @@ pub const Action = union(Key) { /// apprt. open_url: OpenUrl, + /// Show a native GUI notification that the child process has exited. + show_child_exited: apprt.surface.Message.ChildExited, + /// Sync with: ghostty_action_tag_e pub const Key = enum(c_int) { quit, @@ -323,6 +326,7 @@ pub const Action = union(Key) { redo, check_for_updates, open_url, + show_child_exited, }; /// Sync with: ghostty_action_u diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index bdb2f0f24..a3a6ec411 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -521,6 +521,7 @@ pub fn performAction( .ring_bell => try self.ringBell(target), .toggle_command_palette => try self.toggleCommandPalette(target), .open_url => self.openUrl(value), + .show_child_exited => try self.showChildExited(target, value), // Unimplemented .close_all_windows, @@ -846,6 +847,13 @@ fn toggleCommandPalette(_: *App, target: apprt.Target) !void { } } +fn showChildExited(_: *App, target: apprt.Target, value: apprt.surface.Message.ChildExited) !void { + switch (target) { + .app => {}, + .surface => |surface| try surface.rt_surface.showChildExited(value), + } +} + fn quitTimer(self: *App, mode: apprt.action.QuitTimer) void { switch (mode) { .start => self.startQuitTimer(), diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index d16083d5a..7ea11bc17 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -2503,3 +2503,20 @@ fn gtkStreamError(media_file: *gtk.MediaFile, _: *gobject.ParamSpec, _: ?*anyopa fn gtkStreamEnded(media_file: *gtk.MediaFile, _: *gobject.ParamSpec, _: ?*anyopaque) callconv(.c) void { media_file.unref(); } + +pub fn showChildExited(self: *Surface, _: apprt.surface.Message.ChildExited) (error{})!void { + if (!adw_version.supportsBanner()) return; + + const warning_box = gtk.Box.new(.vertical, 0); + + warning_box.as(gtk.Widget).setHalign(.fill); + warning_box.as(gtk.Widget).setValign(.end); + + const warning_text = i18n._("⚠️ Process exited. Press any key to close the terminal."); + const banner = adw.Banner.new(warning_text); + banner.setRevealed(1); + + warning_box.append(banner.as(gtk.Widget)); + + self.overlay.addOverlay(warning_box.as(gtk.Widget)); +} diff --git a/src/apprt/surface.zig b/src/apprt/surface.zig index 9254b2fd5..1cd53b66a 100644 --- a/src/apprt/surface.zig +++ b/src/apprt/surface.zig @@ -98,7 +98,7 @@ pub const Message = union(enum) { // This enum is a placeholder for future title styles. }; - pub const ChildExited = struct { + pub const ChildExited = extern struct { exit_code: u32, runtime_ms: u64, }; From cd9174e7e80285fca5cab9a18c55eba039b50761 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sun, 6 Jul 2025 16:17:37 -0500 Subject: [PATCH 34/46] show child exited: fix macos build --- macos/Sources/Ghostty/Ghostty.App.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index f78585c9a..c94f40291 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -580,7 +580,7 @@ extension Ghostty { fallthrough case GHOSTTY_ACTION_QUIT_TIMER: fallthrough - case GHOSTTY_SHOW_CHILD_EXITED: + case GHOSTTY_ACTION_SHOW_CHILD_EXITED: Ghostty.logger.info("known but unimplemented action action=\(action.tag.rawValue)") return false default: From 103772ee8fdc3b96c5f997455874d79060a62a3b Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sun, 6 Jul 2025 16:30:53 -0500 Subject: [PATCH 35/46] show child exited: make GTK banner transparent --- src/apprt/gtk/Surface.zig | 13 +++++++++++-- src/apprt/gtk/style.css | 12 ++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 7ea11bc17..f8d6dd5da 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -2504,7 +2504,7 @@ fn gtkStreamEnded(media_file: *gtk.MediaFile, _: *gobject.ParamSpec, _: ?*anyopa media_file.unref(); } -pub fn showChildExited(self: *Surface, _: apprt.surface.Message.ChildExited) (error{})!void { +pub fn showChildExited(self: *Surface, info: apprt.surface.Message.ChildExited) (error{})!void { if (!adw_version.supportsBanner()) return; const warning_box = gtk.Box.new(.vertical, 0); @@ -2516,7 +2516,16 @@ pub fn showChildExited(self: *Surface, _: apprt.surface.Message.ChildExited) (er const banner = adw.Banner.new(warning_text); banner.setRevealed(1); - warning_box.append(banner.as(gtk.Widget)); + const banner_widget = banner.as(gtk.Widget); + + banner_widget.addCssClass("child_exited"); + + if (info.exit_code == 0) + banner_widget.addCssClass("child_exited_normally") + else + banner_widget.addCssClass("child_exited_abnormally"); + + warning_box.append(banner_widget); self.overlay.addOverlay(warning_box.as(gtk.Widget)); } diff --git a/src/apprt/gtk/style.css b/src/apprt/gtk/style.css index 2051ab1e3..f3106105f 100644 --- a/src/apprt/gtk/style.css +++ b/src/apprt/gtk/style.css @@ -93,3 +93,15 @@ window.ssd.no-border-radius { margin-left: 4px; margin-right: 8px; } + +banner.child_exited_normally revealer widget { + background-color: rgba(38, 162, 105, 0.5); + /* after GTK 4.16 is a requirement, switch to the following: + /* background-color: color-mix(in srgb, var(--success-bg-color), transparent 50%); */ +} + +banner.child_exited_abnormally revealer widget { + background-color: rgba(192, 28, 40, 0.5); + /* after GTK 4.16 is a requirement, switch to the following: + /* background-color: color-mix(in srgb, var(--error-bg-color), transparent 50%); */ +} From 5219bc51e51d32560642e9de1ae85df48e8e1ffc Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sun, 6 Jul 2025 16:48:15 -0500 Subject: [PATCH 36/46] show child exited: return a boolean if native GUI is shown If a native GUI is shown by the runtime when a child exits, use the returned boolean to determine if text should be show in the terminal to avoid duplicating information. --- src/Surface.zig | 40 ++++++++++++++++++++++----------------- src/apprt/gtk/App.zig | 8 ++++---- src/apprt/gtk/Surface.zig | 6 ++++-- 3 files changed, 31 insertions(+), 23 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 6e58ab5a5..af0a742c6 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1011,19 +1011,21 @@ fn childExited(self: *Surface, info: apprt.surface.Message.ChildExited) void { log.warn("abnormal process exit detected, showing error message", .{}); - // Update our terminal to note the abnormal exit. In the future we - // may want the apprt to handle this to show some native GUI element. - self.childExitedAbnormally(info) catch |err| { - log.err("error handling abnormal child exit err={}", .{err}); - return; - }; - - _ = self.rt_app.performAction( + // Try and show a GUI message. If it returns true, don't do anything else. + if (self.rt_app.performAction( .{ .surface = self }, .show_child_exited, info, - ) catch |err| { + ) catch |err| gui: { log.err("error trying to show native child exited GUI err={}", .{err}); + break :gui false; + }) return; + + // If a native GUI notification was not showm. update our terminal to + // note the abnormal exit. + self.childExitedAbnormally(info) catch |err| { + log.err("error handling abnormal child exit err={}", .{err}); + return; }; return; @@ -1036,6 +1038,18 @@ fn childExited(self: *Surface, info: apprt.surface.Message.ChildExited) void { // surface then they will see this message and know the process has // completed. terminal: { + // First try and show a native GUI message. + if (self.rt_app.performAction( + .{ .surface = self }, + .show_child_exited, + info, + ) catch |err| gui: { + log.err("error trying to show native child exited GUI err={}", .{err}); + break :gui false; + }) break :terminal; + + // If the native GUI can't be shown, display a text message in the + // terminal. self.renderer_state.mutex.lock(); defer self.renderer_state.mutex.unlock(); const t: *terminal.Terminal = self.renderer_state.terminal; @@ -1052,14 +1066,6 @@ fn childExited(self: *Surface, info: apprt.surface.Message.ChildExited) void { t.screen.kitty_keyboard.set(.set, .{}); } - _ = self.rt_app.performAction( - .{ .surface = self }, - .show_child_exited, - info, - ) catch |err| { - log.err("error trying to show native child exited GUI err={}", .{err}); - }; - // Waiting after command we stop here. The terminal is updated, our // state is updated, and now its up to the user to decide what to do. if (self.config.wait_after_command) return; diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index a3a6ec411..8987d3504 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -521,7 +521,7 @@ pub fn performAction( .ring_bell => try self.ringBell(target), .toggle_command_palette => try self.toggleCommandPalette(target), .open_url => self.openUrl(value), - .show_child_exited => try self.showChildExited(target, value), + .show_child_exited => return try self.showChildExited(target, value), // Unimplemented .close_all_windows, @@ -847,10 +847,10 @@ fn toggleCommandPalette(_: *App, target: apprt.Target) !void { } } -fn showChildExited(_: *App, target: apprt.Target, value: apprt.surface.Message.ChildExited) !void { +fn showChildExited(_: *App, target: apprt.Target, value: apprt.surface.Message.ChildExited) (error{})!bool { switch (target) { - .app => {}, - .surface => |surface| try surface.rt_surface.showChildExited(value), + .app => return false, + .surface => |surface| return try surface.rt_surface.showChildExited(value), } } diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index f8d6dd5da..5d835b4b3 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -2504,8 +2504,8 @@ fn gtkStreamEnded(media_file: *gtk.MediaFile, _: *gobject.ParamSpec, _: ?*anyopa media_file.unref(); } -pub fn showChildExited(self: *Surface, info: apprt.surface.Message.ChildExited) (error{})!void { - if (!adw_version.supportsBanner()) return; +pub fn showChildExited(self: *Surface, info: apprt.surface.Message.ChildExited) (error{})!bool { + if (!adw_version.supportsBanner()) return false; const warning_box = gtk.Box.new(.vertical, 0); @@ -2528,4 +2528,6 @@ pub fn showChildExited(self: *Surface, info: apprt.surface.Message.ChildExited) warning_box.append(banner_widget); self.overlay.addOverlay(warning_box.as(gtk.Widget)); + + return true; } From 28c7083876353d4f584c2b3be0885baf8c4fd741 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sun, 6 Jul 2025 16:54:34 -0500 Subject: [PATCH 37/46] gtk show_child_edit: use different text for normal/abnormal exit Without this, the only indication would be the difference between a red or a green background which would be problematic for users with limited vision or color blindness. --- src/apprt/gtk/Surface.zig | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 5d835b4b3..14a1538ab 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -2512,7 +2512,11 @@ pub fn showChildExited(self: *Surface, info: apprt.surface.Message.ChildExited) warning_box.as(gtk.Widget).setHalign(.fill); warning_box.as(gtk.Widget).setValign(.end); - const warning_text = i18n._("⚠️ Process exited. Press any key to close the terminal."); + const warning_text = if (info.exit_code == 0) + i18n._("⚠️ Process exited normally. Press any key to close the terminal.") + else + i18n._("⚠️ Process exited abnormally. Press any key to close the terminal."); + const banner = adw.Banner.new(warning_text); banner.setRevealed(1); From 3d89a68fffe59082ca5532fccc1f340247dd89eb Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Mon, 7 Jul 2025 11:48:08 -0500 Subject: [PATCH 38/46] fix error set for function return signature --- src/apprt/gtk/App.zig | 2 +- src/apprt/gtk/Surface.zig | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 8987d3504..d6a50f0f6 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -847,7 +847,7 @@ fn toggleCommandPalette(_: *App, target: apprt.Target) !void { } } -fn showChildExited(_: *App, target: apprt.Target, value: apprt.surface.Message.ChildExited) (error{})!bool { +fn showChildExited(_: *App, target: apprt.Target, value: apprt.surface.Message.ChildExited) error{}!bool { switch (target) { .app => return false, .surface => |surface| return try surface.rt_surface.showChildExited(value), diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 14a1538ab..b8b5145af 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -2504,7 +2504,7 @@ fn gtkStreamEnded(media_file: *gtk.MediaFile, _: *gobject.ParamSpec, _: ?*anyopa media_file.unref(); } -pub fn showChildExited(self: *Surface, info: apprt.surface.Message.ChildExited) (error{})!bool { +pub fn showChildExited(self: *Surface, info: apprt.surface.Message.ChildExited) error{}!bool { if (!adw_version.supportsBanner()) return false; const warning_box = gtk.Box.new(.vertical, 0); From bf0659f07b8f7bef92b95a3a383ea814736b7690 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Mon, 7 Jul 2025 11:52:13 -0500 Subject: [PATCH 39/46] use checkmark for icon if child exits normally --- src/apprt/gtk/Surface.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index b8b5145af..480907109 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -2513,7 +2513,7 @@ pub fn showChildExited(self: *Surface, info: apprt.surface.Message.ChildExited) warning_box.as(gtk.Widget).setValign(.end); const warning_text = if (info.exit_code == 0) - i18n._("⚠️ Process exited normally. Press any key to close the terminal.") + i18n._("✔️ Process exited normally. Press any key to close the terminal.") else i18n._("⚠️ Process exited abnormally. Press any key to close the terminal."); From f24ec13963567c02d3b850e7ca5df4b279fbd062 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Mon, 7 Jul 2025 11:59:03 -0500 Subject: [PATCH 40/46] remove unused css class from gtk child exited widget --- src/apprt/gtk/Surface.zig | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 480907109..2c405690c 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -2522,8 +2522,6 @@ pub fn showChildExited(self: *Surface, info: apprt.surface.Message.ChildExited) const banner_widget = banner.as(gtk.Widget); - banner_widget.addCssClass("child_exited"); - if (info.exit_code == 0) banner_widget.addCssClass("child_exited_normally") else From 7fd900647e9047b0d6d1e4e10ccbb1806a52e8a1 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Tue, 8 Jul 2025 09:52:57 -0500 Subject: [PATCH 41/46] show_child_exited: remove emojis --- src/apprt/gtk/Surface.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 2c405690c..3128c63e8 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -2513,9 +2513,9 @@ pub fn showChildExited(self: *Surface, info: apprt.surface.Message.ChildExited) warning_box.as(gtk.Widget).setValign(.end); const warning_text = if (info.exit_code == 0) - i18n._("✔️ Process exited normally. Press any key to close the terminal.") + i18n._("Process exited normally. Press any key to close the terminal.") else - i18n._("⚠️ Process exited abnormally. Press any key to close the terminal."); + i18n._("Process exited abnormally. Press any key to close the terminal."); const banner = adw.Banner.new(warning_text); banner.setRevealed(1); From 49243db4b399706323f8b1ae48847a5279fedf9b Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Tue, 8 Jul 2025 12:17:49 -0500 Subject: [PATCH 42/46] gtk: GtkBox not needed for show_child_exited --- src/apprt/gtk/Surface.zig | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 3128c63e8..6ccc132f0 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -2507,11 +2507,6 @@ fn gtkStreamEnded(media_file: *gtk.MediaFile, _: *gobject.ParamSpec, _: ?*anyopa pub fn showChildExited(self: *Surface, info: apprt.surface.Message.ChildExited) error{}!bool { if (!adw_version.supportsBanner()) return false; - const warning_box = gtk.Box.new(.vertical, 0); - - warning_box.as(gtk.Widget).setHalign(.fill); - warning_box.as(gtk.Widget).setValign(.end); - const warning_text = if (info.exit_code == 0) i18n._("Process exited normally. Press any key to close the terminal.") else @@ -2521,15 +2516,15 @@ pub fn showChildExited(self: *Surface, info: apprt.surface.Message.ChildExited) banner.setRevealed(1); const banner_widget = banner.as(gtk.Widget); + banner_widget.setHalign(.fill); + banner_widget.setValign(.end); if (info.exit_code == 0) banner_widget.addCssClass("child_exited_normally") else banner_widget.addCssClass("child_exited_abnormally"); - warning_box.append(banner_widget); - - self.overlay.addOverlay(warning_box.as(gtk.Widget)); + self.overlay.addOverlay(banner_widget); return true; } From 8cea1113290a6b57dc9e3e10dfeddbe964850d8f Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Tue, 8 Jul 2025 12:19:12 -0500 Subject: [PATCH 43/46] gtk: add some comments for show_child_exited --- src/apprt/gtk/Surface.zig | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 6ccc132f0..e2051b2f3 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -2504,6 +2504,9 @@ fn gtkStreamEnded(media_file: *gtk.MediaFile, _: *gobject.ParamSpec, _: ?*anyopa media_file.unref(); } +/// Show native GUI element with a notification that the child process has +/// closed. Return `true` if we are able to show the GUI notification, and +/// `false` if we are not. pub fn showChildExited(self: *Surface, info: apprt.surface.Message.ChildExited) error{}!bool { if (!adw_version.supportsBanner()) return false; From 9ee25e8a6948d23877d066639b694268afc09df1 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Tue, 8 Jul 2025 12:19:41 -0500 Subject: [PATCH 44/46] gtk: use close button in show_child_exited banner --- src/apprt/gtk/Surface.zig | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index e2051b2f3..2cdfb45c9 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -2511,12 +2511,21 @@ pub fn showChildExited(self: *Surface, info: apprt.surface.Message.ChildExited) if (!adw_version.supportsBanner()) return false; const warning_text = if (info.exit_code == 0) - i18n._("Process exited normally. Press any key to close the terminal.") + i18n._("Process exited normally.") else - i18n._("Process exited abnormally. Press any key to close the terminal."); + i18n._("Process exited abnormally."); const banner = adw.Banner.new(warning_text); banner.setRevealed(1); + banner.setButtonLabel(i18n._("Close")); + + _ = adw.Banner.signals.button_clicked.connect( + banner, + *Surface, + showChildExitedButtonClosed, + self, + .{}, + ); const banner_widget = banner.as(gtk.Widget); banner_widget.setHalign(.fill); @@ -2531,3 +2540,7 @@ pub fn showChildExited(self: *Surface, info: apprt.surface.Message.ChildExited) return true; } + +fn showChildExitedButtonClosed(_: *adw.Banner, self: *Surface) callconv(.c) void { + self.close(false); +} From 1abc3ba1dae0d4d769151d2ed2a33cd79134ba9a Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Tue, 8 Jul 2025 14:28:38 -0500 Subject: [PATCH 45/46] gtk: change child exited verbiage to be clearer and code cleanup --- src/apprt/gtk/Surface.zig | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 2cdfb45c9..a468bd48d 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -2510,10 +2510,10 @@ fn gtkStreamEnded(media_file: *gtk.MediaFile, _: *gobject.ParamSpec, _: ?*anyopa pub fn showChildExited(self: *Surface, info: apprt.surface.Message.ChildExited) error{}!bool { if (!adw_version.supportsBanner()) return false; - const warning_text = if (info.exit_code == 0) - i18n._("Process exited normally.") + const warning_text, const css_class = if (info.exit_code == 0) + .{ i18n._("Command succeeded"), "child_exited_normally" } else - i18n._("Process exited abnormally."); + .{ i18n._("Command failed"), "child_exited_abnormally" }; const banner = adw.Banner.new(warning_text); banner.setRevealed(1); @@ -2530,11 +2530,7 @@ pub fn showChildExited(self: *Surface, info: apprt.surface.Message.ChildExited) const banner_widget = banner.as(gtk.Widget); banner_widget.setHalign(.fill); banner_widget.setValign(.end); - - if (info.exit_code == 0) - banner_widget.addCssClass("child_exited_normally") - else - banner_widget.addCssClass("child_exited_abnormally"); + banner_widget.addCssClass(css_class); self.overlay.addOverlay(banner_widget); From 5a5e0df574e4158fafb71ade5851d32ff71eff6c Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Fri, 11 Jul 2025 22:57:17 -0500 Subject: [PATCH 46/46] i18n: update translations --- po/bg_BG.UTF-8.po | 12 ++++++++++-- po/ca_ES.UTF-8.po | 12 ++++++++++-- po/com.mitchellh.ghostty.pot | 12 ++++++++++-- po/de_DE.UTF-8.po | 12 ++++++++++-- po/es_AR.UTF-8.po | 12 ++++++++++-- po/es_BO.UTF-8.po | 12 ++++++++++-- po/fr_FR.UTF-8.po | 12 ++++++++++-- po/ga_IE.UTF-8.po | 12 ++++++++++-- po/he_IL.UTF-8.po | 12 ++++++++++-- po/id_ID.UTF-8.po | 12 ++++++++++-- po/ja_JP.UTF-8.po | 12 ++++++++++-- po/ko_KR.UTF-8.po | 12 ++++++++++-- po/mk_MK.UTF-8.po | 12 ++++++++++-- po/nb_NO.UTF-8.po | 12 ++++++++++-- po/nl_NL.UTF-8.po | 12 ++++++++++-- po/pl_PL.UTF-8.po | 12 ++++++++++-- po/pt_BR.UTF-8.po | 12 ++++++++++-- po/ru_RU.UTF-8.po | 12 ++++++++++-- po/tr_TR.UTF-8.po | 12 ++++++++++-- po/uk_UA.UTF-8.po | 12 ++++++++++-- po/zh_CN.UTF-8.po | 12 ++++++++++-- 21 files changed, 210 insertions(+), 42 deletions(-) diff --git a/po/bg_BG.UTF-8.po b/po/bg_BG.UTF-8.po index 84fd455e2..e92d76b38 100644 --- a/po/bg_BG.UTF-8.po +++ b/po/bg_BG.UTF-8.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2025-07-08 15:13-0500\n" +"POT-Creation-Date: 2025-07-11 22:56-0500\n" "PO-Revision-Date: 2025-05-19 11:34+0300\n" "Last-Translator: Damyan Bogoev \n" "Language-Team: Bulgarian \n" @@ -236,7 +236,7 @@ msgstr "" "Поставянето на този текст в терминала може да е опасно, тъй като изглежда, " "че може да бъдат изпълнени някои команди." -#: src/apprt/gtk/CloseDialog.zig:47 +#: src/apprt/gtk/CloseDialog.zig:47 src/apprt/gtk/Surface.zig:2520 msgid "Close" msgstr "Затвори" @@ -276,6 +276,14 @@ msgstr "Текущият процес в това разделяне ще бъд msgid "Copied to clipboard" msgstr "Копирано в клипборда" +#: src/apprt/gtk/Surface.zig:2514 +msgid "Command succeeded" +msgstr "" + +#: src/apprt/gtk/Surface.zig:2516 +msgid "Command failed" +msgstr "" + #: src/apprt/gtk/Window.zig:216 msgid "Main Menu" msgstr "Главно меню" diff --git a/po/ca_ES.UTF-8.po b/po/ca_ES.UTF-8.po index 11bc99f57..fe0cda009 100644 --- a/po/ca_ES.UTF-8.po +++ b/po/ca_ES.UTF-8.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2025-07-08 15:13-0500\n" +"POT-Creation-Date: 2025-07-11 22:56-0500\n" "PO-Revision-Date: 2025-03-20 08:07+0100\n" "Last-Translator: Francesc Arpi \n" "Language-Team: \n" @@ -236,7 +236,7 @@ msgstr "" "Enganxar aquest text al terminal pot ser perillós, ja que sembla que es " "podrien executar algunes ordres." -#: src/apprt/gtk/CloseDialog.zig:47 +#: src/apprt/gtk/CloseDialog.zig:47 src/apprt/gtk/Surface.zig:2520 msgid "Close" msgstr "Tanca" @@ -276,6 +276,14 @@ msgstr "El procés actualment en execució en aquesta divisió es tancarà." msgid "Copied to clipboard" msgstr "Copiat al porta-retalls" +#: src/apprt/gtk/Surface.zig:2514 +msgid "Command succeeded" +msgstr "" + +#: src/apprt/gtk/Surface.zig:2516 +msgid "Command failed" +msgstr "" + #: src/apprt/gtk/Window.zig:216 msgid "Main Menu" msgstr "Menú principal" diff --git a/po/com.mitchellh.ghostty.pot b/po/com.mitchellh.ghostty.pot index 584f843b6..0c7a39cd1 100644 --- a/po/com.mitchellh.ghostty.pot +++ b/po/com.mitchellh.ghostty.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2025-07-08 15:13-0500\n" +"POT-Creation-Date: 2025-07-11 22:56-0500\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -228,7 +228,7 @@ msgid "" "commands may be executed." msgstr "" -#: src/apprt/gtk/CloseDialog.zig:47 +#: src/apprt/gtk/CloseDialog.zig:47 src/apprt/gtk/Surface.zig:2520 msgid "Close" msgstr "" @@ -268,6 +268,14 @@ msgstr "" msgid "Copied to clipboard" msgstr "" +#: src/apprt/gtk/Surface.zig:2514 +msgid "Command succeeded" +msgstr "" + +#: src/apprt/gtk/Surface.zig:2516 +msgid "Command failed" +msgstr "" + #: src/apprt/gtk/Window.zig:216 msgid "Main Menu" msgstr "" diff --git a/po/de_DE.UTF-8.po b/po/de_DE.UTF-8.po index fcca71101..2b24b68b0 100644 --- a/po/de_DE.UTF-8.po +++ b/po/de_DE.UTF-8.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2025-07-08 15:13-0500\n" +"POT-Creation-Date: 2025-07-11 22:56-0500\n" "PO-Revision-Date: 2025-03-06 14:57+0100\n" "Last-Translator: Robin \n" "Language-Team: German \n" @@ -235,7 +235,7 @@ msgstr "" "Diesen Text in das Terminal einzufügen könnte möglicherweise gefährlich " "sein. Es scheint, dass Anweisungen ausgeführt werden könnten." -#: src/apprt/gtk/CloseDialog.zig:47 +#: src/apprt/gtk/CloseDialog.zig:47 src/apprt/gtk/Surface.zig:2520 msgid "Close" msgstr "Schließen" @@ -275,6 +275,14 @@ msgstr "Der aktuell laufende Prozess in diesem geteilten Fenster wird beendet." msgid "Copied to clipboard" msgstr "In die Zwischenablage kopiert" +#: src/apprt/gtk/Surface.zig:2514 +msgid "Command succeeded" +msgstr "" + +#: src/apprt/gtk/Surface.zig:2516 +msgid "Command failed" +msgstr "" + #: src/apprt/gtk/Window.zig:216 msgid "Main Menu" msgstr "Hauptmenü" diff --git a/po/es_AR.UTF-8.po b/po/es_AR.UTF-8.po index 9b3b68693..3c7e89c00 100644 --- a/po/es_AR.UTF-8.po +++ b/po/es_AR.UTF-8.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2025-07-08 15:13-0500\n" +"POT-Creation-Date: 2025-07-11 22:56-0500\n" "PO-Revision-Date: 2025-05-19 20:17-0300\n" "Last-Translator: Alan Moyano \n" "Language-Team: Argentinian \n" @@ -236,7 +236,7 @@ msgstr "" "Pegar este texto en la terminal puede ser peligroso ya que parece que " "algunos comandos podrían ejecutarse." -#: src/apprt/gtk/CloseDialog.zig:47 +#: src/apprt/gtk/CloseDialog.zig:47 src/apprt/gtk/Surface.zig:2520 msgid "Close" msgstr "Cerrar" @@ -276,6 +276,14 @@ msgstr "El proceso actualmente en ejecución en esta división será terminado." msgid "Copied to clipboard" msgstr "Copiado al portapapeles" +#: src/apprt/gtk/Surface.zig:2514 +msgid "Command succeeded" +msgstr "" + +#: src/apprt/gtk/Surface.zig:2516 +msgid "Command failed" +msgstr "" + #: src/apprt/gtk/Window.zig:216 msgid "Main Menu" msgstr "Menú principal" diff --git a/po/es_BO.UTF-8.po b/po/es_BO.UTF-8.po index c89b53f61..c2b3ae270 100644 --- a/po/es_BO.UTF-8.po +++ b/po/es_BO.UTF-8.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2025-07-08 15:13-0500\n" +"POT-Creation-Date: 2025-07-11 22:56-0500\n" "PO-Revision-Date: 2025-03-28 17:46+0200\n" "Last-Translator: Miguel Peredo \n" "Language-Team: Spanish \n" @@ -236,7 +236,7 @@ msgstr "" "Pegar este texto en la terminal puede ser peligroso ya que parece que " "algunos comandos podrían ejecutarse." -#: src/apprt/gtk/CloseDialog.zig:47 +#: src/apprt/gtk/CloseDialog.zig:47 src/apprt/gtk/Surface.zig:2520 msgid "Close" msgstr "Cerrar" @@ -276,6 +276,14 @@ msgstr "El proceso actualmente en ejecución en esta división será terminado." msgid "Copied to clipboard" msgstr "Copiado al portapapeles" +#: src/apprt/gtk/Surface.zig:2514 +msgid "Command succeeded" +msgstr "" + +#: src/apprt/gtk/Surface.zig:2516 +msgid "Command failed" +msgstr "" + #: src/apprt/gtk/Window.zig:216 msgid "Main Menu" msgstr "Menú principal" diff --git a/po/fr_FR.UTF-8.po b/po/fr_FR.UTF-8.po index 2c227edaf..b63bc044c 100644 --- a/po/fr_FR.UTF-8.po +++ b/po/fr_FR.UTF-8.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2025-07-08 15:13-0500\n" +"POT-Creation-Date: 2025-07-11 22:56-0500\n" "PO-Revision-Date: 2025-03-22 09:31+0100\n" "Last-Translator: Kirwiisp \n" "Language-Team: French \n" @@ -237,7 +237,7 @@ msgstr "" "Coller ce texte dans le terminal pourrait être dangereux, il semblerait que " "certaines commandes pourraient être exécutées." -#: src/apprt/gtk/CloseDialog.zig:47 +#: src/apprt/gtk/CloseDialog.zig:47 src/apprt/gtk/Surface.zig:2520 msgid "Close" msgstr "Fermer" @@ -277,6 +277,14 @@ msgstr "Le processus en cours dans ce panneau va être arrêté." msgid "Copied to clipboard" msgstr "Copié dans le presse-papiers" +#: src/apprt/gtk/Surface.zig:2514 +msgid "Command succeeded" +msgstr "" + +#: src/apprt/gtk/Surface.zig:2516 +msgid "Command failed" +msgstr "" + #: src/apprt/gtk/Window.zig:216 msgid "Main Menu" msgstr "Menu principal" diff --git a/po/ga_IE.UTF-8.po b/po/ga_IE.UTF-8.po index 3c8018ca0..cc884e753 100644 --- a/po/ga_IE.UTF-8.po +++ b/po/ga_IE.UTF-8.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2025-07-08 15:13-0500\n" +"POT-Creation-Date: 2025-07-11 22:56-0500\n" "PO-Revision-Date: 2025-06-29 21:15+0100\n" "Last-Translator: Aindriú Mac Giolla Eoin \n" "Language-Team: Irish \n" @@ -237,7 +237,7 @@ msgstr "" "D’fhéadfadh sé a bheith contúirteach an téacs seo a ghreamú isteach sa " "teirminéal, toisc go d'fhéadfadh roinnt orduithe a fhorghníomhú." -#: src/apprt/gtk/CloseDialog.zig:47 +#: src/apprt/gtk/CloseDialog.zig:47 src/apprt/gtk/Surface.zig:2520 msgid "Close" msgstr "Dún" @@ -278,6 +278,14 @@ msgstr "" msgid "Copied to clipboard" msgstr "Cóipeáilte chuig an ghearrthaisce" +#: src/apprt/gtk/Surface.zig:2514 +msgid "Command succeeded" +msgstr "" + +#: src/apprt/gtk/Surface.zig:2516 +msgid "Command failed" +msgstr "" + #: src/apprt/gtk/Window.zig:216 msgid "Main Menu" msgstr "Príomh-Roghchlár" diff --git a/po/he_IL.UTF-8.po b/po/he_IL.UTF-8.po index 7ca417908..c5fd5b348 100644 --- a/po/he_IL.UTF-8.po +++ b/po/he_IL.UTF-8.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2025-07-08 15:13-0500\n" +"POT-Creation-Date: 2025-07-11 22:56-0500\n" "PO-Revision-Date: 2025-03-13 00:00+0000\n" "Last-Translator: Sl (Shahaf Levi), Sl's Repository Ltd \n" @@ -234,7 +234,7 @@ msgstr "" "הדבקת טקסט זה במסוף עלולה להיות מסוכנת, מכיוון שככל הנראה היא תוביל להרצה של " "פקודות מסוימות." -#: src/apprt/gtk/CloseDialog.zig:47 +#: src/apprt/gtk/CloseDialog.zig:47 src/apprt/gtk/Surface.zig:2520 msgid "Close" msgstr "סגירה" @@ -274,6 +274,14 @@ msgstr "התהליך שרץ כרגע בפיצול זה יסתיים." msgid "Copied to clipboard" msgstr "הועתק ללוח ההעתקה" +#: src/apprt/gtk/Surface.zig:2514 +msgid "Command succeeded" +msgstr "" + +#: src/apprt/gtk/Surface.zig:2516 +msgid "Command failed" +msgstr "" + #: src/apprt/gtk/Window.zig:216 msgid "Main Menu" msgstr "תפריט ראשי" diff --git a/po/id_ID.UTF-8.po b/po/id_ID.UTF-8.po index 51b4bce60..b6fc58c29 100644 --- a/po/id_ID.UTF-8.po +++ b/po/id_ID.UTF-8.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2025-07-08 15:13-0500\n" +"POT-Creation-Date: 2025-07-11 22:56-0500\n" "PO-Revision-Date: 2025-03-20 15:19+0700\n" "Last-Translator: Satrio Bayu Aji \n" "Language-Team: Indonesian \n" @@ -235,7 +235,7 @@ msgstr "" "Menempelkan teks ini ke terminal mungkin berbahaya karena sepertinya " "beberapa perintah mungkin dijalankan." -#: src/apprt/gtk/CloseDialog.zig:47 +#: src/apprt/gtk/CloseDialog.zig:47 src/apprt/gtk/Surface.zig:2520 msgid "Close" msgstr "Tutup" @@ -275,6 +275,14 @@ msgstr "Proses yang sedang berjalan dalam belahan ini akan diakhiri." msgid "Copied to clipboard" msgstr "Disalin ke papan klip" +#: src/apprt/gtk/Surface.zig:2514 +msgid "Command succeeded" +msgstr "" + +#: src/apprt/gtk/Surface.zig:2516 +msgid "Command failed" +msgstr "" + #: src/apprt/gtk/Window.zig:216 msgid "Main Menu" msgstr "Menu utama" diff --git a/po/ja_JP.UTF-8.po b/po/ja_JP.UTF-8.po index c965ea29f..a3e261a83 100644 --- a/po/ja_JP.UTF-8.po +++ b/po/ja_JP.UTF-8.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2025-07-08 15:13-0500\n" +"POT-Creation-Date: 2025-07-11 22:56-0500\n" "PO-Revision-Date: 2025-03-21 00:08+0900\n" "Last-Translator: Lon Sagisawa \n" "Language-Team: Japanese\n" @@ -237,7 +237,7 @@ msgstr "" "このテキストには実行可能なコマンドが含まれており、ターミナルに貼り付けるのは" "危険な可能性があります。" -#: src/apprt/gtk/CloseDialog.zig:47 +#: src/apprt/gtk/CloseDialog.zig:47 src/apprt/gtk/Surface.zig:2520 msgid "Close" msgstr "閉じる" @@ -277,6 +277,14 @@ msgstr "分割ウィンドウ内のすべてのプロセスが終了します。 msgid "Copied to clipboard" msgstr "クリップボードにコピーしました" +#: src/apprt/gtk/Surface.zig:2514 +msgid "Command succeeded" +msgstr "" + +#: src/apprt/gtk/Surface.zig:2516 +msgid "Command failed" +msgstr "" + #: src/apprt/gtk/Window.zig:216 msgid "Main Menu" msgstr "メインメニュー" diff --git a/po/ko_KR.UTF-8.po b/po/ko_KR.UTF-8.po index 875e7a1a5..92c460b9b 100644 --- a/po/ko_KR.UTF-8.po +++ b/po/ko_KR.UTF-8.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2025-07-08 15:13-0500\n" +"POT-Creation-Date: 2025-07-11 22:56-0500\n" "PO-Revision-Date: 2025-07-09 16:11-0400\n" "Last-Translator: Hojin You \n" "Language-Team: Korean \n" @@ -236,7 +236,7 @@ msgstr "" "이 텍스트를 터미널에 붙여넣는 것은 위험할 수 있습니다. 일부 명령이 실행될 수 " "있는 것으로 보입니다." -#: src/apprt/gtk/CloseDialog.zig:47 +#: src/apprt/gtk/CloseDialog.zig:47 src/apprt/gtk/Surface.zig:2520 msgid "Close" msgstr "닫기" @@ -276,6 +276,14 @@ msgstr "이 분할에서 현재 실행 중인 프로세스가 종료됩니다." msgid "Copied to clipboard" msgstr "클립보드에 복사됨" +#: src/apprt/gtk/Surface.zig:2514 +msgid "Command succeeded" +msgstr "" + +#: src/apprt/gtk/Surface.zig:2516 +msgid "Command failed" +msgstr "" + #: src/apprt/gtk/Window.zig:216 msgid "Main Menu" msgstr "메인 메뉴" diff --git a/po/mk_MK.UTF-8.po b/po/mk_MK.UTF-8.po index 75bb81e00..8cc5bc716 100644 --- a/po/mk_MK.UTF-8.po +++ b/po/mk_MK.UTF-8.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2025-07-08 15:13-0500\n" +"POT-Creation-Date: 2025-07-11 22:56-0500\n" "PO-Revision-Date: 2025-03-23 14:17+0100\n" "Last-Translator: Andrej Daskalov \n" "Language-Team: Macedonian\n" @@ -236,7 +236,7 @@ msgstr "" "Вметнувањето на овој текст во терминалот може да биде опасно, бидејќи " "изгледа како да ќе се извршат одредени команди." -#: src/apprt/gtk/CloseDialog.zig:47 +#: src/apprt/gtk/CloseDialog.zig:47 src/apprt/gtk/Surface.zig:2520 msgid "Close" msgstr "Затвори" @@ -276,6 +276,14 @@ msgstr "Процесот кој моментално се извршува во msgid "Copied to clipboard" msgstr "Копирано во привремена меморија" +#: src/apprt/gtk/Surface.zig:2514 +msgid "Command succeeded" +msgstr "" + +#: src/apprt/gtk/Surface.zig:2516 +msgid "Command failed" +msgstr "" + #: src/apprt/gtk/Window.zig:216 msgid "Main Menu" msgstr "Главно мени" diff --git a/po/nb_NO.UTF-8.po b/po/nb_NO.UTF-8.po index 28c1bc559..d583b1a0e 100644 --- a/po/nb_NO.UTF-8.po +++ b/po/nb_NO.UTF-8.po @@ -10,7 +10,7 @@ msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2025-07-08 15:13-0500\n" +"POT-Creation-Date: 2025-07-11 22:56-0500\n" "PO-Revision-Date: 2025-04-14 16:25+0200\n" "Last-Translator: cryptocode \n" "Language-Team: Norwegian Bokmal \n" @@ -239,7 +239,7 @@ msgstr "" "Det ser ut som at kommandoer vil bli kjørt hvis du limer inn dette, vurder " "om du mener det er trygt." -#: src/apprt/gtk/CloseDialog.zig:47 +#: src/apprt/gtk/CloseDialog.zig:47 src/apprt/gtk/Surface.zig:2520 msgid "Close" msgstr "Lukk" @@ -279,6 +279,14 @@ msgstr "Den kjørende prosessen for denne splitten vil bli avsluttet." msgid "Copied to clipboard" msgstr "Kopiert til utklippstavlen" +#: src/apprt/gtk/Surface.zig:2514 +msgid "Command succeeded" +msgstr "" + +#: src/apprt/gtk/Surface.zig:2516 +msgid "Command failed" +msgstr "" + #: src/apprt/gtk/Window.zig:216 msgid "Main Menu" msgstr "Hovedmeny" diff --git a/po/nl_NL.UTF-8.po b/po/nl_NL.UTF-8.po index d64592f6d..c310f38ea 100644 --- a/po/nl_NL.UTF-8.po +++ b/po/nl_NL.UTF-8.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2025-07-08 15:13-0500\n" +"POT-Creation-Date: 2025-07-11 22:56-0500\n" "PO-Revision-Date: 2025-03-24 15:00+0100\n" "Last-Translator: Nico Geesink \n" "Language-Team: Dutch \n" @@ -236,7 +236,7 @@ msgstr "" "Het plakken van deze tekst in de terminal is mogelijk gevaarlijk, omdat het " "lijkt op een commando dat uitgevoerd kan worden." -#: src/apprt/gtk/CloseDialog.zig:47 +#: src/apprt/gtk/CloseDialog.zig:47 src/apprt/gtk/Surface.zig:2520 msgid "Close" msgstr "Afsluiten" @@ -277,6 +277,14 @@ msgstr "" msgid "Copied to clipboard" msgstr "Gekopieerd naar klembord" +#: src/apprt/gtk/Surface.zig:2514 +msgid "Command succeeded" +msgstr "" + +#: src/apprt/gtk/Surface.zig:2516 +msgid "Command failed" +msgstr "" + #: src/apprt/gtk/Window.zig:216 msgid "Main Menu" msgstr "Hoofdmenu" diff --git a/po/pl_PL.UTF-8.po b/po/pl_PL.UTF-8.po index 4f281b415..4c8a8c273 100644 --- a/po/pl_PL.UTF-8.po +++ b/po/pl_PL.UTF-8.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2025-07-08 15:13-0500\n" +"POT-Creation-Date: 2025-07-11 22:56-0500\n" "PO-Revision-Date: 2025-03-17 12:15+0100\n" "Last-Translator: Bartosz Sokorski \n" "Language-Team: Polish \n" @@ -238,7 +238,7 @@ msgstr "" "Wklejenie tego tekstu do terminala może być niebezpieczne, ponieważ może " "spowodować wykonanie komend." -#: src/apprt/gtk/CloseDialog.zig:47 +#: src/apprt/gtk/CloseDialog.zig:47 src/apprt/gtk/Surface.zig:2520 msgid "Close" msgstr "Zamknij" @@ -278,6 +278,14 @@ msgstr "Wszyskie trwające procesy w obecnym podziale zostaną zakończone." msgid "Copied to clipboard" msgstr "Skopiowano do schowka" +#: src/apprt/gtk/Surface.zig:2514 +msgid "Command succeeded" +msgstr "" + +#: src/apprt/gtk/Surface.zig:2516 +msgid "Command failed" +msgstr "" + #: src/apprt/gtk/Window.zig:216 msgid "Main Menu" msgstr "Menu główne" diff --git a/po/pt_BR.UTF-8.po b/po/pt_BR.UTF-8.po index 2979248f2..76ed048a4 100644 --- a/po/pt_BR.UTF-8.po +++ b/po/pt_BR.UTF-8.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2025-07-08 15:13-0500\n" +"POT-Creation-Date: 2025-07-11 22:56-0500\n" "PO-Revision-Date: 2025-06-20 10:19-0300\n" "Last-Translator: Mário Victor Ribeiro Silva \n" "Language-Team: Brazilian Portuguese \n" "Language-Team: Russian \n" @@ -237,7 +237,7 @@ msgstr "" "Вставка этого текста в терминал может быть опасной. Это выглядит как " "команды, которые могут быть исполнены." -#: src/apprt/gtk/CloseDialog.zig:47 +#: src/apprt/gtk/CloseDialog.zig:47 src/apprt/gtk/Surface.zig:2520 msgid "Close" msgstr "Закрыть" @@ -277,6 +277,14 @@ msgstr "Процесс, работающий в этой сплит-област msgid "Copied to clipboard" msgstr "Скопировано в буфер обмена" +#: src/apprt/gtk/Surface.zig:2514 +msgid "Command succeeded" +msgstr "" + +#: src/apprt/gtk/Surface.zig:2516 +msgid "Command failed" +msgstr "" + #: src/apprt/gtk/Window.zig:216 msgid "Main Menu" msgstr "Главное меню" diff --git a/po/tr_TR.UTF-8.po b/po/tr_TR.UTF-8.po index 7d8d055f8..251aa27ca 100644 --- a/po/tr_TR.UTF-8.po +++ b/po/tr_TR.UTF-8.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2025-07-08 15:13-0500\n" +"POT-Creation-Date: 2025-07-11 22:56-0500\n" "PO-Revision-Date: 2025-03-24 22:01+0300\n" "Last-Translator: Emir SARI \n" "Language-Team: Turkish\n" @@ -237,7 +237,7 @@ msgstr "" "Bu metni uçbirime yapıştırmak tehlikeli olabilir; çünkü bir komut " "yürütülebilecekmiş gibi duruyor." -#: src/apprt/gtk/CloseDialog.zig:47 +#: src/apprt/gtk/CloseDialog.zig:47 src/apprt/gtk/Surface.zig:2520 msgid "Close" msgstr "Kapat" @@ -277,6 +277,14 @@ msgstr "Bu bölmedeki şu anda çalışan süreç sonlandırılacaktır." msgid "Copied to clipboard" msgstr "Panoya kopyalandı" +#: src/apprt/gtk/Surface.zig:2514 +msgid "Command succeeded" +msgstr "" + +#: src/apprt/gtk/Surface.zig:2516 +msgid "Command failed" +msgstr "" + #: src/apprt/gtk/Window.zig:216 msgid "Main Menu" msgstr "Ana Menü" diff --git a/po/uk_UA.UTF-8.po b/po/uk_UA.UTF-8.po index 2d01b3932..e5b13f918 100644 --- a/po/uk_UA.UTF-8.po +++ b/po/uk_UA.UTF-8.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2025-07-08 15:13-0500\n" +"POT-Creation-Date: 2025-07-11 22:56-0500\n" "PO-Revision-Date: 2025-03-16 20:16+0200\n" "Last-Translator: Danylo Zalizchuk \n" "Language-Team: Ukrainian \n" @@ -238,7 +238,7 @@ msgstr "" "Вставка цього тексту в термінал може бути небезпечною, оскільки виглядає " "так, ніби деякі команди можуть бути виконані." -#: src/apprt/gtk/CloseDialog.zig:47 +#: src/apprt/gtk/CloseDialog.zig:47 src/apprt/gtk/Surface.zig:2520 msgid "Close" msgstr "Закрити" @@ -279,6 +279,14 @@ msgstr "" msgid "Copied to clipboard" msgstr "Скопійовано в буфер обміну" +#: src/apprt/gtk/Surface.zig:2514 +msgid "Command succeeded" +msgstr "" + +#: src/apprt/gtk/Surface.zig:2516 +msgid "Command failed" +msgstr "" + #: src/apprt/gtk/Window.zig:216 msgid "Main Menu" msgstr "Головне меню" diff --git a/po/zh_CN.UTF-8.po b/po/zh_CN.UTF-8.po index 2b5f9f3a1..e122a9719 100644 --- a/po/zh_CN.UTF-8.po +++ b/po/zh_CN.UTF-8.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2025-07-08 15:13-0500\n" +"POT-Creation-Date: 2025-07-11 22:56-0500\n" "PO-Revision-Date: 2025-02-27 09:16+0100\n" "Last-Translator: Leah \n" "Language-Team: Chinese (simplified) \n" @@ -229,7 +229,7 @@ msgid "" "commands may be executed." msgstr "将以下内容粘贴至终端内将可能执行有害命令。" -#: src/apprt/gtk/CloseDialog.zig:47 +#: src/apprt/gtk/CloseDialog.zig:47 src/apprt/gtk/Surface.zig:2520 msgid "Close" msgstr "关闭" @@ -269,6 +269,14 @@ msgstr "分屏内正在运行中的进程将被终止。" msgid "Copied to clipboard" msgstr "已复制至剪贴板" +#: src/apprt/gtk/Surface.zig:2514 +msgid "Command succeeded" +msgstr "" + +#: src/apprt/gtk/Surface.zig:2516 +msgid "Command failed" +msgstr "" + #: src/apprt/gtk/Window.zig:216 msgid "Main Menu" msgstr "主菜单"