Update to Zig 0.16.0

This commit represents the majority of the work necessary to upgrade
Ghostty to use Zig 0.16.0.

Key parts:

* In addition to its previous responsibilities, the global state now
  houses state for global I/O implementations and the process
  environment. It is now also utilized in the main application along
  with the C library. Where necessary, global state is isolated from key
  parts of the implementation (e.g., in libghostty subsystems), and it's
  expected that this list will grow.

* We currently manage our own C translation layer where necessary. In
  these cases, cImport has been removed in favor of the new external
  translate-c package. Due to fixes that have needed be made to properly
  translate the dependencies that were swapped out, as mentioned, we
  have had to backport fixes from the current translate-c package (and
  the upstream Arocc dependency). We will host this ourselves until Zig
  0.17.0 is released with these fixes.

* Where necessary (only a small number of cases), some stdlib code from
  0.15.2 (and even from 0.17.0) has been taken, adopted, and vendored in
  lib/compat.

Co-authored-by: Leah Amelia Chen <hi@pluie.me>
This commit is contained in:
Chris Marchesi
2026-05-07 09:11:14 -07:00
parent 74d0c72fd9
commit e8525c0fd9
357 changed files with 8958 additions and 6098 deletions

View File

@@ -15,6 +15,7 @@ const Config = configpkg.Config;
const BlockingQueue = @import("datastruct/main.zig").BlockingQueue;
const renderer = @import("renderer.zig");
const font = @import("font/main.zig");
const global = @import("global.zig");
const log = std.log.scoped(.app);
@@ -56,7 +57,7 @@ font_grid_set: font.SharedGridSet,
// Used to rate limit desktop notifications. Some platforms (notably macOS) will
// run out of resources if desktop notifications are sent too fast and the OS
// will kill Ghostty.
last_notification_time: ?std.time.Instant = null,
last_notification_time: ?std.Io.Timestamp = null,
last_notification_digest: u64 = 0,
/// The conditional state of the configuration. See the equivalent field
@@ -95,7 +96,7 @@ pub fn init(
self.* = .{
.alloc = alloc,
.surfaces = .{},
.surfaces = .empty,
.mailbox = .{},
.font_grid_set = font_grid_set,
.config_conditional_state = .{},
@@ -236,7 +237,7 @@ pub fn needsConfirmQuit(self: *const App) bool {
/// Drain the mailbox.
fn drainMailbox(self: *App, rt_app: *apprt.App) !void {
while (self.mailbox.pop()) |message| {
while (self.mailbox.pop(global.io())) |message| {
if (comptime std.log.logEnabled(.debug, .app)) {
switch (message) {
// these tend to be way too verbose for normal debugging
@@ -566,7 +567,7 @@ pub const Mailbox = struct {
/// Send a message to the surface.
pub fn push(self: Mailbox, msg: Message, timeout: Queue.Timeout) Queue.Size {
const result = self.mailbox.push(msg, timeout);
const result = self.mailbox.push(global.io(), msg, timeout);
// Wake up our app loop
self.rt_app.wakeup();

View File

@@ -14,12 +14,15 @@
//! * posix_spawn is used for Mac, but doesn't support the necessary
//! features for tty setup.
//!
//!
//! TODO: This may have changed a lot now with the new I/O implementations in
//! >= 0.16.0, so this might warrant a recheck.
const Command = @This();
const std = @import("std");
const builtin = @import("builtin");
const configpkg = @import("config.zig");
const global_state = &@import("global.zig").state;
const global = @import("global.zig");
const internal_os = @import("os/main.zig");
const windows = internal_os.windows;
const TempDir = internal_os.TempDir;
@@ -29,8 +32,8 @@ const posix = std.posix;
const debug = std.debug;
const testing = std.testing;
const Allocator = std.mem.Allocator;
const File = std.fs.File;
const EnvMap = std.process.EnvMap;
const File = std.Io.File;
const EnvMap = std.process.Environ.Map;
const apprt = @import("apprt.zig");
/// Function prototype for a function executed /in the child process/ after the
@@ -65,7 +68,7 @@ env: ?*const EnvMap = null,
/// Working directory to change to in the child process. If not set, the
/// working directory of the calling process is preserved.
cwd: ?[]const u8 = null,
cwd: ?[:0]const u8 = null,
/// The file handle to set for stdin/out/err. If this isn't set, we do
/// nothing explicitly so it is up to the behavior of the operating system.
@@ -98,7 +101,7 @@ rt_post_fork_info: RtPostForkInfo,
/// If set, then the process will be created attached to this pseudo console.
/// `stdin`, `stdout`, and `stderr` will be ignored if set.
pseudo_console: if (builtin.os.tag == .windows) ?windows.exp.HPCON else void =
pseudo_console: if (builtin.os.tag == .windows) ?windows.HPCON else void =
if (builtin.os.tag == .windows) null else {},
/// User data that is sent to the callback. Set with setData and getData
@@ -106,7 +109,7 @@ pseudo_console: if (builtin.os.tag == .windows) ?windows.exp.HPCON else void =
data: ?*anyopaque = null,
/// Process ID is set after start is called.
pid: ?posix.pid_t = null,
pid: ?posix.system.pid_t = null,
/// The various methods a process may exit.
pub const Exit = if (builtin.os.tag == .windows) union(enum) {
@@ -128,9 +131,9 @@ pub const Exit = if (builtin.os.tag == .windows) union(enum) {
return if (posix.W.IFEXITED(status))
Exit{ .Exited = posix.W.EXITSTATUS(status) }
else if (posix.W.IFSIGNALED(status))
Exit{ .Signal = posix.W.TERMSIG(status) }
Exit{ .Signal = @intFromEnum(posix.W.TERMSIG(status)) }
else if (posix.W.IFSTOPPED(status))
Exit{ .Stopped = posix.W.STOPSIG(status) }
Exit{ .Stopped = @intFromEnum(posix.W.STOPSIG(status)) }
else
Exit{ .Unknown = status };
}
@@ -186,7 +189,7 @@ fn startPosix(self: *Command, arena: Allocator) !void {
@compileError("missing env vars");
// Fork.
const pid = try posix.fork();
const pid = posix.system.fork();
if (pid != 0) {
// Parent, return immediately.
@@ -205,37 +208,68 @@ fn startPosix(self: *Command, arena: Allocator) !void {
if (self.stderr) |f| setupFd(f.handle, posix.STDERR_FILENO) catch
return error.ExecFailedInChild;
// Setup our working directory
if (self.cwd) |cwd| posix.chdir(cwd) catch {
// This can fail if we don't have permission to go to
// this directory or if due to race conditions it doesn't
// exist or any various other reasons. We don't want to
// crash the entire process if this fails so we ignore it.
// We don't log because that'll show up in the output.
};
// Setup our working directory.
//
// NOTE: this can fail if we don't have permission to go to this directory
// or if due to race conditions it doesn't exist or any various other
// reasons. We don't want to crash the entire process if this fails so we
// ignore it. We don't log because that'll show up in the output.
if (self.cwd) |cwd| _ = posix.system.chdir(cwd);
// Restore any rlimits that were set by Ghostty. This might fail but
// any failures are ignored (its best effort).
global_state.rlimits.restore();
global.rlimits().restore();
// If there are pre exec callbacks, call them now.
if (self.os_pre_exec) |f| if (f(self)) |exitcode| posix.exit(exitcode);
if (self.rt_pre_exec) |f| if (f(self)) |exitcode| posix.exit(exitcode);
if (self.os_pre_exec) |f| if (f(self)) |exitcode| posix.system.exit(exitcode);
if (self.rt_pre_exec) |f| if (f(self)) |exitcode| posix.system.exit(exitcode);
// Finally, replace our process.
// Note: we must use the "p"-variant of exec here because we
// do not guarantee our command is looked up already in the path.
const err = posix.execvpeZ(self.path, argsZ, envp);
const err: posix.E = execve: {
// This functionality has been taken from Zig stdlib, a simplified
// version of the exec bits with PATH search so that we can just
// offload to execve below.
const file_slice = std.mem.sliceTo(self.path, 0);
if (std.mem.findScalar(u8, file_slice, '/') != null) {
break :execve posix.errno(posix.system.execve(self.path, argsZ, envp));
}
var path_expanded_buf: [std.fs.max_path_bytes]u8 = undefined;
const PATH = global.environ().getPosix("PATH") orelse "/usr/local/bin:/bin/:/usr/bin";
var it = std.mem.tokenizeScalar(u8, PATH, ':');
var err: posix.system.E = undefined;
var seen_eacces = false;
while (it.next()) |search_path| {
const path_len = search_path.len + file_slice.len + 1;
if (path_expanded_buf.len < path_len + 1) return error.NameTooLong;
@memcpy(path_expanded_buf[0..search_path.len], search_path);
path_expanded_buf[search_path.len] = '/';
@memcpy(path_expanded_buf[search_path.len + 1 ..][0..file_slice.len], file_slice);
path_expanded_buf[path_len] = 0;
const full_path = path_expanded_buf[0..path_len :0].ptr;
// Replace here, switch on error (any error means that replace
// failed, but we might need to retry).
err = posix.errno(posix.system.execve(full_path, argsZ, envp));
switch (err) {
.ACCES => seen_eacces = true,
.NOENT, .NOTDIR => {},
else => break :execve err,
}
}
if (seen_eacces) break :execve .ACCES;
break :execve err;
};
// If we are executing this code, the exec failed. We're in the
// child process so there isn't much we can do. We try to output
// something reasonable. Its important to note we MUST NOT return
// any other error condition from here on out.
var stderr_buf: [1024]u8 = undefined;
var stderr_writer = std.fs.File.stderr().writer(&stderr_buf);
var stderr_writer = std.Io.File.stderr().writer(global.io(), &stderr_buf);
const stderr = &stderr_writer.interface;
switch (err) {
error.FileNotFound => stderr.print(
posix.system.E.NOENT => stderr.print(
\\Requested executable not found. Please verify the command is on
\\the PATH and try again.
\\
@@ -243,11 +277,11 @@ fn startPosix(self: *Command, arena: Allocator) !void {
.{},
) catch {},
else => stderr.print(
\\exec syscall failed with unexpected error: {}
else => |e| stderr.print(
\\exec syscall failed with unexpected error: E{s}
\\
,
.{err},
.{@tagName(e)},
) catch {},
}
stderr.flush() catch {};
@@ -275,15 +309,37 @@ fn startWindows(self: *Command, arena: Allocator) !void {
const env_w = if (self.env) |env_map| try createWindowsEnvBlock(arena, env_map) else null;
const any_null_fd = self.stdin == null or self.stdout == null or self.stderr == null;
const null_fd = if (any_null_fd) try windows.OpenFile(
&[_]u16{ '\\', 'D', 'e', 'v', 'i', 'c', 'e', '\\', 'N', 'u', 'l', 'l' },
.{
.access_mask = windows.GENERIC_READ | windows.SYNCHRONIZE,
.share_access = windows.FILE_SHARE_READ,
.creation = windows.OPEN_EXISTING,
},
) else null;
defer if (null_fd) |fd| posix.close(fd);
const null_fd = if (any_null_fd) null_fd: {
// path = "\Device\Null"
const path = [_]u16{ '\\', 'D', 'e', 'v', 'i', 'c', 'e', '\\', 'N', 'u', 'l', 'l' };
var path_unicode_string: windows.UNICODE_STRING = .init(&path);
var attrs: windows.OBJECT_ATTRIBUTES = .{ .ObjectName = &path_unicode_string };
var fd: windows.HANDLE = undefined;
var io_status: windows.IO_STATUS_BLOCK = undefined; // unused
const result = windows.exp.ntdll.NtCreateFile(
&fd,
.{ .GENERIC = .{ .READ = true }, .STANDARD = .{ .SYNCHRONIZE = true } },
&attrs,
&io_status,
null,
windows.FILE_ATTRIBUTE_NORMAL,
windows.FILE_SHARE_READ,
windows.OPEN_EXISTING,
windows.FILE_NON_DIRECTORY_FILE,
null,
0,
);
if (result != .SUCCESS) {
return windows.unexpectedStatus(result);
}
break :null_fd fd;
} else null;
defer {
if (null_fd) |fd| _ = windows.exp.kernel32.CloseHandle(fd);
}
// TODO: In the case of having FDs instead of pty, need to set up
// attributes such that the child process only inherits these handles,
@@ -304,17 +360,17 @@ fn startWindows(self: *Command, arena: Allocator) !void {
1,
0,
&attribute_list_size,
) == 0) return windows.unexpectedError(windows.kernel32.GetLastError());
) == windows.FALSE) return windows.unexpectedError(windows.GetLastError());
if (windows.exp.kernel32.UpdateProcThreadAttribute(
attribute_list_buf.ptr,
0,
windows.exp.PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE,
windows.PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE,
pseudo_console,
@sizeOf(windows.exp.HPCON),
@sizeOf(windows.HPCON),
null,
null,
) == 0) return windows.unexpectedError(windows.kernel32.GetLastError());
) == windows.FALSE) return windows.unexpectedError(windows.GetLastError());
break :b .{ attribute_list_buf.ptr, null, null, null };
} else b: {
@@ -324,9 +380,9 @@ fn startWindows(self: *Command, arena: Allocator) !void {
break :b .{ null, stdin, stdout, stderr };
};
var startup_info_ex = windows.exp.STARTUPINFOEX{
var startup_info_ex = windows.STARTUPINFOEX{
.StartupInfo = .{
.cb = if (attribute_list != null) @sizeOf(windows.exp.STARTUPINFOEX) else @sizeOf(windows.STARTUPINFOW),
.cb = if (attribute_list != null) @sizeOf(windows.STARTUPINFOEX) else @sizeOf(windows.STARTUPINFOW),
.hStdError = stderr,
.hStdOutput = stdout,
.hStdInput = stdin,
@@ -348,8 +404,8 @@ fn startWindows(self: *Command, arena: Allocator) !void {
.lpAttributeList = attribute_list,
};
var flags: windows.DWORD = windows.exp.CREATE_UNICODE_ENVIRONMENT;
if (attribute_list != null) flags |= windows.exp.EXTENDED_STARTUPINFO_PRESENT;
var flags: windows.DWORD = windows.CREATE_UNICODE_ENVIRONMENT;
if (attribute_list != null) flags |= windows.EXTENDED_STARTUPINFO_PRESENT;
var process_information: windows.PROCESS_INFORMATION = undefined;
if (windows.exp.kernel32.CreateProcessW(
@@ -363,21 +419,18 @@ fn startWindows(self: *Command, arena: Allocator) !void {
if (cwd_w) |w| w.ptr else null,
@ptrCast(&startup_info_ex.StartupInfo),
&process_information,
) == 0) return windows.unexpectedError(windows.kernel32.GetLastError());
) == windows.FALSE) return windows.unexpectedError(windows.GetLastError());
self.pid = process_information.hProcess;
}
fn setupFd(src: File.Handle, target: i32) !void {
switch (builtin.os.tag) {
.linux => {
// We use dup3 so that we can clear CLO_ON_EXEC. We do NOT want this
// file descriptor to be closed on exec since we're exactly exec-ing after
// this.
const PosixCall = struct {
fn f(func: anytype, args: anytype) !usize {
while (true) {
const rc = linux.dup3(src, target, 0);
const rc = @call(.auto, func, args);
switch (posix.errno(rc)) {
.SUCCESS => break,
.SUCCESS => return @intCast(rc),
.INTR => continue,
.AGAIN, .ACCES => return error.Locked,
.BADF => unreachable,
@@ -391,16 +444,28 @@ fn setupFd(src: File.Handle, target: i32) !void {
else => |err| return posix.unexpectedErrno(err),
}
}
}
};
switch (builtin.os.tag) {
.linux => {
// We use dup3 so that we can clear CLO_ON_EXEC. We do NOT want this
// file descriptor to be closed on exec since we're exactly exec-ing after
// this.
_ = try PosixCall.f(linux.dup3, .{ src, target, 0 });
},
.freebsd, .ios, .macos => {
// Mac doesn't support dup3 so we use dup2. We purposely clear
// CLO_ON_EXEC for this fd.
const flags = try posix.fcntl(src, posix.F.GETFD, 0);
const flags = try PosixCall.f(posix.system.fcntl, .{ src, posix.F.GETFD });
if (flags & posix.FD_CLOEXEC != 0) {
_ = try posix.fcntl(src, posix.F.SETFD, flags & ~@as(u32, posix.FD_CLOEXEC));
_ = try PosixCall.f(
posix.system.fcntl,
.{ src, posix.F.SETFD, flags & ~@as(u32, posix.FD_CLOEXEC) },
);
}
try posix.dup2(src, target);
_ = try PosixCall.f(posix.system.dup2, .{ src, target });
},
else => @compileError("unsupported platform"),
}
@@ -411,21 +476,29 @@ pub fn wait(self: Command, block: bool) !Exit {
if (comptime builtin.os.tag == .windows) {
// Block until the process exits. This returns immediately if the
// process already exited.
const result = windows.kernel32.WaitForSingleObject(self.pid.?, windows.INFINITE);
//
// NOTE: We can use the pid directly as posix.system.pid_t is still an
// alias for a handle under Windows. We might want to keep an eye on if
// this changes, though.
const result = windows.exp.kernel32.WaitForSingleObject(self.pid.?, windows.INFINITE);
if (result == windows.WAIT_FAILED) {
return windows.unexpectedError(windows.kernel32.GetLastError());
return windows.unexpectedError(windows.GetLastError());
}
var exit_code: windows.DWORD = undefined;
const has_code = windows.kernel32.GetExitCodeProcess(self.pid.?, &exit_code) != 0;
const has_code = windows.exp.kernel32.GetExitCodeProcess(self.pid.?, &exit_code) != windows.FALSE;
if (!has_code) {
return windows.unexpectedError(windows.kernel32.GetLastError());
return windows.unexpectedError(windows.GetLastError());
}
return .{ .Exited = exit_code };
}
const res = if (block) posix.waitpid(self.pid.?, 0) else res: {
const status = if (block) wait_block: {
var status: c_int = undefined;
_ = posix.system.waitpid(self.pid.?, &status, 0);
break :wait_block status;
} else wait_nohang: {
// We specify NOHANG because its not our fault if the process we launch
// for the tty doesn't properly waitpid its children. We don't want
// to hang the terminal over it.
@@ -434,12 +507,13 @@ pub fn wait(self: Command, block: bool) !Exit {
// wait call has not been performed, so we need to keep trying until we get
// a non-zero pid back, otherwise we end up with zombie processes.
while (true) {
const res = posix.waitpid(self.pid.?, std.c.W.NOHANG);
if (res.pid != 0) break :res res;
var status: c_int = undefined;
const pid = posix.system.waitpid(self.pid.?, &status, posix.system.W.NOHANG);
if (pid != 0) break :wait_nohang status;
}
};
return .init(res.status);
return .init(@bitCast(status));
}
/// Sets command->data to data.
@@ -587,7 +661,7 @@ test "Command: os pre exec 1" {
fn do(_: *Command) ?u8 {
// This runs in the child, so we can exit and it won't
// kill the test runner.
posix.exit(42);
posix.system.exit(42);
}
}).do,
.rt_pre_exec = null,
@@ -638,7 +712,7 @@ test "Command: rt pre exec 1" {
fn do(_: *Command) ?u8 {
// This runs in the child, so we can exit and it won't
// kill the test runner.
posix.exit(42);
posix.system.exit(42);
}
}).do,
.rt_post_fork = null,
@@ -697,27 +771,31 @@ test "Command: rt post fork 1" {
try testing.expectError(error.PostForkError, cmd.testingStart());
}
fn createTestStdout(dir: std.fs.Dir) !File {
const file = try dir.createFile("stdout.txt", .{ .read = true });
fn createTestStdout(io: std.Io, dir: std.Io.Dir) !File {
const file = try dir.createFile(io, "stdout.txt", .{ .read = true });
if (builtin.os.tag == .windows) {
try windows.SetHandleInformation(
if (windows.exp.kernel32.SetHandleInformation(
file.handle,
windows.HANDLE_FLAG_INHERIT,
windows.HANDLE_FLAG_INHERIT,
);
) == windows.FALSE) {
return windows.unexpectedError(windows.GetLastError());
}
}
return file;
}
fn createTestStderr(dir: std.fs.Dir) !File {
const file = try dir.createFile("stderr.txt", .{ .read = true });
fn createTestStderr(io: std.Io, dir: std.Io.Dir) !File {
const file = try dir.createFile(io, "stderr.txt", .{ .read = true });
if (builtin.os.tag == .windows) {
try windows.SetHandleInformation(
if (windows.exp.kernel32.SetHandleInformation(
file.handle,
windows.HANDLE_FLAG_INHERIT,
windows.HANDLE_FLAG_INHERIT,
);
) == windows.FALSE) {
return windows.unexpectedError(windows.GetLastError());
}
}
return file;
@@ -726,8 +804,8 @@ fn createTestStderr(dir: std.fs.Dir) !File {
test "Command: redirect stdout to file" {
var td = try TempDir.init();
defer td.deinit();
var stdout = try createTestStdout(td.dir);
defer stdout.close();
var stdout = try createTestStdout(testing.io, td.dir);
defer stdout.close(testing.io);
var cmd: Command = if (builtin.os.tag == .windows) .{
.path = "C:\\Windows\\System32\\whoami.exe",
@@ -756,8 +834,13 @@ test "Command: redirect stdout to file" {
try testing.expectEqual(@as(u32, 0), @as(u32, exit.Exited));
// Read our stdout
try stdout.seekTo(0);
const contents = try stdout.readToEndAlloc(testing.allocator, 1024 * 128);
const contents = contents: {
const size = (try stdout.stat(testing.io)).size;
const data = try testing.allocator.alloc(u8, size);
errdefer testing.allocator.free(data);
try testing.expectEqual(size, try stdout.readPositionalAll(testing.io, data, 0));
break :contents data;
};
defer testing.allocator.free(contents);
try testing.expect(contents.len > 0);
}
@@ -765,8 +848,8 @@ test "Command: redirect stdout to file" {
test "Command: custom env vars" {
var td = try TempDir.init();
defer td.deinit();
var stdout = try createTestStdout(td.dir);
defer stdout.close();
var stdout = try createTestStdout(testing.io, td.dir);
defer stdout.close(testing.io);
var env = EnvMap.init(testing.allocator);
defer env.deinit();
@@ -801,8 +884,13 @@ test "Command: custom env vars" {
try testing.expect(exit.Exited == 0);
// Read our stdout
try stdout.seekTo(0);
const contents = try stdout.readToEndAlloc(testing.allocator, 4096);
const contents = contents: {
const size = (try stdout.stat(testing.io)).size;
const data = try testing.allocator.alloc(u8, size);
errdefer testing.allocator.free(data);
try testing.expectEqual(size, try stdout.readPositionalAll(testing.io, data, 0));
break :contents data;
};
defer testing.allocator.free(contents);
if (builtin.os.tag == .windows) {
@@ -815,8 +903,8 @@ test "Command: custom env vars" {
test "Command: custom working directory" {
var td = try TempDir.init();
defer td.deinit();
var stdout = try createTestStdout(td.dir);
defer stdout.close();
var stdout = try createTestStdout(testing.io, td.dir);
defer stdout.close(testing.io);
var cmd: Command = if (builtin.os.tag == .windows) .{
.path = "C:\\Windows\\System32\\cmd.exe",
@@ -847,8 +935,13 @@ test "Command: custom working directory" {
try testing.expect(exit.Exited == 0);
// Read our stdout
try stdout.seekTo(0);
const contents = try stdout.readToEndAlloc(testing.allocator, 4096);
const contents = contents: {
const size = (try stdout.stat(testing.io)).size;
const data = try testing.allocator.alloc(u8, size);
errdefer testing.allocator.free(data);
try testing.expectEqual(size, try stdout.readPositionalAll(testing.io, data, 0));
break :contents data;
};
defer testing.allocator.free(contents);
if (builtin.os.tag == .windows) {
@@ -872,10 +965,10 @@ test "Command: posix fork handles execveZ failure" {
}
var td = try TempDir.init();
defer td.deinit();
var stdout = try createTestStdout(td.dir);
defer stdout.close();
var stderr = try createTestStderr(td.dir);
defer stderr.close();
var stdout = try createTestStdout(testing.io, td.dir);
defer stdout.close(testing.io);
var stderr = try createTestStderr(testing.io, td.dir);
defer stderr.close(testing.io);
var cmd: Command = .{
.path = "/not/a/binary",
@@ -904,7 +997,7 @@ fn testingStart(self: *Command) !void {
self.start(testing.allocator) catch |err| {
if (err == error.ExecFailedInChild) {
// I am a child process, I must not get confused and continue running the rest of the test suite.
posix.exit(1);
posix.system.exit(1);
}
return err;
};

View File

@@ -20,7 +20,7 @@ const builtin = @import("builtin");
const assert = @import("quirks.zig").inlineAssert;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const global_state = &@import("global.zig").state;
const global = @import("global.zig");
const oni = @import("oniguruma");
const crash = @import("crash/main.zig");
const unicode = @import("unicode/main.zig");
@@ -168,13 +168,13 @@ readonly: bool = false,
/// precision timestamp. It does not necessarily need to correspond to the
/// actual time, but we must be able to compare two subsequent timestamps to get
/// the wall clock time that has elapsed between timestamps.
command_timer: ?std.time.Instant = null,
command_timer: ?std.Io.Timestamp = null,
/// Search state
search: ?Search = null,
/// Used to rate limit BEL handling.
last_bell_time: ?std.time.Instant = null,
last_bell_time: ?std.Io.Timestamp = null,
/// The effect of an input event. This can be used by callers to take
/// the appropriate action after an input event. For example, key
@@ -559,8 +559,8 @@ pub fn init(
errdefer renderer_impl.deinit();
// The mutex used to protect our renderer state.
const mutex = try alloc.create(std.Thread.Mutex);
mutex.* = .{};
const mutex = try alloc.create(std.Io.Mutex);
mutex.* = .init;
errdefer alloc.destroy(mutex);
// Create the renderer thread
@@ -581,7 +581,11 @@ pub fn init(
self.* = .{
.id = id: {
while (true) {
const candidate = std.crypto.random.int(u64);
const candidate = candidate: {
const rng_impl: std.Random.IoSource = .{ .io = global.io() };
const rng = rng_impl.interface();
break :candidate rng.int(u64);
};
if (candidate == 0) continue;
break :id candidate;
}
@@ -631,13 +635,12 @@ pub fn init(
var env = rt_surface.defaultTermioEnv() catch |err| env: {
// If an error occurs, we don't want to block surface startup.
log.warn("error getting env map for surface err={}", .{err});
break :env internal_os.getEnvMap(alloc) catch
std.process.EnvMap.init(alloc);
break :env global.environMap() catch std.process.Environ.Map.init(alloc);
};
errdefer env.deinit();
// don't leak GHOSTTY_LOG to any subprocesses
env.remove("GHOSTTY_LOG");
_ = env.orderedRemove("GHOSTTY_LOG");
var buf: [18]u8 = undefined;
try env.put(
@@ -654,7 +657,7 @@ pub fn init(
.shell_integration_features = config.@"shell-integration-features",
.cursor_blink = config.@"cursor-style-blink",
.working_directory = if (config.@"working-directory") |wd| wd.value() else null,
.resources_dir = global_state.resources_dir.host(),
.resources_dir = global.resourcesDir().host(),
.term = config.term,
.rt_pre_exec_info = .init(config),
.rt_post_fork_info = .init(config),
@@ -717,7 +720,7 @@ pub fn init(
rendererpkg.Thread.threadMain,
.{&self.renderer_thread},
);
self.renderer_thr.setName("renderer") catch {};
self.renderer_thr.setName(global.io(), "renderer") catch {};
// Start our IO thread
self.io_thr = try std.Thread.spawn(
@@ -725,7 +728,7 @@ pub fn init(
termio.Thread.threadMain,
.{ &self.io_thread, &self.io },
);
self.io_thr.setName("io") catch {};
self.io_thr.setName(global.io(), "io") catch {};
// Determine our initial window size if configured. We need to do this
// quite late in the process because our height/width are in grid dimensions,
@@ -900,14 +903,14 @@ pub fn activateInspector(self: *Surface) !void {
// Put the inspector onto the render state
{
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
self.renderer_state.mutex.lockUncancelable(global.io());
defer self.renderer_state.mutex.unlock(global.io());
assert(self.renderer_state.inspector == null);
self.renderer_state.inspector = self.inspector;
}
// Notify our components we have an inspector active
_ = self.renderer_thread.mailbox.push(.{ .inspector = true }, .{ .forever = {} });
_ = self.renderer_thread.mailbox.push(global.io(), .{ .inspector = true }, .{ .forever = {} });
self.queueIo(.{ .inspector = true }, .unlocked);
}
@@ -917,14 +920,14 @@ pub fn deactivateInspector(self: *Surface) void {
// Remove the inspector from the render state
{
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
self.renderer_state.mutex.lockUncancelable(global.io());
defer self.renderer_state.mutex.unlock(global.io());
assert(self.renderer_state.inspector != null);
self.renderer_state.inspector = null;
}
// Notify our components we have deactivated inspector
_ = self.renderer_thread.mailbox.push(.{ .inspector = false }, .{ .forever = {} });
_ = self.renderer_thread.mailbox.push(global.io(), .{ .inspector = false }, .{ .forever = {} });
self.queueIo(.{ .inspector = false }, .unlocked);
// Deinit the inspector
@@ -948,8 +951,8 @@ pub fn needsConfirmQuit(self: *Surface) bool {
.always => true,
.false => false,
.true => true: {
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
self.renderer_state.mutex.lockUncancelable(global.io());
defer self.renderer_state.mutex.unlock(global.io());
break :true !self.io.terminal.cursorIsAtPrompt();
},
};
@@ -1096,9 +1099,9 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
.password_input => |v| try self.passwordInput(v),
.ring_bell => bell: {
const now = std.time.Instant.now() catch unreachable;
const now: std.Io.Timestamp = .now(global.io(), .awake);
if (self.last_bell_time) |last| {
if (now.since(last) < 100 * std.time.ns_per_ms) break :bell;
if (last.durationTo(now).toMilliseconds() < 100) break :bell;
}
self.last_bell_time = now;
_ = self.rt_app.performAction(
@@ -1126,15 +1129,22 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
},
.start_command => {
self.command_timer = try .now();
self.command_timer = .now(global.io(), .awake);
},
.stop_command => |v| timer: {
const end: std.time.Instant = try .now();
const end: std.Io.Timestamp = .now(global.io(), .awake);
const start = self.command_timer orelse break :timer;
self.command_timer = null;
const duration_raw = start.durationTo(end).nanoseconds;
assert(duration_raw >= 0 and duration_raw <= std.math.maxInt(u64));
const duration: Duration = .{ .duration = end.since(start) };
const duration: Duration = .{
.duration = @as(
u64,
@intCast(std.math.clamp(start.durationTo(end).nanoseconds, 0, std.math.maxInt(u64))),
),
};
log.debug("command took {f}", .{duration});
_ = self.rt_app.performAction(
@@ -1185,8 +1195,8 @@ fn selectionScrollTick(self: *Surface) !void {
const pos_vp = self.posToViewport(pos.x, pos.y);
// We need our locked state for the remainder
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
self.renderer_state.mutex.lockUncancelable(global.io());
defer self.renderer_state.mutex.unlock(global.io());
const t: *terminal.Terminal = self.renderer_state.terminal;
const selection = self.mouse.selection_gesture.autoscrollTick(t, .{
@@ -1276,8 +1286,8 @@ fn childExited(self: *Surface, info: apprt.surface.Message.ChildExited) void {
// 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();
self.renderer_state.mutex.lockUncancelable(global.io());
defer self.renderer_state.mutex.unlock(global.io());
const t: *terminal.Terminal = self.renderer_state.terminal;
t.carriageReturn();
t.linefeed() catch break :terminal;
@@ -1316,8 +1326,8 @@ fn childExitedAbnormally(
});
const runtime_str = try std.fmt.allocPrint(alloc, "{d} ms", .{info.runtime_ms});
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
self.renderer_state.mutex.lockUncancelable(global.io());
defer self.renderer_state.mutex.unlock(global.io());
const t: *terminal.Terminal = self.renderer_state.terminal;
// No matter what move the cursor back to the column 0.
@@ -1383,8 +1393,8 @@ fn childExitedAbnormally(
/// Called when the terminal detects there is a password input prompt.
fn passwordInput(self: *Surface, v: bool) !void {
{
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
self.renderer_state.mutex.lockUncancelable(global.io());
defer self.renderer_state.mutex.unlock(global.io());
// If our password input state is unchanged then we don't
// waste time doing anything more.
@@ -1436,6 +1446,7 @@ fn searchCallback_(
for (matches) |*m| m.* = try m.clone(alloc);
_ = self.renderer_thread.mailbox.push(
global.io(),
.{ .search_viewport_matches = .{
.arena = arena,
.matches = matches,
@@ -1454,6 +1465,7 @@ fn searchCallback_(
const match = try sel.highlight.clone(alloc);
_ = self.renderer_thread.mailbox.push(
global.io(),
.{ .search_selected_match = .{
.arena = arena,
.match = match,
@@ -1469,6 +1481,7 @@ fn searchCallback_(
} else {
// Reset our selected match
_ = self.renderer_thread.mailbox.push(
global.io(),
.{ .search_selected_match = null },
.forever,
);
@@ -1493,10 +1506,12 @@ fn searchCallback_(
// When we quit, tell our renderer to reset any search state.
.quit => {
_ = self.renderer_thread.mailbox.push(
global.io(),
.{ .search_selected_match = null },
.forever,
);
_ = self.renderer_thread.mailbox.push(
global.io(),
.{ .search_viewport_matches = .{
.arena = .init(self.alloc),
.matches = &.{},
@@ -1539,8 +1554,8 @@ fn modsChanged(self: *Surface, mods: input.Mods) void {
// highlight links. Additionally, mark the screen as dirty so
// that the highlight state of all links is properly updated.
{
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
self.renderer_state.mutex.lockUncancelable(global.io());
defer self.renderer_state.mutex.unlock(global.io());
self.renderer_state.mouse.mods = self.mouseModsWithCapture(self.mouse.mods);
// We use the clear screen dirty flag to force a rebuild of all
@@ -1786,7 +1801,7 @@ pub fn updateConfig(
termio_config_ptr.* = try termio.Termio.DerivedConfig.init(self.alloc, config);
errdefer termio_config_ptr.deinit();
_ = self.renderer_thread.mailbox.push(renderer_message, .{ .forever = {} });
_ = self.renderer_thread.mailbox.push(global.io(), renderer_message, .{ .forever = {} });
self.queueIo(.{
.change_config = .{
.alloc = self.alloc,
@@ -1907,8 +1922,8 @@ pub fn dumpText(
alloc: Allocator,
sel: terminal.Selection,
) !Text {
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
self.renderer_state.mutex.lockUncancelable(global.io());
defer self.renderer_state.mutex.unlock(global.io());
return try self.dumpTextLocked(alloc, sel);
}
@@ -2033,15 +2048,15 @@ pub fn dumpTextLocked(
/// Returns true if the terminal has a selection.
pub fn hasSelection(self: *const Surface) bool {
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
self.renderer_state.mutex.lockUncancelable(global.io());
defer self.renderer_state.mutex.unlock(global.io());
return self.io.terminal.screens.active.selection != null;
}
/// Returns the selected text. This is allocated.
pub fn selectionString(self: *Surface, alloc: Allocator) !?[:0]const u8 {
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
self.renderer_state.mutex.lockUncancelable(global.io());
defer self.renderer_state.mutex.unlock(global.io());
const sel = self.io.terminal.screens.active.selection orelse return null;
return try self.io.terminal.screens.active.selectionString(alloc, .{
.sel = sel,
@@ -2056,8 +2071,8 @@ pub fn pwd(
self: *const Surface,
alloc: Allocator,
) Allocator.Error!?[]const u8 {
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
self.renderer_state.mutex.lockUncancelable(global.io());
defer self.renderer_state.mutex.unlock(global.io());
const terminal_pwd = self.io.terminal.getPwd() orelse return null;
return try alloc.dupe(u8, terminal_pwd);
}
@@ -2074,7 +2089,7 @@ fn resolvePathForOpening(
const resolved = try std.fs.path.resolve(self.alloc, &.{ terminal_pwd, path });
std.fs.accessAbsolute(resolved, .{}) catch {
std.Io.Dir.accessAbsolute(global.io(), resolved, .{}) catch {
self.alloc.free(resolved);
return null;
};
@@ -2088,10 +2103,10 @@ fn resolvePathForOpening(
/// Returns the x/y coordinate of where the IME (Input Method Editor)
/// keyboard should be rendered.
pub fn imePoint(self: *const Surface) apprt.IMEPos {
self.renderer_state.mutex.lock();
self.renderer_state.mutex.lockUncancelable(global.io());
const cursor = self.renderer_state.terminal.screens.active.cursor;
const preedit_width: usize = if (self.renderer_state.preedit) |preedit| preedit.width() else 0;
self.renderer_state.mutex.unlock();
self.renderer_state.mutex.unlock(global.io());
// TODO: need to handle when scrolling and the cursor is not
// in the visible portion of the screen.
@@ -2440,7 +2455,7 @@ pub fn setFontSize(self: *Surface, size: font.face.DesiredSize) !void {
// Notify our render thread of the new font stack. The renderer
// MUST accept the new font grid and deref the old.
_ = self.renderer_thread.mailbox.push(.{
_ = self.renderer_thread.mailbox.push(global.io(), .{
.font_grid = .{
.grid = font_grid,
.set = &self.app.font_grid_set,
@@ -2530,8 +2545,8 @@ pub fn preeditCallback(self: *Surface, preedit_: ?[]const u8) !void {
crash.sentry.thread_state = self.crashThreadState();
defer crash.sentry.thread_state = null;
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
self.renderer_state.mutex.lockUncancelable(global.io());
defer self.renderer_state.mutex.unlock(global.io());
// We clear our selection when ANY OF:
// 1. We have an existing preedit
@@ -2566,7 +2581,7 @@ pub fn preeditCallback(self: *Surface, preedit_: ?[]const u8) !void {
// Allocate the codepoints slice
const Codepoint = rendererpkg.State.Preedit.Codepoint;
var codepoints: std.ArrayListUnmanaged(Codepoint) = .{};
var codepoints: std.ArrayList(Codepoint) = .empty;
defer codepoints.deinit(self.alloc);
while (it.nextCodepoint()) |cp| {
const width: usize = @intCast(unicode.table.get(cp).width);
@@ -2697,8 +2712,8 @@ pub fn keyCallback(
)) |v| return v;
// If we allow KAM and KAM is enabled then we do nothing.
if (self.config.vt_kam_allowed) {
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
self.renderer_state.mutex.lockUncancelable(global.io());
defer self.renderer_state.mutex.unlock(global.io());
if (self.io.terminal.modes.get(.disable_keyboard)) return .consumed;
}
@@ -2728,8 +2743,8 @@ pub fn keyCallback(
{
// Refresh our link state
const pos = self.rt_surface.getCursorPos() catch break :mouse_mods;
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
self.renderer_state.mutex.lockUncancelable(global.io());
defer self.renderer_state.mutex.unlock(global.io());
self.mouseRefreshLinks(
pos,
self.posToViewport(pos.x, pos.y),
@@ -2821,8 +2836,8 @@ pub fn keyCallback(
// some data to send to the pty, then we move the viewport down to the
// bottom. We also clear the selection for any key other then modifiers.
if (!event.key.modifier()) {
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
self.renderer_state.mutex.lockUncancelable(global.io());
defer self.renderer_state.mutex.unlock(global.io());
if (self.config.selection_clear_on_typing or
event.key == .escape)
@@ -3253,8 +3268,8 @@ fn encodeKey(
}
fn encodeKeyOpts(self: *const Surface) input.key_encode.Options {
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
self.renderer_state.mutex.lockUncancelable(global.io());
defer self.renderer_state.mutex.unlock(global.io());
const t = &self.io.terminal;
var opts: input.key_encode.Options = .fromTerminal(t);
@@ -3295,7 +3310,7 @@ pub fn occlusionCallback(self: *Surface, visible: bool) !void {
crash.sentry.thread_state = self.crashThreadState();
defer crash.sentry.thread_state = null;
_ = self.renderer_thread.mailbox.push(.{
_ = self.renderer_thread.mailbox.push(global.io(), .{
.visible = visible,
}, .{ .forever = {} });
try self.queueRender();
@@ -3315,7 +3330,7 @@ pub fn focusCallback(self: *Surface, focused: bool) !void {
self.focused = focused;
// Notify our render thread of the new state
_ = self.renderer_thread.mailbox.push(.{
_ = self.renderer_thread.mailbox.push(global.io(), .{
.focus = focused,
}, .{ .forever = {} });
@@ -3382,9 +3397,9 @@ pub fn focusCallback(self: *Surface, focused: bool) !void {
// Update the focus state and notify the terminal
{
self.renderer_state.mutex.lock();
self.renderer_state.mutex.lockUncancelable(global.io());
self.io.terminal.flags.focused = focused;
self.renderer_state.mutex.unlock();
self.renderer_state.mutex.unlock(global.io());
self.queueIo(.{ .focused = focused }, .unlocked);
}
}
@@ -3516,8 +3531,8 @@ pub fn scrollCallback(
// log.info("SCROLL: delta_y={} delta_x={}", .{ y.delta, x.delta });
{
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
self.renderer_state.mutex.lockUncancelable(global.io());
defer self.renderer_state.mutex.unlock(global.io());
// If we have an active mouse reporting mode, clear the selection.
// The selection can occur if the user uses the shift mod key to
@@ -3717,8 +3732,8 @@ fn mouseShiftCapture(self: *const Surface, lock: bool) bool {
.false, .true => {},
}
if (lock) self.renderer_state.mutex.lock();
defer if (lock) self.renderer_state.mutex.unlock();
if (lock) self.renderer_state.mutex.lockUncancelable(global.io());
defer if (lock) self.renderer_state.mutex.unlock(global.io());
// If the terminal explicitly requests it then we always allow it
// since we processed never/always at this point.
@@ -3739,8 +3754,8 @@ fn mouseShiftCapture(self: *const Surface, lock: bool) bool {
/// Returns true if the mouse is currently captured by the terminal
/// (i.e. reporting events).
pub fn mouseCaptured(self: *Surface) bool {
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
self.renderer_state.mutex.lockUncancelable(global.io());
defer self.renderer_state.mutex.unlock(global.io());
return self.io.terminal.flags.mouse_event != .none;
}
@@ -3796,21 +3811,12 @@ pub fn mouseButtonCallback(
// If we are within the interval that the click would register
// an increment then we do not extend the selection.
if (std.time.Instant.now()) |now| {
const click_time = self.mouse.selection_gesture.left_click_time orelse
break :extend_selection;
const since = now.since(click_time);
if (since <= self.config.mouse_interval) {
// Click interval very short, we may be increasing
// click counts so we don't extend the selection.
break :extend_selection;
}
} else |err| {
// This is a weird behavior, I think either behavior is actually
// fine. This failure should be exceptionally rare anyways.
// My thinking here is that we can't be sure if we should extend
// the selection or not so we just don't.
log.warn("failed to get time, not extending selection err={}", .{err});
const click_time = self.mouse.selection_gesture.left_click_time orelse
break :extend_selection;
const since = click_time.untilNow(global.io(), .awake);
if (since.toNanoseconds() <= self.config.mouse_interval) {
// Click interval very short, we may be increasing
// click counts so we don't extend the selection.
break :extend_selection;
}
@@ -3821,8 +3827,8 @@ pub fn mouseButtonCallback(
}
if (button == .left and action == .release) {
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
self.renderer_state.mutex.lockUncancelable(global.io());
defer self.renderer_state.mutex.unlock(global.io());
// The selection gesture tracks whether a press became a drag by
// comparing the release cell to the original press cell. Resolve the
@@ -3897,8 +3903,8 @@ pub fn mouseButtonCallback(
// Report mouse events if enabled
{
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
self.renderer_state.mutex.lockUncancelable(global.io());
defer self.renderer_state.mutex.unlock(global.io());
if (self.isMouseReporting()) report: {
// If we have shift-pressed and we aren't allowed to capture it,
// then we do not do a mouse report.
@@ -3937,8 +3943,8 @@ pub fn mouseButtonCallback(
// For left button clicks we always record some information for
// selection/highlighting purposes.
if (button == .left and action == .press) click: {
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
self.renderer_state.mutex.lockUncancelable(global.io());
defer self.renderer_state.mutex.unlock(global.io());
const t: *terminal.Terminal = self.renderer_state.terminal;
const screen: *terminal.Screen = self.renderer_state.terminal.screens.active;
@@ -3962,12 +3968,8 @@ pub fn mouseButtonCallback(
break :pin pin;
};
const time = std.time.Instant.now() catch |err| time: {
log.err("error reading time, mouse multi-click won't work err={}", .{err});
break :time null;
};
var press_selection = try self.mouse.selection_gesture.press(t, .{
.time = time,
.time = std.Io.Timestamp.now(global.io(), .awake),
.pin = pin,
.xpos = pos.x,
.ypos = pos.y,
@@ -4047,8 +4049,8 @@ pub fn mouseButtonCallback(
// want to be careful in the future we can add a function to apprts
// that let's us know.
if (button == .right and action == .press) sel: {
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
self.renderer_state.mutex.lockUncancelable(global.io());
defer self.renderer_state.mutex.unlock(global.io());
// Get our viewport pin
const screen: *terminal.Screen = self.renderer_state.terminal.screens.active;
@@ -4119,8 +4121,8 @@ pub fn mouseButtonCallback(
} else {
// Pasting can trigger a lock grab in complete clipboard
// request so we need to unlock.
self.renderer_state.mutex.unlock();
defer self.renderer_state.mutex.lock();
self.renderer_state.mutex.unlock(global.io());
defer self.renderer_state.mutex.lockUncancelable(global.io());
_ = try self.startClipboardRequest(.standard, .paste);
// We don't need to clear selection because we didn't have
@@ -4134,8 +4136,8 @@ pub fn mouseButtonCallback(
// Pasting can trigger a lock grab in complete clipboard
// request so we need to unlock.
self.renderer_state.mutex.unlock();
defer self.renderer_state.mutex.lock();
self.renderer_state.mutex.unlock(global.io());
defer self.renderer_state.mutex.lockUncancelable(global.io());
_ = try self.startClipboardRequest(.standard, .paste);
},
}
@@ -4428,7 +4430,6 @@ fn openUrl(
// apprts to handle this themselves.
log.warn("apprt did not handle open URL action, falling back to default opener", .{});
try internal_os.open(
self.alloc,
action.kind,
action.url,
);
@@ -4474,8 +4475,8 @@ pub fn mousePressureCallback(
if (self.mouse.click_state[left_idx] == .press and
stage == .deep)
select: {
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
self.renderer_state.mutex.lockUncancelable(global.io());
defer self.renderer_state.mutex.unlock(global.io());
const sel = self.mouse.selection_gesture.deepPress(
self.renderer_state.terminal,
@@ -4540,8 +4541,8 @@ pub fn cursorPosCallback(
try self.queueRender();
}
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
self.renderer_state.mutex.lockUncancelable(global.io());
defer self.renderer_state.mutex.unlock(global.io());
// No mouse point so we don't highlight links
self.renderer_state.mouse.point = null;
@@ -4567,8 +4568,8 @@ pub fn cursorPosCallback(
self.mouse.over_link = false;
// We are reading/writing state for the remainder
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
self.renderer_state.mutex.lockUncancelable(global.io());
defer self.renderer_state.mutex.unlock(global.io());
// Update our mouse state. We set this to null initially because we only
// want to set it when we're not selecting or doing any other mouse
@@ -4824,8 +4825,8 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
// CSI/ESC triggers a scroll.
{
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
self.renderer_state.mutex.lockUncancelable(global.io());
defer self.renderer_state.mutex.unlock(global.io());
self.scrollToBottom() catch |err| {
log.warn("error scrolling to bottom err={}", .{err});
};
@@ -4851,8 +4852,8 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
// Text triggers a scroll.
{
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
self.renderer_state.mutex.lockUncancelable(global.io());
defer self.renderer_state.mutex.unlock(global.io());
self.scrollToBottom() catch |err| {
log.warn("error scrolling to bottom err={}", .{err});
};
@@ -4864,8 +4865,8 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
// in cursor keys mode. We're in "normal" mode if cursor
// keys mode is NOT set.
const normal = normal: {
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
self.renderer_state.mutex.lockUncancelable(global.io());
defer self.renderer_state.mutex.unlock(global.io());
// With the lock held, we must scroll to the bottom.
// We always scroll to the bottom for these inputs.
@@ -4884,8 +4885,8 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
},
.reset => {
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
self.renderer_state.mutex.lockUncancelable(global.io());
defer self.renderer_state.mutex.unlock(global.io());
self.renderer_state.terminal.fullReset();
},
@@ -4955,7 +4956,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
terminal.search.Thread.threadMain,
.{&s.state},
);
s.thread.setName("search") catch {};
s.thread.setName(global.io(), "search") catch {};
break :init s;
};
@@ -4968,6 +4969,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
}
_ = s.state.mailbox.push(
global.io(),
.{ .change_needle = try .init(
self.alloc,
text,
@@ -4980,6 +4982,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
.navigate_search => |nav| {
const s: *Search = if (self.search) |*s| s else return false;
_ = s.state.mailbox.push(
global.io(),
.{ .select = switch (nav) {
.next => .next,
.previous => .prev,
@@ -4990,8 +4993,8 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
},
.copy_to_clipboard => |format| {
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
self.renderer_state.mutex.lockUncancelable(global.io());
defer self.renderer_state.mutex.unlock(global.io());
if (self.io.terminal.screens.active.selection) |sel| {
try self.copySelectionToClipboards(
@@ -5022,8 +5025,8 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
if (!self.mouse.over_link) return false;
const pos = try self.rt_surface.getCursorPos();
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
self.renderer_state.mutex.lockUncancelable(global.io());
defer self.renderer_state.mutex.unlock(global.io());
if (try self.linkAtPos(pos)) |link_info| {
const url_text = switch (link_info.action) {
.open => url_text: {
@@ -5168,8 +5171,8 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
// alternate screen then clear screen does nothing so we want to
// return false so the keybind can be unconsumed.
{
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
self.renderer_state.mutex.lockUncancelable(global.io());
defer self.renderer_state.mutex.unlock(global.io());
if (self.io.terminal.screens.active_key == .alternate) return false;
}
@@ -5192,8 +5195,8 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
.scroll_to_row => |n| {
{
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
self.renderer_state.mutex.lockUncancelable(global.io());
defer self.renderer_state.mutex.unlock(global.io());
const t: *terminal.Terminal = self.renderer_state.terminal;
t.screens.active.scroll(.{ .row = n });
}
@@ -5203,8 +5206,8 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
.scroll_to_selection => {
{
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
self.renderer_state.mutex.lockUncancelable(global.io());
defer self.renderer_state.mutex.unlock(global.io());
const sel = self.io.terminal.screens.active.selection orelse return false;
const tl = sel.topLeft(self.io.terminal.screens.active);
self.io.terminal.screens.active.scroll(.{ .pin = tl });
@@ -5442,8 +5445,8 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
),
.select_all => {
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
self.renderer_state.mutex.lockUncancelable(global.io());
defer self.renderer_state.mutex.unlock(global.io());
const sel = self.io.terminal.screens.active.selectAll();
if (sel) |s| {
@@ -5564,7 +5567,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
.main => @panic("crash binding action, crashing intentionally"),
.render => {
_ = self.renderer_thread.mailbox.push(.{ .crash = {} }, .{ .forever = {} });
_ = self.renderer_thread.mailbox.push(global.io(), .{ .crash = {} }, .{ .forever = {} });
self.queueRender() catch |err| {
// Not a big deal if this fails.
log.warn("failed to notify renderer of crash message err={}", .{err});
@@ -5575,8 +5578,8 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
},
.adjust_selection => |direction| {
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
self.renderer_state.mutex.lockUncancelable(global.io());
defer self.renderer_state.mutex.unlock(global.io());
const screen: *terminal.Screen = self.io.terminal.screens.active;
const sel = if (screen.selection) |*sel| sel else {
@@ -5672,23 +5675,24 @@ fn writeScreenFile(
// Open our scrollback file
var file = try tmp_dir.dir.createFile(
global.io(),
filename,
switch (builtin.os.tag) {
.windows => .{},
else => .{ .mode = 0o600 },
else => .{ .permissions = .fromMode(0o600) },
},
);
defer file.close();
defer file.close(global.io());
// Screen.dumpString writes byte-by-byte, so buffer it
var buf: [4096]u8 = undefined;
var file_writer = file.writer(&buf);
var file_writer = file.writer(global.io(), &buf);
var buf_writer = &file_writer.interface;
// Write the scrollback contents. This requires a lock.
{
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
self.renderer_state.mutex.lockUncancelable(global.io());
defer self.renderer_state.mutex.unlock(global.io());
// We only dump history if we have history. We still keep
// the file and write the empty file to the pty so that this
@@ -5751,7 +5755,11 @@ fn writeScreenFile(
// Get the final path
var path_buf: [std.fs.max_path_bytes]u8 = undefined;
const path = try tmp_dir.dir.realpath(filename, &path_buf);
const path = path_buf[0..try tmp_dir.dir.realPathFile(
global.io(),
filename,
&path_buf,
)];
switch (write_screen.action) {
.copy => {
@@ -5849,8 +5857,8 @@ fn completeClipboardPaste(
if (data.len == 0) return;
const encode_opts: input.paste.Options = encode_opts: {
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
self.renderer_state.mutex.lockUncancelable(global.io());
defer self.renderer_state.mutex.unlock(global.io());
const opts: input.paste.Options = .fromTerminal(&self.io.terminal);
// If we have paste protection enabled, we detect unsafe pastes and return
@@ -5970,12 +5978,12 @@ fn showDesktopNotification(self: *Surface, title: [:0]const u8, body: [:0]const
// how fast identical notifications can be sent sequentially.
const hash_algorithm = std.hash.Wyhash;
const now = try std.time.Instant.now();
const now: std.Io.Timestamp = .now(global.io(), .awake);
// Set a limit of one desktop notification per second so that the OS
// doesn't kill us when we run out of resources.
if (self.app.last_notification_time) |last| {
if (now.since(last) < 1 * std.time.ns_per_s) {
if (last.durationTo(now).toSeconds() < 1) {
log.warn("rate limiting desktop notifications", .{});
return;
}
@@ -5992,7 +6000,7 @@ fn showDesktopNotification(self: *Surface, title: [:0]const u8, body: [:0]const
// notifications with identical content.
if (self.app.last_notification_time) |last| {
if (self.app.last_notification_digest == new_digest) {
if (now.since(last) < 5 * std.time.ns_per_s) {
if (last.durationTo(now).toSeconds() < 5) {
log.warn("suppressing identical desktop notification", .{});
return;
}

View File

@@ -8,6 +8,7 @@ const renderer = @import("../renderer.zig");
const terminal = @import("../terminal/main.zig");
const CoreSurface = @import("../Surface.zig");
const lib = @import("../lib/main.zig");
const compat_testing = @import("../lib/compat/testing.zig");
/// The target for an action. This is generally the thing that had focus
/// while the action was made but the concept of "focus" is not guaranteed
@@ -424,8 +425,11 @@ pub const Action = union(Key) {
/// Sync with: ghostty_action_u
pub const CValue = cvalue: {
const key_fields = @typeInfo(Key).@"enum".fields;
var union_fields: [key_fields.len]std.builtin.Type.UnionField = undefined;
for (key_fields, 0..) |field, i| {
var names: [key_fields.len][]const u8 = undefined;
var types: [key_fields.len]type = undefined;
var attrs: [key_fields.len]std.builtin.Type.UnionField.Attributes = undefined;
for (key_fields, &names, &types, &attrs) |field, *name, *ty, *attr| {
const action = @unionInit(Action, field.name, undefined);
const Type = t: {
const Type = @TypeOf(@field(action, field.name));
@@ -434,19 +438,12 @@ pub const Action = union(Key) {
break :t Type;
};
union_fields[i] = .{
.name = field.name,
.type = Type,
.alignment = @alignOf(Type),
};
name.* = field.name;
ty.* = Type;
attr.* = .{ .@"align" = @alignOf(Type) };
}
break :cvalue @Type(.{ .@"union" = .{
.layout = .@"extern",
.tag_type = null,
.fields = &union_fields,
.decls = &.{},
} });
break :cvalue @Union(.@"extern", null, &names, &types, &attrs);
};
/// Sync with: ghostty_action_s
@@ -713,7 +710,7 @@ pub const SetTitle = struct {
pub fn format(
value: @This(),
comptime _: []const u8,
_: std.fmt.FormatOptions,
_: std.fmt.Options,
writer: *std.Io.Writer,
) !void {
try writer.print("{s}{{ {s} }}", .{ @typeName(@This()), value.title });
@@ -737,7 +734,7 @@ pub const Pwd = struct {
pub fn format(
value: @This(),
comptime _: []const u8,
_: std.fmt.FormatOptions,
_: std.fmt.Options,
writer: *std.Io.Writer,
) !void {
try writer.print("{s}{{ {s} }}", .{ @typeName(@This()), value.pwd });
@@ -765,7 +762,7 @@ pub const DesktopNotification = struct {
pub fn format(
value: @This(),
comptime _: []const u8,
_: std.fmt.FormatOptions,
_: std.fmt.Options,
writer: *std.Io.Writer,
) !void {
try writer.print("{s}{{ title: {s}, body: {s} }}", .{
@@ -1008,5 +1005,5 @@ pub const SearchSelected = struct {
};
test {
_ = std.testing.refAllDeclsRecursive(@This());
_ = compat_testing.refAllDeclsRecursive(@This());
}

View File

@@ -11,6 +11,7 @@ const Allocator = std.mem.Allocator;
const objc = @import("objc");
const apprt = @import("../apprt.zig");
const font = @import("../font/main.zig");
const global = @import("../global.zig");
const input = @import("../input.zig");
const internal_os = @import("../os/main.zig");
const renderer = @import("../renderer.zig");
@@ -333,7 +334,7 @@ pub const App = struct {
_: apprt.ipc.Target,
comptime action: apprt.ipc.Action.Key,
_: apprt.ipc.Action.Value(action),
) (Allocator.Error || std.posix.WriteError || apprt.ipc.Errors)!bool {
) (Allocator.Error || apprt.ipc.Errors)!bool {
switch (action) {
.new_window => return false,
.toggle_quick_terminal => return false,
@@ -372,7 +373,7 @@ pub const Platform = union(PlatformTag) {
/// Initialize a Platform a tag and configuration from the C ABI.
pub fn init(tag_int: c_int, c_platform: C) !Platform {
const tag = try std.meta.intToEnum(PlatformTag, tag_int);
const tag = std.enums.fromInt(PlatformTag, tag_int) orelse return error.InvalidEnumTag;
return switch (tag) {
.macos => if (MacOS != void) macos: {
const config = c_platform.macos;
@@ -490,16 +491,16 @@ pub const Surface = struct {
if (opts.working_directory) |c_wd| {
const wd = std.mem.sliceTo(c_wd, 0);
if (wd.len > 0) wd: {
var dir = std.fs.openDirAbsolute(wd, .{}) catch |err| {
var dir = std.Io.Dir.openDirAbsolute(global.io(), wd, .{}) catch |err| {
log.warn(
"error opening requested working directory dir={s} err={}",
.{ wd, err },
);
break :wd;
};
defer dir.close();
defer dir.close(global.io());
const stat = dir.stat() catch |err| {
const stat = dir.stat(global.io()) catch |err| {
log.warn(
"failed to stat requested working directory dir={s} err={}",
.{ wd, err },
@@ -950,32 +951,32 @@ pub const Surface = struct {
};
}
pub fn defaultTermioEnv(self: *const Surface) !std.process.EnvMap {
const alloc = self.app.core_app.alloc;
var env = try internal_os.getEnvMap(alloc);
pub fn defaultTermioEnv(self: *const Surface) !std.process.Environ.Map {
_ = self;
var env = try global.environMap();
errdefer env.deinit();
if (comptime builtin.target.os.tag.isDarwin()) {
if (env.get("__XCODE_BUILT_PRODUCTS_DIR_PATHS") != null) {
env.remove("__XCODE_BUILT_PRODUCTS_DIR_PATHS");
env.remove("__XPC_DYLD_LIBRARY_PATH");
env.remove("DYLD_FRAMEWORK_PATH");
env.remove("DYLD_INSERT_LIBRARIES");
env.remove("DYLD_LIBRARY_PATH");
env.remove("LD_LIBRARY_PATH");
env.remove("SECURITYSESSIONID");
env.remove("XPC_SERVICE_NAME");
_ = env.orderedRemove("__XCODE_BUILT_PRODUCTS_DIR_PATHS");
_ = env.orderedRemove("__XPC_DYLD_LIBRARY_PATH");
_ = env.orderedRemove("DYLD_FRAMEWORK_PATH");
_ = env.orderedRemove("DYLD_INSERT_LIBRARIES");
_ = env.orderedRemove("DYLD_LIBRARY_PATH");
_ = env.orderedRemove("LD_LIBRARY_PATH");
_ = env.orderedRemove("SECURITYSESSIONID");
_ = env.orderedRemove("XPC_SERVICE_NAME");
}
// Remove this so that running `ghostty` within Ghostty works.
env.remove("GHOSTTY_MAC_LAUNCH_SOURCE");
_ = env.orderedRemove("GHOSTTY_MAC_LAUNCH_SOURCE");
// If we were launched from the desktop then we want to
// remove the LANGUAGE env var so that we don't inherit
// our translation settings for Ghostty. If we aren't from
// the desktop then we didn't set our LANGUAGE var so we
// don't need to remove it.
if (internal_os.launchedFromDesktop()) env.remove("LANGUAGE");
if (internal_os.launchedFromDesktop()) _ = env.orderedRemove("LANGUAGE");
}
return env;
@@ -1000,7 +1001,7 @@ pub const Inspector = struct {
content_scale: f64 = 1,
/// Our previous instant used to calculate delta time for animations.
instant: ?std.time.Instant = null,
instant: ?std.Io.Timestamp = null,
const Backend = enum {
metal,
@@ -1229,9 +1230,9 @@ pub const Inspector = struct {
const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO();
// Determine our delta time
const now = try std.time.Instant.now();
const now: std.Io.Timestamp = .now(global.io(), .awake);
io.DeltaTime = if (self.instant) |prev| delta: {
const since_ns: f64 = @floatFromInt(now.since(prev));
const since_ns: f64 = @floatFromInt(prev.durationTo(now).toNanoseconds());
const ns_per_s: f64 = @floatFromInt(std.time.ns_per_s);
const since_s: f32 = @floatCast(since_ns / ns_per_s);
break :delta @max(0.00001, since_s);
@@ -1242,8 +1243,6 @@ pub const Inspector = struct {
// C API
pub const CAPI = struct {
const global = &@import("../global.zig").state;
/// This is the same as Surface.KeyEvent but this is the raw C API version.
const KeyEvent = extern struct {
action: input.Action,
@@ -1300,7 +1299,7 @@ pub const CAPI = struct {
pub fn deinit(self: *Text) void {
if (self.text) |ptr| {
global.alloc.free(ptr[0..self.text_len :0]);
global.alloc().free(ptr[0..self.text_len :0]);
}
}
};
@@ -1410,12 +1409,12 @@ pub const CAPI = struct {
opts: *const apprt.runtime.App.Options,
config: *const Config,
) !*App {
const core_app = try CoreApp.create(global.alloc);
const core_app = try CoreApp.create(global.alloc());
errdefer core_app.destroy();
// Create our runtime app
var app = try global.alloc.create(App);
errdefer global.alloc.destroy(app);
var app = try global.alloc().create(App);
errdefer global.alloc().destroy(app);
try app.init(core_app, config, opts.*);
errdefer app.terminate();
@@ -1438,7 +1437,7 @@ pub const CAPI = struct {
export fn ghostty_app_free(v: *App) void {
const core_app = v.core_app;
v.terminate();
global.alloc.destroy(v);
global.alloc().destroy(v);
core_app.destroy();
}
@@ -1520,13 +1519,7 @@ pub const CAPI = struct {
/// Update the color scheme of the app.
export fn ghostty_app_set_color_scheme(v: *App, scheme_raw: c_int) void {
const scheme = std.meta.intToEnum(apprt.ColorScheme, scheme_raw) catch {
log.warn(
"invalid color scheme to ghostty_surface_set_color_scheme value={}",
.{scheme_raw},
);
return;
};
const scheme = std.enums.fromInt(apprt.ColorScheme, scheme_raw) orelse return;
v.core_app.colorSchemeEvent(v, scheme) catch |err| {
log.err("error setting color scheme err={}", .{err});
@@ -1612,8 +1605,8 @@ pub const CAPI = struct {
result: *Text,
) bool {
const core_surface = &surface.core_surface;
core_surface.renderer_state.mutex.lock();
defer core_surface.renderer_state.mutex.unlock();
core_surface.renderer_state.mutex.lockUncancelable(global.io());
defer core_surface.renderer_state.mutex.unlock(global.io());
// If we don't have a selection, do nothing.
const core_sel = core_surface.io.terminal.screens.active.selection orelse return false;
@@ -1632,8 +1625,8 @@ pub const CAPI = struct {
sel: Selection,
result: *Text,
) bool {
surface.core_surface.renderer_state.mutex.lock();
defer surface.core_surface.renderer_state.mutex.unlock();
surface.core_surface.renderer_state.mutex.lockUncancelable(global.io());
defer surface.core_surface.renderer_state.mutex.unlock(global.io());
const core_sel = sel.core(
surface.core_surface.renderer_state.terminal.screens.active,
@@ -1651,7 +1644,7 @@ pub const CAPI = struct {
// Get our text directly from the core surface.
const text = core_surface.dumpTextLocked(
global.alloc,
global.alloc(),
core_sel,
) catch |err| {
log.warn("error reading text err={}", .{err});
@@ -1730,14 +1723,7 @@ pub const CAPI = struct {
/// Update the color scheme of the surface.
export fn ghostty_surface_set_color_scheme(surface: *Surface, scheme_raw: c_int) void {
const scheme = std.meta.intToEnum(apprt.ColorScheme, scheme_raw) catch {
log.warn(
"invalid color scheme to ghostty_surface_set_color_scheme value={}",
.{scheme_raw},
);
return;
};
const scheme = std.enums.fromInt(apprt.ColorScheme, scheme_raw) orelse return;
surface.colorSchemeCallback(scheme);
}
@@ -1891,17 +1877,7 @@ pub const CAPI = struct {
stage_raw: u32,
pressure: f64,
) void {
const stage = std.meta.intToEnum(
input.MousePressureStage,
stage_raw,
) catch {
log.warn(
"invalid mouse pressure stage value={}",
.{stage_raw},
);
return;
};
const stage = std.enums.fromInt(input.MousePressureStage, stage_raw) orelse return;
surface.mousePressureCallback(stage, pressure);
}
@@ -2134,6 +2110,7 @@ pub const CAPI = struct {
export fn ghostty_surface_set_display_id(ptr: *Surface, display_id: u32) void {
const surface = &ptr.core_surface;
_ = surface.renderer_thread.mailbox.push(
global.io(),
.{ .macos_display_id = display_id },
.{ .forever = {} },
);
@@ -2157,8 +2134,8 @@ pub const CAPI = struct {
// read the font face. It should not be deferred since
// we're loading the primary face.
const grid = ptr.core_surface.renderer.font_grid;
grid.lock.lockShared();
defer grid.lock.unlockShared();
grid.lock.lockSharedUncancelable(global.io());
defer grid.lock.unlockShared(global.io());
const collection = &grid.resolver.collection;
const face = collection.getFace(.{}) catch return null;
@@ -2195,8 +2172,8 @@ pub const CAPI = struct {
result: *Text,
) bool {
const surface = &ptr.core_surface;
surface.renderer_state.mutex.lock();
defer surface.renderer_state.mutex.unlock();
surface.renderer_state.mutex.lockUncancelable(global.io());
defer surface.renderer_state.mutex.unlock(global.io());
// Get our word selection
const sel = sel: {

View File

@@ -94,7 +94,7 @@ pub fn setClipboard(
);
}
pub fn defaultTermioEnv(self: *Self) !std.process.EnvMap {
pub fn defaultTermioEnv(self: *Self) !std.process.Environ.Map {
return try self.surface.defaultTermioEnv();
}

View File

@@ -4,18 +4,16 @@ const std = @import("std");
// Ghostty, we need to import `adwaita.h` directly to ensure that the version
// macros match the version of `libadwaita` that we are building/linking
// against.
const c = @cImport({
@cInclude("adwaita.h");
});
const adw_c = @import("adw_c");
const adw = @import("adw");
const log = std.log.scoped(.gtk);
pub const comptime_version: std.SemanticVersion = .{
.major = c.ADW_MAJOR_VERSION,
.minor = c.ADW_MINOR_VERSION,
.patch = c.ADW_MICRO_VERSION,
.major = adw_c.ADW_MAJOR_VERSION,
.minor = adw_c.ADW_MINOR_VERSION,
.patch = adw_c.ADW_MICRO_VERSION,
};
pub fn getRuntimeVersion() std.SemanticVersion {
@@ -89,14 +87,14 @@ test "versionAtLeast" {
const funs = &.{ atLeast, runtimeAtLeast };
inline for (funs) |fun| {
try testing.expect(fun(c.ADW_MAJOR_VERSION, c.ADW_MINOR_VERSION, c.ADW_MICRO_VERSION));
try testing.expect(!fun(c.ADW_MAJOR_VERSION, c.ADW_MINOR_VERSION, c.ADW_MICRO_VERSION + 1));
try testing.expect(!fun(c.ADW_MAJOR_VERSION, c.ADW_MINOR_VERSION + 1, c.ADW_MICRO_VERSION));
try testing.expect(!fun(c.ADW_MAJOR_VERSION + 1, c.ADW_MINOR_VERSION, c.ADW_MICRO_VERSION));
try testing.expect(fun(c.ADW_MAJOR_VERSION - 1, c.ADW_MINOR_VERSION, c.ADW_MICRO_VERSION));
try testing.expect(fun(c.ADW_MAJOR_VERSION - 1, c.ADW_MINOR_VERSION + 1, c.ADW_MICRO_VERSION));
try testing.expect(fun(c.ADW_MAJOR_VERSION - 1, c.ADW_MINOR_VERSION, c.ADW_MICRO_VERSION + 1));
try testing.expect(fun(c.ADW_MAJOR_VERSION, c.ADW_MINOR_VERSION - 1, c.ADW_MICRO_VERSION + 1));
try testing.expect(fun(adw_c.ADW_MAJOR_VERSION, adw_c.ADW_MINOR_VERSION, adw_c.ADW_MICRO_VERSION));
try testing.expect(!fun(adw_c.ADW_MAJOR_VERSION, adw_c.ADW_MINOR_VERSION, adw_c.ADW_MICRO_VERSION + 1));
try testing.expect(!fun(adw_c.ADW_MAJOR_VERSION, adw_c.ADW_MINOR_VERSION + 1, adw_c.ADW_MICRO_VERSION));
try testing.expect(!fun(adw_c.ADW_MAJOR_VERSION + 1, adw_c.ADW_MINOR_VERSION, adw_c.ADW_MICRO_VERSION));
try testing.expect(fun(adw_c.ADW_MAJOR_VERSION - 1, adw_c.ADW_MINOR_VERSION, adw_c.ADW_MICRO_VERSION));
try testing.expect(fun(adw_c.ADW_MAJOR_VERSION - 1, adw_c.ADW_MINOR_VERSION + 1, adw_c.ADW_MICRO_VERSION));
try testing.expect(fun(adw_c.ADW_MAJOR_VERSION - 1, adw_c.ADW_MINOR_VERSION, adw_c.ADW_MICRO_VERSION + 1));
try testing.expect(fun(adw_c.ADW_MAJOR_VERSION, adw_c.ADW_MINOR_VERSION - 1, adw_c.ADW_MICRO_VERSION + 1));
}
}

View File

@@ -6,10 +6,7 @@
//! Example: blueprint.zig 1 5 output.ui input.blp
const std = @import("std");
pub const c = @cImport({
@cInclude("adwaita.h");
});
const adw_c = @import("adw_c");
pub const blueprint_compiler_help =
\\
@@ -26,23 +23,24 @@ pub const blueprint_compiler_help =
;
const adwaita_version = std.SemanticVersion{
.major = c.ADW_MAJOR_VERSION,
.minor = c.ADW_MINOR_VERSION,
.patch = c.ADW_MICRO_VERSION,
.major = adw_c.ADW_MAJOR_VERSION,
.minor = adw_c.ADW_MINOR_VERSION,
.patch = adw_c.ADW_MICRO_VERSION,
};
const required_blueprint_version = std.SemanticVersion{
.major = 0,
.minor = 16,
.patch = 0,
};
pub fn main() !void {
pub fn main(init: std.process.Init) !void {
var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
defer _ = debug_allocator.deinit();
const alloc = debug_allocator.allocator();
// Get our args
var it = try std.process.argsWithAllocator(alloc);
var it = try init.minimal.args.iterateAllocator(alloc);
defer it.deinit();
_ = it.next(); // Skip argv0
const arg_major = it.next() orelse return error.NoMajorVersion;
@@ -63,51 +61,37 @@ pub fn main() !void {
\\compile this blueprint. Please install it, ensure that it is
\\available on your PATH, and then retry building Ghostty.
, .{required_adwaita_version});
std.posix.exit(1);
std.process.exit(1);
}
// Version checks
{
var stdout: std.ArrayListUnmanaged(u8) = .empty;
defer stdout.deinit(alloc);
var stderr: std.ArrayListUnmanaged(u8) = .empty;
defer stderr.deinit(alloc);
var blueprint_compiler = std.process.Child.init(
&.{
"blueprint-compiler",
"--version",
},
alloc,
);
blueprint_compiler.stdout_behavior = .Pipe;
blueprint_compiler.stderr_behavior = .Pipe;
try blueprint_compiler.spawn();
try blueprint_compiler.collectOutput(
alloc,
&stdout,
&stderr,
std.math.maxInt(u16),
);
const term = blueprint_compiler.wait() catch |err| switch (err) {
const blueprint_compiler = std.process.run(alloc, init.io, .{
.argv = &.{ "blueprint-compiler", "--version" },
}) catch |err| switch (err) {
error.FileNotFound => {
std.debug.print(
\\`blueprint-compiler` not found.
++ blueprint_compiler_help,
.{required_blueprint_version},
);
std.posix.exit(1);
std.process.exit(1);
},
else => return err,
};
switch (term) {
.Exited => |rc| if (rc != 0) std.process.exit(1),
defer {
alloc.free(blueprint_compiler.stdout);
alloc.free(blueprint_compiler.stderr);
}
switch (blueprint_compiler.term) {
.exited => |rc| if (rc != 0) std.process.exit(1),
else => std.process.exit(1),
}
const version = try std.SemanticVersion.parse(std.mem.trim(
u8,
stdout.items,
blueprint_compiler.stdout,
&std.ascii.whitespace,
));
if (version.order(required_blueprint_version) == .lt) {
@@ -116,57 +100,45 @@ pub fn main() !void {
++ blueprint_compiler_help,
.{required_blueprint_version},
);
std.posix.exit(1);
std.process.exit(1);
}
}
// Compilation
{
var stdout: std.ArrayListUnmanaged(u8) = .empty;
defer stdout.deinit(alloc);
var stderr: std.ArrayListUnmanaged(u8) = .empty;
defer stderr.deinit(alloc);
var blueprint_compiler = std.process.Child.init(
&.{
const blueprint_compiler = std.process.run(alloc, init.io, .{
.argv = &.{
"blueprint-compiler",
"compile",
"--output",
output,
input,
},
alloc,
);
blueprint_compiler.stdout_behavior = .Pipe;
blueprint_compiler.stderr_behavior = .Pipe;
try blueprint_compiler.spawn();
try blueprint_compiler.collectOutput(
alloc,
&stdout,
&stderr,
std.math.maxInt(u16),
);
const term = blueprint_compiler.wait() catch |err| switch (err) {
}) catch |err| switch (err) {
error.FileNotFound => {
std.debug.print(
\\`blueprint-compiler` not found.
++ blueprint_compiler_help,
.{required_blueprint_version},
);
std.posix.exit(1);
std.process.exit(1);
},
else => return err,
};
defer {
alloc.free(blueprint_compiler.stdout);
alloc.free(blueprint_compiler.stderr);
}
switch (term) {
.Exited => |rc| {
switch (blueprint_compiler.term) {
.exited => |rc| {
if (rc != 0) {
std.debug.print("{s}", .{stderr.items});
std.debug.print("{s}", .{blueprint_compiler.stderr});
std.process.exit(1);
}
},
else => {
std.debug.print("{s}", .{stderr.items});
std.debug.print("{s}", .{blueprint_compiler.stderr});
std.process.exit(1);
},
}

View File

@@ -122,18 +122,17 @@ pub fn blueprint(comptime bp: Blueprint) [:0]const u8 {
}
}
pub fn main() !void {
var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
defer _ = debug_allocator.deinit();
const alloc = debug_allocator.allocator();
pub fn main(init: std.process.Init) !void {
const alloc = init.arena.allocator();
// Collect the UI files that are passed in as arguments.
var ui_files: std.ArrayListUnmanaged([]const u8) = .empty;
var ui_files: std.ArrayList([]const u8) = .empty;
defer {
for (ui_files.items) |item| alloc.free(item);
ui_files.deinit(alloc);
}
var it = try std.process.argsWithAllocator(alloc);
var it = try init.minimal.args.iterateAllocator(alloc);
defer it.deinit();
while (it.next()) |arg| {
if (!std.mem.endsWith(u8, arg, ".ui")) continue;
@@ -144,7 +143,7 @@ pub fn main() !void {
}
var buf: [4096]u8 = undefined;
var stdout = std.fs.File.stdout().writer(&buf);
var stdout = std.Io.File.stdout().writer(init.io, &buf);
const writer = &stdout.interface;
try writer.writeAll(
\\<?xml version="1.0" encoding="UTF-8"?>
@@ -152,8 +151,8 @@ pub fn main() !void {
\\
);
try genRoot(writer);
try genIcons(writer);
try genRoot(init.io, writer);
try genIcons(init.io, writer);
try genUi(alloc, writer, &ui_files);
try writer.writeAll(
@@ -167,19 +166,19 @@ pub fn main() !void {
/// Generate the icon resources. This works by looking up all the icons
/// specified by `icon_sizes` in `images/icons/`. They are asserted to exist
/// by trying to access the file.
fn genIcons(writer: *std.Io.Writer) !void {
fn genIcons(io: std.Io, writer: *std.Io.Writer) !void {
try writer.print(
\\ <gresource prefix="{s}/icons">
\\
, .{build_info.resource_path});
const cwd = std.fs.cwd();
const cwd: std.Io.Dir = .cwd();
inline for (icon_sizes) |size| {
// 1x
{
const alias = std.fmt.comptimePrint("{d}x{d}", .{ size, size });
const source = std.fmt.comptimePrint("images/gnome/{d}.png", .{size});
try cwd.access(source, .{});
try cwd.access(io, source, .{});
try writer.print(
\\ <file alias="{s}/apps/{s}.png">{s}</file>
\\
@@ -192,7 +191,7 @@ fn genIcons(writer: *std.Io.Writer) !void {
{
const alias = std.fmt.comptimePrint("{d}x{d}@2", .{ size, size });
const source = std.fmt.comptimePrint("images/gnome/{d}.png", .{size * 2});
try cwd.access(source, .{});
try cwd.access(io, source, .{});
try writer.print(
\\ <file alias="{s}/apps/{s}.png">{s}</file>
\\
@@ -209,19 +208,19 @@ fn genIcons(writer: *std.Io.Writer) !void {
}
/// Generate the resources at the root prefix.
fn genRoot(writer: *std.Io.Writer) !void {
fn genRoot(io: std.Io, writer: *std.Io.Writer) !void {
try writer.print(
\\ <gresource prefix="{s}">
\\
, .{build_info.resource_path});
const cwd = std.fs.cwd();
const cwd: std.Io.Dir = .cwd();
inline for (css) |name| {
const source = std.fmt.comptimePrint(
"{s}/{s}",
.{ css_path, name },
);
try cwd.access(source, .{});
try cwd.access(io, source, .{});
try writer.print(
\\ <file compressed="true" alias="{s}">{s}</file>
\\
@@ -242,7 +241,7 @@ fn genRoot(writer: *std.Io.Writer) !void {
fn genUi(
alloc: Allocator,
writer: *std.Io.Writer,
files: *const std.ArrayListUnmanaged([]const u8),
files: *const std.ArrayList([]const u8),
) !void {
try writer.print(
\\ <gresource prefix="{s}/ui">

View File

@@ -145,16 +145,24 @@ pub fn Common(
/// as the virtual method but the self parameter points to the
/// target instead of the original class.
fn ImplementFunc(comptime T: type) type {
var params: [fn_info.params.len]std.builtin.Type.Fn.Param = undefined;
@memcpy(&params, fn_info.params);
params[0].type = *ClassInstance(T);
return @Type(.{ .@"fn" = .{
.calling_convention = fn_info.calling_convention,
.is_generic = fn_info.is_generic,
.is_var_args = fn_info.is_var_args,
.return_type = fn_info.return_type,
.params = &params,
} });
var types: [fn_info.params.len]type = undefined;
var attrs: [fn_info.params.len]std.builtin.Type.Fn.Param.Attributes = undefined;
for (fn_info.params, &types, &attrs) |info, *ty, *attr| {
ty.* = info.type.?;
attr.* = .{ .@"noalias" = info.is_noalias };
}
types[0] = *ClassInstance(T);
return @Fn(
&types,
&attrs,
fn_info.return_type.?,
.{
.@"callconv" = fn_info.calling_convention,
.varargs = fn_info.is_var_args,
},
);
}
};
}

View File

@@ -1,3 +1,4 @@
const builtin = @import("builtin");
const std = @import("std");
const assert = @import("../../../quirks.zig").inlineAssert;
const Allocator = std.mem.Allocator;
@@ -10,16 +11,17 @@ const gtk = @import("gtk");
const build_config = @import("../../../build_config.zig");
const build_info = @import("../build/info.zig");
const state = &@import("../../../global.zig").state;
const global = @import("../../../global.zig");
const i18n = @import("../../../os/main.zig").i18n;
const apprt = @import("../../../apprt.zig");
const CoreApp = @import("../../../App.zig");
const compat_file = @import("../../../lib/compat/file.zig");
const configpkg = @import("../../../config.zig");
const input = @import("../../../input.zig");
const internal_os = @import("../../../os/main.zig");
const systemd = @import("../../../os/systemd.zig");
const terminal = @import("../../../terminal/main.zig");
const xev = @import("../../../global.zig").xev;
const xev = global.xev;
const Binding = @import("../../../input.zig").Binding;
const CoreConfig = configpkg.Config;
const CoreSurface = @import("../../../Surface.zig");
@@ -45,6 +47,8 @@ const OpenURI = @import("../portal.zig").OpenURI;
const log = std.log.scoped(.gtk_ghostty_application);
extern "c" fn setenv(name: ?[*]const u8, value: ?[*]const u8, overwrite: c_int) c_int;
/// Function used to funnel GLib/GObject/GTK log messages into Zig's logging
/// system rather than just getting dumped directly to stderr.
fn glibLogWriterFunction(
@@ -271,15 +275,17 @@ pub const Application = extern struct {
};
defer config.deinit();
// Set the old language,
const saved_language: ?[:0]const u8 = saved_language: {
const old_language = old_language: {
const result = (internal_os.getenv(alloc, "LANG") catch break :old_language null) orelse break :old_language null;
defer result.deinit(alloc);
break :old_language alloc.dupeZ(u8, result.value) catch break :old_language null;
const lang = global.environ().getPosix("LANG") orelse break :old_language null;
break :old_language alloc.dupeSentinel(u8, @ptrCast(lang), 0) catch null;
};
if (config.language) |language| _ = internal_os.setenv("LANG", language);
if (config.language) |language| {
// Override LANG if we need to (sync global environs if so)
_ = setenv("LANG", @ptrCast(language), 1);
global.syncEnviron();
}
break :saved_language old_language;
};
@@ -294,7 +300,7 @@ pub const Application = extern struct {
// Setup our GTK init env vars
setGtkEnv(&config) catch |err| switch (err) {
error.NoSpaceLeft => {
error.WriteFailed => {
// If we fail to set GTK environment variables then we still
// try to start the application...
log.warn(
@@ -338,7 +344,7 @@ pub const Application = extern struct {
// I'm unsure of any scenario where this happens. Because we don't
// want to litter null checks everywhere, we just exit here.
log.warn("gdk display is null, exiting", .{});
std.posix.exit(1);
std.process.exit(1);
};
// Setup our windowing protocol logic
@@ -1063,7 +1069,11 @@ pub const Application = extern struct {
}
}
fn loadCustomCss(self: *Self) (std.fs.File.ReadError || Allocator.Error)!void {
const LoadCustomCssError = std.Io.File.OpenError ||
compat_file.ReadToEndAllocError ||
std.mem.Allocator.Error;
fn loadCustomCss(self: *Self) LoadCustomCssError!void {
const priv: *Private = self.private();
const alloc = self.allocator();
const display = gdk.Display.getDefault() orelse {
@@ -1087,7 +1097,11 @@ pub const Application = extern struct {
.optional => |path| .{ path, true },
.required => |path| .{ path, false },
};
const file = std.fs.openFileAbsolute(path, .{}) catch |err| {
const file = std.Io.Dir.openFileAbsolute(
global.io(),
path,
.{},
) catch |err| {
if (err != error.FileNotFound or !optional) {
log.warn(
"error opening gtk-custom-css file {s}: {}",
@@ -1096,12 +1110,14 @@ pub const Application = extern struct {
}
continue;
};
defer file.close();
defer file.close(global.io());
const css_file_size_limit = 5 * 1024 * 1024; // 5MB
log.info("loading gtk-custom-css path={s}", .{path});
const contents = file.readToEndAlloc(
const contents = compat_file.readToEndAlloc(
file,
alloc,
css_file_size_limit,
) catch |err| switch (err) {
@@ -1111,6 +1127,7 @@ pub const Application = extern struct {
},
else => |e| return e,
};
defer alloc.free(contents);
const bytes = glib.Bytes.new(contents.ptr, contents.len);
@@ -1401,7 +1418,7 @@ pub const Application = extern struct {
const priv = self.private();
assert(priv.signal_source == null);
priv.signal_source = glib.unixSignalAdd(
std.posix.SIG.USR2,
@intFromEnum(std.posix.SIG.USR2),
handleSigusr2,
self,
);
@@ -1461,7 +1478,7 @@ pub const Application = extern struct {
// Queue a new window
const priv = self.private();
_ = priv.core_app.mailbox.push(.{
_ = priv.core_app.mailbox.push(global.io(), .{
.new_window = .{},
}, .{ .forever = {} });
@@ -1813,7 +1830,7 @@ pub const Application = extern struct {
_: ?*glib.Variant,
self: *Self,
) callconv(.c) void {
_ = self.core().mailbox.push(.open_config, .forever);
_ = self.core().mailbox.push(global.io(), .open_config, .forever);
}
fn actionPresentSurface(
@@ -1839,6 +1856,7 @@ pub const Application = extern struct {
const surface = self.core().findSurfaceByID(surface_id) orelse return;
_ = self.core().mailbox.push(
global.io(),
.{
.surface_message = .{
.surface = surface,
@@ -1866,11 +1884,8 @@ pub const Application = extern struct {
fn init(class: *Class) callconv(.c) void {
// Register our compiled resources exactly once.
{
const c = @cImport({
// generated header files
@cInclude("ghostty_resources.h");
});
if (c.ghostty_get_resource()) |ptr| {
const ghostty_gtk_resources = @import("ghostty_gtk_resources");
if (ghostty_gtk_resources.ghostty_get_resource()) |ptr| {
gio.resourcesRegister(@ptrCast(@alignCast(ptr)));
} else {
// If we fail to load resources then things will
@@ -1894,11 +1909,11 @@ pub const Application = extern struct {
};
pub fn openUrlFallback(self: *Application, kind: apprt.action.OpenUrl.Kind, url: []const u8) void {
_ = self;
// Fallback to the minimal cross-platform way of opening a URL.
// This is always a safe fallback and enables for example Windows
// to open URLs (GTK on Windows via WSL is a thing).
internal_os.open(
self.allocator(),
kind,
url,
) catch |err| log.warn("unable to open url: {}", .{err});
@@ -2846,7 +2861,7 @@ const Action = struct {
/// given the runtime environment or configuration.
///
/// This must be called BEFORE GTK initialization.
fn setGtkEnv(config: *const CoreConfig) error{NoSpaceLeft}!void {
fn setGtkEnv(config: *const CoreConfig) std.Io.Writer.Error!void {
assert(gtk.isInitialized() == 0);
var gdk_debug: struct {
@@ -2863,7 +2878,7 @@ fn setGtkEnv(config: *const CoreConfig) error{NoSpaceLeft}!void {
} = .{
// `gtk-opengl-debug` dumps logs directly to stderr so both must be true
// to enable OpenGL debugging.
.opengl = state.logging.stderr and config.@"gtk-opengl-debug",
.opengl = global.logging().stderr and config.@"gtk-opengl-debug",
};
var gdk_disable: struct {
@@ -2915,8 +2930,7 @@ fn setGtkEnv(config: *const CoreConfig) error{NoSpaceLeft}!void {
{
var buf: [1024]u8 = undefined;
var fmt = std.io.fixedBufferStream(&buf);
const writer = fmt.writer();
var writer: std.Io.Writer = .fixed(&buf);
var first: bool = true;
inline for (@typeInfo(@TypeOf(gdk_debug)).@"struct".fields) |field| {
if (@field(gdk_debug, field.name)) {
@@ -2926,15 +2940,14 @@ fn setGtkEnv(config: *const CoreConfig) error{NoSpaceLeft}!void {
}
}
try writer.writeByte(0);
const value = fmt.getWritten();
const value = writer.buffered();
log.warn("setting GDK_DEBUG={s}", .{value[0 .. value.len - 1]});
_ = internal_os.setenv("GDK_DEBUG", value[0 .. value.len - 1 :0]);
_ = setenv("GDK_DEBUG", @ptrCast(value[0 .. value.len - 1 :0]), 1);
}
{
var buf: [1024]u8 = undefined;
var fmt = std.io.fixedBufferStream(&buf);
const writer = fmt.writer();
var writer: std.Io.Writer = .fixed(&buf);
var first: bool = true;
inline for (@typeInfo(@TypeOf(gdk_disable)).@"struct".fields) |field| {
if (@field(gdk_disable, field.name)) {
@@ -2944,10 +2957,13 @@ fn setGtkEnv(config: *const CoreConfig) error{NoSpaceLeft}!void {
}
}
try writer.writeByte(0);
const value = fmt.getWritten();
const value = writer.buffered();
log.warn("setting GDK_DISABLE={s}", .{value[0 .. value.len - 1]});
_ = internal_os.setenv("GDK_DISABLE", value[0 .. value.len - 1 :0]);
_ = setenv("GDK_DISABLE", @ptrCast(value[0 .. value.len - 1 :0]), 1);
}
// Sync environ after altering system env
global.syncEnviron();
}
fn findActiveWindow(data: ?*const anyopaque, _: ?*const anyopaque) callconv(.c) c_int {

View File

@@ -153,7 +153,7 @@ pub const CommandPalette = extern struct {
priv.source.removeAll();
const alloc = Application.default().allocator();
var commands: std.ArrayList(*Command) = .{};
var commands: std.ArrayList(*Command) = .empty;
defer {
for (commands.items) |cmd| cmd.unref();
commands.deinit(alloc);

View File

@@ -4,6 +4,7 @@ const Allocator = std.mem.Allocator;
const gio = @import("gio");
const glib = @import("glib");
const gobject = @import("gobject");
const global = @import("../../../global.zig");
const Binding = @import("../../../input.zig").Binding;
const key = @import("../key.zig");
@@ -630,6 +631,9 @@ fn generateToken(buf: *Token) [:0]const u8 {
return std.fmt.bufPrintZ(
buf,
"ghostty_{x:0<7}",
.{std.crypto.random.int(u28)},
.{rand_int: {
const rng_impl: std.Random.IoSource = .{ .io = global.io() };
break :rand_int rng_impl.interface().int(u28);
}},
) catch unreachable;
}

View File

@@ -7,6 +7,7 @@ const adw = @import("adw");
const gdk = @import("gdk");
const gobject = @import("gobject");
const gtk = @import("gtk");
const global = @import("../../../global.zig");
const input = @import("../../../input.zig");
const gresource = @import("../build/gresource.zig");
@@ -61,13 +62,13 @@ pub const ImguiWidget = extern struct {
ig_context: ?*cimgui.c.ImGuiContext = null,
/// Our previous instant used to calculate delta time for animations.
instant: ?std.time.Instant = null,
instant: ?std.Io.Timestamp = null,
/// Tick callback ID for timed updates.
tick_callback_id: c_uint = 0,
/// Last render time for throttling to 30 FPS.
last_render_time: ?std.time.Instant = null,
last_render_time: ?std.Io.Timestamp = null,
pub var offset: c_int = 0;
};
@@ -140,10 +141,11 @@ pub const ImguiWidget = extern struct {
const priv = self.private();
const io: *cimgui.c.ImGuiIO = cimgui.c.ImGui_GetIO();
const now: std.Io.Timestamp = .now(global.io(), .awake);
// Determine our delta time
const now = std.time.Instant.now() catch unreachable;
io.DeltaTime = if (priv.instant) |prev| delta: {
const since_ns: f64 = @floatFromInt(now.since(prev));
const since_ns: f64 = @floatFromInt(prev.durationTo(now).nanoseconds);
const ns_per_s: f64 = @floatFromInt(std.time.ns_per_s);
const since_s: f32 = @floatCast(since_ns / ns_per_s);
break :delta @max(0.00001, since_s);
@@ -298,7 +300,7 @@ pub const ImguiWidget = extern struct {
// Update last render time for tick callback throttling.
const priv = self.private();
priv.last_render_time = std.time.Instant.now() catch null;
priv.last_render_time = .now(global.io(), .awake);
// Setup our frame. We render twice because some ImGui behaviors
// take multiple renders to process. I don't know how to make this
@@ -457,15 +459,10 @@ pub const ImguiWidget = extern struct {
const self: *Self = gobject.ext.cast(Self, widget) orelse return 0;
const priv = self.private();
const now = std.time.Instant.now() catch {
self.queueRender();
return 1;
};
// Throttle to 30 FPS (~33ms between frames)
const frame_time_ns: u64 = std.time.ns_per_s / 30;
const should_render = if (priv.last_render_time) |last|
now.since(last) >= frame_time_ns
last.untilNow(global.io(), .awake).nanoseconds >= frame_time_ns
else
true;

View File

@@ -36,6 +36,7 @@ const Window = @import("window.zig").Window;
const InspectorWindow = @import("inspector_window.zig").InspectorWindow;
const i18n = @import("../../../os/i18n.zig");
const media = @import("../media.zig");
const global = @import("../../../global.zig");
const log = std.log.scoped(.gtk_ghostty_surface);
@@ -1589,32 +1590,32 @@ pub const Surface = extern struct {
return self.private().cursor_pos;
}
pub fn defaultTermioEnv(self: *Self) !std.process.EnvMap {
pub fn defaultTermioEnv(self: *Self) !std.process.Environ.Map {
const app = Application.default();
const alloc = app.allocator();
var env = try internal_os.getEnvMap(alloc);
var env = try global.environMap();
errdefer env.deinit();
if (app.savedLanguage()) |language| {
try env.put("LANG", language);
} else {
env.remove("LANG");
_ = env.orderedRemove("LANG");
}
// Don't leak these GTK environment variables to child processes.
env.remove("GDK_DEBUG");
env.remove("GDK_DISABLE");
env.remove("GSK_RENDERER");
_ = env.orderedRemove("GDK_DEBUG");
_ = env.orderedRemove("GDK_DISABLE");
_ = env.orderedRemove("GSK_RENDERER");
// Remove some environment variables that are set when Ghostty is launched
// from a `.desktop` file, by D-Bus activation, or systemd.
env.remove("GIO_LAUNCHED_DESKTOP_FILE");
env.remove("GIO_LAUNCHED_DESKTOP_FILE_PID");
env.remove("DBUS_STARTER_ADDRESS");
env.remove("DBUS_STARTER_BUS_TYPE");
env.remove("INVOCATION_ID");
env.remove("JOURNAL_STREAM");
env.remove("NOTIFY_SOCKET");
_ = env.orderedRemove("GIO_LAUNCHED_DESKTOP_FILE");
_ = env.orderedRemove("GIO_LAUNCHED_DESKTOP_FILE_PID");
_ = env.orderedRemove("DBUS_STARTER_ADDRESS");
_ = env.orderedRemove("DBUS_STARTER_BUS_TYPE");
_ = env.orderedRemove("INVOCATION_ID");
_ = env.orderedRemove("JOURNAL_STREAM");
_ = env.orderedRemove("NOTIFY_SOCKET");
// Unset environment varies set by snaps if we're running in a snap.
// This allows Ghostty to further launch additional snaps.
@@ -1641,7 +1642,7 @@ pub const Surface = extern struct {
}
/// Filter out environment variables that start with forbidden prefixes.
fn filterSnapPaths(gpa: std.mem.Allocator, env_map: *std.process.EnvMap) !void {
fn filterSnapPaths(gpa: std.mem.Allocator, env_map: *std.process.Environ.Map) !void {
comptime assert(build_config.snap);
const snap_vars = [_][]const u8{
@@ -1708,7 +1709,7 @@ pub const Surface = extern struct {
item.key,
item.value,
);
for (env_to_remove.items) |key| _ = env_map.remove(key);
for (env_to_remove.items) |key| _ = env_map.orderedRemove(key);
}
pub fn clipboardRequest(

View File

@@ -176,7 +176,11 @@ test "adding actions to an object" {
_ = addAsGroup(gtk.Box, box, "test", &actions);
}
const expected = std.crypto.random.intRangeAtMost(i32, 1, std.math.maxInt(u31));
const expected = expected: {
const rng_impl: std.Random.IoSource = .{ .io = testing.io };
const rng = rng_impl.interface();
break :expected rng.intRangeAtMost(i32, 1, std.math.maxInt(u31));
};
const parameter = glib.Variant.newInt32(expected);
try testing.expect(box.as(gtk.Widget).activateActionVariant("test.test", parameter) != 0);

View File

@@ -3,18 +3,16 @@ const std = @import("std");
// Until the gobject bindings are built at the same time we are building
// Ghostty, we need to import `gtk/gtk.h` directly to ensure that the version
// macros match the version of `gtk4` that we are building/linking against.
const c = @cImport({
@cInclude("gtk/gtk.h");
});
const gtk_c = @import("gtk_c");
const gtk = @import("gtk");
const log = std.log.scoped(.gtk);
pub const comptime_version: std.SemanticVersion = .{
.major = c.GTK_MAJOR_VERSION,
.minor = c.GTK_MINOR_VERSION,
.patch = c.GTK_MICRO_VERSION,
.major = gtk_c.GTK_MAJOR_VERSION,
.minor = gtk_c.GTK_MINOR_VERSION,
.patch = gtk_c.GTK_MICRO_VERSION,
};
pub fn getRuntimeVersion() std.SemanticVersion {
@@ -105,17 +103,17 @@ test "atLeast" {
const funs = &.{ atLeast, runtimeAtLeast };
inline for (funs) |fun| {
try testing.expect(fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION));
try testing.expect(fun(gtk_c.GTK_MAJOR_VERSION, gtk_c.GTK_MINOR_VERSION, gtk_c.GTK_MICRO_VERSION));
try testing.expect(!fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION + 1));
try testing.expect(!fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION + 1, c.GTK_MICRO_VERSION));
try testing.expect(!fun(c.GTK_MAJOR_VERSION + 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION));
try testing.expect(!fun(gtk_c.GTK_MAJOR_VERSION, gtk_c.GTK_MINOR_VERSION, gtk_c.GTK_MICRO_VERSION + 1));
try testing.expect(!fun(gtk_c.GTK_MAJOR_VERSION, gtk_c.GTK_MINOR_VERSION + 1, gtk_c.GTK_MICRO_VERSION));
try testing.expect(!fun(gtk_c.GTK_MAJOR_VERSION + 1, gtk_c.GTK_MINOR_VERSION, gtk_c.GTK_MICRO_VERSION));
try testing.expect(fun(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION));
try testing.expect(fun(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION + 1, c.GTK_MICRO_VERSION));
try testing.expect(fun(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION + 1));
try testing.expect(fun(gtk_c.GTK_MAJOR_VERSION - 1, gtk_c.GTK_MINOR_VERSION, gtk_c.GTK_MICRO_VERSION));
try testing.expect(fun(gtk_c.GTK_MAJOR_VERSION - 1, gtk_c.GTK_MINOR_VERSION + 1, gtk_c.GTK_MICRO_VERSION));
try testing.expect(fun(gtk_c.GTK_MAJOR_VERSION - 1, gtk_c.GTK_MINOR_VERSION, gtk_c.GTK_MICRO_VERSION + 1));
try testing.expect(fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION - 1, c.GTK_MICRO_VERSION + 1));
try testing.expect(fun(gtk_c.GTK_MAJOR_VERSION, gtk_c.GTK_MINOR_VERSION - 1, gtk_c.GTK_MICRO_VERSION + 1));
}
}
@@ -125,16 +123,16 @@ test "runtimeUntil" {
// This is an array in case we add a comptime variant.
const funs = &.{runtimeUntil};
inline for (funs) |fun| {
try testing.expect(!fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION));
try testing.expect(!fun(gtk_c.GTK_MAJOR_VERSION, gtk_c.GTK_MINOR_VERSION, gtk_c.GTK_MICRO_VERSION));
try testing.expect(fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION + 1));
try testing.expect(fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION + 1, c.GTK_MICRO_VERSION));
try testing.expect(fun(c.GTK_MAJOR_VERSION + 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION));
try testing.expect(fun(gtk_c.GTK_MAJOR_VERSION, gtk_c.GTK_MINOR_VERSION, gtk_c.GTK_MICRO_VERSION + 1));
try testing.expect(fun(gtk_c.GTK_MAJOR_VERSION, gtk_c.GTK_MINOR_VERSION + 1, gtk_c.GTK_MICRO_VERSION));
try testing.expect(fun(gtk_c.GTK_MAJOR_VERSION + 1, gtk_c.GTK_MINOR_VERSION, gtk_c.GTK_MICRO_VERSION));
try testing.expect(!fun(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION));
try testing.expect(!fun(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION + 1, c.GTK_MICRO_VERSION));
try testing.expect(!fun(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION + 1));
try testing.expect(!fun(gtk_c.GTK_MAJOR_VERSION - 1, gtk_c.GTK_MINOR_VERSION, gtk_c.GTK_MICRO_VERSION));
try testing.expect(!fun(gtk_c.GTK_MAJOR_VERSION - 1, gtk_c.GTK_MINOR_VERSION + 1, gtk_c.GTK_MICRO_VERSION));
try testing.expect(!fun(gtk_c.GTK_MAJOR_VERSION - 1, gtk_c.GTK_MINOR_VERSION, gtk_c.GTK_MICRO_VERSION + 1));
try testing.expect(!fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION - 1, c.GTK_MICRO_VERSION + 1));
try testing.expect(!fun(gtk_c.GTK_MAJOR_VERSION, gtk_c.GTK_MINOR_VERSION - 1, gtk_c.GTK_MICRO_VERSION + 1));
}
}

View File

@@ -6,6 +6,7 @@ const Allocator = std.mem.Allocator;
const gio = @import("gio");
const glib = @import("glib");
const global = @import("../../../global.zig");
const apprt = @import("../../../apprt.zig");
const ApprtApp = @import("../App.zig");
@@ -28,10 +29,12 @@ payload_builder: *glib.VariantBuilder,
/// Used to build the parameters for the IPC.
parameters_builder: *glib.VariantBuilder,
pub const InitError = Allocator.Error || std.Io.Writer.Error || apprt.ipc.Errors;
/// Initialize the helper.
pub fn init(alloc: Allocator, target: apprt.ipc.Target, action: [:0]const u8) (Allocator.Error || std.Io.Writer.Error || apprt.ipc.Errors)!Self {
pub fn init(alloc: Allocator, target: apprt.ipc.Target, action: [:0]const u8) InitError!Self {
var buf: [256]u8 = undefined;
var stderr_writer = std.fs.File.stderr().writer(&buf);
var stderr_writer = std.Io.File.stderr().writer(global.io(), &buf);
const stderr = &stderr_writer.interface;
// Get the appropriate bus name and object path for contacting the
@@ -133,7 +136,7 @@ pub fn addParameter(self: *Self, variant: *glib.Variant) void {
/// should be done with this object other than call `deinit`.
pub fn send(self: *Self) (std.Io.Writer.Error || apprt.ipc.Errors)!void {
var buf: [256]u8 = undefined;
var stderr_writer = std.fs.File.stderr().writer(&buf);
var stderr_writer = std.Io.File.stderr().writer(global.io(), &buf);
const stderr = &stderr_writer.interface;
// finish building the parameters

View File

@@ -7,10 +7,15 @@ const gio = @import("gio");
const glib = @import("glib");
const gobject = @import("gobject");
const gtk = @import("gtk");
const global = @import("../../global.zig");
pub fn fromFilename(path: [:0]const u8) ?*gtk.MediaFile {
assert(std.fs.path.isAbsolute(path));
std.fs.accessAbsolute(path, .{ .mode = .read_only }) catch |err| {
std.Io.Dir.accessAbsolute(
global.io(),
path,
.{ .read = true },
) catch |err| {
log.warn("unable to access {s}: {t}", .{ path, err });
return null;
};

View File

@@ -1,6 +1,7 @@
const std = @import("std");
const gio = @import("gio");
const global = @import("../../global.zig");
const Allocator = std.mem.Allocator;
@@ -11,7 +12,8 @@ const token_format = std.fmt.comptimePrint("{{x:0>{}}}", .{token_hex_len});
/// Generate a token suitable for use in requests to the XDG Desktop Portal
pub fn generateToken() usize {
return std.crypto.random.int(usize);
const rng_impl: std.Random.IoSource = .{ .io = global.io() };
return rng_impl.interface().int(usize);
}
/// Format a request token consistently for use in portal object paths and payloads.

View File

@@ -9,6 +9,7 @@ const assert = std.debug.assert;
const gio = @import("gio");
const glib = @import("glib");
const gobject = @import("gobject");
const global = @import("../../../global.zig");
const App = @import("../App.zig");
const portal = @import("../portal.zig");
@@ -23,7 +24,7 @@ app: *App,
dbus: ?*gio.DBusConnection = null,
/// Mutex to protect modification of the entries map or the cleanup timer.
mutex: std.Thread.Mutex = .{},
mutex: std.Io.Mutex = .init,
/// Map to store data about any in-flight calls to the portal.
entries: std.AutoArrayHashMapUnmanaged(usize, *Entry) = .empty,
@@ -78,7 +79,7 @@ const RequestData = struct {
/// Data about any in-flight calls to the portal.
pub const Entry = struct {
/// When the request started.
start: std.time.Instant,
start: std.Io.Timestamp,
/// A token used by the portal to identify requests and responses. The
/// actual format of the token does not really matter as long as it can be
/// used as part of a D-Bus object path. `usize` was chosen since it's easy
@@ -123,8 +124,8 @@ pub fn setDbusConnection(self: *OpenURI, dbus: ?*gio.DBusConnection) void {
pub fn deinit(self: *OpenURI) void {
const alloc = self.app.app.allocator();
self.mutex.lock();
defer self.mutex.unlock();
self.mutex.lockUncancelable(global.io());
defer self.mutex.unlock(global.io());
if (!self.alive) return;
self.alive = false;
@@ -157,8 +158,8 @@ pub fn start(self: *OpenURI, value: apprt.action.OpenUrl) (Allocator.Error || Er
alloc.destroy(request);
}
self.mutex.lock();
defer self.mutex.unlock();
self.mutex.lockUncancelable(global.io());
defer self.mutex.unlock(global.io());
// Create an entry that is used to track the results of the D-Bus method
// call.
@@ -166,7 +167,7 @@ pub fn start(self: *OpenURI, value: apprt.action.OpenUrl) (Allocator.Error || Er
const entry = try alloc.create(Entry);
errdefer alloc.destroy(entry);
entry.* = .{
.start = std.time.Instant.now() catch return error.TimerUnavailable,
.start = std.Io.Timestamp.now(global.io(), .awake),
.token = token,
.kind = value.kind,
.uri = try alloc.dupeZ(u8, value.url),
@@ -235,8 +236,8 @@ fn destroyEntry(alloc: Allocator, entry: *Entry) void {
}
fn failRequest(self: *OpenURI, token: usize) ?*Entry {
self.mutex.lock();
defer self.mutex.unlock();
self.mutex.lockUncancelable(global.io());
defer self.mutex.unlock(global.io());
if (!self.alive) return null;
@@ -422,8 +423,8 @@ fn requestCallback(
return;
}
self.mutex.lock();
defer self.mutex.unlock();
self.mutex.lockUncancelable(global.io());
defer self.mutex.unlock(global.io());
if (!self.alive) return;
@@ -461,8 +462,8 @@ fn responseReceived(
return;
};
self.mutex.lock();
defer self.mutex.unlock();
self.mutex.lockUncancelable(global.io());
defer self.mutex.unlock(global.io());
if (!self.alive) return;
@@ -534,22 +535,18 @@ fn cleanup(ud: ?*anyopaque) callconv(.c) c_int {
const alloc = self.app.app.allocator();
self.mutex.lock();
defer self.mutex.unlock();
self.mutex.lockUncancelable(global.io());
defer self.mutex.unlock(global.io());
self.cleanup_timer = null;
if (!self.alive) return @intFromBool(glib.SOURCE_REMOVE);
const now = std.time.Instant.now() catch {
// `now()` should never fail, but if it does, don't crash, just return.
// This might cause a small memory leak in rare circumstances but it
// should get cleaned up the next time a URL is clicked.
return @intFromBool(glib.SOURCE_REMOVE);
};
loop: while (true) {
for (self.entries.entries.items(.value)) |entry| {
if (now.since(entry.start) > cleanup_timeout * std.time.ns_per_s) {
if (entry.start.untilNow(
global.io(),
.awake,
).toSeconds() > cleanup_timeout) {
log.warn("open uri request timed out token={x}", .{entry.token});
self.unsubscribeFromResponse(entry);
_ = self.entries.swapRemove(entry.token);

View File

@@ -2,6 +2,7 @@ const std = @import("std");
const gio = @import("gio");
const glib = @import("glib");
const global = @import("../../global.zig");
const log = std.log.scoped(.gtk_post_fork);
@@ -85,12 +86,10 @@ pub fn postFork(cmd: *Command) Command.PostForkError!void {
return;
};
const start = std.time.Instant.now() catch unreachable;
const start: std.Io.Timestamp = .now(global.io(), .awake);
loop: while (true) {
const now = std.time.Instant.now() catch unreachable;
if (now.since(start) > 250 * std.time.ns_per_ms) {
if (start.untilNow(global.io(), .awake).toMilliseconds() > 250) {
if (cmd.rt_pre_exec_info.linux_cgroup_hard_fail) {
log.err("transition to new transient systemd scope {s} took too long", .{expected_cgroup});
return error.PostForkError;
@@ -116,6 +115,6 @@ pub fn postFork(cmd: *Command) Command.PostForkError!void {
}
}
std.Thread.sleep(25 * std.time.ns_per_ms);
std.Io.sleep(global.io(), .fromMilliseconds(25), .awake) catch unreachable;
}
}

View File

@@ -3,6 +3,7 @@ const std = @import("std");
const log = std.log.scoped(.gtk_pre_exec);
const configpkg = @import("../../config.zig");
const global = @import("../../global.zig");
const internal_os = @import("../../os/main.zig");
const Command = @import("../../Command.zig");
@@ -47,12 +48,10 @@ pub fn preExec(cmd: *Command) ?u8 {
var expected_cgroup_buf: [256]u8 = undefined;
const expected_cgroup = cgroup.fmtScope(&expected_cgroup_buf, pid);
const start = std.time.Instant.now() catch unreachable;
const start: std.Io.Timestamp = .now(global.io(), .awake);
while (true) {
const now = std.time.Instant.now() catch unreachable;
if (now.since(start) > 250 * std.time.ns_per_ms) {
if (start.untilNow(global.io(), .awake).toMilliseconds() > 250) {
if (cmd.rt_pre_exec_info.linux_cgroup_hard_fail) {
log.err("transition to new transient systemd scope took too long", .{});
return 127;
@@ -74,7 +73,7 @@ pub fn preExec(cmd: *Command) ?u8 {
if (std.mem.eql(u8, current_cgroup, expected_cgroup)) return null;
}
std.Thread.sleep(25 * std.time.ns_per_ms);
std.Io.sleep(global.io(), .fromMilliseconds(25), .awake) catch unreachable;
}
return null;

View File

@@ -141,7 +141,7 @@ pub const Window = union(Protocol) {
};
}
pub fn addSubprocessEnv(self: *Window, env: *std.process.EnvMap) !void {
pub fn addSubprocessEnv(self: *Window, env: *std.process.Environ.Map) !void {
switch (self.*) {
inline else => |*v| try v.addSubprocessEnv(env),
}

View File

@@ -67,7 +67,7 @@ pub const Window = struct {
return true;
}
pub fn addSubprocessEnv(_: *Window, _: *std.process.EnvMap) !void {}
pub fn addSubprocessEnv(_: *Window, _: *std.process.Environ.Map) !void {}
pub fn setUrgent(_: *Window, _: bool) !void {}
};

View File

@@ -225,7 +225,7 @@ pub const Window = struct {
};
}
pub fn addSubprocessEnv(self: *Window, env: *std.process.EnvMap) !void {
pub fn addSubprocessEnv(self: *Window, env: *std.process.Environ.Map) !void {
_ = self;
_ = env;
}

View File

@@ -317,7 +317,7 @@ pub const Window = struct {
self.last_applied_decoration_hints = hints;
}
pub fn addSubprocessEnv(self: *Window, env: *std.process.EnvMap) !void {
pub fn addSubprocessEnv(self: *Window, env: *std.process.Environ.Map) !void {
var buf: [64]u8 = undefined;
const window_id = try std.fmt.bufPrint(
&buf,

View File

@@ -126,8 +126,12 @@ pub const Action = union(enum) {
/// Sync with: ghostty_ipc_action_u
pub const CValue = cvalue: {
const key_fields = @typeInfo(Key).@"enum".fields;
var union_fields: [key_fields.len]std.builtin.Type.UnionField = undefined;
for (key_fields, 0..) |field, i| {
var names: [key_fields.len][]const u8 = undefined;
var types: [key_fields.len]type = undefined;
var attrs: [key_fields.len]std.builtin.Type.UnionField.Attributes = undefined;
for (key_fields, &names, &types, &attrs) |field, *name, *ty, *attr| {
const action = @unionInit(Action, field.name, undefined);
const Type = t: {
const Type = @TypeOf(@field(action, field.name));
@@ -135,20 +139,12 @@ pub const Action = union(enum) {
if (Type != void and @hasDecl(Type, "C")) break :t Type.C;
break :t Type;
};
union_fields[i] = .{
.name = field.name,
.type = Type,
.alignment = @alignOf(Type),
};
name.* = field.name;
ty.* = Type;
attr.* = .{ .@"align" = @alignOf(Type) };
}
break :cvalue @Type(.{ .@"union" = .{
.layout = .@"extern",
.tag_type = null,
.fields = &union_fields,
.decls = &.{},
} });
break :cvalue @Union(.@"extern", null, &names, &types, &attrs);
};
/// Sync with: ghostty_ipc_action_s

View File

@@ -12,6 +12,7 @@ const Allocator = std.mem.Allocator;
const terminalpkg = @import("../terminal/main.zig");
const Benchmark = @import("Benchmark.zig");
const options = @import("options.zig");
const global = @import("../global.zig");
const log = std.log.scoped(.@"apc-parser-bench");
@@ -19,7 +20,7 @@ opts: Options,
stream: Stream,
/// The file, opened in the setup function.
data_f: ?std.fs.File = null,
data_f: ?std.Io.File = null,
pub const Options = struct {
/// The data to read as a filepath. If this is "-" then
@@ -103,7 +104,7 @@ fn setup(ptr: *anyopaque) Benchmark.Error!void {
fn teardown(ptr: *anyopaque) void {
const self: *ApcParser = @ptrCast(@alignCast(ptr));
if (self.data_f) |f| {
f.close();
f.close(global.io());
self.data_f = null;
}
}
@@ -114,7 +115,7 @@ fn step(ptr: *anyopaque) Benchmark.Error!void {
const f = self.data_f orelse return;
var read_buf: [64 * 1024]u8 align(std.atomic.cache_line) = undefined;
var f_reader = f.reader(&read_buf);
var f_reader = f.reader(global.io(), &read_buf);
const r = &f_reader.interface;
// This buffer size matches the read buffer size used by the

View File

@@ -6,6 +6,7 @@ const builtin = @import("builtin");
const assert = std.debug.assert;
const macos = @import("macos");
const build_config = @import("../build_config.zig");
const global = @import("../global.zig");
ptr: *anyopaque,
vtable: VTable,
@@ -64,21 +65,23 @@ pub fn run(
signpost.log.release();
};
const start = std.time.Instant.now() catch return error.BenchmarkFailed;
const start: std.Io.Timestamp = .now(global.io(), .awake);
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 now: std.Io.Timestamp = .now(global.io(), .awake);
const elapsed = start.durationTo(now).nanoseconds;
assert(elapsed >= 0);
const exit = switch (mode) {
.once => true,
.duration => |ns| now.since(start) >= ns,
.duration => |ns| elapsed >= ns,
};
if (exit) {
result.duration = now.since(start);
result.duration = @as(u64, @intCast(std.math.clamp(elapsed, 0, std.math.maxInt(u64))));
return result;
}
}

View File

@@ -1,6 +1,6 @@
const std = @import("std");
const cli = @import("cli.zig");
const state = &@import("../global.zig").state;
const global = @import("../global.zig");
const log = std.log.scoped(.benchmark);
@@ -19,7 +19,7 @@ export fn ghostty_benchmark_cli(
};
cli.mainAction(
state.alloc,
global.alloc(),
action,
.{ .string = std.mem.sliceTo(args, 0) },
) catch |err| {

View File

@@ -14,13 +14,14 @@ 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 global = @import("../global.zig");
const log = std.log.scoped(.@"terminal-stream-bench");
opts: Options,
/// The file, opened in the setup function.
data_f: ?std.fs.File = null,
data_f: ?std.Io.File = null,
pub const Options = struct {
/// The type of codepoint width calculation to use.
@@ -93,7 +94,7 @@ fn setup(ptr: *anyopaque) Benchmark.Error!void {
fn teardown(ptr: *anyopaque) void {
const self: *CodepointWidth = @ptrCast(@alignCast(ptr));
if (self.data_f) |f| {
f.close();
f.close(global.io());
self.data_f = null;
}
}
@@ -114,7 +115,7 @@ fn stepWcwidth(ptr: *anyopaque) Benchmark.Error!void {
const f = self.data_f orelse return;
var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
var f_reader = f.reader(&read_buf);
var f_reader = f.reader(global.io(), &read_buf);
var r = &f_reader.interface;
var d: UTF8Decoder = .{};
@@ -141,7 +142,7 @@ fn stepTable(ptr: *anyopaque) Benchmark.Error!void {
const f = self.data_f orelse return;
var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
var f_reader = f.reader(&read_buf);
var f_reader = f.reader(global.io(), &read_buf);
var r = &f_reader.interface;
var d: UTF8Decoder = .{};
@@ -173,7 +174,7 @@ fn stepSimd(ptr: *anyopaque) Benchmark.Error!void {
const f = self.data_f orelse return;
var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
var f_reader = f.reader(&read_buf);
var f_reader = f.reader(global.io(), &read_buf);
var r = &f_reader.interface;
var d: UTF8Decoder = .{};

View File

@@ -11,13 +11,14 @@ const options = @import("options.zig");
const UTF8Decoder = @import("../terminal/UTF8Decoder.zig");
const unicode = @import("../unicode/main.zig");
const uucode = @import("uucode");
const global = @import("../global.zig");
const log = std.log.scoped(.@"terminal-stream-bench");
opts: Options,
/// The file, opened in the setup function.
data_f: ?std.fs.File = null,
data_f: ?std.Io.File = null,
pub const Options = struct {
/// The type of codepoint width calculation to use.
@@ -82,7 +83,7 @@ fn setup(ptr: *anyopaque) Benchmark.Error!void {
fn teardown(ptr: *anyopaque) void {
const self: *GraphemeBreak = @ptrCast(@alignCast(ptr));
if (self.data_f) |f| {
f.close();
f.close(global.io());
self.data_f = null;
}
}
@@ -92,7 +93,7 @@ fn stepNoop(ptr: *anyopaque) Benchmark.Error!void {
const f = self.data_f orelse return;
var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
var f_reader = f.reader(&read_buf);
var f_reader = f.reader(global.io(), &read_buf);
var r = &f_reader.interface;
var d: UTF8Decoder = .{};
@@ -115,7 +116,7 @@ fn stepTable(ptr: *anyopaque) Benchmark.Error!void {
const f = self.data_f orelse return;
var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
var f_reader = f.reader(&read_buf);
var f_reader = f.reader(global.io(), &read_buf);
var r = &f_reader.interface;
var d: UTF8Decoder = .{};

View File

@@ -11,13 +11,14 @@ const options = @import("options.zig");
const UTF8Decoder = @import("../terminal/UTF8Decoder.zig");
const uucode = @import("uucode");
const symbols_table = @import("../unicode/symbols_table.zig").table;
const global = @import("../global.zig");
const log = std.log.scoped(.@"is-symbol-bench");
opts: Options,
/// The file, opened in the setup function.
data_f: ?std.fs.File = null,
data_f: ?std.Io.File = null,
pub const Options = struct {
/// Which test to run.
@@ -80,7 +81,7 @@ fn setup(ptr: *anyopaque) Benchmark.Error!void {
fn teardown(ptr: *anyopaque) void {
const self: *IsSymbol = @ptrCast(@alignCast(ptr));
if (self.data_f) |f| {
f.close();
f.close(global.io());
self.data_f = null;
}
}
@@ -90,7 +91,7 @@ fn stepUucode(ptr: *anyopaque) Benchmark.Error!void {
const f = self.data_f orelse return;
var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
var f_reader = f.reader(&read_buf);
var f_reader = f.reader(global.io(), &read_buf);
var r = &f_reader.interface;
var d: UTF8Decoder = .{};
@@ -117,7 +118,7 @@ fn stepTable(ptr: *anyopaque) Benchmark.Error!void {
const f = self.data_f orelse return;
var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
var f_reader = f.reader(&read_buf);
var f_reader = f.reader(global.io(), &read_buf);
var r = &f_reader.interface;
var d: UTF8Decoder = .{};

View File

@@ -9,11 +9,12 @@ const Benchmark = @import("Benchmark.zig");
const options = @import("options.zig");
const Parser = @import("../terminal/osc.zig").Parser;
const log = std.log.scoped(.@"osc-parser-bench");
const global = @import("../global.zig");
opts: Options,
/// The file, opened in the setup function.
data_f: ?std.fs.File = null,
data_f: ?std.Io.File = null,
parser: Parser,
@@ -70,7 +71,7 @@ fn setup(ptr: *anyopaque) Benchmark.Error!void {
fn teardown(ptr: *anyopaque) void {
const self: *OscParser = @ptrCast(@alignCast(ptr));
if (self.data_f) |f| {
f.close();
f.close(global.io());
self.data_f = null;
}
}
@@ -80,7 +81,7 @@ fn step(ptr: *anyopaque) Benchmark.Error!void {
const f = self.data_f orelse return;
var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
var r = f.reader(&read_buf);
var r = f.reader(global.io(), &read_buf);
var osc_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
while (true) {

View File

@@ -67,6 +67,8 @@ const options = @import("options.zig");
const compress = @import("../terminal/compress.zig");
const CompressedPage = compress.Page;
const lz4 = compress.lz4;
const compat_file = @import("../lib/compat/file.zig");
const global = @import("../global.zig");
const log = std.log.scoped(.@"page-compression-bench");
@@ -210,9 +212,9 @@ fn setupData(self: *PageCompression) !void {
return error.InvalidRetainedPages;
const data_file = try options.dataFile(self.opts.data) orelse return;
defer data_file.close();
defer data_file.close(global.io());
self.data = try data_file.readToEndAlloc(self.alloc, max_data_size);
self.data = try compat_file.readToEndAlloc(data_file, self.alloc, max_data_size);
errdefer {
self.alloc.free(self.data);
self.data = &.{};

View File

@@ -11,6 +11,7 @@ const terminalpkg = @import("../terminal/main.zig");
const Benchmark = @import("Benchmark.zig");
const options = @import("options.zig");
const Terminal = terminalpkg.Terminal;
const global = @import("../global.zig");
const log = std.log.scoped(.@"terminal-stream-bench");
@@ -79,7 +80,7 @@ pub fn create(
ptr.* = .{
.opts = opts,
.terminal = try .init(alloc, .{
.terminal = try .init(global.io(), alloc, .{
.rows = opts.@"terminal-rows",
.cols = opts.@"terminal-cols",
}),
@@ -122,7 +123,7 @@ fn setup(ptr: *anyopaque) Benchmark.Error!void {
s.nextSlice("hello");
// Setup our terminal state
const data_f: std.fs.File = (options.dataFile(
const data_f: std.Io.File = (options.dataFile(
self.opts.data,
) catch |err| {
log.warn("error opening data file err={}", .{err});
@@ -133,7 +134,7 @@ fn setup(ptr: *anyopaque) Benchmark.Error!void {
defer stream.deinit();
var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
var f_reader = data_f.reader(&read_buf);
var f_reader = data_f.reader(global.io(), &read_buf);
const r = &f_reader.interface;
var buf: [4096]u8 = undefined;
@@ -171,6 +172,7 @@ fn stepClone(ptr: *anyopaque) Benchmark.Error!void {
for (0..1000) |_| {
const s: *terminalpkg.Screen = self.terminal.screens.active;
const copy = s.clone(
s.io,
s.alloc,
.{ .viewport = .{} },
null,

View File

@@ -74,6 +74,7 @@ const Benchmark = @import("Benchmark.zig");
const options = @import("options.zig");
const PageList = terminalpkg.PageList;
const Terminal = terminalpkg.Terminal;
const global = @import("../global.zig");
const log = std.log.scoped(.@"scrollback-compression-bench");
@@ -134,7 +135,7 @@ pub fn create(
ptr.* = .{
.opts = opts,
.terminal = try .init(alloc, .{
.terminal = try .init(global.io(), alloc, .{
.rows = opts.@"terminal-rows",
.cols = opts.@"terminal-cols",
.max_scrollback = opts.@"max-scrollback",
@@ -182,13 +183,13 @@ fn setup(ptr: *anyopaque) Benchmark.Error!void {
/// terminal-stream benchmark. Parser and file IO costs remain in setup.
fn loadCorpus(self: *ScrollbackCompression) !void {
const data_file = try options.dataFile(self.opts.data) orelse return;
defer data_file.close();
defer data_file.close(global.io());
var stream = self.terminal.vtStream();
defer stream.deinit();
var read_buf: [64 * 1024]u8 align(std.atomic.cache_line) = undefined;
var file_reader = data_file.reader(&read_buf);
var file_reader = data_file.reader(global.io(), &read_buf);
const reader = &file_reader.interface;
var buf: [64 * 1024]u8 = undefined;

View File

@@ -7,13 +7,14 @@ const Allocator = std.mem.Allocator;
const terminalpkg = @import("../terminal/main.zig");
const Benchmark = @import("Benchmark.zig");
const options = @import("options.zig");
const global = @import("../global.zig");
const log = std.log.scoped(.@"terminal-stream-bench");
opts: Options,
/// The file, opened in the setup function.
data_f: ?std.fs.File = null,
data_f: ?std.Io.File = null,
pub const Options = struct {
/// The data to read as a filepath. If this is "-" then
@@ -61,7 +62,7 @@ fn setup(ptr: *anyopaque) Benchmark.Error!void {
fn teardown(ptr: *anyopaque) void {
const self: *TerminalParser = @ptrCast(@alignCast(ptr));
if (self.data_f) |f| {
f.close();
f.close(global.io());
self.data_f = null;
}
}
@@ -76,7 +77,7 @@ fn step(ptr: *anyopaque) Benchmark.Error!void {
// aren't currently IO bound.
const f = self.data_f orelse return;
var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
var f_reader = f.reader(&read_buf);
var f_reader = f.reader(global.io(), &read_buf);
var r = &f_reader.interface;
var p: terminalpkg.Parser = .init();

View File

@@ -19,6 +19,7 @@ const Benchmark = @import("Benchmark.zig");
const options = @import("options.zig");
const Terminal = terminalpkg.Terminal;
const Stream = terminalpkg.TerminalStream;
const global = @import("../global.zig");
const log = std.log.scoped(.@"terminal-stream-bench");
@@ -27,7 +28,7 @@ terminal: Terminal,
stream: Stream,
/// The file, opened in the setup function.
data_f: ?std.fs.File = null,
data_f: ?std.Io.File = null,
pub const Options = struct {
/// The size of the terminal. This affects benchmarking when
@@ -54,7 +55,7 @@ pub fn create(
ptr.* = .{
.opts = opts,
.terminal = try .init(alloc, .{
.terminal = try .init(global.io(), alloc, .{
.rows = opts.@"terminal-rows",
.cols = opts.@"terminal-cols",
}),
@@ -97,7 +98,7 @@ fn setup(ptr: *anyopaque) Benchmark.Error!void {
fn teardown(ptr: *anyopaque) void {
const self: *TerminalStream = @ptrCast(@alignCast(ptr));
if (self.data_f) |f| {
f.close();
f.close(global.io());
self.data_f = null;
}
}
@@ -113,7 +114,7 @@ fn step(ptr: *anyopaque) Benchmark.Error!void {
const f = self.data_f orelse return;
var read_buf: [64 * 1024]u8 align(std.atomic.cache_line) = undefined;
var f_reader = f.reader(&read_buf);
var f_reader = f.reader(global.io(), &read_buf);
const r = &f_reader.interface;
// This buffer size matches the read buffer size used by the

View File

@@ -1,6 +1,7 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const cli = @import("../cli.zig");
const global = @import("../global.zig");
/// The available actions for the CLI. This is the list of available
/// benchmarks. View docs for each individual one in the predictably
@@ -44,19 +45,20 @@ pub const Action = enum {
};
/// An entrypoint for the benchmark CLI.
pub fn main() !void {
pub fn main(init: std.process.Init) !void {
try global.init(.{ .main = init });
const alloc = std.heap.c_allocator;
const action_ = try cli.action.detectArgs(Action, alloc);
const action_ = try cli.action.detectArgs(Action, alloc, init.minimal.args);
const action = action_ orelse return error.NoAction;
try mainAction(alloc, action, .cli);
try mainAction(alloc, action, .{ .cli = init.minimal.args });
}
/// Arguments that can be passed to the benchmark.
pub const Args = union(enum) {
/// The arguments passed to the CLI via argc/argv.
cli,
cli: std.process.Args,
/// Simple string arguments, parsed via std.process.ArgIteratorGeneral.
/// Simple string arguments, parsed via ArgIteratorGeneral.
string: []const u8,
};
@@ -83,13 +85,13 @@ fn mainActionImpl(
var opts: Options = .{};
defer if (@hasDecl(Options, "deinit")) opts.deinit();
switch (args) {
.cli => {
var iter = try cli.args.argsIterator(alloc);
.cli => |process_args| {
var iter = try cli.args.argsIterator(alloc, process_args);
defer iter.deinit();
try cli.args.parse(Options, alloc, &opts, &iter);
},
.string => |str| {
var iter = try std.process.ArgIteratorGeneral(.{}).init(
var iter = try std.process.Args.IteratorGeneral(.{}).init(
alloc,
str,
);

View File

@@ -1,20 +1,21 @@
//! This file contains helpers for CLI options.
const std = @import("std");
const global = @import("../global.zig");
/// 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 {
pub fn dataFile(path_: ?[]const u8) !?std.Io.File {
const path = path_ orelse return null;
// Stdin
if (std.mem.eql(u8, path, "-")) return .stdin();
// Normal file
const file = try std.fs.cwd().openFile(path, .{});
errdefer file.close();
const file = try std.Io.Dir.cwd().openFile(global.io(), path, .{});
errdefer file.close(global.io());
return file;
}

View File

@@ -43,7 +43,7 @@ lib_version: std.SemanticVersion = .{ .major = 0, .minor = 0, .patch = 0 },
/// Binary properties
pie: bool = false,
strip: bool = false,
patch_rpath: ?[]const u8 = null,
patchelf: ?PatchElf = null,
/// Artifacts
flatpak: bool = false,
@@ -67,7 +67,7 @@ emit_unicode_table_gen: bool = false,
is_dep: bool = false,
/// Environmental properties
env: std.process.EnvMap,
env: *const std.process.Environ.Map,
pub fn init(b: *std.Build, appVersion: []const u8, libVersion: []const u8) !Config {
// Setup our standard Zig target and optimize options, i.e.
@@ -124,10 +124,10 @@ pub fn init(b: *std.Build, appVersion: []const u8, libVersion: []const u8) !Conf
// defaults.
const gtk_targets = gtk.targets(b);
// We use env vars throughout the build so we grab them immediately here.
var env = try std.process.getEnvMap(b.allocator);
errdefer env.deinit();
// Grab the environment from build state
const env = &b.graph.environ_map;
// We use env vars throughout the build so we grab them immediately here.
var config: Config = .{
.optimize = optimize,
.target = target,
@@ -312,24 +312,38 @@ pub fn init(b: *std.Build, appVersion: []const u8, libVersion: []const u8) !Conf
//---------------------------------------------------------------
// Binary Properties
// On NixOS, the built binary from `zig build` needs to patch the rpath
// into the built binary for it to be portable across the NixOS system
// it was built for. We default this to true if we can detect we're in
// a Nix shell and have LD_LIBRARY_PATH set.
config.patch_rpath = b.option(
[]const u8,
"patch-rpath",
"Inject the LD_LIBRARY_PATH as the rpath in the built binary. " ++
"This defaults to LD_LIBRARY_PATH if we're in a Nix shell environment on NixOS.",
) orelse patch_rpath: {
// We only do the patching if we're targeting our own CPU and its Linux.
if (!(target.result.os.tag == .linux) or !target.query.isNativeCpu()) break :patch_rpath null;
// If we're in a nix shell we default to doing this.
// Note: we purposely never deinit envmap because we leak the strings
if (env.get("IN_NIX_SHELL") == null) break :patch_rpath null;
break :patch_rpath env.get("LD_LIBRARY_PATH");
};
// On NixOS, the built binary from `zig build` needs to patch the interp
// and rpath into the built binary for it to be portable across the NixOS
// system it was built for. We default this to true if we can detect we're
// in a Nix shell.
//
// Note that this option is only available on Linux and if building
// natively.
//
// NOTE: Zig does now have an option to pass in the dynamic linker
// (-Ddynamic-linker), but it does seem to have some issues in 0.16.0 that
// may be fixed in 0.17.0. We may want to revisit this afterwards; although
// I'm not too sure if that helps to clean up rpath, this may just be the
// better option. See https://codeberg.org/ziglang/zig/issues/31760.
if ((target.result.os.tag == .linux) and target.query.isNativeCpu()) {
const in_nix_shell = env.get("IN_NIX_SHELL") != null;
if (b.option(
bool,
"patchelf",
"Patch interpreter and rpath in the built binary (default if IN_NIX_SHELL is set)",
) orelse in_nix_shell) {
var patchelf: PatchElf = .{};
if (b.findProgram(&.{"ld.so"}, &.{})) |ld_so| {
patchelf.interp = std.Io.Dir.realPathFileAbsoluteAlloc(b.graph.io, ld_so, b.allocator) catch null;
} else |_| {}
if (env.get("LD_LIBRARY_PATH")) |ld_library_path| {
patchelf.rpath = if (ld_library_path.len > 0) ld_library_path else null;
}
if (patchelf.interp != null or patchelf.rpath != null) {
config.patchelf = patchelf;
}
}
}
config.pie = b.option(
bool,
@@ -401,7 +415,7 @@ pub fn init(b: *std.Build, appVersion: []const u8, libVersion: []const u8) !Conf
if (system_package) break :emit_docs true;
// We only default to true if we can find pandoc.
const path = expandPath(b.allocator, "pandoc") catch
const path = expandPath(b.graph.io, b.allocator, &b.graph.environ_map, "pandoc") catch
break :emit_docs false;
defer if (path) |p| b.allocator.free(p);
break :emit_docs path != null;
@@ -450,7 +464,7 @@ pub fn init(b: *std.Build, appVersion: []const u8, libVersion: []const u8) !Conf
if (config.emit_lib_vt) {
// In lib-vt mode default to whether xcodebuild is available,
// since xcodebuild is required to produce the XCFramework.
const path = expandPath(b.allocator, "xcodebuild") catch
const path = expandPath(b.graph.io, b.allocator, &b.graph.environ_map, "xcodebuild") catch
break :emit_xcfw false;
defer if (path) |p| b.allocator.free(p);
break :emit_xcfw path != null;
@@ -522,6 +536,27 @@ pub fn init(b: *std.Build, appVersion: []const u8, libVersion: []const u8) !Conf
return config;
}
const PatchElf = struct {
interp: ?[]const u8 = null,
rpath: ?[]const u8 = null,
};
/// Add a patchelf step for the supplied `artifact`, depending on the supplied
/// `step`, if `patchelf` is present in the config.
pub fn addPatchElf(self: *const Config, artifact: *std.Build.Step.Compile, step: *std.Build.Step) void {
const b = artifact.step.owner;
std.debug.assert(b == step.owner);
if (self.patchelf) |patchelf| {
const run = std.Build.Step.Run.create(b, "patchelf");
run.addArgs(&.{"patchelf"});
if (patchelf.interp) |interp| run.addArgs(&.{ "--set-interpreter", interp });
if (patchelf.rpath) |rpath| run.addArgs(&.{ "--set-rpath", rpath });
run.addArtifactArg(artifact);
step.dependOn(&run.step);
}
}
/// Configure the build options with our values.
pub fn addOptions(self: *const Config, step: *std.Build.Step.Options) !void {
// We need to break these down individual because addOption doesn't
@@ -570,7 +605,11 @@ pub fn addOptions(self: *const Config, step: *std.Build.Step.Options) !void {
/// Returns the build options for the terminal module. This assumes a
/// Ghostty executable being built. Callers should modify this as needed.
pub fn terminalOptions(self: *const Config, artifact: TerminalBuildOptions.Artifact) TerminalBuildOptions {
pub fn terminalOptions(
self: *const Config,
artifact: TerminalBuildOptions.Artifact,
optimize: std.builtin.OptimizeMode,
) TerminalBuildOptions {
return .{
.artifact = artifact,
.simd = self.simd,
@@ -580,7 +619,7 @@ pub fn terminalOptions(self: *const Config, artifact: TerminalBuildOptions.Artif
.ghostty => self.version,
.lib => self.lib_version,
},
.slow_runtime_safety = switch (self.optimize) {
.slow_runtime_safety = switch (optimize) {
.Debug => true,
.ReleaseSafe,
.ReleaseSmall,
@@ -591,7 +630,7 @@ pub fn terminalOptions(self: *const Config, artifact: TerminalBuildOptions.Artif
}
/// Returns a baseline CPU target retaining all the other CPU configs.
pub fn baselineTarget(self: *const Config) std.Build.ResolvedTarget {
pub fn baselineTarget(self: *const Config, io: std.Io) std.Build.ResolvedTarget {
// Set our cpu model as baseline. There may need to be other modifications
// we need to make such as resetting CPU features but for now this works.
var q = self.target.query;
@@ -601,7 +640,7 @@ pub fn baselineTarget(self: *const Config) std.Build.ResolvedTarget {
// handle the native case.
return .{
.query = q,
.result = std.zig.system.resolveTargetQuery(q) catch
.result = std.zig.system.resolveTargetQuery(io, q) catch
@panic("unable to resolve baseline query"),
};
}

View File

@@ -23,9 +23,9 @@ pub fn init(
// We always want our datagen to be fast because it
// takes awhile to run.
.optimize = .ReleaseFast,
.link_libc = true,
}),
});
exe.linkLibC();
_ = try deps.add(exe);
try steps.append(b.allocator, exe);
}
@@ -39,9 +39,9 @@ pub fn init(
.target = deps.config.target,
// We always want our benchmarks to be in release mode.
.optimize = .ReleaseFast,
.link_libc = true,
}),
});
exe.linkLibC();
_ = try deps.add(exe);
try steps.append(b.allocator, exe);
}

View File

@@ -237,11 +237,11 @@ pub const Resource = struct {
/// Returns true if the dist path exists at build time.
pub fn exists(self: *const Resource, b: *std.Build) bool {
if (b.build_root.handle.access(self.dist, .{})) {
if (b.build_root.handle.access(b.graph.io, self.dist, .{})) {
// If we have a ".git" directory then we're a git checkout
// and we never want to use the dist path. This shouldn't happen
// so show a warning to the user.
if (b.build_root.handle.access(".git", .{})) {
if (b.build_root.handle.access(b.graph.io, ".git", .{})) {
std.log.warn(
"dist resource '{s}' should not be in a git checkout",
.{self.dist},

View File

@@ -50,7 +50,7 @@ pub fn init(
generate_markdown.root_module.addOptions("build_options", generate_markdown_options);
const generate_markdown_step = b.addRunArtifact(generate_markdown);
const markdown_output = generate_markdown_step.captureStdOut();
const markdown_output = generate_markdown_step.captureStdOut(.{});
try steps.append(b.allocator, &b.addInstallFile(
markdown_output,
@@ -68,7 +68,7 @@ pub fn init(
generate_html.addFileArg(markdown_output);
try steps.append(b.allocator, &b.addInstallFile(
generate_html.captureStdOut(),
generate_html.captureStdOut(.{}),
"share/ghostty/doc/" ++ manpage.name ++ "." ++ manpage.section ++ ".html",
).step);
@@ -83,7 +83,7 @@ pub fn init(
generate_manpage.addFileArg(markdown_output);
try steps.append(b.allocator, &b.addInstallFile(
generate_manpage.captureStdOut(),
generate_manpage.captureStdOut(.{}),
"share/man/man" ++ manpage.section ++ "/" ++ manpage.name ++ "." ++ manpage.section,
).step);
}

View File

@@ -36,21 +36,14 @@ pub fn init(b: *std.Build, cfg: *const Config, deps: *const SharedDeps) !Ghostty
// Check for possible issues
try checkNixShell(exe, cfg);
// Patch our rpath if that option is specified.
if (cfg.patch_rpath) |rpath| {
if (rpath.len > 0) {
const run = std.Build.Step.Run.create(b, "patchelf rpath");
run.addArgs(&.{ "patchelf", "--set-rpath", rpath });
run.addArtifactArg(exe);
install_step.step.dependOn(&run.step);
}
}
// Add patchelf step if that option is specified.
cfg.addPatchElf(exe, &install_step.step);
// OS-specific
switch (cfg.target.result.os.tag) {
.windows => {
exe.subsystem = .Windows;
exe.addWin32ResourceFile(.{
exe.root_module.addWin32ResourceFile(.{
.file = b.path("dist/windows/ghostty.rc"),
});
},
@@ -85,7 +78,7 @@ fn checkNixShell(exe: *std.Build.Step.Compile, cfg: *const Config) !void {
if (!cfg.target.query.isNativeOs()) return;
// Verify we're in NixOS
std.fs.accessAbsolute("/etc/NIXOS", .{}) catch return;
std.Io.Dir.accessAbsolute(exe.step.owner.graph.io, "/etc/NIXOS", .{}) catch return;
// If we're in a nix shell, not a problem
if (cfg.env.get("IN_NIX_SHELL") != null) return;

View File

@@ -40,16 +40,16 @@ pub fn distResources(b: *std.Build) struct {
.name = "framegen",
.root_module = b.createModule(.{
.target = b.graph.host,
.link_libc = true,
}),
});
exe.addCSourceFile(.{
exe.root_module.addCSourceFile(.{
.file = b.path("src/build/framegen/main.c"),
.flags = &.{},
});
exe.linkLibC();
if (b.systemIntegrationOption("zlib", .{})) {
exe.linkSystemLibrary2("zlib", .{
exe.root_module.linkSystemLibrary("zlib", .{
.preferred_link_mode = .dynamic,
.search_strategy = .mode_first,
});
@@ -58,7 +58,7 @@ pub fn distResources(b: *std.Build) struct {
.target = b.graph.host,
.optimize = .ReleaseFast,
})) |zlib_dep| {
exe.linkLibrary(zlib_dep.artifact("z"));
exe.root_module.linkLibrary(zlib_dep.artifact("z"));
}
}

View File

@@ -34,7 +34,7 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyI18n {
msgfmt.addFileArg(b.path("po/" ++ locale ++ ".po"));
try steps.append(b.allocator, &b.addInstallFile(
msgfmt.captureStdOut(),
msgfmt.captureStdOut(.{}),
std.fmt.comptimePrint(
"share/locale/{s}/LC_MESSAGES/{s}.mo",
.{ target_locale, domain },
@@ -103,14 +103,15 @@ fn createUpdateStep(b: *std.Build) !*std.Build.Step {
}
var gtk_dir = try b.build_root.handle.openDir(
b.graph.io,
"src/apprt/gtk",
.{ .iterate = true },
);
defer gtk_dir.close();
defer gtk_dir.close(b.graph.io);
var walk = try gtk_dir.walk(b.allocator);
defer walk.deinit();
while (try walk.next()) |src| {
while (try walk.next(b.graph.io)) |src| {
switch (src.kind) {
.file => if (!std.mem.endsWith(
u8,
@@ -178,15 +179,15 @@ fn createUpdateStep(b: *std.Build) !*std.Build.Step {
xgettext_merge.addFileArg(gtk_pot);
const usf = b.addUpdateSourceFiles();
usf.addCopyFileToSource(
xgettext_merge.captureStdOut(),
xgettext_merge.captureStdOut(.{}),
"po/" ++ domain ++ ".pot",
);
inline for (locales) |locale| {
const msgmerge = b.addSystemCommand(&.{ "msgmerge", "--quiet", "--no-fuzzy-matching" });
msgmerge.addFileArg(b.path("po/" ++ locale ++ ".po"));
msgmerge.addFileArg(xgettext_merge.captureStdOut());
usf.addCopyFileToSource(msgmerge.captureStdOut(), "po/" ++ locale ++ ".po");
msgmerge.addFileArg(xgettext_merge.captureStdOut(.{}));
usf.addCopyFileToSource(msgmerge.captureStdOut(.{}), "po/" ++ locale ++ ".po");
}
return &usf.step;

View File

@@ -29,12 +29,12 @@ pub fn initStatic(
.strip = deps.config.strip,
.omit_frame_pointer = deps.config.strip,
.unwind_tables = if (deps.config.strip) .none else .sync,
.link_libc = true,
}),
// Fails on self-hosted x86_64 on macOS
.use_llvm = true,
});
lib.linkLibC();
// These must be bundled since we're compiling into a static lib.
// Otherwise, you get undefined symbol errors.
@@ -72,6 +72,14 @@ pub fn initShared(
b: *std.Build,
deps: *const SharedDeps,
) !GhosttyLib {
// For dynamic linking, we prefer dynamic linking and to search by
// mode first. Mode first will search all paths for a dynamic library
// before falling back to static.
const dynamic_link_opts: std.Build.Module.LinkSystemLibraryOptions = .{
.preferred_link_mode = .dynamic,
.search_strategy = .mode_first,
};
const lib = b.addLibrary(.{
.name = "ghostty",
.linkage = .dynamic,
@@ -99,12 +107,12 @@ pub fn initShared(
{
// The CRT initialization code in msvcrt.lib calls __vcrt_initialize
// and __acrt_initialize, which are in the static CRT libraries.
lib.linkSystemLibrary("libvcruntime");
lib.root_module.linkSystemLibrary("libvcruntime", dynamic_link_opts);
// ucrt.lib is in the Windows SDK 'ucrt' dir. Detect the SDK
// installation and add the UCRT library path.
const arch = deps.config.target.result.cpu.arch;
const sdk = std.zig.WindowsSdk.find(b.allocator, arch) catch null;
const sdk = std.zig.WindowsSdk.find(b.allocator, b.graph.io, arch, &b.graph.environ_map) catch null;
if (sdk) |s| {
if (s.windows10sdk) |w10| {
const arch_str: []const u8 = switch (arch) {
@@ -120,11 +128,11 @@ pub fn initShared(
) catch null;
if (ucrt_lib_path) |path| {
lib.addLibraryPath(.{ .cwd_relative = path });
lib.root_module.addLibraryPath(.{ .cwd_relative = path });
}
}
}
lib.linkSystemLibrary("libucrt");
lib.root_module.linkSystemLibrary("libucrt", dynamic_link_opts);
}
// Get our debug symbols

View File

@@ -174,7 +174,12 @@ pub fn initStaticAppleUniversal(
.os_tag = p.os_tag,
.os_version_min = Config.osVersionMin(p.os_tag),
};
if (detectAppleSDK(b.resolveTargetQuery(target_query).result)) {
if (detectAppleSDK(
b.graph.io,
b.allocator,
&b.graph.environ_map,
b.resolveTargetQuery(target_query).result,
)) {
const dev_zig = try zig.retarget(b, cfg, deps, b.resolveTargetQuery(target_query));
result.put(p.device, try initStatic(b, &dev_zig));
@@ -438,12 +443,21 @@ pub fn xcframework(
}
/// Returns true if the Apple SDK for the given target is installed.
fn detectAppleSDK(target: std.Target) bool {
_ = std.zig.LibCInstallation.findNative(.{
.allocator = std.heap.page_allocator,
.target = &target,
.verbose = false,
}) catch return false;
fn detectAppleSDK(
io: std.Io,
alloc: std.mem.Allocator,
environ_map: *const std.process.Environ.Map,
target: std.Target,
) bool {
_ = std.zig.LibCInstallation.findNative(
alloc,
io,
.{
.environ_map = environ_map,
.target = &target,
.verbose = false,
},
) catch return false;
return true;
}

View File

@@ -21,9 +21,9 @@ pub fn init(b: *std.Build, cfg: *const Config, deps: *const SharedDeps) !Ghostty
.strip = false,
.omit_frame_pointer = false,
.unwind_tables = .sync,
.link_libc = true,
}),
});
build_data_exe.linkLibC();
deps.help_strings.addImport(build_data_exe);
@@ -39,7 +39,7 @@ pub fn init(b: *std.Build, cfg: *const Config, deps: *const SharedDeps) !Ghostty
const run = b.addRunArtifact(build_data_exe);
run.addArg("+terminfo");
const wf = b.addWriteFiles();
const source = wf.addCopyFile(run.captureStdOut(), "ghostty.terminfo");
const source = wf.addCopyFile(run.captureStdOut(.{}), "ghostty.terminfo");
if (cfg.emit_terminfo) {
const source_install = b.addInstallFile(
@@ -63,8 +63,8 @@ pub fn init(b: *std.Build, cfg: *const Config, deps: *const SharedDeps) !Ghostty
const run_step = RunStep.create(b, "infotocap");
run_step.addArg("infotocap");
run_step.addFileArg(source);
const out_source = run_step.captureStdOut();
_ = run_step.captureStdErr(); // so we don't see stderr
const out_source = run_step.captureStdOut(.{});
_ = run_step.captureStdErr(.{}); // so we don't see stderr
const cap_install = b.addInstallFile(
out_source,
@@ -84,7 +84,7 @@ pub fn init(b: *std.Build, cfg: *const Config, deps: *const SharedDeps) !Ghostty
const path = run_step.addOutputFileArg(terminfo_share_dir);
run_step.addFileArg(source);
_ = run_step.captureStdErr(); // so we don't see stderr
_ = run_step.captureStdErr(.{}); // so we don't see stderr
// Ensure that `share/terminfo` is a directory, otherwise the `cp
// -R` will create a file named `share/terminfo`
@@ -143,7 +143,7 @@ pub fn init(b: *std.Build, cfg: *const Config, deps: *const SharedDeps) !Ghostty
const run = b.addRunArtifact(build_data_exe);
run.addArg("+fish");
const wf = b.addWriteFiles();
_ = wf.addCopyFile(run.captureStdOut(), "ghostty.fish");
_ = wf.addCopyFile(run.captureStdOut(.{}), "ghostty.fish");
const install_step = b.addInstallDirectory(.{
.source_dir = wf.getDirectory(),
@@ -158,7 +158,7 @@ pub fn init(b: *std.Build, cfg: *const Config, deps: *const SharedDeps) !Ghostty
const run = b.addRunArtifact(build_data_exe);
run.addArg("+zsh");
const wf = b.addWriteFiles();
_ = wf.addCopyFile(run.captureStdOut(), "_ghostty");
_ = wf.addCopyFile(run.captureStdOut(.{}), "_ghostty");
const install_step = b.addInstallDirectory(.{
.source_dir = wf.getDirectory(),
@@ -173,7 +173,7 @@ pub fn init(b: *std.Build, cfg: *const Config, deps: *const SharedDeps) !Ghostty
const run = b.addRunArtifact(build_data_exe);
run.addArg("+bash");
const wf = b.addWriteFiles();
_ = wf.addCopyFile(run.captureStdOut(), "ghostty.bash");
_ = wf.addCopyFile(run.captureStdOut(.{}), "ghostty.bash");
const install_step = b.addInstallDirectory(.{
.source_dir = wf.getDirectory(),
@@ -190,22 +190,22 @@ pub fn init(b: *std.Build, cfg: *const Config, deps: *const SharedDeps) !Ghostty
{
const run = b.addRunArtifact(build_data_exe);
run.addArg("+vim-syntax");
_ = wf.addCopyFile(run.captureStdOut(), "syntax/ghostty.vim");
_ = wf.addCopyFile(run.captureStdOut(.{}), "syntax/ghostty.vim");
}
{
const run = b.addRunArtifact(build_data_exe);
run.addArg("+vim-ftdetect");
_ = wf.addCopyFile(run.captureStdOut(), "ftdetect/ghostty.vim");
_ = wf.addCopyFile(run.captureStdOut(.{}), "ftdetect/ghostty.vim");
}
{
const run = b.addRunArtifact(build_data_exe);
run.addArg("+vim-ftplugin");
_ = wf.addCopyFile(run.captureStdOut(), "ftplugin/ghostty.vim");
_ = wf.addCopyFile(run.captureStdOut(.{}), "ftplugin/ghostty.vim");
}
{
const run = b.addRunArtifact(build_data_exe);
run.addArg("+vim-compiler");
_ = wf.addCopyFile(run.captureStdOut(), "compiler/ghostty.vim");
_ = wf.addCopyFile(run.captureStdOut(.{}), "compiler/ghostty.vim");
}
const vim_step = b.addInstallDirectory(.{
@@ -233,7 +233,7 @@ pub fn init(b: *std.Build, cfg: *const Config, deps: *const SharedDeps) !Ghostty
const run = b.addRunArtifact(build_data_exe);
run.addArg("+sublime");
const wf = b.addWriteFiles();
_ = wf.addCopyFile(run.captureStdOut(), "ghostty.sublime-syntax");
_ = wf.addCopyFile(run.captureStdOut(.{}), "ghostty.sublime-syntax");
const install_step = b.addInstallDirectory(.{
.source_dir = wf.getDirectory(),
@@ -358,10 +358,10 @@ fn addLinuxAppResources(
// Template output has a single header line we want to remove.
// We use `tail` to do it since its part of the POSIX standard.
const tail = b.addSystemCommand(&.{ "tail", "-n", "+2" });
tail.setStdIn(.{ .lazy_path = tpl.getOutput() });
tail.setStdIn(.{ .lazy_path = tpl.getOutputFile() });
const copy = b.addInstallFile(
tail.captureStdOut(),
tail.captureStdOut(.{}),
template[1],
);

View File

@@ -40,7 +40,7 @@ pub fn init(
}
const webgen_config_step = b.addRunArtifact(webgen_config);
const webgen_config_out = webgen_config_step.captureStdOut();
const webgen_config_out = webgen_config_step.captureStdOut(.{});
try steps.append(b.allocator, &b.addInstallFile(
webgen_config_out,
@@ -71,7 +71,7 @@ pub fn init(
}
const webgen_actions_step = b.addRunArtifact(webgen_actions);
const webgen_actions_out = webgen_actions_step.captureStdOut();
const webgen_actions_out = webgen_actions_step.captureStdOut(.{});
try steps.append(b.allocator, &b.addInstallFile(
webgen_actions_out,
@@ -102,7 +102,7 @@ pub fn init(
}
const webgen_commands_step = b.addRunArtifact(webgen_commands);
const webgen_commands_out = webgen_commands_step.captureStdOut();
const webgen_commands_out = webgen_commands_step.captureStdOut(.{});
try steps.append(b.allocator, &b.addInstallFile(
webgen_commands_out,

View File

@@ -48,21 +48,21 @@ pub fn init(
},
};
const env = try std.process.getEnvMap(b.allocator);
const env = b.graph.environ_map;
const app_path = b.fmt("macos/build/{s}/Ghostty.app", .{xc_config});
// Our step to build the Ghostty macOS app.
const build = build: {
// External environment variables can mess up xcodebuild, so
// we create a new empty environment.
const env_map = try b.allocator.create(std.process.EnvMap);
const env_map = try b.allocator.create(std.process.Environ.Map);
env_map.* = .init(b.allocator);
if (env.get("PATH")) |v| try env_map.put("PATH", v);
const step = RunStep.create(b, "xcodebuild");
step.has_side_effects = true;
step.cwd = b.path("macos");
step.env_map = env_map;
step.environ_map = env_map;
step.addArgs(&.{
"xcodebuild",
"-target",
@@ -91,14 +91,14 @@ pub fn init(
};
const xctest = xctest: {
const env_map = try b.allocator.create(std.process.EnvMap);
const env_map = try b.allocator.create(std.process.Environ.Map);
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.environ_map = env_map;
step.addArgs(&.{
"xcodebuild",
"test",

View File

@@ -64,7 +64,7 @@ fn initInner(
vt_c_name: []const u8,
) !GhosttyZig {
// Terminal module build options
var vt_options = cfg.terminalOptions(.lib);
var vt_options = cfg.terminalOptions(.lib, cfg.optimize);
vt_options.artifact = .lib;
// We presently don't allow Oniguruma in our Zig module at all.
// We should expose this as a build option in the future so we can
@@ -135,7 +135,7 @@ fn initVt(
deps.unicode_tables.addModuleImport(vt);
// We need uucode for grapheme break support
deps.addUucode(b, vt, cfg.target, cfg.optimize);
vt.addImport("uucode", deps.uucode_mod);
// If SIMD is enabled, add all our SIMD dependencies.
if (cfg.simd) {

View File

@@ -23,7 +23,7 @@ pub fn detect(b: *std.Build) !Version {
const tmp: []u8 = b.runAllowFail(
&[_][]const u8{ "git", "-C", b.build_root.path orelse ".", "rev-parse", "--abbrev-ref", "HEAD" },
&code,
.Ignore,
.ignore,
) catch |err| switch (err) {
error.FileNotFound => return error.GitNotFound,
error.ExitCodeFailure => return error.GitNotRepository,
@@ -44,19 +44,19 @@ pub fn detect(b: *std.Build) !Version {
const output = b.runAllowFail(
&[_][]const u8{ "git", "-C", b.build_root.path orelse ".", "-c", "log.showSignature=false", "log", "--pretty=format:%h", "-n", "1" },
&code,
.Ignore,
.ignore,
) catch |err| switch (err) {
error.FileNotFound => return error.GitNotFound,
else => return err,
};
break :short_hash std.mem.trimRight(u8, output, "\r\n ");
break :short_hash std.mem.trimEnd(u8, output, "\r\n ");
};
const tag = b.runAllowFail(
&[_][]const u8{ "git", "-C", b.build_root.path orelse ".", "describe", "--exact-match", "--tags" },
&code,
.Ignore,
.ignore,
) catch |err| switch (err) {
error.FileNotFound => return error.GitNotFound,
error.ExitCodeFailure => "", // expected
@@ -70,7 +70,7 @@ pub fn detect(b: *std.Build) !Version {
"diff",
"--quiet",
"--exit-code",
}, &code, .Ignore) catch |err| switch (err) {
}, &code, .ignore) catch |err| switch (err) {
error.FileNotFound => return error.GitNotFound,
error.ExitCodeFailure => {}, // expected
else => return err,
@@ -80,7 +80,7 @@ pub fn detect(b: *std.Build) !Version {
return .{
.short_hash = short_hash,
.changes = changes,
.tag = if (tag.len > 0) std.mem.trimRight(u8, tag, "\r\n ") else null,
.branch = std.mem.trimRight(u8, branch, "\r\n "),
.tag = if (tag.len > 0) std.mem.trimEnd(u8, tag, "\r\n ") else null,
.branch = std.mem.trimEnd(u8, branch, "\r\n "),
};
}

View File

@@ -34,7 +34,7 @@ pub fn init(b: *std.Build, cfg: *const Config) !HelpStrings {
// Generated Zig files have to end with .zig
const wf = b.addWriteFiles();
const output = wf.addCopyFile(help_run.captureStdOut(), "helpgen.zig");
const output = wf.addCopyFile(help_run.captureStdOut(.{}), "helpgen.zig");
return .{
.exe = exe,

View File

@@ -9,6 +9,7 @@ const MetallibStep = @import("MetallibStep.zig");
const UnicodeTables = @import("UnicodeTables.zig");
const GhosttyFrameData = @import("GhosttyFrameData.zig");
const DistResource = @import("GhosttyDist.zig").Resource;
const gtk_helpers = @import("gtk.zig");
config: *const Config,
@@ -19,6 +20,43 @@ unicode_tables: UnicodeTables,
framedata: GhosttyFrameData,
uucode_tables: std.Build.LazyPath,
/// Singleton uucode module, instantiated once in `init` and reused
/// everywhere so that ghostty and vaxis share the same compiled tables in
/// each final binary instead of each linking its own copy.
///
/// Sharing one instance is also a hard requirement (not just an
/// optimization) for Zig 0.16's strict module model. `SharedDeps.add` runs
/// many times across different (target, optimize) tuples (macos-aarch64,
/// macos-x86_64, ios-aarch64, Debug + ReleaseFast, etc.), and on each
/// call we have to wire uucode into both the step's root module and into
/// vaxis_mod (because vaxis's `Parser.zig` does `@import("uucode")` and
/// we pass `external_uucode = true` to vaxis's build.zig so vaxis doesn't
/// instantiate its own uucode dep). If those two import bindings ever
/// resolve to *different* `*Module` pointers within a single Compile
/// step's analysis, Zig fails with:
///
/// vaxis/src/Parser.zig: file exists in modules 'uucode' and 'uucode0'
///
/// because all those uucode module instances share the same physical
/// `uucode/src/root.zig` file on disk, and Zig requires every file to belong
/// to exactly one module within a Compile graph.
///
/// The natural way to keep them the same would be to call
/// `b.lazyDependency("uucode", .{ .tables_path, .build_config_path })`
/// from each call site and let Zig's dependency cache deduplicate
/// identical args. That fails because of a bug in Zig's
/// `userLazyPathsAreTheSame` (Build.zig) where the `.src_path` and
/// `.generated` equality checks are inverted: `if (std.mem.eql(...))
/// return false` instead of `if (!std.mem.eql(...)) return false`. The
/// dep cache key therefore always misses whenever any arg is a
/// `b.path(...)` LazyPath, so each call returns a fresh `*Dependency`
/// with a fresh `*Module`. Hoisting the dep into one eager
/// `b.dependency` call here sidesteps the cache entirely.
///
/// This conflict is independent of whether vaxis itself is acquired as a
/// singleton or per-target dep.
uucode_mod: *std.Build.Module,
/// Used to keep track of a list of file sources.
pub const LazyPathList = std.ArrayList(std.Build.LazyPath);
@@ -31,12 +69,20 @@ pub fn init(b: *std.Build, cfg: *const Config) !SharedDeps {
break :blk uucode.namedLazyPath("tables.zig");
};
// Instantiate the singleton uucode module that both ghostty and vaxis
// import. See the doc comment on `uucode_mod`.
const uucode_mod = b.dependency("uucode", .{
.tables_path = uucode_tables,
.build_config_path = b.path("src/build/uucode_config.zig"),
}).module("uucode");
var result: SharedDeps = .{
.config = cfg,
.help_strings = try .init(b, cfg),
.unicode_tables = try .init(b, uucode_tables),
.framedata = try .init(b),
.uucode_tables = uucode_tables,
.uucode_mod = uucode_mod,
// Setup by retarget
.options = undefined,
@@ -133,7 +179,10 @@ pub fn add(
step.root_module.addOptions("build_options", self.options);
// Every exe needs the terminal options
self.config.terminalOptions(.ghostty).add(b, step.root_module);
self.config.terminalOptions(.ghostty, optimize).add(b, step.root_module);
// Every exe needs the uucode module
step.root_module.addImport("uucode", self.uucode_mod);
// C imports for locale constants and functions
{
@@ -143,11 +192,15 @@ pub fn add(
.optimize = optimize,
});
if (target.result.os.tag.isDarwin()) {
const libc = try std.zig.LibCInstallation.findNative(.{
.allocator = b.allocator,
.target = &target.result,
.verbose = false,
});
const libc = try std.zig.LibCInstallation.findNative(
b.allocator,
b.graph.io,
.{
.environ_map = &b.graph.environ_map,
.target = &target.result,
.verbose = false,
},
);
c.addSystemIncludePath(.{ .cwd_relative = libc.sys_include_dir.? });
}
step.root_module.addImport("locale-c", c.createModule());
@@ -166,11 +219,15 @@ pub fn add(
});
switch (target.result.os.tag) {
.macos => {
const libc = try std.zig.LibCInstallation.findNative(.{
.allocator = b.allocator,
.target = &target.result,
.verbose = false,
});
const libc = try std.zig.LibCInstallation.findNative(
b.allocator,
b.graph.io,
.{
.environ_map = &b.graph.environ_map,
.target = &target.result,
.verbose = false,
},
);
c.addSystemIncludePath(.{ .cwd_relative = libc.sys_include_dir.? });
},
else => {},
@@ -194,10 +251,10 @@ pub fn add(
);
if (b.systemIntegrationOption("freetype", .{})) {
step.linkSystemLibrary2("bzip2", dynamic_link_opts);
step.linkSystemLibrary2("freetype2", dynamic_link_opts);
step.root_module.linkSystemLibrary("bzip2", dynamic_link_opts);
step.root_module.linkSystemLibrary("freetype2", dynamic_link_opts);
} else {
step.linkLibrary(freetype_dep.artifact("freetype"));
step.root_module.linkLibrary(freetype_dep.artifact("freetype"));
try static_libs.append(
b.allocator,
freetype_dep.artifact("freetype").getEmittedBin(),
@@ -219,9 +276,9 @@ pub fn add(
harfbuzz_dep.module("harfbuzz"),
);
if (b.systemIntegrationOption("harfbuzz", .{})) {
step.linkSystemLibrary2("harfbuzz", dynamic_link_opts);
step.root_module.linkSystemLibrary("harfbuzz", dynamic_link_opts);
} else {
step.linkLibrary(harfbuzz_dep.artifact("harfbuzz"));
step.root_module.linkLibrary(harfbuzz_dep.artifact("harfbuzz"));
try static_libs.append(
b.allocator,
harfbuzz_dep.artifact("harfbuzz").getEmittedBin(),
@@ -243,9 +300,9 @@ pub fn add(
);
if (b.systemIntegrationOption("fontconfig", .{})) {
step.linkSystemLibrary2("fontconfig", dynamic_link_opts);
step.root_module.linkSystemLibrary("fontconfig", dynamic_link_opts);
} else {
step.linkLibrary(fontconfig_dep.artifact("fontconfig"));
step.root_module.linkLibrary(fontconfig_dep.artifact("fontconfig"));
try static_libs.append(
b.allocator,
fontconfig_dep.artifact("fontconfig").getEmittedBin(),
@@ -263,7 +320,7 @@ pub fn add(
.target = target,
.optimize = optimize,
})) |libpng_dep| {
step.linkLibrary(libpng_dep.artifact("png"));
step.root_module.linkLibrary(libpng_dep.artifact("png"));
try static_libs.append(
b.allocator,
libpng_dep.artifact("png").getEmittedBin(),
@@ -277,7 +334,7 @@ pub fn add(
.target = target,
.optimize = optimize,
})) |zlib_dep| {
step.linkLibrary(zlib_dep.artifact("z"));
step.root_module.linkLibrary(zlib_dep.artifact("z"));
try static_libs.append(
b.allocator,
zlib_dep.artifact("z").getEmittedBin(),
@@ -295,9 +352,9 @@ pub fn add(
oniguruma_dep.module("oniguruma"),
);
if (b.systemIntegrationOption("oniguruma", .{})) {
step.linkSystemLibrary2("oniguruma", dynamic_link_opts);
step.root_module.linkSystemLibrary("oniguruma", dynamic_link_opts);
} else {
step.linkLibrary(oniguruma_dep.artifact("oniguruma"));
step.root_module.linkLibrary(oniguruma_dep.artifact("oniguruma"));
try static_libs.append(
b.allocator,
oniguruma_dep.artifact("oniguruma").getEmittedBin(),
@@ -312,13 +369,13 @@ pub fn add(
})) |glslang_dep| {
step.root_module.addImport("glslang", glslang_dep.module("glslang"));
if (b.systemIntegrationOption("glslang", .{})) {
step.linkSystemLibrary2("glslang", dynamic_link_opts);
step.linkSystemLibrary2(
step.root_module.linkSystemLibrary("glslang", dynamic_link_opts);
step.root_module.linkSystemLibrary(
"glslang-default-resource-limits",
dynamic_link_opts,
);
} else {
step.linkLibrary(glslang_dep.artifact("glslang"));
step.root_module.linkLibrary(glslang_dep.artifact("glslang"));
try static_libs.append(
b.allocator,
glslang_dep.artifact("glslang").getEmittedBin(),
@@ -336,9 +393,9 @@ pub fn add(
spirv_cross_dep.module("spirv_cross"),
);
if (b.systemIntegrationOption("spirv-cross", .{})) {
step.linkSystemLibrary2("spirv-cross-c-shared", dynamic_link_opts);
step.root_module.linkSystemLibrary("spirv-cross-c-shared", dynamic_link_opts);
} else {
step.linkLibrary(spirv_cross_dep.artifact("spirv_cross"));
step.root_module.linkLibrary(spirv_cross_dep.artifact("spirv_cross"));
try static_libs.append(
b.allocator,
spirv_cross_dep.artifact("spirv_cross").getEmittedBin(),
@@ -357,7 +414,7 @@ pub fn add(
"sentry",
sentry_dep.module("sentry"),
);
step.linkLibrary(sentry_dep.artifact("sentry"));
step.root_module.linkLibrary(sentry_dep.artifact("sentry"));
try static_libs.append(
b.allocator,
sentry_dep.artifact("sentry").getEmittedBin(),
@@ -404,18 +461,18 @@ pub fn add(
if (step.rootModuleTarget().os.tag == .linux) {
const triple = try step.rootModuleTarget().linuxTriple(b.allocator);
const path = b.fmt("/usr/lib/{s}", .{triple});
if (std.fs.accessAbsolute(path, .{})) {
step.addLibraryPath(.{ .cwd_relative = path });
if (std.Io.Dir.accessAbsolute(b.graph.io, path, .{})) {
step.root_module.addLibraryPath(.{ .cwd_relative = path });
} else |_| {}
}
// C files
step.linkLibC();
step.addIncludePath(b.path("src/stb"));
step.root_module.link_libc = true;
step.root_module.addIncludePath(b.path("src/stb"));
// Disable ubsan for MSVC: Zig's ubsan runtime cannot be bundled
// on Windows (LNK4229), leaving __ubsan_handle_* unresolved when
// the static archive is consumed by an external linker.
step.addCSourceFiles(.{
step.root_module.addCSourceFiles(.{
.files = &.{"src/stb/stb.c"},
.flags = if (step.rootModuleTarget().abi == .msvc)
&.{ "-fno-sanitize=undefined", "-fno-sanitize-trap=undefined" }
@@ -423,7 +480,7 @@ pub fn add(
&.{},
});
if (step.rootModuleTarget().os.tag == .linux) {
step.addIncludePath(b.path("src/apprt/gtk"));
step.root_module.addIncludePath(b.path("src/apprt/gtk"));
}
// libcpp is required for various dependencies. On MSVC, we must
@@ -433,7 +490,7 @@ pub fn add(
// include directories (already added via linkLibC above) contain
// both C and C++ headers, so linkLibCpp is not needed.
if (step.rootModuleTarget().abi != .msvc) {
step.linkLibCpp();
step.root_module.link_libcpp = true;
}
// We always require the system SDK so that our system headers are available.
@@ -452,8 +509,14 @@ pub fn add(
if (b.lazyDependency("opengl", .{})) |dep| {
step.root_module.addImport("opengl", dep.module("opengl"));
}
if (b.lazyDependency("vaxis", .{})) |dep| {
step.root_module.addImport("vaxis", dep.module("vaxis"));
if (b.lazyDependency("vaxis", .{
.target = target,
.optimize = optimize,
.external_uucode = true,
})) |dep| {
const vaxis = dep.module("vaxis");
step.root_module.addImport("vaxis", vaxis);
vaxis.addImport("uucode", self.uucode_mod);
}
if (b.lazyDependency("wuffs", .{
.target = target,
@@ -473,7 +536,6 @@ pub fn add(
})) |dep| {
step.root_module.addImport("z2d", dep.module("z2d"));
}
self.addUucode(b, step.root_module, target, optimize);
if (b.lazyDependency("zf", .{
.target = target,
.optimize = optimize,
@@ -502,7 +564,7 @@ pub fn add(
"macos",
macos_dep.module("macos"),
);
step.linkLibrary(
step.root_module.linkLibrary(
macos_dep.artifact("macos"),
);
try static_libs.append(
@@ -512,7 +574,7 @@ pub fn add(
}
if (self.config.renderer == .opengl) {
step.linkFramework("OpenGL");
step.root_module.linkFramework("OpenGL", .{});
}
// Apple platforms do not include libc libintl so we bundle it.
@@ -523,7 +585,7 @@ pub fn add(
.target = target,
.optimize = optimize,
})) |libintl_dep| {
step.linkLibrary(libintl_dep.artifact("intl"));
step.root_module.linkLibrary(libintl_dep.artifact("intl"));
try static_libs.append(
b.allocator,
libintl_dep.artifact("intl").getEmittedBin(),
@@ -543,7 +605,7 @@ pub fn add(
.@"backend-opengl3" = !target.result.os.tag.isDarwin(),
})) |dep| {
step.root_module.addImport("dcimgui", dep.module("dcimgui"));
step.linkLibrary(dep.artifact("dcimgui"));
step.root_module.linkLibrary(dep.artifact("dcimgui"));
try static_libs.append(
b.allocator,
dep.artifact("dcimgui").getEmittedBin(),
@@ -592,15 +654,38 @@ pub fn add(
// If we're building an exe then we have additional dependencies.
if (step.kind != .lib) {
// We always statically compile glad
step.addIncludePath(b.path("vendor/glad/include/"));
step.addCSourceFile(.{
step.root_module.addIncludePath(b.path("vendor/glad/include/"));
step.root_module.addCSourceFile(.{
.file = b.path("vendor/glad/src/gl.c"),
.flags = &.{},
});
// When we're targeting flatpak we ALWAYS link GTK so we
// get access to glib for dbus.
if (self.config.flatpak) step.linkSystemLibrary2("gtk4", dynamic_link_opts);
if (self.config.flatpak) {
step.root_module.linkSystemLibrary("gtk4", dynamic_link_opts);
// We need to translate gio headers too
gio_translate: {
// translate-c stuff
const translate_c = b.lazyImport(@import("../../build.zig"), "translate_c") orelse
break :gio_translate;
const translate_c_dep = b.lazyDependency("translate_c", .{}) orelse
break :gio_translate;
const translated: translate_c.Translator = .init(translate_c_dep, .{
.c_source_file = b.addWriteFiles().add("gio_c.h",
\\#include <gio/gio.h>
\\#include <gio/gunixfdlist.h>
),
.target = target,
.optimize = optimize,
.link_system_libs = &.{
.{ .name = "gio-2.0", .options = dynamic_link_opts },
},
});
step.root_module.addImport("gio_c", translated.mod);
}
}
switch (self.config.app_runtime) {
.none => {},
@@ -643,12 +728,44 @@ fn addGtkNg(
step.root_module.addImport(name, gobject.module(module));
}
}
gtk_adw_translate: {
// translate-c stuff
const translate_c = b.lazyImport(@import("../../build.zig"), "translate_c") orelse break :gtk_adw_translate;
const translate_c_dep = b.lazyDependency("translate_c", .{}) orelse break :gtk_adw_translate;
const Translator = translate_c.Translator;
step.linkSystemLibrary2("gtk4", dynamic_link_opts);
step.linkSystemLibrary2("libadwaita-1", dynamic_link_opts);
{
// GTK headers
const translated: Translator = .init(translate_c_dep, .{
.c_source_file = b.addWriteFiles().add("gtk_c.h",
\\#include <gtk/gtk.h>
),
.target = target,
.optimize = optimize,
.link_system_libs = &.{
.{ .name = "gtk4", .options = dynamic_link_opts },
},
});
step.root_module.addImport("gtk_c", translated.mod);
}
{
// Adwaita headers
const translated: Translator = .init(translate_c_dep, .{
.c_source_file = b.addWriteFiles().add("adw_c.h",
\\#include <adwaita.h>
),
.target = target,
.optimize = optimize,
.link_system_libs = &.{
.{ .name = "libadwaita-1", .options = dynamic_link_opts },
},
});
step.root_module.addImport("adw_c", translated.mod);
}
}
if (self.config.x11) {
step.linkSystemLibrary2("X11", dynamic_link_opts);
step.root_module.linkSystemLibrary("X11", dynamic_link_opts);
if (gobject_) |gobject| {
step.root_module.addImport(
"gdk_x11",
@@ -732,24 +849,39 @@ fn addGtkNg(
// IMPORTANT: gtk4-layer-shell must be linked BEFORE
// wayland-client, as it relies on shimming libwayland's APIs.
if (b.systemIntegrationOption("gtk4-layer-shell", .{})) {
step.linkSystemLibrary2("gtk4-layer-shell-0", dynamic_link_opts);
step.root_module.linkSystemLibrary("gtk4-layer-shell-0", dynamic_link_opts);
} else {
// gtk4-layer-shell *must* be dynamically linked,
// so we don't add it as a static library
const shared_lib = gtk4_layer_shell.artifact("gtk4-layer-shell");
b.installArtifact(shared_lib);
step.linkLibrary(shared_lib);
step.root_module.linkLibrary(shared_lib);
}
}
step.linkSystemLibrary2("wayland-client", dynamic_link_opts);
step.root_module.linkSystemLibrary("wayland-client", dynamic_link_opts);
}
{
ghostty_resources_translate: {
// Get our gresource c/h files and add them to our build.
const dist = gtkNgDistResources(b);
step.addCSourceFile(.{ .file = dist.resources_c.path(b), .flags = &.{} });
step.addIncludePath(dist.resources_h.path(b).dirname());
const translate_c = b.lazyImport(@import("../../build.zig"), "translate_c") orelse
break :ghostty_resources_translate;
const translate_c_dep = b.lazyDependency("translate_c", .{}) orelse
break :ghostty_resources_translate;
const translated: translate_c.Translator = .init(translate_c_dep, .{
.c_source_file = b.addWriteFiles().add("c.h",
\\#include <ghostty_resources.h>
),
.target = target,
.optimize = optimize,
.link_system_libs = &.{
.{ .name = "glib-2.0", .options = dynamic_link_opts },
},
});
translated.addIncludePath(dist.resources_h.path(b).dirname());
translated.mod.addCSourceFile(.{ .file = dist.resources_c.path(b), .flags = &.{} });
step.root_module.addImport("ghostty_gtk_resources", translated.mod);
}
}
@@ -890,11 +1022,25 @@ pub fn gtkNgDistResources(
.root_module = b.createModule(.{
.root_source_file = b.path("src/apprt/gtk/build/blueprint.zig"),
.target = b.graph.host,
.link_libc = true,
}),
});
blueprint_exe.linkLibC();
blueprint_exe.linkSystemLibrary2("gtk4", dynamic_link_opts);
blueprint_exe.linkSystemLibrary2("libadwaita-1", dynamic_link_opts);
adw_translate: {
// Adwaita headers
const translate_c = b.lazyImport(@import("../../build.zig"), "translate_c") orelse break :adw_translate;
const translate_c_dep = b.lazyDependency("translate_c", .{}) orelse break :adw_translate;
const translated: translate_c.Translator = .init(translate_c_dep, .{
.c_source_file = b.addWriteFiles().add("adw_c.h",
\\#include <adwaita.h>
),
.target = b.graph.host,
.optimize = .Debug,
.link_system_libs = &.{
.{ .name = "libadwaita-1", .options = dynamic_link_opts },
},
});
blueprint_exe.root_module.addImport("adw_c", translated.mod);
}
for (gresource.blueprints) |bp| {
const blueprint_run = b.addRunArtifact(blueprint_exe);
@@ -923,7 +1069,7 @@ pub fn gtkNgDistResources(
xml_run.addFileArg(ui_file);
}
break :gresource_xml xml_run.captureStdOut();
break :gresource_xml xml_run.captureStdOut(.{});
};
const generate_c = b.addSystemCommand(&.{
@@ -964,23 +1110,6 @@ pub fn gtkNgDistResources(
};
}
pub fn addUucode(
self: *const SharedDeps,
b: *std.Build,
module: *std.Build.Module,
target: std.Build.ResolvedTarget,
optimize: std.builtin.OptimizeMode,
) void {
if (b.lazyDependency("uucode", .{
.target = target,
.optimize = optimize,
.tables_path = self.uucode_tables,
.build_config_path = b.path("src/build/uucode_config.zig"),
})) |dep| {
module.addImport("uucode", dep.module("uucode"));
}
}
// For dynamic linking, we prefer dynamic linking and to search by
// mode first. Mode first will search all paths for a dynamic library
// before falling back to static.

View File

@@ -54,8 +54,8 @@ pub fn init(b: *std.Build, uucode_tables: std.Build.LazyPath) !UnicodeTables {
// Generated Zig files have to end with .zig
const wf = b.addWriteFiles();
const props_output = wf.addCopyFile(props_run.captureStdOut(), "props.zig");
const symbols_output = wf.addCopyFile(symbols_run.captureStdOut(), "symbols.zig");
const props_output = wf.addCopyFile(props_run.captureStdOut(.{}), "props.zig");
const symbols_output = wf.addCopyFile(symbols_run.captureStdOut(.{}), "symbols.zig");
return .{
.props_exe = props_exe,

View File

@@ -63,8 +63,8 @@ pub fn create(b: *std.Build, opts: Options) *XCFrameworkStep {
run.addArg("-output");
run.addArg(opts.out_path);
run.expectExitCode(0);
_ = run.captureStdOut();
_ = run.captureStdErr();
_ = run.captureStdOut(.{});
_ = run.captureStdErr(.{});
break :run run;
};
run_create.step.dependOn(&run_delete.step);

View File

@@ -11,11 +11,10 @@
const std = @import("std");
pub fn main() !void {
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
const alloc = gpa.allocator();
pub fn main(init: std.process.Init) !void {
const alloc = init.arena.allocator();
const args = try std.process.argsAlloc(alloc);
const args = try init.minimal.args.toSlice(alloc);
if (args.len < 4) {
std.log.err("usage: combine_archives <zig_exe> <output> <input...>", .{});
std.process.exit(1);
@@ -37,19 +36,20 @@ pub fn main() !void {
}
try script.appendSlice(alloc, "SAVE\nEND\n");
var child: std.process.Child = .init(&.{ zig_exe, "ar", "-M" }, alloc);
child.stdin_behavior = .Pipe;
child.stdout_behavior = .Inherit;
child.stderr_behavior = .Inherit;
var child = try std.process.spawn(init.io, .{
.argv = &.{ zig_exe, "ar", "-M" },
.stdin = .pipe,
.stdout = .inherit,
.stderr = .inherit,
});
try child.spawn();
try child.stdin.?.writeAll(script.items);
child.stdin.?.close();
try child.stdin.?.writeStreamingAll(init.io, script.items);
child.stdin.?.close(init.io);
child.stdin = null;
const term = try child.wait();
if (term.Exited != 0) {
std.log.err("zig ar -M exited with code {d}", .{term.Exited});
const term = try child.wait(init.io);
if (term.exited != 0) {
std.log.err("zig ar -M exited with code {d}", .{term.exited});
std.process.exit(1);
}
}

View File

@@ -14,7 +14,7 @@ pub fn targets(b: *std.Build) Targets {
const output = b.runAllowFail(
&.{ "pkg-config", "--variable=targets", "gtk4" },
&code,
.Ignore,
.ignore,
) catch return .{};
const x11 = std.mem.indexOf(u8, output, "x11") != null;

View File

@@ -1,12 +1,11 @@
const std = @import("std");
const gen = @import("mdgen.zig");
pub fn main() !void {
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
const alloc = gpa.allocator();
pub fn main(init: std.process.Init) !void {
const alloc = init.arena.allocator();
var buffer: [1024]u8 = undefined;
var stdout_writer = std.fs.File.stdout().writer(&buffer);
var stdout_writer = std.Io.File.stdout().writer(init.io, &buffer);
const writer = &stdout_writer.interface;
try gen.substitute(alloc, @embedFile("ghostty_1_header.md"), writer);
try gen.genActions(writer);

View File

@@ -1,12 +1,11 @@
const std = @import("std");
const gen = @import("mdgen.zig");
pub fn main() !void {
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
const alloc = gpa.allocator();
pub fn main(init: std.process.Init) !void {
const alloc = init.arena.allocator();
var buffer: [1024]u8 = undefined;
var stdout_writer = std.fs.File.stdout().writer(&buffer);
var stdout_writer = std.Io.File.stdout().writer(init.io, &buffer);
const writer = &stdout_writer.interface;
try gen.substitute(alloc, @embedFile("ghostty_5_header.md"), writer);
try gen.genConfig(writer, false);

View File

@@ -1,109 +1,138 @@
const std = @import("std");
const assert = std.debug.assert;
const config = @import("config.zig");
const config_x = @import("config.x.zig");
const d = config.default;
const wcwidth = config_x.wcwidth;
const grapheme_break_no_control = config_x.grapheme_break_no_control;
const Allocator = std.mem.Allocator;
fn computeWidth(
alloc: std.mem.Allocator,
cp: u21,
data: anytype,
backing: anytype,
tracking: anytype,
) Allocator.Error!void {
_ = alloc;
_ = cp;
_ = backing;
_ = tracking;
// This condition is needed as Ghostty currently has a singular concept for
// the `width` of a code point, while `uucode` splits the concept into
// `wcwidth_standalone` and `wcwidth_zero_in_grapheme`. The two cases where
// we want to use the `wcwidth_standalone` despite the code point occupying
// zero width in a grapheme (`wcwidth_zero_in_grapheme`) are emoji
// modifiers and prepend code points. For emoji modifiers we want to
// support displaying them in isolation as color patches, and if prepend
// characters were to be width 0 they would disappear from the output with
// Ghostty's current width 0 handling. Future work will take advantage of
// the new uucode `wcwidth_standalone` vs `wcwidth_zero_in_grapheme` split.
if (data.wcwidth_zero_in_grapheme and !data.is_emoji_modifier and data.grapheme_break_no_control != .prepend) {
data.width = 0;
} else {
data.width = @min(2, data.wcwidth_standalone);
}
}
const width = config.Extension{
.inputs = &.{
"wcwidth_standalone",
"wcwidth_zero_in_grapheme",
"is_emoji_modifier",
"grapheme_break_no_control",
pub const fields = &config.mergeFields(config.fields, &.{
.{ .name = "width", .type = u2 },
.{ .name = "is_symbol", .type = bool },
});
pub const build_components = &config.mergeComponents(config.build_components, &.{
.{
.Impl = WidthComponent,
.inputs = &.{
"wcwidth_standalone",
"wcwidth_zero_in_grapheme",
"is_emoji_modifier",
"grapheme_break_no_control",
},
.fields = &.{"width"},
},
.compute = &computeWidth,
.fields = &.{
.{ .name = "width", .type = u2 },
.{
.Impl = IsSymbolComponent,
.inputs = &.{ "block", "general_category" },
.fields = &.{"is_symbol"},
},
};
});
fn computeIsSymbol(
alloc: Allocator,
cp: u21,
data: anytype,
backing: anytype,
tracking: anytype,
) Allocator.Error!void {
_ = alloc;
_ = cp;
_ = backing;
_ = tracking;
const block = data.block;
data.is_symbol = data.general_category == .other_private_use or
block == .arrows or
block == .dingbats or
block == .emoticons or
block == .miscellaneous_symbols or
block == .enclosed_alphanumerics or
block == .enclosed_alphanumeric_supplement or
block == .miscellaneous_symbols_and_pictographs or
block == .transport_and_map_symbols;
}
const is_symbol = config.Extension{
.inputs = &.{ "block", "general_category" },
.compute = &computeIsSymbol,
.fields = &.{
.{ .name = "is_symbol", .type = bool },
},
};
pub const get_components: []const config.Component = &.{};
pub const tables = [_]config.Table{
.{
.name = "runtime",
.extensions = &.{},
.fields = &.{
d.field("is_emoji_presentation"),
d.field("case_folding_full"),
"is_emoji_presentation",
"case_folding_full",
},
},
.{
// Fields that libvaxis needs that aren't included in the `runtime`
// table.
.name = "libvaxis_only",
.fields = &.{
"east_asian_width",
"general_category",
"grapheme_break",
},
},
.{
.name = "buildtime",
.extensions = &.{
wcwidth,
grapheme_break_no_control,
width,
is_symbol,
},
.fields = &.{
width.field("width"),
wcwidth.field("wcwidth_zero_in_grapheme"),
grapheme_break_no_control.field("grapheme_break_no_control"),
is_symbol.field("is_symbol"),
d.field("is_emoji_vs_base"),
"width",
"wcwidth_zero_in_grapheme",
"grapheme_break_no_control",
"is_symbol",
"is_emoji_vs_base",
},
},
};
const WidthComponent = struct {
pub fn build(
comptime InputRow: type,
comptime Row: type,
allocator: std.mem.Allocator,
io: std.Io,
inputs: config.MultiSlice(InputRow),
rows: *config.MultiSlice(Row),
backing: anytype,
tracking: anytype,
) !void {
_ = allocator;
_ = io;
_ = backing;
_ = tracking;
rows.len = config.num_code_points;
const items = rows.items(.width);
const standalone = inputs.items(.wcwidth_standalone);
const zero_in_grapheme = inputs.items(.wcwidth_zero_in_grapheme);
const is_emoji_modifier = inputs.items(.is_emoji_modifier);
const grapheme_break_no_control = inputs.items(.grapheme_break_no_control);
// This condition is needed as Ghostty currently has a singular concept for
// the `width` of a code point, while `uucode` splits the concept into
// `wcwidth_standalone` and `wcwidth_zero_in_grapheme`. The two cases where
// we want to use the `wcwidth_standalone` despite the code point occupying
// zero width in a grapheme (`wcwidth_zero_in_grapheme`) are emoji
// modifiers and prepend code points. For emoji modifiers we want to
// support displaying them in isolation as color patches, and if prepend
// characters were to be width 0 they would disappear from the output with
// Ghostty's current width 0 handling. Future work will take advantage of
// the new uucode `wcwidth_standalone` vs `wcwidth_zero_in_grapheme` split.
for (0..config.num_code_points) |i| {
if (zero_in_grapheme[i] and !is_emoji_modifier[i] and grapheme_break_no_control[i] != .prepend) {
items[i] = 0;
} else {
items[i] = @min(2, standalone[i]);
}
}
}
};
const IsSymbolComponent = struct {
pub fn build(
comptime InputRow: type,
comptime Row: type,
allocator: std.mem.Allocator,
io: std.Io,
inputs: config.MultiSlice(InputRow),
rows: *config.MultiSlice(Row),
backing: anytype,
tracking: anytype,
) !void {
_ = allocator;
_ = io;
_ = backing;
_ = tracking;
rows.len = config.num_code_points;
const items = rows.items(.is_symbol);
const block = inputs.items(.block);
const general_category = inputs.items(.general_category);
for (0..config.num_code_points) |i| {
items[i] =
general_category[i] == .other_private_use or
block[i] == .arrows or
block[i] == .dingbats or
block[i] == .emoticons or
block[i] == .miscellaneous_symbols or
block[i] == .enclosed_alphanumerics or
block[i] == .enclosed_alphanumeric_supplement or
block[i] == .miscellaneous_symbols_and_pictographs or
block[i] == .transport_and_map_symbols;
}
}
};

View File

@@ -12,15 +12,13 @@ const std = @import("std");
const testing = std.testing;
const Allocator = std.mem.Allocator;
pub fn main() !void {
pub fn main(init: std.process.Init) !void {
// This is a one-off patcher, so we leak all our memory on purpose
// and let the OS clean it up when we exit.
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
const alloc = gpa.allocator();
const alloc = init.arena.allocator();
// Parse args: program input output
const args = try std.process.argsAlloc(alloc);
defer std.process.argsFree(alloc, args);
const args = try init.minimal.args.toSlice(alloc);
if (args.len != 3) {
std.log.err("usage: wasm_growable_table <input.wasm> <output.wasm>", .{});
std.process.exit(1);
@@ -30,17 +28,18 @@ pub fn main() !void {
// Patch the file.
const output: []const u8 = try patchTableGrowable(
alloc,
try std.fs.cwd().readFileAlloc(
alloc,
try std.Io.Dir.cwd().readFileAlloc(
init.io,
args[1],
std.math.maxInt(usize),
alloc,
.unlimited,
),
);
// Write our output
const out_file = try std.fs.cwd().createFile(args[2], .{});
defer out_file.close();
try out_file.writeAll(output);
const out_file = try std.Io.Dir.cwd().createFile(init.io, args[2], .{});
defer out_file.close(init.io);
try out_file.writePositionalAll(init.io, output, 0);
}
/// Patch the WASM binary's table section to remove the maximum size

View File

@@ -1,9 +1,9 @@
const std = @import("std");
const helpgen_actions = @import("../../input/helpgen_actions.zig");
pub fn main() !void {
pub fn main(init: std.process.Init) !void {
var buffer: [2048]u8 = undefined;
var stdout_writer = std.fs.File.stdout().writer(&buffer);
var stdout_writer = std.Io.File.stdout().writer(init.io, &buffer);
const stdout = &stdout_writer.interface;
try helpgen_actions.generate(stdout, .markdown, true, std.heap.page_allocator);
}

View File

@@ -2,9 +2,9 @@ const std = @import("std");
const Action = @import("../../cli/ghostty.zig").Action;
const help_strings = @import("help_strings");
pub fn main() !void {
pub fn main(init: std.process.Init) !void {
var buffer: [2048]u8 = undefined;
var stdout_writer = std.fs.File.stdout().writer(&buffer);
var stdout_writer = std.Io.File.stdout().writer(init.io, &buffer);
const stdout = &stdout_writer.interface;
try genActions(stdout);
}

View File

@@ -2,9 +2,9 @@ const std = @import("std");
const Config = @import("../../config/Config.zig");
const help_strings = @import("help_strings");
pub fn main() !void {
pub fn main(init: std.process.Init) !void {
var buffer: [2048]u8 = undefined;
var stdout_writer = std.fs.File.stdout().writer(&buffer);
var stdout_writer = std.Io.File.stdout().writer(init.io, &buffer);
const stdout = &stdout_writer.interface;
try genConfig(stdout);
}

View File

@@ -6,29 +6,31 @@
//! Setting either env var to an empty string disables paging.
//! If stdout is not a TTY, writes go directly to stdout.
const Pager = @This();
const builtin = @import("builtin");
const std = @import("std");
const Allocator = std.mem.Allocator;
const internal_os = @import("../os/main.zig");
const global = @import("../global.zig");
/// The pager child process, if one was spawned.
child: ?std.process.Child = null,
/// The buffered file writer used for both the pager pipe and direct
/// stdout paths.
file_writer: std.fs.File.Writer = undefined,
file_writer: std.Io.File.Writer = undefined,
/// Initialize the pager. If stdout is a TTY, this spawns the pager
/// process. Otherwise, output goes directly to stdout.
pub fn init(alloc: Allocator) Pager {
return .{ .child = initPager(alloc) };
pub fn init() Pager {
return .{ .child = initPager() };
}
/// Writes to the pager process if available; otherwise, stdout.
pub fn writer(self: *Pager, buffer: []u8) *std.Io.Writer {
if (self.child) |child| {
self.file_writer = child.stdin.?.writer(buffer);
self.file_writer = child.stdin.?.writer(global.io(), buffer);
} else {
self.file_writer = std.fs.File.stdout().writer(buffer);
self.file_writer = std.Io.File.stdout().writer(global.io(), buffer);
}
return &self.file_writer.interface;
}
@@ -40,45 +42,44 @@ pub fn deinit(self: *Pager) void {
// pager sees EOF, then wait for it to exit.
self.file_writer.interface.flush() catch {};
if (child.stdin) |stdin| {
stdin.close();
stdin.close(global.io());
child.stdin = null;
}
_ = child.wait() catch {};
_ = child.wait(global.io()) catch {};
}
self.* = undefined;
}
fn initPager(alloc: Allocator) ?std.process.Child {
const stdout_file: std.fs.File = .stdout();
if (!stdout_file.isTty()) return null;
fn initPager() ?std.process.Child {
const stdout_file: std.Io.File = .stdout();
if (!(stdout_file.isTty(global.io()) catch return null)) return null;
var env = global.environMap() catch return null;
defer env.deinit();
// Resolve the pager command: $GHOSTTY_PAGER > $PAGER > `less`.
// An empty value for either env var disables paging.
const ghostty_var = internal_os.getenv(alloc, "GHOSTTY_PAGER") catch null;
defer if (ghostty_var) |v| v.deinit(alloc);
const pager_var = internal_os.getenv(alloc, "PAGER") catch null;
defer if (pager_var) |v| v.deinit(alloc);
const ghostty_var = env.get("GHOSTTY_PAGER");
const pager_var = env.get("PAGER");
const cmd: ?[]const u8 = cmd: {
if (ghostty_var) |v| break :cmd if (v.value.len > 0) v.value else null;
if (pager_var) |v| break :cmd if (v.value.len > 0) v.value else null;
if (ghostty_var) |v| break :cmd if (v.len > 0) v else null;
if (pager_var) |v| break :cmd if (v.len > 0) v else null;
break :cmd "less";
};
if (cmd == null) return null;
var child: std.process.Child = .init(&.{cmd.?}, alloc);
child.stdin_behavior = .Pipe;
child.stdout_behavior = .Inherit;
child.stderr_behavior = .Inherit;
child.spawn() catch return null;
return child;
return std.process.spawn(global.io(), .{
.argv = &.{cmd.?},
.stdin = .pipe,
.stdout = .inherit,
.stderr = .inherit,
}) catch null;
}
test "pager: non-tty" {
var pager: Pager = .init(std.testing.allocator);
var pager: Pager = .init();
defer pager.deinit();
try std.testing.expect(pager.child == null);
}

View File

@@ -11,8 +11,8 @@ pub const DetectError = error{
};
/// Detect the action from CLI args.
pub fn detectArgs(comptime E: type, alloc: Allocator) !?E {
var iter = try std.process.argsWithAllocator(alloc);
pub fn detectArgs(comptime E: type, alloc: Allocator, args: std.process.Args) !?E {
var iter = try args.iterateAllocator(alloc);
defer iter.deinit();
return try detectIter(E, &iter);
}
@@ -83,7 +83,7 @@ test "detect direct match" {
const alloc = testing.allocator;
const Enum = enum { foo, bar, baz };
var iter = try std.process.ArgIteratorGeneral(.{}).init(
var iter = try std.process.Args.IteratorGeneral(.{}).init(
alloc,
"+foo",
);
@@ -97,7 +97,7 @@ test "detect invalid match" {
const alloc = testing.allocator;
const Enum = enum { foo, bar, baz };
var iter = try std.process.ArgIteratorGeneral(.{}).init(
var iter = try std.process.Args.IteratorGeneral(.{}).init(
alloc,
"+invalid",
);
@@ -113,7 +113,7 @@ test "detect multiple actions" {
const alloc = testing.allocator;
const Enum = enum { foo, bar, baz };
var iter = try std.process.ArgIteratorGeneral(.{}).init(
var iter = try std.process.Args.IteratorGeneral(.{}).init(
alloc,
"+foo +bar",
);
@@ -129,7 +129,7 @@ test "detect no match" {
const alloc = testing.allocator;
const Enum = enum { foo, bar, baz };
var iter = try std.process.ArgIteratorGeneral(.{}).init(
var iter = try std.process.Args.IteratorGeneral(.{}).init(
alloc,
"--some-flag",
);
@@ -154,7 +154,7 @@ test "detect special case action" {
};
{
var iter = try std.process.ArgIteratorGeneral(.{}).init(
var iter = try std.process.Args.IteratorGeneral(.{}).init(
alloc,
"--special +bar",
);
@@ -164,7 +164,7 @@ test "detect special case action" {
}
{
var iter = try std.process.ArgIteratorGeneral(.{}).init(
var iter = try std.process.Args.IteratorGeneral(.{}).init(
alloc,
"+bar --special",
);
@@ -174,7 +174,7 @@ test "detect special case action" {
}
{
var iter = try std.process.ArgIteratorGeneral(.{}).init(
var iter = try std.process.Args.IteratorGeneral(.{}).init(
alloc,
"+bar",
);
@@ -200,7 +200,7 @@ test "detect special case fallback" {
};
{
var iter = try std.process.ArgIteratorGeneral(.{}).init(
var iter = try std.process.Args.IteratorGeneral(.{}).init(
alloc,
"--special",
);
@@ -210,7 +210,7 @@ test "detect special case fallback" {
}
{
var iter = try std.process.ArgIteratorGeneral(.{}).init(
var iter = try std.process.Args.IteratorGeneral(.{}).init(
alloc,
"+bar --special",
);
@@ -220,7 +220,7 @@ test "detect special case fallback" {
}
{
var iter = try std.process.ArgIteratorGeneral(.{}).init(
var iter = try std.process.Args.IteratorGeneral(.{}).init(
alloc,
"--special +bar",
);
@@ -246,7 +246,7 @@ test "detect special case abort_if_no_action" {
};
{
var iter = try std.process.ArgIteratorGeneral(.{}).init(
var iter = try std.process.Args.IteratorGeneral(.{}).init(
alloc,
"-e",
);
@@ -256,7 +256,7 @@ test "detect special case abort_if_no_action" {
}
{
var iter = try std.process.ArgIteratorGeneral(.{}).init(
var iter = try std.process.Args.IteratorGeneral(.{}).init(
alloc,
"+foo -e",
);
@@ -266,7 +266,7 @@ test "detect special case abort_if_no_action" {
}
{
var iter = try std.process.ArgIteratorGeneral(.{}).init(
var iter = try std.process.Args.IteratorGeneral(.{}).init(
alloc,
"-e +bar",
);

View File

@@ -489,18 +489,13 @@ pub fn parseTaggedUnion(comptime T: type, alloc: Allocator, v: []const u8) !T {
// We need to create a struct that looks like this union field.
// This lets us use parseIntoField as if its a dedicated struct.
const Target = @Type(.{ .@"struct" = .{
.layout = .auto,
.fields = &.{.{
.name = field.name,
.type = field.type,
.default_value_ptr = null,
.is_comptime = false,
.alignment = @alignOf(field.type),
}},
.decls = &.{},
.is_tuple = false,
} });
const Target = @Struct(
.auto,
null,
&.{field.name},
&.{field.type},
&.{.{ .@"align" = @alignOf(field.type) }},
);
// Parse the value into the struct
var t: Target = undefined;
@@ -677,7 +672,7 @@ test "parse: simple" {
} = .{};
defer if (data._arena) |arena| arena.deinit();
var iter = try std.process.ArgIteratorGeneral(.{}).init(
var iter = try std.process.Args.IteratorGeneral(.{}).init(
testing.allocator,
"--a=42 --b --b-f=false",
);
@@ -689,7 +684,7 @@ test "parse: simple" {
try testing.expect(!data.@"b-f");
// Reparsing works
var iter2 = try std.process.ArgIteratorGeneral(.{}).init(
var iter2 = try std.process.Args.IteratorGeneral(.{}).init(
testing.allocator,
"--a=84",
);
@@ -711,7 +706,7 @@ test "parse: quoted value" {
} = .{};
defer if (data._arena) |arena| arena.deinit();
var iter = try std.process.ArgIteratorGeneral(.{}).init(
var iter = try std.process.Args.IteratorGeneral(.{}).init(
testing.allocator,
"--a=\"42\" --b=\"hello!\"",
);
@@ -731,7 +726,7 @@ test "parse: empty value resets to default" {
} = .{};
defer if (data._arena) |arena| arena.deinit();
var iter = try std.process.ArgIteratorGeneral(.{}).init(
var iter = try std.process.Args.IteratorGeneral(.{}).init(
testing.allocator,
"--a= --b=",
);
@@ -750,7 +745,7 @@ test "parse: positional arguments are invalid" {
} = .{};
defer if (data._arena) |arena| arena.deinit();
var iter = try std.process.ArgIteratorGeneral(.{}).init(
var iter = try std.process.Args.IteratorGeneral(.{}).init(
testing.allocator,
"--a=84 what",
);
@@ -774,7 +769,7 @@ test "parse: diagnostic tracking" {
} = .{};
defer if (data._arena) |arena| arena.deinit();
var iter = try std.process.ArgIteratorGeneral(.{}).init(
var iter = try std.process.Args.IteratorGeneral(.{}).init(
testing.allocator,
"--what --a=42",
);
@@ -858,7 +853,7 @@ test "parse: compatibility handler" {
} = .{};
defer if (data._arena) |arena| arena.deinit();
var iter = try std.process.ArgIteratorGeneral(.{}).init(
var iter = try std.process.Args.IteratorGeneral(.{}).init(
testing.allocator,
"--a=yuh",
);
@@ -884,7 +879,7 @@ test "parse: compatibility renamed" {
} = .{};
defer if (data._arena) |arena| arena.deinit();
var iter = try std.process.ArgIteratorGeneral(.{}).init(
var iter = try std.process.Args.IteratorGeneral(.{}).init(
testing.allocator,
"--old=true --b=true",
);
@@ -1363,8 +1358,11 @@ pub fn ArgsIterator(comptime Iterator: type) type {
}
/// Create an args iterator for the process args. This will skip argv0.
pub fn argsIterator(alloc_gpa: Allocator) internal_os.args.ArgIterator.InitError!ArgsIterator(internal_os.args.ArgIterator) {
var iter = try internal_os.args.iterator(alloc_gpa);
pub fn argsIterator(
alloc_gpa: Allocator,
args: std.process.Args,
) std.process.Args.Iterator.InitError!ArgsIterator(std.process.Args.Iterator) {
var iter: std.process.Args.Iterator = try .initAllocator(args, alloc_gpa);
errdefer iter.deinit();
_ = iter.next(); // skip argv0
return .{ .iterator = iter };
@@ -1373,7 +1371,7 @@ pub fn argsIterator(alloc_gpa: Allocator) internal_os.args.ArgIterator.InitError
test "ArgsIterator" {
const testing = std.testing;
const child = try std.process.ArgIteratorGeneral(.{}).init(
const child = try std.process.Args.IteratorGeneral(.{}).init(
testing.allocator,
"--what +list-things --a=42",
);

View File

@@ -3,6 +3,7 @@ const builtin = @import("builtin");
const args = @import("args.zig");
const Action = @import("ghostty.zig").Action;
const Allocator = std.mem.Allocator;
const global = @import("../global.zig");
const vaxis = @import("vaxis");
const framedata = @import("framedata").compressed;
@@ -173,6 +174,9 @@ const Boo = struct {
/// The `boo` command is used to display the animation from the Ghostty website in the terminal
pub fn run(gpa: Allocator) !u8 {
var env_map = try global.environMap();
defer env_map.deinit();
// Disable on non-desktop systems.
switch (builtin.os.tag) {
.windows, .macos, .linux, .freebsd => {},
@@ -183,7 +187,7 @@ pub fn run(gpa: Allocator) !u8 {
defer opts.deinit();
{
var iter = try args.argsIterator(gpa);
var iter = try args.argsIterator(gpa, global.args());
defer iter.deinit();
try args.parse(Options, gpa, &opts, &iter);
}
@@ -194,7 +198,7 @@ pub fn run(gpa: Allocator) !u8 {
gpa.free(decompressed_data);
}
var app = try vxfw.App.init(gpa);
var app = try vxfw.App.init(global.io(), gpa, &env_map, &.{});
defer app.deinit();
var boo: Boo = undefined;

View File

@@ -4,6 +4,7 @@ const args = @import("args.zig");
const Action = @import("ghostty.zig").Action;
const Config = @import("../config.zig").Config;
const crash = @import("../crash/main.zig");
const global = @import("../global.zig");
pub const Options = struct {
pub fn deinit(self: Options) void {
@@ -33,14 +34,14 @@ pub fn run(alloc_gpa: Allocator) !u8 {
defer opts.deinit();
{
var iter = try args.argsIterator(alloc_gpa);
var iter = try args.argsIterator(alloc_gpa, global.args());
defer iter.deinit();
try args.parse(Options, alloc_gpa, &opts, &iter);
}
var buffer: [1024]u8 = undefined;
var stdout_file: std.fs.File = .stdout();
var stdout_writer = stdout_file.writer(&buffer);
var stdout_file: std.Io.File = .stdout();
var stdout_writer = stdout_file.writer(global.io(), &buffer);
const stdout = &stdout_writer.interface;
const result = runInner(alloc, &stdout_file, stdout);
@@ -50,7 +51,7 @@ pub fn run(alloc_gpa: Allocator) !u8 {
fn runInner(
alloc: Allocator,
stdout_file: *std.fs.File,
stdout_file: *std.Io.File,
stdout: *std.Io.Writer,
) !u8 {
const crash_dir = try crash.defaultDir(alloc);
@@ -66,7 +67,7 @@ fn runInner(
// If we have no reports, then we're done. If we have a tty then we
// print a message, otherwise we do nothing.
if (reports.items.len == 0) {
if (std.posix.isatty(stdout_file.handle)) {
if (try stdout_file.isTty(global.io())) {
try stdout.writeAll("No crash reports! 👻\n");
}
return 0;
@@ -76,7 +77,7 @@ fn runInner(
for (reports.items) |report| {
var buf: [128]u8 = undefined;
const now = std.time.nanoTimestamp();
const now = std.Io.Timestamp.now(global.io(), .real).toNanoseconds();
const diff = now - report.mtime;
const since = if (diff <= 0) "now" else s: {
const d = Config.Duration{ .duration = @intCast(diff) };

View File

@@ -93,7 +93,7 @@ pub const Location = union(enum) {
/// and potentially in the future structure them differently.
pub const DiagnosticList = struct {
/// The list of diagnostics.
list: std.ArrayListUnmanaged(Diagnostic) = .{},
list: std.ArrayList(Diagnostic) = .empty,
/// Precomputed data for diagnostics. This is used specifically
/// when we build libghostty so that we can precompute the messages
@@ -111,7 +111,7 @@ pub const DiagnosticList = struct {
};
const Precompute = if (precompute_enabled) struct {
messages: std.ArrayListUnmanaged([:0]const u8) = .{},
messages: std.ArrayList([:0]const u8) = .empty,
pub fn clone(
self: *const Precompute,

View File

@@ -7,6 +7,7 @@ const Action = @import("ghostty.zig").Action;
const configpkg = @import("../config.zig");
const internal_os = @import("../os/main.zig");
const Config = configpkg.Config;
const global = @import("../global.zig");
pub const Options = struct {
pub fn deinit(self: Options) void {
@@ -48,14 +49,14 @@ pub fn run(alloc: Allocator) !u8 {
// critical where setting up the defer cleanup is a problem.
var buffer: [1024]u8 = undefined;
var stderr_writer = std.fs.File.stderr().writer(&buffer);
var stderr_writer = std.Io.File.stderr().writer(global.io(), &buffer);
const stderr = &stderr_writer.interface;
var opts: Options = .{};
defer opts.deinit();
{
var iter = try args.argsIterator(alloc);
var iter = try args.argsIterator(alloc, global.args());
defer iter.deinit();
try args.parse(Options, alloc, &opts, &iter);
}
@@ -67,6 +68,12 @@ pub fn run(alloc: Allocator) !u8 {
}
fn runInner(alloc: Allocator, stderr: *std.Io.Writer) !u8 {
// We require libc because we want to use std.c.environ for envp
// and not have to build that ourselves. We can remove this
// limitation later but Ghostty already heavily requires libc
// so this is not a big deal.
comptime assert(builtin.link_libc);
// We load the configuration once because that will write our
// default configuration files to disk. We don't use the config.
var config = try Config.load(alloc);
@@ -91,22 +98,19 @@ fn runInner(alloc: Allocator, stderr: *std.Io.Writer) !u8 {
}
// Get our editor
const get_env_: ?internal_os.GetEnvResult = env: {
const editor = env: {
// VISUAL vs. EDITOR: https://unix.stackexchange.com/questions/4859/visual-vs-editor-what-s-the-difference
if (try internal_os.getenv(alloc, "VISUAL")) |v| {
if (v.value.len > 0) break :env v;
v.deinit(alloc);
if (try global.environ().containsUnempty(alloc, "VISUAL")) {
break :env try global.environ().getAlloc(alloc, "VISUAL");
}
if (try internal_os.getenv(alloc, "EDITOR")) |v| {
if (v.value.len > 0) break :env v;
v.deinit(alloc);
if (try global.environ().containsUnempty(alloc, "EDITOR")) {
break :env try global.environ().getAlloc(alloc, "EDITOR");
}
break :env null;
break :env "";
};
defer if (get_env_) |v| v.deinit(alloc);
const editor: []const u8 = if (get_env_) |v| v.value else "";
defer alloc.free(editor);
// If we don't have `$EDITOR` set then we can't do anything
// but we can still print a helpful message.
@@ -136,8 +140,9 @@ fn runInner(alloc: Allocator, stderr: *std.Io.Writer) !u8 {
return 1;
}
// Build the command
const command = command: {
var buffer: std.io.Writer.Allocating = .init(alloc);
var buffer: std.Io.Writer.Allocating = .init(alloc);
defer buffer.deinit();
const writer = &buffer.writer;
try writer.writeAll(editor);
@@ -152,21 +157,14 @@ fn runInner(alloc: Allocator, stderr: *std.Io.Writer) !u8 {
};
defer alloc.free(command);
// We require libc because we want to use std.c.environ for envp
// and not have to build that ourselves. We can remove this
// limitation later but Ghostty already heavily requires libc
// so this is not a big deal.
comptime assert(builtin.link_libc);
const err = std.posix.execvpeZ(
"/bin/sh",
&.{ "/bin/sh", "-c", command },
std.c.environ,
);
// Run/replace process (using execve)
const argv = &.{ "/bin/sh", "-c", command };
const envp = std.c.environ;
const err = std.posix.errno(std.posix.system.execve(argv[0], @ptrCast(argv), envp));
// If we reached this point then exec failed.
try stderr.print(
\\Failed to execute the editor. Error code={}.
\\Failed to execute the editor (E{s}).
\\
\\This is usually due to the executable path not existing, invalid
\\permissions, or the shell environment not being set up
@@ -175,6 +173,6 @@ fn runInner(alloc: Allocator, stderr: *std.Io.Writer) !u8 {
\\Editor: {s}
\\Path: {s}
\\
, .{ err, editor, path });
, .{ @tagName(err), editor, path });
return 1;
}

View File

@@ -7,6 +7,7 @@ const Config = @import("../config/Config.zig");
const ConfigKey = @import("../config/key.zig").Key;
const KeybindAction = @import("../input/Binding.zig").Action;
const Pager = @import("Pager.zig");
const global = @import("../global.zig");
pub const Options = struct {
/// The config option to explain. For example:
@@ -51,7 +52,7 @@ pub fn run(alloc: Allocator) !u8 {
var positional: ?[]const u8 = null;
var no_pager: bool = false;
var iter = try args.argsIterator(alloc);
var iter = try args.argsIterator(alloc, global.args());
defer iter.deinit();
defer if (option_name) |s| alloc.free(s);
defer if (keybind_name) |s| alloc.free(s);
@@ -75,9 +76,9 @@ pub fn run(alloc: Allocator) !u8 {
// respective lookup. A bare positional argument tries config
// options first, then keybind actions as a fallback.
const name = keybind_name orelse option_name orelse positional orelse {
var stderr: std.fs.File = .stderr();
var stderr: std.Io.File = .stderr();
var buffer: [4096]u8 = undefined;
var stderr_writer = stderr.writer(&buffer);
var stderr_writer = stderr.writer(global.io(), &buffer);
try stderr_writer.interface.writeAll("Usage: ghostty +explain-config <option>\n");
try stderr_writer.interface.writeAll(" ghostty +explain-config --option=<option>\n");
try stderr_writer.interface.writeAll(" ghostty +explain-config --keybind=<action>\n");
@@ -92,7 +93,7 @@ pub fn run(alloc: Allocator) !u8 {
else
explainOption(name) orelse explainKeybind(name);
var pager: Pager = if (!no_pager) .init(alloc) else .{};
var pager: Pager = if (!no_pager) .init() else .{};
defer pager.deinit();
var buffer: [4096]u8 = undefined;
const writer = pager.writer(&buffer);

View File

@@ -22,6 +22,7 @@ const show_face = @import("show_face.zig");
const boo = @import("boo.zig");
const new_window = @import("new_window.zig");
const toggle_quick_terminal = @import("toggle_quick_terminal.zig");
const global = @import("../global.zig");
/// Special commands that can be invoked via CLI flags. These are all
/// invoked by using `+<action>` as a CLI flag. The only exception is
@@ -120,7 +121,10 @@ pub const Action = enum {
if (std.mem.eql(u8, field.name, @tagName(self))) {
var buffer: [1024]u8 = undefined;
var stdout_writer = std.fs.File.stdout().writer(&buffer);
var stdout_writer = std.Io.File.stdout().writer(
global.io(),
&buffer,
);
const stdout = &stdout_writer.interface;
const text = @field(help_strings.Action, field.name) ++ "\n";
stdout.writeAll(text) catch |write_err| {
@@ -213,7 +217,7 @@ test "parse action none" {
const testing = std.testing;
const alloc = testing.allocator;
var iter = try std.process.ArgIteratorGeneral(.{}).init(
var iter = try std.process.Args.IteratorGeneral(.{}).init(
alloc,
"--a=42 --b --b-f=false",
);
@@ -227,7 +231,7 @@ test "parse action version" {
const alloc = testing.allocator;
{
var iter = try std.process.ArgIteratorGeneral(.{}).init(
var iter = try std.process.Args.IteratorGeneral(.{}).init(
alloc,
"--a=42 --b --b-f=false --version",
);
@@ -237,7 +241,7 @@ test "parse action version" {
}
{
var iter = try std.process.ArgIteratorGeneral(.{}).init(
var iter = try std.process.Args.IteratorGeneral(.{}).init(
alloc,
"--version --a=42 --b --b-f=false",
);
@@ -247,7 +251,7 @@ test "parse action version" {
}
{
var iter = try std.process.ArgIteratorGeneral(.{}).init(
var iter = try std.process.Args.IteratorGeneral(.{}).init(
alloc,
"--c=84 --d --version --a=42 --b --b-f=false",
);
@@ -262,7 +266,7 @@ test "parse action plus" {
const alloc = testing.allocator;
{
var iter = try std.process.ArgIteratorGeneral(.{}).init(
var iter = try std.process.Args.IteratorGeneral(.{}).init(
alloc,
"--a=42 --b --b-f=false +version",
);
@@ -272,7 +276,7 @@ test "parse action plus" {
}
{
var iter = try std.process.ArgIteratorGeneral(.{}).init(
var iter = try std.process.Args.IteratorGeneral(.{}).init(
alloc,
"+version --a=42 --b --b-f=false",
);
@@ -282,7 +286,7 @@ test "parse action plus" {
}
{
var iter = try std.process.ArgIteratorGeneral(.{}).init(
var iter = try std.process.Args.IteratorGeneral(.{}).init(
alloc,
"--c=84 --d +version --a=42 --b --b-f=false",
);
@@ -297,7 +301,7 @@ test "parse action plus ignores -e" {
const alloc = testing.allocator;
{
var iter = try std.process.ArgIteratorGeneral(.{}).init(
var iter = try std.process.Args.IteratorGeneral(.{}).init(
alloc,
"--a=42 -e +version",
);
@@ -307,7 +311,7 @@ test "parse action plus ignores -e" {
}
{
var iter = try std.process.ArgIteratorGeneral(.{}).init(
var iter = try std.process.Args.IteratorGeneral(.{}).init(
alloc,
"+list-fonts --a=42 -e +version",
);

View File

@@ -2,6 +2,7 @@ const std = @import("std");
const Allocator = std.mem.Allocator;
const args = @import("args.zig");
const Action = @import("ghostty.zig").Action;
const global = @import("../global.zig");
// 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
@@ -25,13 +26,13 @@ pub fn run(alloc: Allocator) !u8 {
defer opts.deinit();
{
var iter = try args.argsIterator(alloc);
var iter = try args.argsIterator(alloc, global.args());
defer iter.deinit();
try args.parse(Options, alloc, &opts, &iter);
}
var buffer: [2048]u8 = undefined;
var stdout_writer = std.fs.File.stdout().writer(&buffer);
var stdout_writer = std.Io.File.stdout().writer(global.io(), &buffer);
const stdout = &stdout_writer.interface;
try stdout.writeAll(
\\Usage: ghostty [+action] [options]

View File

@@ -3,6 +3,7 @@ const args = @import("args.zig");
const Action = @import("ghostty.zig").Action;
const Allocator = std.mem.Allocator;
const helpgen_actions = @import("../input/helpgen_actions.zig");
const global = @import("../global.zig");
pub const Options = struct {
/// If `true`, print out documentation about the action associated with the
@@ -32,14 +33,14 @@ pub fn run(alloc: Allocator) !u8 {
defer opts.deinit();
{
var iter = try args.argsIterator(alloc);
var iter = try args.argsIterator(alloc, global.args());
defer iter.deinit();
try args.parse(Options, alloc, &opts, &iter);
}
var stdout: std.fs.File = .stdout();
var stdout: std.Io.File = .stdout();
var buffer: [4096]u8 = undefined;
var stdout_writer = stdout.writer(&buffer);
var stdout_writer = stdout.writer(global.io(), &buffer);
try helpgen_actions.generate(
&stdout_writer.interface,
.plaintext,

View File

@@ -6,6 +6,7 @@ const args = @import("args.zig");
const x11_color = @import("../terminal/main.zig").x11_color;
const vaxis = @import("vaxis");
const tui = @import("tui.zig");
const global = @import("../global.zig");
pub const Options = struct {
pub fn deinit(self: Options) void {
@@ -34,7 +35,7 @@ pub fn run(alloc: Allocator) !u8 {
defer opts.deinit();
{
var iter = try args.argsIterator(alloc);
var iter = try args.argsIterator(alloc, global.args());
defer iter.deinit();
try args.parse(Options, alloc, &opts, &iter);
}
@@ -49,15 +50,14 @@ pub fn run(alloc: Allocator) !u8 {
}
}.lessThan);
// Despite being under the posix namespace, this also works on Windows as of zig 0.13.0
var stdout: std.fs.File = .stdout();
if (tui.can_pretty_print and !opts.plain and std.posix.isatty(stdout.handle)) {
var stdout: std.Io.File = .stdout();
if (tui.can_pretty_print and !opts.plain and try stdout.isTty(global.io())) {
var arena = std.heap.ArenaAllocator.init(alloc);
defer arena.deinit();
return prettyPrint(arena.allocator(), keys.items);
} else {
var buffer: [4096]u8 = undefined;
var stdout_writer = stdout.writer(&buffer);
var stdout_writer = stdout.writer(global.io(), &buffer);
const writer = &stdout_writer.interface;
for (keys.items) |name| {
const rgb = x11_color.map.get(name).?;
@@ -74,11 +74,14 @@ pub fn run(alloc: Allocator) !u8 {
}
fn prettyPrint(alloc: Allocator, keys: [][]const u8) !u8 {
var env_map = try global.environMap();
defer env_map.deinit();
// Set up vaxis
var buf: [1024]u8 = undefined;
var tty = try vaxis.Tty.init(&buf);
var tty = try vaxis.Tty.init(global.io(), &buf);
defer tty.deinit();
var vx = try vaxis.init(alloc, .{});
var vx = try vaxis.init(global.io(), alloc, &env_map, .{});
defer vx.deinit(alloc, tty.writer());
// We know we are ghostty, so let's enable mode 2027. Vaxis normally does this but you need an
@@ -97,7 +100,7 @@ fn prettyPrint(alloc: Allocator, keys: [][]const u8) !u8 {
.y_pixel = 768,
},
else => try vaxis.Tty.getWinsize(tty.fd),
else => try tty.getWinsize(),
};
try vx.resize(alloc, tty.writer(), winsize);

View File

@@ -4,6 +4,7 @@ const ArenaAllocator = std.heap.ArenaAllocator;
const Action = @import("ghostty.zig").Action;
const args = @import("args.zig");
const font = @import("../font/main.zig");
const global = @import("../global.zig");
const log = std.log.scoped(.list_fonts);
@@ -60,7 +61,7 @@ pub const Options = struct {
/// is identical to the `font-family` set of Ghostty configuration values, so
/// this can be used to debug why your desired font may not be loading.
pub fn run(alloc: Allocator) !u8 {
var iter = try args.argsIterator(alloc);
var iter = try args.argsIterator(alloc, global.args());
defer iter.deinit();
return try runArgs(alloc, &iter);
}
@@ -78,7 +79,7 @@ fn runArgs(alloc_gpa: Allocator, argsIter: anytype) !u8 {
// Its possible to build Ghostty without font discovery!
if (comptime font.Discover == void) {
var buffer: [1024]u8 = undefined;
var stderr_writer = std.fs.File.stderr().writer(&buffer);
var stderr_writer = std.Io.File.stderr().writer(global.io(), &buffer);
const stderr = &stderr_writer.interface;
try stderr.print(
\\Ghostty was built without a font discovery mechanism. This is a compile-time
@@ -92,7 +93,7 @@ fn runArgs(alloc_gpa: Allocator, argsIter: anytype) !u8 {
}
var buffer: [2048]u8 = undefined;
var stdout_writer = std.fs.File.stdout().writer(&buffer);
var stdout_writer = std.Io.File.stdout().writer(global.io(), &buffer);
const stdout = &stdout_writer.interface;
// We'll be putting our fonts into a list categorized by family
@@ -133,7 +134,7 @@ fn runArgs(alloc_gpa: Allocator, argsIter: anytype) !u8 {
const gop = try map.getOrPut(family);
if (!gop.found_existing) {
try families.append(alloc, family);
gop.value_ptr.* = .{};
gop.value_ptr.* = .empty;
}
try gop.value_ptr.append(alloc, full_name);
}

View File

@@ -10,6 +10,7 @@ const vaxis = @import("vaxis");
const input = @import("../input.zig");
const tui = @import("tui.zig");
const Binding = input.Binding;
const global = @import("../global.zig");
pub const Options = struct {
/// If `true`, print out the default keybinds instead of the ones configured
@@ -56,7 +57,7 @@ pub fn run(alloc: Allocator) !u8 {
defer opts.deinit();
{
var iter = try args.argsIterator(alloc);
var iter = try args.argsIterator(alloc, global.args());
defer iter.deinit();
try args.parse(Options, alloc, &opts, &iter);
}
@@ -65,11 +66,11 @@ pub fn run(alloc: Allocator) !u8 {
defer config.deinit();
var buffer: [1024]u8 = undefined;
const stdout: std.fs.File = .stdout();
var stdout_writer = stdout.writer(&buffer);
const stdout: std.Io.File = .stdout();
var stdout_writer = stdout.writer(global.io(), &buffer);
const writer = &stdout_writer.interface;
if (tui.can_pretty_print and !opts.plain and stdout.isTty()) {
if (tui.can_pretty_print and !opts.plain and try stdout.isTty(global.io())) {
var arena = std.heap.ArenaAllocator.init(alloc);
defer arena.deinit();
return prettyPrint(arena.allocator(), config.keybind);
@@ -218,11 +219,14 @@ const ChordBinding = struct {
};
fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 {
var env_map = try global.environMap();
defer env_map.deinit();
// Set up vaxis
var buf: [1024]u8 = undefined;
var tty = try vaxis.Tty.init(&buf);
var tty = try vaxis.Tty.init(global.io(), &buf);
defer tty.deinit();
var vx = try vaxis.init(alloc, .{});
var vx = try vaxis.init(global.io(), alloc, &env_map, .{});
const writer = tty.writer();
defer vx.deinit(alloc, writer);
@@ -242,7 +246,7 @@ fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 {
.y_pixel = 768,
},
else => try vaxis.Tty.getWinsize(tty.fd),
else => try tty.getWinsize(),
};
try vx.resize(alloc, writer, winsize);

View File

@@ -5,7 +5,7 @@ const Config = @import("../config/Config.zig");
const configpkg = @import("../config.zig");
const themepkg = @import("../config/theme.zig");
const tui = @import("tui.zig");
const global_state = &@import("../global.zig").state;
const global = @import("../global.zig");
const vaxis = @import("vaxis");
const zf = @import("zf");
@@ -108,7 +108,7 @@ pub fn run(gpa_alloc: std.mem.Allocator) !u8 {
defer opts.deinit();
{
var iter = try args.argsIterator(gpa_alloc);
var iter = try args.argsIterator(gpa_alloc, global.args());
defer iter.deinit();
try args.parse(Options, gpa_alloc, &opts, &iter);
}
@@ -117,15 +117,15 @@ pub fn run(gpa_alloc: std.mem.Allocator) !u8 {
const alloc = arena.allocator();
var stdout_buf: [4096]u8 = undefined;
var stdout_file: std.fs.File = .stdout();
var stdout_writer = stdout_file.writer(&stdout_buf);
var stdout_file: std.Io.File = .stdout();
var stdout_writer = stdout_file.writer(global.io(), &stdout_buf);
const stdout = &stdout_writer.interface;
var stderr_buf: [4096]u8 = undefined;
var stderr_writer = std.fs.File.stderr().writer(&stderr_buf);
var stderr_writer = std.Io.File.stderr().writer(global.io(), &stderr_buf);
const stderr = &stderr_writer.interface;
const resources_dir = global_state.resources_dir.app();
const resources_dir = global.resourcesDir().app();
if (resources_dir == null)
try stderr.print("Could not find the Ghostty resources directory. Please ensure " ++
"that Ghostty is installed correctly.\n", .{});
@@ -137,18 +137,22 @@ pub fn run(gpa_alloc: std.mem.Allocator) !u8 {
var it: themepkg.LocationIterator = .{ .arena_alloc = arena.allocator() };
while (try it.next()) |loc| {
var dir = std.fs.cwd().openDir(loc.dir, .{ .iterate = true }) catch |err| switch (err) {
var dir = std.Io.Dir.cwd().openDir(
global.io(),
loc.dir,
.{ .iterate = true },
) catch |err| switch (err) {
error.FileNotFound => continue,
else => {
std.debug.print("error trying to open {s}: {}\n", .{ loc.dir, err });
continue;
},
};
defer dir.close();
defer dir.close(global.io());
var walker = dir.iterate();
while (try walker.next()) |entry| {
while (try walker.next(global.io())) |entry| {
switch (entry.kind) {
.file, .sym_link => {
if (std.mem.eql(u8, entry.name, ".DS_Store"))
@@ -174,7 +178,7 @@ pub fn run(gpa_alloc: std.mem.Allocator) !u8 {
std.mem.sortUnstable(ThemeListElement, themes.items, {}, ThemeListElement.lessThan);
if (tui.can_pretty_print and !opts.plain and stdout_file.isTty()) {
if (tui.can_pretty_print and !opts.plain and try stdout_file.isTty(global.io())) {
try preview(gpa_alloc, themes.items, opts.color);
return 0;
}
@@ -210,14 +214,18 @@ fn writeAutoThemeFile(alloc: std.mem.Allocator, theme_name: []const u8) !void {
defer alloc.free(auto_path);
if (std.fs.path.dirname(auto_path)) |dir| {
try std.fs.cwd().makePath(dir);
try std.Io.Dir.cwd().createDirPath(global.io(), dir);
}
var f = try std.fs.createFileAbsolute(auto_path, .{ .truncate = true });
defer f.close();
var f = try std.Io.Dir.createFileAbsolute(
global.io(),
auto_path,
.{ .truncate = true },
);
defer f.close(global.io());
var buf: [128]u8 = undefined;
var w = f.writer(&buf);
var w = f.writer(global.io(), &buf);
try w.interface.print("theme = {s}\n", .{theme_name});
try w.interface.flush();
}
@@ -256,13 +264,16 @@ const Preview = struct {
theme_filter: ColorScheme,
buf: []u8,
) !*Preview {
var env_map = try global.environMap();
defer env_map.deinit();
const self = try allocator.create(Preview);
self.* = .{
.allocator = allocator,
.should_quit = false,
.tty = try .init(buf),
.vx = try vaxis.init(allocator, .{}),
.tty = try .init(global.io(), buf),
.vx = try vaxis.init(global.io(), allocator, &env_map, .{}),
.mouse = null,
.themes = themes,
.filtered = try .initCapacity(allocator, themes.len),
@@ -290,18 +301,12 @@ const Preview = struct {
}
pub fn run(self: *Preview) !void {
var loop: vaxis.Loop(Event) = .{
.tty = &self.tty,
.vaxis = &self.vx,
};
try loop.init();
try loop.start();
var loop: vaxis.Loop(Event) = .init(global.io(), &self.tty, &self.vx);
const writer = self.tty.writer();
try self.vx.enterAltScreen(writer);
try self.vx.setTitle(writer, "👻 Ghostty Theme Preview 👻");
try self.vx.queryTerminal(writer, 1 * std.time.ns_per_s);
try self.vx.queryTerminal(writer, .fromSeconds(1));
try self.vx.setMouseMode(writer, true);
if (self.vx.caps.color_scheme_updates)
try self.vx.subscribeToColorSchemeUpdates(writer);
@@ -311,8 +316,8 @@ const Preview = struct {
defer arena.deinit();
const alloc = arena.allocator();
loop.pollEvent();
while (loop.tryEvent()) |event| {
try loop.pollEvent();
while (try loop.tryEvent()) |event| {
try self.update(event, alloc);
}
try self.draw(alloc);
@@ -366,7 +371,12 @@ const Preview = struct {
if (!shouldIncludeTheme(self.theme_filter, theme_config)) continue;
theme.rank = zf.rank(theme.theme, tokens.items, .{
.to_lower = true,
// NOTE: Changed from ".to_lower = true" (the option was
// renamed). I think this is the correct analog for case
// insensitive ranking (which is what I'm guessing
// ".to_lower = true" implies, but this comment serves as a
// hint if there's a regression.
.case_sensitive = false,
.plain = true,
});
if (theme.rank != null) try self.filtered.append(self.allocator, i);
@@ -620,13 +630,21 @@ const Preview = struct {
self.down(1);
}
if (theme_list.hasMouse(mouse)) |_| {
// NOTE: mouse co-ordinates can be negative (see
// https://github.com/rockorager/libvaxis/pull/276).
// Working around it in this case, but putting this here in
// case there are issues.
if (mouse.button == .left and mouse.type == .release) {
const selection = self.window + mouse.row;
const selection: usize = selection: {
var window: i32 = @min(self.window, std.math.maxInt(i32));
window += mouse.row;
break :selection @max(0, window);
};
if (selection < self.filtered.items.len) {
self.current = selection;
}
}
highlight = mouse.row;
highlight = @max(0, mouse.row);
}
}
}

View File

@@ -7,8 +7,12 @@ const args = @import("args.zig");
const diagnostics = @import("diagnostics.zig");
const lib = @import("../lib/main.zig");
const homedir = @import("../os/homedir.zig");
const global = @import("../global.zig");
pub const Options = struct {
/// We store an I/O implementation to make maintenance easier.
_io: std.Io,
/// This is set by the CLI parser for deinit.
_arena: ?ArenaAllocator = null,
@@ -26,8 +30,18 @@ pub const Options = struct {
/// there is a "normal" config setting on the cli.
_diagnostics: diagnostics.DiagnosticList = .{},
pub const ParseManuallyHookError = error{InvalidValue} ||
homedir.ExpandError ||
std.Io.Dir.RealPathFileAllocError ||
Allocator.Error;
/// Manual parse hook, collect all of the arguments after `+new-window`.
pub fn parseManuallyHook(self: *Options, alloc: Allocator, arg: []const u8, iter: anytype) (error{InvalidValue} || homedir.ExpandError || std.fs.Dir.RealPathAllocError || Allocator.Error)!bool {
pub fn parseManuallyHook(
self: *Options,
alloc: Allocator,
arg: []const u8,
iter: anytype,
) ParseManuallyHookError!bool {
var e_seen: bool = std.mem.eql(u8, arg, "-e");
// Include the argument that triggered the manual parse hook.
@@ -50,7 +64,12 @@ pub const Options = struct {
return false;
}
fn checkArg(self: *Options, alloc: Allocator, arg: []const u8) (error{InvalidValue} || homedir.ExpandError || std.fs.Dir.RealPathAllocError || Allocator.Error)!?[:0]const u8 {
const CheckArgError = error{InvalidValue} ||
homedir.ExpandError ||
std.Io.Dir.RealPathFileAllocError ||
Allocator.Error;
fn checkArg(self: *Options, alloc: Allocator, arg: []const u8) CheckArgError!?[:0]const u8 {
if (lib.cutPrefix(u8, arg, "--class=")) |rest| {
self.class = try alloc.dupeZ(u8, std.mem.trim(u8, rest, &std.ascii.whitespace));
return null;
@@ -60,11 +79,15 @@ pub const Options = struct {
const stripped = std.mem.trim(u8, rest, &std.ascii.whitespace);
if (std.mem.eql(u8, stripped, "home")) return try alloc.dupeZ(u8, arg);
if (std.mem.eql(u8, stripped, "inherit")) return try alloc.dupeZ(u8, arg);
const cwd: std.fs.Dir = std.fs.cwd();
const cwd: std.Io.Dir = .cwd();
var expandhome_buf: [std.fs.max_path_bytes]u8 = undefined;
const expanded = try homedir.expandHome(stripped, &expandhome_buf);
const expanded = expanded: {
var environ_map = try global.environMap();
defer environ_map.deinit();
break :expanded try homedir.expandHome(&environ_map, stripped, &expandhome_buf);
};
var realpath_buf: [std.fs.max_path_bytes]u8 = undefined;
const realpath = try cwd.realpath(expanded, &realpath_buf);
const realpath = realpath_buf[0..try cwd.realPathFile(self._io, expanded, &realpath_buf)];
self._working_directory_seen = true;
return try std.fmt.allocPrintSentinel(alloc, "--working-directory={s}", .{realpath}, 0);
}
@@ -147,11 +170,11 @@ pub const Options = struct {
///
/// Available since: 1.2.0
pub fn run(alloc: Allocator) !u8 {
var iter = try args.argsIterator(alloc);
var iter = try args.argsIterator(alloc, global.args());
defer iter.deinit();
var buffer: [1024]u8 = undefined;
var stderr_writer = std.fs.File.stderr().writer(&buffer);
var stderr_writer = std.Io.File.stderr().writer(global.io(), &buffer);
const stderr = &stderr_writer.interface;
const result = runArgs(alloc, &iter, stderr);
@@ -164,7 +187,7 @@ fn runArgs(
argsIter: anytype,
stderr: *std.Io.Writer,
) !u8 {
var opts: Options = .{};
var opts: Options = .{ ._io = global.io() };
defer opts.deinit();
args.parse(Options, alloc_gpa, &opts, argsIter) catch |err| switch (err) {
@@ -195,11 +218,16 @@ fn runArgs(
if (!opts._working_directory_seen) {
const alloc = opts._arena.?.allocator();
const cwd: std.fs.Dir = std.fs.cwd();
const cwd: std.Io.Dir = .cwd();
var buf: [std.fs.max_path_bytes]u8 = undefined;
const wd = try cwd.realpath(".", &buf);
const wd = buf[0..try cwd.realPathFile(global.io(), ".", &buf)];
// This should be inserted at the beginning of the list, just in case `-e` was used.
try opts._arguments.insert(alloc, 0, try std.fmt.allocPrintSentinel(alloc, "--working-directory={s}", .{wd}, 0));
try opts._arguments.insert(alloc, 0, try std.fmt.allocPrintSentinel(
alloc,
"--working-directory={s}",
.{wd},
0,
));
}
var arena = ArenaAllocator.init(alloc_gpa);

View File

@@ -5,6 +5,7 @@ const Action = @import("ghostty.zig").Action;
const configpkg = @import("../config.zig");
const Config = configpkg.Config;
const Pager = @import("Pager.zig");
const global = @import("../global.zig");
pub const Options = struct {
/// If true, do not load the user configuration, only load the defaults.
@@ -66,7 +67,7 @@ pub fn run(alloc: Allocator) !u8 {
defer opts.deinit();
{
var iter = try args.argsIterator(alloc);
var iter = try args.argsIterator(alloc, global.args());
defer iter.deinit();
try args.parse(Options, alloc, &opts, &iter);
}
@@ -81,7 +82,7 @@ pub fn run(alloc: Allocator) !u8 {
.docs = opts.docs,
};
var pager: Pager = if (!opts.@"no-pager") .init(alloc) else .{};
var pager: Pager = if (!opts.@"no-pager") .init() else .{};
defer pager.deinit();
var buffer: [4096]u8 = undefined;
const writer = pager.writer(&buffer);

View File

@@ -7,6 +7,7 @@ const diagnostics = @import("diagnostics.zig");
const font = @import("../font/main.zig");
const configpkg = @import("../config.zig");
const Config = configpkg.Config;
const global = @import("../global.zig");
pub const Options = struct {
/// This is set by the CLI parser for deinit.
@@ -62,15 +63,15 @@ pub const Options = struct {
/// style. Valid options are `text` and `emoji`. If unset, the presentation
/// style of a codepoint will be inferred from the Unicode standard.
pub fn run(alloc: Allocator) !u8 {
var iter = try args.argsIterator(alloc);
var iter = try args.argsIterator(alloc, global.args());
defer iter.deinit();
var stdout_buffer: [1024]u8 = undefined;
var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer);
var stdout_writer = std.Io.File.stdout().writer(global.io(), &stdout_buffer);
const stdout = &stdout_writer.interface;
var stderr_buffer: [1024]u8 = undefined;
var stderr_writer = std.fs.File.stdout().writer(&stderr_buffer);
var stderr_writer = std.Io.File.stdout().writer(global.io(), &stderr_buffer);
const stderr = &stderr_writer.interface;
const result = runArgs(

View File

@@ -10,6 +10,7 @@ const Allocator = std.mem.Allocator;
const internal_os = @import("../../os/main.zig");
const xdg = internal_os.xdg;
const Entry = @import("Entry.zig");
const global = @import("../../global.zig");
// 512KB - sufficient for approximately 10k entries
const MAX_CACHE_SIZE = 512 * 1024;
@@ -26,8 +27,11 @@ pub fn defaultPath(
alloc: Allocator,
program: []const u8,
) ![]const u8 {
var environ_map = try global.environMap();
defer environ_map.deinit();
const state_dir: []const u8 = xdg.state(
alloc,
&environ_map,
.{ .subdir = program },
) catch |err| return switch (err) {
error.OutOfMemory => error.OutOfMemory,
@@ -41,7 +45,7 @@ pub fn defaultPath(
/// This removes the cache file from disk, effectively clearing all cached
/// SSH terminfo entries.
pub fn clear(self: DiskCache) !void {
std.fs.cwd().deleteFile(self.path) catch |err| switch (err) {
std.Io.Dir.cwd().deleteFile(global.io(), self.path) catch |err| switch (err) {
error.FileNotFound => {},
else => return err,
};
@@ -59,36 +63,43 @@ pub fn add(
// Create cache directory if needed
if (std.fs.path.dirname(self.path)) |dir| {
std.fs.cwd().makePath(dir) catch |err| switch (err) {
std.Io.Dir.cwd().createDirPath(global.io(), dir) catch |err| switch (err) {
error.PathAlreadyExists => {},
else => return err,
};
}
// Open or create cache file with secure permissions
const file = std.fs.createFileAbsolute(self.path, .{
const file = std.Io.Dir.createFileAbsolute(global.io(), self.path, .{
.read = true,
.truncate = false,
.mode = 0o600,
.permissions = if (builtin.os.tag != .windows and std.posix.mode_t != u0)
.fromMode(0o600)
else
.default_file,
}) catch |err| switch (err) {
error.PathAlreadyExists => blk: {
const existing_file = try std.fs.openFileAbsolute(
const existing_file = try std.Io.Dir.openFileAbsolute(
global.io(),
self.path,
.{ .mode = .read_write },
);
errdefer existing_file.close();
errdefer existing_file.close(global.io());
try fixupPermissions(existing_file);
break :blk existing_file;
},
else => return err,
};
defer file.close();
defer file.close(global.io());
// Lock
// Causes a compile failure in the Zig std library on Windows, see:
// https://github.com/ziglang/zig/issues/18430
if (comptime builtin.os.tag != .windows) _ = file.tryLock(.exclusive) catch return error.CacheLocked;
defer if (comptime builtin.os.tag != .windows) file.unlock();
if (comptime builtin.os.tag != .windows) _ = file.tryLock(
global.io(),
.exclusive,
) catch return error.CacheLocked;
defer if (comptime builtin.os.tag != .windows) file.unlock(global.io());
var entries = try readEntries(alloc, file);
defer deinitEntries(alloc, &entries);
@@ -125,21 +136,25 @@ pub fn remove(
if (!isValidCacheKey(key)) return error.InvalidCacheKey;
// Open our file
const file = std.fs.openFileAbsolute(
const file = std.Io.Dir.openFileAbsolute(
global.io(),
self.path,
.{ .mode = .read_write },
) catch |err| switch (err) {
error.FileNotFound => return false,
else => return err,
};
defer file.close();
defer file.close(global.io());
try fixupPermissions(file);
// Lock
// Causes a compile failure in the Zig std library on Windows, see:
// https://github.com/ziglang/zig/issues/18430
if (comptime builtin.os.tag != .windows) _ = file.tryLock(.exclusive) catch return error.CacheLocked;
defer if (comptime builtin.os.tag != .windows) file.unlock();
if (comptime builtin.os.tag != .windows) _ = file.tryLock(
global.io(),
.exclusive,
) catch return error.CacheLocked;
defer if (comptime builtin.os.tag != .windows) file.unlock(global.io());
// Read existing entries
var entries = try readEntries(alloc, file);
@@ -165,28 +180,32 @@ pub fn prune(
alloc: Allocator,
max_age_s: u64,
) !usize {
const file = std.fs.openFileAbsolute(
const file = std.Io.Dir.openFileAbsolute(
global.io(),
self.path,
.{ .mode = .read_write },
) catch |err| switch (err) {
error.FileNotFound => return 0,
else => return err,
};
defer file.close();
defer file.close(global.io());
try fixupPermissions(file);
// Lock
// Causes a compile failure in the Zig std library on Windows, see:
// https://github.com/ziglang/zig/issues/18430
if (comptime builtin.os.tag != .windows) _ = file.tryLock(.exclusive) catch return error.CacheLocked;
defer if (comptime builtin.os.tag != .windows) file.unlock();
if (comptime builtin.os.tag != .windows) _ = file.tryLock(
global.io(),
.exclusive,
) catch return error.CacheLocked;
defer if (comptime builtin.os.tag != .windows) file.unlock(global.io());
// Read existing entries
var entries = try readEntries(alloc, file);
defer deinitEntries(alloc, &entries);
// Drop expired entries from the map, then persist what remains.
const now = std.time.timestamp();
const now = std.Io.Timestamp.now(global.io(), .real).toSeconds();
var expired: std.ArrayList([]const u8) = .empty;
defer expired.deinit(alloc);
var iter = entries.iterator();
@@ -215,14 +234,15 @@ pub fn contains(
if (!isValidCacheKey(key)) return error.InvalidCacheKey;
// Open our file
const file = std.fs.openFileAbsolute(
const file = std.Io.Dir.openFileAbsolute(
global.io(),
self.path,
.{},
) catch |err| switch (err) {
error.FileNotFound => return false,
else => return err,
};
defer file.close();
defer file.close(global.io());
// Read existing entries
var entries = try readEntries(alloc, file);
@@ -231,15 +251,15 @@ pub fn contains(
return entries.contains(key);
}
fn fixupPermissions(file: std.fs.File) !void {
fn fixupPermissions(file: std.Io.File) !void {
// Windows does not support chmod
if (comptime builtin.os.tag == .windows) return;
// Ensure file has correct permissions (readable/writable by
// owner only)
const stat = try file.stat();
if (stat.mode & 0o777 != 0o600) {
try file.chmod(0o600);
const stat = try file.stat(global.io());
if (stat.permissions.toMode() & 0o777 != 0o600) {
try file.setPermissions(global.io(), .fromMode(0o600));
}
}
@@ -250,22 +270,27 @@ fn writeCacheFile(
const cache_dir = std.fs.path.dirname(self.path) orelse return error.InvalidCachePath;
const cache_basename = std.fs.path.basename(self.path);
var dir = try std.fs.cwd().openDir(cache_dir, .{});
defer dir.close();
var dir = try std.Io.Dir.cwd().openDir(global.io(), cache_dir, .{});
defer dir.close(global.io());
var buf: [1024]u8 = undefined;
var atomic_file = try dir.atomicFile(cache_basename, .{
.mode = 0o600,
.write_buffer = &buf,
var atomic_file = try dir.createFileAtomic(global.io(), cache_basename, .{
.permissions = if (builtin.os.tag != .windows and std.posix.mode_t != u0)
.fromMode(0o600)
else
.default_file,
.replace = true,
});
defer atomic_file.deinit();
defer atomic_file.deinit(global.io());
var file_writer = atomic_file.file.writer(global.io(), &buf);
var iter = entries.iterator();
while (iter.next()) |kv| {
try kv.value_ptr.format(&atomic_file.file_writer.interface);
try kv.value_ptr.format(&file_writer.interface);
}
try atomic_file.finish();
try file_writer.flush();
try atomic_file.replace(global.io());
}
/// List all entries in the cache.
@@ -276,14 +301,15 @@ pub fn list(
alloc: Allocator,
) !std.StringHashMap(Entry) {
// Open our file
const file = std.fs.openFileAbsolute(
const file = std.Io.Dir.openFileAbsolute(
global.io(),
self.path,
.{},
) catch |err| switch (err) {
error.FileNotFound => return .init(alloc),
else => return err,
};
defer file.close();
defer file.close(global.io());
return readEntries(alloc, file);
}
@@ -306,9 +332,9 @@ pub fn deinitEntries(
fn readEntries(
alloc: Allocator,
file: std.fs.File,
file: std.Io.File,
) !std.StringHashMap(Entry) {
var reader = file.reader(&.{});
var reader = file.reader(global.io(), &.{});
const content = try reader.interface.allocRemaining(
alloc,
.limited(MAX_CACHE_SIZE),
@@ -392,7 +418,7 @@ fn isValidHost(host: []const u8) bool {
// We also accept valid IP addresses. In practice, IPv4 addresses are also
// considered valid hostnames due to their overlapping syntax, so we can
// simplify this check to be IPv6-specific.
if (std.net.Address.parseIp6(host, 0)) |_| {
if (std.Io.net.IpAddress.parseIp6(host, 0)) |_| {
return true;
} else |_| {
return false;
@@ -428,12 +454,12 @@ test "disk cache clear" {
defer tmp.cleanup();
var buf: [4096]u8 = undefined;
{
var file = try tmp.dir.createFile("cache", .{});
defer file.close();
var file_writer = file.writer(&buf);
var file = try tmp.dir.createFile(testing.io, "cache", .{});
defer file.close(testing.io);
var file_writer = file.writer(testing.io, &buf);
try file_writer.interface.writeAll("HELLO!");
}
const path = try tmp.dir.realpathAlloc(alloc, "cache");
const path = try tmp.dir.realPathFileAlloc(testing.io, "cache", alloc);
defer alloc.free(path);
// Setup our cache
@@ -443,7 +469,7 @@ test "disk cache clear" {
// Verify the file is gone
try testing.expectError(
error.FileNotFound,
tmp.dir.openFile("cache", .{}),
tmp.dir.openFile(testing.io, "cache", .{}),
);
}
@@ -456,21 +482,21 @@ test "disk cache operations" {
defer tmp.cleanup();
var buf: [4096]u8 = undefined;
{
var file = try tmp.dir.createFile("cache", .{});
defer file.close();
var file_writer = file.writer(&buf);
var file = try tmp.dir.createFile(testing.io, "cache", .{});
defer file.close(testing.io);
var file_writer = file.writer(testing.io, &buf);
const writer = &file_writer.interface;
try writer.writeAll("HELLO!");
try writer.flush();
}
const path = try tmp.dir.realpathAlloc(alloc, "cache");
const path = try tmp.dir.realPathFileAlloc(testing.io, "cache", alloc);
defer alloc.free(path);
// Setup our cache. Adding the same key twice exercises both the new
// and existing-entry paths.
const cache: DiskCache = .{ .path = path };
try cache.add(alloc, "example.com", std.time.timestamp());
try cache.add(alloc, "example.com", std.time.timestamp());
try cache.add(alloc, "example.com", std.Io.Timestamp.now(testing.io, .real).toSeconds());
try cache.add(alloc, "example.com", std.Io.Timestamp.now(testing.io, .real).toSeconds());
try testing.expect(try cache.contains(alloc, "example.com"));
// List
@@ -482,7 +508,7 @@ test "disk cache operations" {
try testing.expect(try cache.remove(alloc, "example.com"));
try testing.expect(!try cache.remove(alloc, "example.com"));
try testing.expect(!(try cache.contains(alloc, "example.com")));
try cache.add(alloc, "example.com", std.time.timestamp());
try cache.add(alloc, "example.com", std.Io.Timestamp.now(testing.io, .real).toSeconds());
}
test "disk cache cleans up temp files" {
@@ -492,19 +518,19 @@ test "disk cache cleans up temp files" {
var tmp = testing.tmpDir(.{ .iterate = true });
defer tmp.cleanup();
const tmp_path = try tmp.dir.realpathAlloc(alloc, ".");
const tmp_path = try tmp.dir.realPathFileAlloc(testing.io, ".", alloc);
defer alloc.free(tmp_path);
const cache_path = try std.fs.path.join(alloc, &.{ tmp_path, "cache" });
defer alloc.free(cache_path);
const cache: DiskCache = .{ .path = cache_path };
try cache.add(alloc, "example.com", std.time.timestamp());
try cache.add(alloc, "example.org", std.time.timestamp());
try cache.add(alloc, "example.com", std.Io.Timestamp.now(testing.io, .real).toSeconds());
try cache.add(alloc, "example.org", std.Io.Timestamp.now(testing.io, .real).toSeconds());
// Verify only the cache file exists and no temp files left behind
var count: usize = 0;
var iter = tmp.dir.iterate();
while (try iter.next()) |entry| {
while (try iter.next(testing.io)) |entry| {
count += 1;
try testing.expectEqualStrings("cache", entry.name);
}
@@ -514,10 +540,11 @@ test "disk cache cleans up temp files" {
test "disk cache prune" {
const testing = std.testing;
const alloc = testing.allocator;
const io = testing.io;
var tmp = testing.tmpDir(.{});
defer tmp.cleanup();
const tmp_path = try tmp.dir.realpathAlloc(alloc, ".");
const tmp_path = try tmp.dir.realPathFileAlloc(io, ".", alloc);
defer alloc.free(tmp_path);
const cache_path = try std.fs.path.join(alloc, &.{ tmp_path, "cache" });
defer alloc.free(cache_path);
@@ -527,7 +554,7 @@ test "disk cache prune" {
// Back-date one entry an hour old and one 100 days old.
const day = std.time.s_per_day;
const hour = std.time.s_per_hour;
const now = std.time.timestamp();
const now = std.Io.Timestamp.now(testing.io, .real).toSeconds();
try cache.add(alloc, "recent.com", now - hour);
try cache.add(alloc, "old.com", now - 100 * day);
@@ -547,11 +574,12 @@ test "disk cache prune" {
test "disk cache prune missing file" {
const testing = std.testing;
const alloc = testing.allocator;
const io = testing.io;
var tmp = testing.tmpDir(.{});
defer tmp.cleanup();
const tmp_path = try tmp.dir.realpathAlloc(alloc, ".");
const tmp_path = try tmp.dir.realPathFileAlloc(io, ".", alloc);
defer alloc.free(tmp_path);
const cache_path = try std.fs.path.join(alloc, &.{ tmp_path, "cache" });
defer alloc.free(cache_path);
@@ -563,6 +591,7 @@ test "disk cache prune missing file" {
test "disk cache reads duplicate keys" {
const testing = std.testing;
const alloc = testing.allocator;
const io = testing.io;
var tmp = testing.tmpDir(.{});
defer tmp.cleanup();
@@ -571,16 +600,16 @@ test "disk cache reads duplicate keys" {
// key with the updated entry and ensure (via testing.allocator) that
// we don't double-free or leak.
{
var file = try tmp.dir.createFile("cache", .{});
defer file.close();
var file = try tmp.dir.createFile(io, "cache", .{});
defer file.close(io);
var buf: [256]u8 = undefined;
var file_writer = file.writer(&buf);
var file_writer = file.writer(io, &buf);
try file_writer.interface.writeAll(
"example.com|100|xterm-ghostty\nexample.com|200|xterm-newer\n",
);
try file_writer.interface.flush();
}
const path = try tmp.dir.realpathAlloc(alloc, "cache");
const path = try tmp.dir.realPathFileAlloc(io, "cache", alloc);
defer alloc.free(path);
const cache: DiskCache = .{ .path = path };
@@ -602,10 +631,10 @@ test "disk cache reads survive allocation failure" {
// Exercise a populated cache containing a duplicate key to ensure
// that we hit all of the possible allocation behaviors below.
{
var file = try tmp.dir.createFile("cache", .{});
defer file.close();
var file = try tmp.dir.createFile(testing.io, "cache", .{});
defer file.close(testing.io);
var buf: [256]u8 = undefined;
var file_writer = file.writer(&buf);
var file_writer = file.writer(testing.io, &buf);
try file_writer.interface.writeAll(
"a.com|100|xterm-ghostty\n" ++
"b.com|100|xterm-ghostty\n" ++
@@ -614,7 +643,7 @@ test "disk cache reads survive allocation failure" {
);
try file_writer.interface.flush();
}
const path = try tmp.dir.realpathAlloc(testing.allocator, "cache");
const path = try tmp.dir.realPathFileAlloc(testing.io, "cache", testing.allocator);
defer testing.allocator.free(path);
const cache: DiskCache = .{ .path = path };
@@ -647,7 +676,7 @@ test "disk cache add survives allocation failure" {
var tmp = testing.tmpDir(.{});
defer tmp.cleanup();
const tmp_path = try tmp.dir.realpathAlloc(testing.allocator, ".");
const tmp_path = try tmp.dir.realPathFileAlloc(testing.io, ".", testing.allocator);
defer testing.allocator.free(tmp_path);
const path = try std.fs.path.join(testing.allocator, &.{ tmp_path, "cache" });
defer testing.allocator.free(path);
@@ -660,7 +689,7 @@ test "disk cache add survives allocation failure" {
// from a clean cache file.
var fail_index: usize = 0;
while (true) : (fail_index += 1) {
std.fs.cwd().deleteFile(path) catch {};
std.Io.Dir.cwd().deleteFile(testing.io, path) catch {};
var failing = std.testing.FailingAllocator.init(
testing.allocator,
.{ .fail_index = fail_index },

View File

@@ -1,4 +1,5 @@
const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const cli_args = @import("args.zig");
@@ -7,6 +8,7 @@ const Action = @import("ghostty.zig").Action;
const DiskCache = @import("ssh_cache.zig").DiskCache;
const internal_os = @import("../os/main.zig");
const ghostty_terminfo = @import("../terminfo/main.zig").ghostty;
const global = @import("../global.zig");
const log = std.log.scoped(.ssh);
@@ -181,14 +183,14 @@ pub fn run(alloc_gpa: Allocator) !u8 {
defer opts.deinit();
{
var iter = try cli_args.argsIterator(alloc_gpa);
var iter = try cli_args.argsIterator(alloc_gpa, global.args());
defer iter.deinit();
try cli_args.parse(Options, alloc_gpa, &opts, &iter);
}
var stderr_buffer: [1024]u8 = undefined;
var stderr_file: std.fs.File = .stderr();
var stderr_writer = stderr_file.writer(&stderr_buffer);
var stderr_file: std.Io.File = .stderr();
var stderr_writer = stderr_file.writer(global.io(), &stderr_buffer);
const stderr = &stderr_writer.interface;
// Any diagnostic from the arg parser is an unknown flag or bad
@@ -294,7 +296,7 @@ fn runInner(
});
verbosePrint(opts, stderr, "exec: {f}", .{Joined{ .items = argv }});
const exit_code = childExec(alloc, argv) catch |err| {
const exit_code = childExec(argv) catch |err| {
try stderr.print("Error: failed to run {s}: {}\n", .{ argv[0], err });
return 1;
};
@@ -302,7 +304,10 @@ fn runInner(
// Attempt to cache (if needed) on a successful ssh execution.
if (exit_code == 0) if (session.to_cache) |entry| {
if (entry.cache.add(alloc, entry.dest, std.time.timestamp())) |_| {
if (entry.cache.add(alloc, entry.dest, std.Io.Timestamp.now(
global.io(),
.real,
).toSeconds())) |_| {
verbosePrint(opts, stderr, "cache: wrote {s}", .{entry.dest});
} else |err| {
log.debug("cache add failed for '{s}': {}", .{ entry.dest, err });
@@ -370,7 +375,7 @@ const Joined = struct {
fn checkExit(term: std.process.Child.Term, label: []const u8) error{ChildFailed}!void {
switch (term) {
.Exited => |rc| if (rc != 0) {
.exited => |rc| if (rc != 0) {
log.warn("{s} exited with non-zero status: {d}", .{ label, rc });
return error.ChildFailed;
},
@@ -393,10 +398,11 @@ fn resolveDestination(
&.{ ssh, "-G" },
args,
}) catch return null;
const result = std.process.Child.run(.{
.allocator = alloc,
.argv = argv,
}) catch |err| {
const result = std.process.run(
alloc,
global.io(),
.{ .argv = argv },
) catch |err| {
log.warn("ssh -G spawn failed: {}", .{err});
return null;
};
@@ -491,23 +497,23 @@ fn installRemoteTerminfo(
});
verbosePrint(opts, stderr, "exec: {f}", .{Joined{ .items = argv }});
var child: std.process.Child = .init(argv, alloc);
child.stdin_behavior = .Pipe;
child.stdout_behavior = .Ignore;
child.stderr_behavior = if (opts.verbose) .Inherit else .Ignore;
child.spawn() catch |err| {
var child = std.process.spawn(global.io(), .{
.argv = argv,
.stdin = .pipe,
.stdout = .ignore,
.stderr = if (opts.verbose) .inherit else .ignore,
}) catch |err| {
log.warn("terminfo install spawn failed: {}", .{err});
return error.InstallFailed;
};
if (child.stdin) |stdin| {
stdin.writeAll(terminfo) catch {};
stdin.close();
stdin.writeStreamingAll(global.io(), terminfo) catch {};
stdin.close(global.io());
child.stdin = null;
}
const term = child.wait() catch |err| {
const term = child.wait(global.io()) catch |err| {
log.warn("terminfo install wait failed: {}", .{err});
return error.InstallFailed;
};
@@ -515,23 +521,24 @@ fn installRemoteTerminfo(
}
/// Returns `128 + signum` for signal-killed children, matching shell convention.
fn childExec(alloc: Allocator, argv: []const []const u8) !u8 {
var child: std.process.Child = .init(argv, alloc);
child.stdin_behavior = .Inherit;
child.stdout_behavior = .Inherit;
child.stderr_behavior = .Inherit;
fn childExec(argv: []const []const u8) !u8 {
var child = try std.process.spawn(global.io(), .{
.argv = argv,
.stdin = .inherit,
.stdout = .inherit,
.stderr = .inherit,
});
try child.spawn();
const term = try child.wait();
const term = try child.wait(global.io());
return switch (term) {
.Exited => |rc| rc,
.Signal => |sig| @as(u8, 128) + @as(u8, @intCast(@min(sig, 127))),
.Stopped, .Unknown => 1,
.exited => |rc| rc,
.signal => |sig| @as(u8, 128) + @as(u8, @intCast(@min(@intFromEnum(sig), 127))),
.stopped, .unknown => 1,
};
}
fn parseTestArgs(alloc: Allocator, opts: *Options, line: []const u8) !void {
var iter = try std.process.ArgIteratorGeneral(.{}).init(alloc, line);
var iter = try std.process.Args.IteratorGeneral(.{}).init(alloc, line);
defer iter.deinit();
try cli_args.parse(Options, alloc, opts, &iter);
}

View File

@@ -2,6 +2,7 @@ const std = @import("std");
const fs = std.fs;
const Allocator = std.mem.Allocator;
const args = @import("args.zig");
const global = @import("../global.zig");
const Action = @import("ghostty.zig").Action;
const Duration = @import("../config.zig").Config.Duration;
pub const Entry = @import("ssh-cache/Entry.zig");
@@ -64,13 +65,13 @@ pub fn run(alloc_gpa: Allocator) !u8 {
defer opts.deinit();
var stdout_buffer: [1024]u8 = undefined;
var stdout_file: std.fs.File = .stdout();
var stdout_writer = stdout_file.writer(&stdout_buffer);
var stdout_file: std.Io.File = .stdout();
var stdout_writer = stdout_file.writer(global.io(), &stdout_buffer);
const stdout = &stdout_writer.interface;
var stderr_buffer: [1024]u8 = undefined;
var stderr_file: std.fs.File = .stderr();
var stderr_writer = stderr_file.writer(&stderr_buffer);
var stderr_file: std.Io.File = .stderr();
var stderr_writer = stderr_file.writer(global.io(), &stderr_buffer);
const stderr = &stderr_writer.interface;
// The cache is queried by a positional destination (`user@host` or a
@@ -81,7 +82,7 @@ pub fn run(alloc_gpa: Allocator) !u8 {
var query: ?[]const u8 = null;
var flags: std.ArrayList([]const u8) = .empty;
{
var iter = try args.argsIterator(alloc_gpa);
var iter = try args.argsIterator(alloc_gpa, global.args());
defer iter.deinit();
while (iter.next()) |arg| {
const is_host_flag = std.mem.startsWith(u8, arg, "--host=");
@@ -174,7 +175,11 @@ pub fn runInner(
}
if (opts.add) |dest| {
cache.add(alloc, dest, std.time.timestamp()) catch |err| switch (err) {
cache.add(
alloc,
dest,
std.Io.Timestamp.now(global.io(), .real).toSeconds(),
) catch |err| switch (err) {
error.InvalidCacheKey => {
try stderr.print(
"Error: Invalid destination '{s}' (expected hostname or user@hostname)\n",
@@ -295,7 +300,7 @@ fn listEntries(
widest = @max(widest, entry.hostname.len);
}
const now = std.time.timestamp();
const now = std.Io.Timestamp.now(global.io(), .real).toSeconds();
for (items.items) |entry| {
try writer.print("{s}", .{entry.hostname});
try writer.splatByteAll(' ', widest - entry.hostname.len + 2);

View File

@@ -2,6 +2,7 @@ const std = @import("std");
const Allocator = std.mem.Allocator;
const Action = @import("../cli.zig").ghostty.Action;
const apprt = @import("../apprt.zig");
const global = @import("../global.zig");
pub const Options = struct {
/// If set, connect to a custom instance of Ghostty.
@@ -39,7 +40,7 @@ pub const Options = struct {
/// Available since: 1.4.0
pub fn run(alloc: Allocator) !u8 {
var buf: [256]u8 = undefined;
var stderr_writer = std.fs.File.stderr().writer(&buf);
var stderr_writer = std.Io.File.stderr().writer(global.io(), &buf);
const stderr = &stderr_writer.interface;
if (apprt.App.performIpc(

View File

@@ -3,6 +3,7 @@ const Allocator = std.mem.Allocator;
const args = @import("args.zig");
const Action = @import("ghostty.zig").Action;
const Config = @import("../config.zig").Config;
const global = @import("../global.zig");
pub const Options = struct {
/// The path of the config file to validate. If this isn't specified,
@@ -34,13 +35,13 @@ pub fn run(alloc: std.mem.Allocator) !u8 {
defer opts.deinit();
{
var iter = try args.argsIterator(alloc);
var iter = try args.argsIterator(alloc, global.args());
defer iter.deinit();
try args.parse(Options, alloc, &opts, &iter);
}
var buffer: [1024]u8 = undefined;
var stdout_writer = std.fs.File.stdout().writer(&buffer);
var stdout_writer = std.Io.File.stdout().writer(global.io(), &buffer);
const stdout = &stdout_writer.interface;
const result = runInner(alloc, opts, stdout);
try stdout_writer.end();
@@ -58,7 +59,11 @@ fn runInner(
// If a config path is passed, validate it, otherwise validate default configs
if (opts.@"config-file") |config_path| {
var buf: [std.fs.max_path_bytes]u8 = undefined;
const abs_path = try std.fs.cwd().realpath(config_path, &buf);
const abs_path = buf[0..try std.Io.Dir.cwd().realPathFile(
global.io(),
config_path,
&buf,
)];
try cfg.loadFile(alloc, abs_path);
try cfg.loadRecursiveFiles(alloc);
} else {

View File

@@ -6,6 +6,7 @@ const build_config = @import("../build_config.zig");
const internal_os = @import("../os/main.zig");
const xev = @import("../global.zig").xev;
const renderer = @import("../renderer.zig");
const global = @import("../global.zig");
const gtk_version = @import("../apprt/gtk/gtk_version.zig");
const adw_version = @import("../apprt/gtk/adw_version.zig");
@@ -16,11 +17,14 @@ pub const Options = struct {};
/// either `+version` or `--version`.
pub fn run(alloc: Allocator) !u8 {
var buffer: [1024]u8 = undefined;
const stdout_file: std.fs.File = .stdout();
var stdout_writer = stdout_file.writer(&buffer);
const stdout_file: std.Io.File = .stdout();
var stdout_writer = stdout_file.writer(global.io(), &buffer);
var environ_map = try global.environMap();
defer environ_map.deinit();
const stdout = &stdout_writer.interface;
const tty = stdout_file.isTty();
const tty = try stdout_file.isTty(global.io());
if (tty) if (build_config.version.build) |commit_hash| {
try stdout.print(
@@ -48,7 +52,7 @@ pub fn run(alloc: Allocator) !u8 {
defer if (kernel_info) |k| alloc.free(k);
try stdout.print(" - kernel version: {s}\n", .{kernel_info orelse "Kernel information unavailable"});
}
try stdout.print(" - desktop env : {t}\n", .{internal_os.desktopEnvironment()});
try stdout.print(" - desktop env : {t}\n", .{internal_os.desktopEnvironment(&environ_map)});
try stdout.print(" - GTK version :\n", .{});
try stdout.print(" build : {f}\n", .{gtk_version.comptime_version});
try stdout.print(" runtime : {f}\n", .{gtk_version.getRuntimeVersion()});

View File

@@ -1,7 +1,7 @@
const builtin = @import("builtin");
const std = @import("std");
const inputpkg = @import("../input.zig");
const state = &@import("../global.zig").state;
const global = @import("../global.zig");
const String = @import("../main_c.zig").String;
const Config = @import("Config.zig");
@@ -13,14 +13,14 @@ const log = std.log.scoped(.config);
/// Create a new configuration filled with the initial default values.
export fn ghostty_config_new() ?*Config {
const result = state.alloc.create(Config) catch |err| {
const result = global.alloc().create(Config) catch |err| {
log.err("error allocating config err={}", .{err});
return null;
};
result.* = Config.default(state.alloc) catch |err| {
result.* = Config.default(global.alloc()) catch |err| {
log.err("error creating config err={}", .{err});
state.alloc.destroy(result);
global.alloc().destroy(result);
return null;
};
@@ -30,20 +30,20 @@ export fn ghostty_config_new() ?*Config {
export fn ghostty_config_free(ptr: ?*Config) void {
if (ptr) |v| {
v.deinit();
state.alloc.destroy(v);
global.alloc().destroy(v);
}
}
/// Deep clone the configuration.
export fn ghostty_config_clone(self: *Config) ?*Config {
const result = state.alloc.create(Config) catch |err| {
const result = global.alloc().create(Config) catch |err| {
log.err("error allocating config err={}", .{err});
return null;
};
result.* = self.clone(state.alloc) catch |err| {
result.* = self.clone(global.alloc()) catch |err| {
log.err("error cloning config err={}", .{err});
state.alloc.destroy(result);
global.alloc().destroy(result);
return null;
};
@@ -52,7 +52,7 @@ export fn ghostty_config_clone(self: *Config) ?*Config {
/// Load the configuration from the CLI args.
export fn ghostty_config_load_cli_args(self: *Config) void {
self.loadCliArgs(state.alloc) catch |err| {
self.loadCliArgs(global.alloc()) catch |err| {
log.err("error loading config err={}", .{err});
};
}
@@ -61,7 +61,7 @@ export fn ghostty_config_load_cli_args(self: *Config) void {
/// is usually done first. The default file locations are locations
/// such as the home directory.
export fn ghostty_config_load_default_files(self: *Config) void {
self.loadDefaultFiles(state.alloc) catch |err| {
self.loadDefaultFiles(global.alloc()) catch |err| {
log.err("error loading config err={}", .{err});
};
}
@@ -70,7 +70,7 @@ export fn ghostty_config_load_default_files(self: *Config) void {
/// The path must be null-terminated.
export fn ghostty_config_load_file(self: *Config, path: [*:0]const u8) void {
const path_slice = std.mem.span(path);
self.loadFile(state.alloc, path_slice) catch |err| {
self.loadFile(global.alloc(), path_slice) catch |err| {
log.err("error loading config from file path={s} err={}", .{ path_slice, err });
};
}
@@ -79,7 +79,7 @@ export fn ghostty_config_load_file(self: *Config, path: [*:0]const u8) void {
/// file locations in the previously loaded configuration. This will
/// recursively continue to load up to a built-in limit.
export fn ghostty_config_load_recursive_files(self: *Config) void {
self.loadRecursiveFiles(state.alloc) catch |err| {
self.loadRecursiveFiles(global.alloc()) catch |err| {
log.err("error loading config err={}", .{err});
};
}
@@ -133,7 +133,7 @@ export fn ghostty_config_get_diagnostic(self: *Config, idx: u32) Diagnostic {
}
export fn ghostty_config_open_path() String {
const path = edit.openPath(state.alloc) catch |err| {
const path = edit.openPath(global.alloc()) catch |err| {
log.err("error opening config in editor err={}", .{err});
return .empty;
};

View File

@@ -16,7 +16,7 @@ const build_config = @import("../build_config.zig");
const assert = @import("../quirks.zig").inlineAssert;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const global_state = &@import("../global.zig").state;
const global = @import("../global.zig");
const deepEqual = @import("../datastruct/comparison.zig").deepEqual;
const fontpkg = @import("../font/main.zig");
const inputpkg = @import("../input.zig");
@@ -3848,7 +3848,7 @@ _conditional_set: std.EnumSet(conditional.Key) = .{},
/// The steps we can use to reload the configuration after it has been loaded
/// without reopening the files. This is used in very specific cases such
/// as loadTheme which has more details on why.
_replay_steps: std.ArrayListUnmanaged(Replay.Step) = .{},
_replay_steps: std.ArrayList(Replay.Step) = .empty,
/// Set to true if Ghostty was executed as xdg-terminal-exec on Linux.
@"_xdg-terminal-exec": bool = false,
@@ -3922,7 +3922,7 @@ pub fn loadIter(
/// `path` must be resolved and absolute.
pub fn loadFile(self: *Config, alloc: Allocator, path: []const u8) !void {
assert(std.fs.path.isAbsolute(path));
var file = file_load.open(path) catch |err| switch (err) {
var file = file_load.open(global.io(), path) catch |err| switch (err) {
error.NotAFile => {
log.warn(
"config-file {s}: not reading because it is not a file",
@@ -3933,16 +3933,16 @@ pub fn loadFile(self: *Config, alloc: Allocator, path: []const u8) !void {
else => return err,
};
defer file.close();
defer file.close(global.io());
try self.loadFsFile(alloc, &file, path);
}
/// Load config from the given File.
fn loadFsFile(self: *Config, alloc: Allocator, file: *std.fs.File, path: []const u8) !void {
fn loadFsFile(self: *Config, alloc: Allocator, file: *std.Io.File, path: []const u8) !void {
std.log.info("reading configuration file path={s}", .{path});
var buf: [2048]u8 = undefined;
var file_reader = file.reader(&buf);
var file_reader = file.reader(global.io(), &buf);
const reader = &file_reader.interface;
try self.loadReader(alloc, reader, path);
}
@@ -4035,12 +4035,12 @@ pub fn loadOptionalFile(
fn writeConfigTemplate(path: []const u8) !void {
log.info("creating template config file: path={s}", .{path});
if (std.fs.path.dirname(path)) |dir_path| {
try std.fs.cwd().makePath(dir_path);
try std.Io.Dir.cwd().createDirPath(global.io(), dir_path);
}
const file = try std.fs.createFileAbsolute(path, .{});
defer file.close();
const file = try std.Io.Dir.createFileAbsolute(global.io(), path, .{});
defer file.close(global.io());
var buf: [4096]u8 = undefined;
var file_writer = file.writer(&buf);
var file_writer = file.writer(global.io(), &buf);
const writer = &file_writer.interface;
try writer.print(
@embedFile("./config-template"),
@@ -4134,7 +4134,7 @@ pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void {
.windows => {},
// Fast-path if we are Linux/BSD and have no args.
.linux, .freebsd => if (std.os.argv.len <= 1) return,
.linux, .freebsd => if (global.args().vector.len <= 1) return,
// Everything else we have to at least try because it may
// not use std.os.argv.
@@ -4153,7 +4153,7 @@ pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void {
//
// See: https://github.com/Vladimir-csp/xdg-terminal-exec
if ((comptime builtin.os.tag == .linux) or (comptime builtin.os.tag == .freebsd)) {
if (internal_os.xdg.parseTerminalExec(std.os.argv)) |args| {
if (internal_os.xdg.parseTerminalExec(global.args().vector)) |args| {
const arena_alloc = self._arena.?.allocator();
// First, we add an artificial "-e" so that if we
@@ -4203,7 +4203,7 @@ pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void {
}
// Initialize our CLI iterator.
var iter = try cli.args.argsIterator(alloc_gpa);
var iter = try cli.args.argsIterator(alloc_gpa, global.args());
defer iter.deinit();
try self.loadIter(alloc_gpa, &iter);
@@ -4229,7 +4229,11 @@ pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void {
// Any paths referenced from the CLI are relative to the current working
// directory.
var buf: [std.fs.max_path_bytes]u8 = undefined;
try self.expandPaths(try std.fs.cwd().realpath(".", &buf));
try self.expandPaths(buf[0..try std.Io.Dir.cwd().realPathFile(
global.io(),
".",
&buf,
)]);
}
/// Load and parse the config files that were added in the "config-file" key.
@@ -4292,7 +4296,7 @@ pub fn loadRecursiveFiles(self: *Config, alloc_gpa: Allocator) !void {
continue;
}
var file = std.fs.openFileAbsolute(path, .{}) catch |err| {
var file = std.Io.Dir.openFileAbsolute(global.io(), path, .{}) catch |err| {
if (err != error.FileNotFound or !optional) {
const diag: cli.Diagnostic = .{
.message = try std.fmt.allocPrintSentinel(
@@ -4308,9 +4312,9 @@ pub fn loadRecursiveFiles(self: *Config, alloc_gpa: Allocator) !void {
}
continue;
};
defer file.close();
defer file.close(global.io());
const stat = try file.stat();
const stat = try file.stat(global.io());
switch (stat.kind) {
.file => {},
else => |kind| {
@@ -4455,7 +4459,7 @@ fn loadTheme(self: *Config, theme: Theme) !void {
)) orelse return;
const path = themefile.path;
const file = themefile.file;
defer file.close();
defer file.close(global.io());
// From this point onwards, we load the theme and do a bit of a dance
// to achieve two separate goals:
@@ -4477,7 +4481,7 @@ fn loadTheme(self: *Config, theme: Theme) !void {
// Load our theme
var buf: [2048]u8 = undefined;
var file_reader = file.reader(&buf);
var file_reader = file.reader(global.io(), &buf);
const reader = &file_reader.interface;
var iter: cli.args.LineIterator = .{ .r = reader, .filepath = path };
try new_config.loadIter(alloc_gpa, &iter);
@@ -4614,15 +4618,19 @@ pub fn finalize(self: *Config) !void {
// read from SHELL if we're in a probable CLI environment.
if (!probable_cli) break :shell_env;
if (std.process.getEnvVarOwned(alloc, "SHELL")) |value| {
log.info("default shell source=env value={s}", .{value});
const value = global.environ().getAlloc(alloc, "SHELL") catch |err| switch (err) {
error.EnvironmentVariableMissing => break :shell_env,
else => return err,
};
defer alloc.free(value);
const copy = try alloc.dupeZ(u8, value);
self.command = .{ .shell = copy };
log.info("default shell source=env value={s}", .{value});
// If we don't need the working directory, then we can exit now.
if (wd != .home) break :command;
} else |_| {}
const copy = try alloc.dupeZ(u8, value);
self.command = .{ .shell = copy };
// If we don't need the working directory, then we can exit now.
if (wd != .home) break :command;
}
switch (builtin.os.tag) {
@@ -4633,8 +4641,10 @@ pub fn finalize(self: *Config) !void {
}
if (wd == .home) {
var environ_map = try global.environMap();
defer environ_map.deinit();
var buf: [std.fs.max_path_bytes]u8 = undefined;
if (try internal_os.home(&buf)) |home| {
if (try internal_os.home(&environ_map, &buf)) |home| {
wd = .{ .path = try alloc.dupe(u8, home) };
} else {
wd = .inherit;
@@ -5123,14 +5133,17 @@ fn probableCliEnvironment() bool {
else => {},
}
// If we have TERM_PROGRAM set to a non-empty value, we assume
// a graphical terminal environment.
if (std.posix.getenv("TERM_PROGRAM")) |v| {
if (v.len > 0) return true;
}
// If we have TERM_PROGRAM set to a non-empty value, we assume a graphical
// terminal environment.
//
// TODO: This is not available on WASI without libc due to the memory
// allocation requirement. This restricts this function to said platforms.
// To be fair, the legacy getenv path had more restrictive issues (no WASI
// period, or Windows for that matter).
if (global.environ().containsUnemptyConstant("TERM_PROGRAM")) return true;
// CLI arguments makes things probable
if (std.os.argv.len > 1) return true;
if (global.args().vector.len > 1) return true;
// Unlikely CLI environment
return false;
@@ -5358,7 +5371,11 @@ pub const WorkingDirectory = union(enum) {
if (!std.mem.startsWith(u8, path, "~/")) return;
var buf: [std.fs.max_path_bytes]u8 = undefined;
const expanded = internal_os.expandHome(path, &buf) catch |err| {
const expanded = expanded: {
var environ_map = global.environMap() catch |err| break :expanded err;
defer environ_map.deinit();
break :expanded internal_os.expandHome(&environ_map, path, &buf);
} catch |err| {
log.warn(
"error expanding home directory for working-directory path={s}: {}",
.{ path, err },
@@ -5417,6 +5434,8 @@ pub const WorkingDirectory = union(enum) {
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var environ_map = try testing.environ.createMap(testing.allocator);
defer environ_map.deinit();
{
var wd: Self = .{ .path = "~/projects/ghostty" };
@@ -5424,6 +5443,7 @@ pub const WorkingDirectory = union(enum) {
var buf: [std.fs.max_path_bytes]u8 = undefined;
const expected = internal_os.expandHome(
&environ_map,
"~/projects/ghostty",
&buf,
) catch "~/projects/ghostty";
@@ -5658,8 +5678,8 @@ pub const BoldColor = union(enum) {
pub const ColorList = struct {
const Self = @This();
colors: std.ArrayListUnmanaged(Color) = .{},
colors_c: std.ArrayListUnmanaged(Color.C) = .{},
colors: std.ArrayList(Color) = .empty,
colors_c: std.ArrayList(Color.C) = .empty,
/// ghostty_config_color_list_s
pub const C = extern struct {
@@ -5972,7 +5992,7 @@ pub const RepeatableString = struct {
const Self = @This();
// Allocator for the list is the arena for the parent config.
list: std.ArrayListUnmanaged([:0]const u8) = .{},
list: std.ArrayList([:0]const u8) = .empty,
// If true, then the next value will clear the list and start over
// rather than append. This is a bit of a hack but is here to make
@@ -6262,7 +6282,7 @@ pub const RepeatableFontVariation = struct {
const Self = @This();
// Allocator for the list is the arena for the parent config.
list: std.ArrayListUnmanaged(fontpkg.face.Variation) = .{},
list: std.ArrayList(fontpkg.face.Variation) = .empty,
pub fn parseCLI(self: *Self, alloc: Allocator, input_: ?[]const u8) !void {
const input = input_ orelse return error.ValueRequired;
@@ -8545,7 +8565,7 @@ pub const FontShapingBreak = packed struct {
pub const RepeatableLink = struct {
const Self = @This();
links: std.ArrayListUnmanaged(inputpkg.Link) = .{},
links: std.ArrayList(inputpkg.Link) = .empty,
pub fn parseCLI(self: *Self, alloc: Allocator, input_: ?[]const u8) !void {
_ = self;
@@ -10412,23 +10432,23 @@ test "clone can then change conditional state" {
defer td.deinit();
var buf: [4096]u8 = undefined;
{
var file = try td.dir.createFile("theme_light", .{});
defer file.close();
var writer = file.writer(&buf);
var file = try td.dir.createFile(testing.io, "theme_light", .{});
defer file.close(testing.io);
var writer = file.writer(testing.io, &buf);
try writer.interface.writeAll(@embedFile("testdata/theme_light"));
try writer.end();
}
{
var file = try td.dir.createFile("theme_dark", .{});
defer file.close();
var writer = file.writer(&buf);
var file = try td.dir.createFile(testing.io, "theme_dark", .{});
defer file.close(testing.io);
var writer = file.writer(testing.io, &buf);
try writer.interface.writeAll(@embedFile("testdata/theme_dark"));
try writer.end();
}
var light_buf: [std.fs.max_path_bytes]u8 = undefined;
const light = try td.dir.realpath("theme_light", &light_buf);
const light = light_buf[0..try td.dir.realPathFile(testing.io, "theme_light", &light_buf)];
var dark_buf: [std.fs.max_path_bytes]u8 = undefined;
const dark = try td.dir.realpath("theme_dark", &dark_buf);
const dark = dark_buf[0..try td.dir.realPathFile(testing.io, "theme_dark", &dark_buf)];
var cfg_light = try Config.default(alloc);
defer cfg_light.deinit();
@@ -10490,6 +10510,8 @@ test "clone preserves conditional set" {
test "working-directory expands tilde" {
const testing = std.testing;
const alloc = testing.allocator;
var environ_map = try testing.environ.createMap(testing.allocator);
defer environ_map.deinit();
var cfg = try Config.default(alloc);
defer cfg.deinit();
@@ -10501,6 +10523,7 @@ test "working-directory expands tilde" {
var buf: [std.fs.max_path_bytes]u8 = undefined;
const expected = internal_os.expandHome(
&environ_map,
"~/projects/ghostty",
&buf,
) catch "~/projects/ghostty";
@@ -10571,14 +10594,14 @@ test "theme loading" {
defer td.deinit();
var buf: [4096]u8 = undefined;
{
var file = try td.dir.createFile("theme", .{});
defer file.close();
var writer = file.writer(&buf);
var file = try td.dir.createFile(testing.io, "theme", .{});
defer file.close(testing.io);
var writer = file.writer(testing.io, &buf);
try writer.interface.writeAll(@embedFile("testdata/theme_simple"));
try writer.end();
}
var path_buf: [std.fs.max_path_bytes]u8 = undefined;
const path = try td.dir.realpath("theme", &path_buf);
const path = path_buf[0..try td.dir.realPathFile(testing.io, "theme", &path_buf)];
var cfg = try Config.default(alloc);
defer cfg.deinit();
@@ -10610,14 +10633,14 @@ test "theme loading preserves conditional state" {
defer td.deinit();
var buf: [4096]u8 = undefined;
{
var file = try td.dir.createFile("theme", .{});
defer file.close();
var writer = file.writer(&buf);
var file = try td.dir.createFile(testing.io, "theme", .{});
defer file.close(testing.io);
var writer = file.writer(testing.io, &buf);
try writer.interface.writeAll(@embedFile("testdata/theme_simple"));
try writer.end();
}
var path_buf: [std.fs.max_path_bytes]u8 = undefined;
const path = try td.dir.realpath("theme", &path_buf);
const path = path_buf[0..try td.dir.realPathFile(testing.io, "theme", &path_buf)];
var cfg = try Config.default(alloc);
defer cfg.deinit();
@@ -10643,14 +10666,14 @@ test "theme priority is lower than config" {
defer td.deinit();
var buf: [4096]u8 = undefined;
{
var file = try td.dir.createFile("theme", .{});
defer file.close();
var writer = file.writer(&buf);
var file = try td.dir.createFile(testing.io, "theme", .{});
defer file.close(testing.io);
var writer = file.writer(testing.io, &buf);
try writer.interface.writeAll(@embedFile("testdata/theme_simple"));
try writer.end();
}
var path_buf: [std.fs.max_path_bytes]u8 = undefined;
const path = try td.dir.realpath("theme", &path_buf);
const path = path_buf[0..try td.dir.realPathFile(testing.io, "theme", &path_buf)];
var cfg = try Config.default(alloc);
defer cfg.deinit();
@@ -10680,23 +10703,23 @@ test "theme loading correct light/dark" {
defer td.deinit();
var buf: [4096]u8 = undefined;
{
var file = try td.dir.createFile("theme_light", .{});
defer file.close();
var writer = file.writer(&buf);
var file = try td.dir.createFile(testing.io, "theme_light", .{});
defer file.close(testing.io);
var writer = file.writer(testing.io, &buf);
try writer.interface.writeAll(@embedFile("testdata/theme_light"));
try writer.end();
}
{
var file = try td.dir.createFile("theme_dark", .{});
defer file.close();
var writer = file.writer(&buf);
var file = try td.dir.createFile(testing.io, "theme_dark", .{});
defer file.close(testing.io);
var writer = file.writer(testing.io, &buf);
try writer.interface.writeAll(@embedFile("testdata/theme_dark"));
try writer.end();
}
var light_buf: [std.fs.max_path_bytes]u8 = undefined;
const light = try td.dir.realpath("theme_light", &light_buf);
const light = light_buf[0..try td.dir.realPathFile(testing.io, "theme_light", &light_buf)];
var dark_buf: [std.fs.max_path_bytes]u8 = undefined;
const dark = try td.dir.realpath("theme_dark", &dark_buf);
const dark = dark_buf[0..try td.dir.realPathFile(testing.io, "theme_dark", &dark_buf)];
// Light
{

View File

@@ -84,7 +84,7 @@ fn getValue(ptr_raw: *anyopaque, value: anytype) bool {
ptr.* = @intCast(@as(Backing, @bitCast(value)));
},
.@"union" => |_| {
.@"union" => {
if (@hasDecl(T, "cval")) {
const PtrT = @typeInfo(@TypeOf(T.cval)).@"fn".return_type.?;
const ptr: *PtrT = @ptrCast(@alignCast(ptr_raw));

View File

@@ -123,7 +123,7 @@ pub const Command = union(enum) {
/// Iterates over each argument in the command.
pub const ArgIterator = union(enum) {
shell: std.process.ArgIteratorGeneral(.{}),
shell: std.process.Args.IteratorGeneral(.{}),
direct: struct {
i: usize = 0,
args: []const [:0]const u8,

View File

@@ -39,18 +39,16 @@ pub const State = struct {
/// An enum of the available conditional configuration keys.
pub const Key = key: {
const stateInfo = @typeInfo(State).@"struct";
var fields: [stateInfo.fields.len]std.builtin.Type.EnumField = undefined;
for (stateInfo.fields, 0..) |field, i| fields[i] = .{
.name = field.name,
.value = i,
};
const TagInt = std.math.IntFittingRange(0, stateInfo.fields.len - 1);
var names: [stateInfo.fields.len][]const u8 = undefined;
var values: [stateInfo.fields.len]TagInt = undefined;
break :key @Type(.{ .@"enum" = .{
.tag_type = std.math.IntFittingRange(0, fields.len - 1),
.fields = &fields,
.decls = &.{},
.is_exhaustive = true,
} });
for (stateInfo.fields, &names, &values, 0..) |field, *name, *v, i| {
name.* = field.name;
v.* = @intCast(i);
}
break :key @Enum(TagInt, .exhaustive, &names, &values);
};
/// A single conditional that can be true or false.

View File

@@ -4,6 +4,7 @@ const assert = @import("../quirks.zig").inlineAssert;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const file_load = @import("file_load.zig");
const global = @import("../global.zig");
/// The path to the configuration that should be opened for editing.
///
@@ -28,11 +29,12 @@ pub fn openPath(alloc_gpa: Allocator) ![:0]const u8 {
// Create config directory recursively.
if (std.fs.path.dirname(config_path)) |config_dir| {
try std.fs.cwd().makePath(config_dir);
try std.Io.Dir.cwd().createDirPath(global.io(), config_dir);
}
// Try to create file and go on if it already exists
_ = std.fs.createFileAbsolute(
_ = std.Io.Dir.createFileAbsolute(
global.io(),
config_path,
.{ .exclusive = true },
) catch |err| {
@@ -58,7 +60,7 @@ fn configPath(alloc_arena: Allocator) ![]const u8 {
// exists.
var exists: ?[]const u8 = null;
for (paths) |path| {
const f = std.fs.openFileAbsolute(path, .{}) catch |err| {
const f = std.Io.Dir.openFileAbsolute(global.io(), path, .{}) catch |err| {
switch (err) {
// File doesn't exist, continue.
error.BadPathName, error.FileNotFound => continue,
@@ -67,10 +69,10 @@ fn configPath(alloc_arena: Allocator) ![]const u8 {
else => return err,
}
};
defer f.close();
defer f.close(global.io());
// We expect stat to succeed because we just opened the file.
const stat = try f.stat();
const stat = try f.stat(global.io());
// If the file is non-empty, return it.
if (stat.size > 0) return path;

View File

@@ -3,14 +3,18 @@ const builtin = @import("builtin");
const assert = @import("../quirks.zig").inlineAssert;
const Allocator = std.mem.Allocator;
const internal_os = @import("../os/main.zig");
const global = @import("../global.zig");
const log = std.log.scoped(.config);
/// Default path for the XDG home configuration file. Returned value
/// must be freed by the caller.
pub fn defaultXdgPath(alloc: Allocator) ![]const u8 {
var environ_map = try global.environMap();
defer environ_map.deinit();
return try internal_os.xdg.config(
alloc,
&environ_map,
.{ .subdir = "ghostty/config.ghostty" },
);
}
@@ -18,8 +22,11 @@ pub fn defaultXdgPath(alloc: Allocator) ![]const u8 {
/// Ghostty <1.3.0 default path for the XDG home configuration file.
/// Returned value must be freed by the caller.
pub fn legacyDefaultXdgPath(alloc: Allocator) ![]const u8 {
var environ_map = try global.environMap();
defer environ_map.deinit();
return try internal_os.xdg.config(
alloc,
&environ_map,
.{ .subdir = "ghostty/config" },
);
}
@@ -29,16 +36,16 @@ pub fn legacyDefaultXdgPath(alloc: Allocator) ![]const u8 {
pub fn preferredXdgPath(alloc: Allocator) ![]const u8 {
// If the XDG path exists, use that.
const xdg_path = try defaultXdgPath(alloc);
if (open(xdg_path)) |f| {
f.close();
if (open(global.io(), xdg_path)) |f| {
f.close(global.io());
return xdg_path;
} else |_| {}
// Try the legacy path
errdefer alloc.free(xdg_path);
const legacy_xdg_path = try legacyDefaultXdgPath(alloc);
if (open(legacy_xdg_path)) |f| {
f.close();
if (open(global.io(), legacy_xdg_path)) |f| {
f.close(global.io());
alloc.free(xdg_path);
return legacy_xdg_path;
} else |_| {}
@@ -66,16 +73,16 @@ pub fn legacyDefaultAppSupportPath(alloc: Allocator) ![]const u8 {
pub fn preferredAppSupportPath(alloc: Allocator) ![]const u8 {
// If the app support path exists, use that.
const app_support_path = try defaultAppSupportPath(alloc);
if (open(app_support_path)) |f| {
f.close();
if (open(global.io(), app_support_path)) |f| {
f.close(global.io());
return app_support_path;
} else |_| {}
// Try the legacy path
errdefer alloc.free(app_support_path);
const legacy_app_support_path = try legacyDefaultAppSupportPath(alloc);
if (open(legacy_app_support_path)) |f| {
f.close();
if (open(global.io(), legacy_app_support_path)) |f| {
f.close(global.io());
alloc.free(app_support_path);
return legacy_app_support_path;
} else |_| {}
@@ -99,19 +106,19 @@ pub fn preferredDefaultFilePath(alloc: Allocator) ![]const u8 {
// macOS prefers the Application Support directory
// if it exists.
const app_support_path = try preferredAppSupportPath(alloc);
const app_support_file = open(app_support_path) catch {
const app_support_file = open(global.io(), app_support_path) catch {
// Try the XDG path if it exists
const xdg_path = try preferredXdgPath(alloc);
const xdg_file = open(xdg_path) catch {
const xdg_file = open(global.io(), xdg_path) catch {
// If neither file exists, use app support
alloc.free(xdg_path);
return app_support_path;
};
xdg_file.close();
xdg_file.close(global.io());
alloc.free(app_support_path);
return xdg_path;
};
app_support_file.close();
app_support_file.close(global.io());
return app_support_path;
},
@@ -130,10 +137,11 @@ const OpenFileError = error{
/// Opens the file at the given path and returns the file handle
/// if it exists and is non-empty. This also constrains the possible
/// errors to a smaller set that we can explicitly handle.
pub fn open(path: []const u8) OpenFileError!std.fs.File {
pub fn open(io: std.Io, path: []const u8) OpenFileError!std.Io.File {
assert(std.fs.path.isAbsolute(path));
var file = std.fs.openFileAbsolute(
var file = std.Io.Dir.openFileAbsolute(
io,
path,
.{},
) catch |err| switch (err) {
@@ -146,9 +154,9 @@ pub fn open(path: []const u8) OpenFileError!std.fs.File {
return OpenFileError.FileOpenFailed;
},
};
errdefer file.close();
errdefer file.close(io);
const stat = file.stat() catch |err| {
const stat = file.stat(io) catch |err| {
log.warn("error getting file stat path={s} err={}", .{
path,
err,

Some files were not shown because too many files have changed in this diff Show More