diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-09-10 21:51:57 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-10 21:51:57 +0300 |
| commit | 425c86179791cd32e2d4ce0ae7f269940b4b0663 (patch) | |
| tree | e52c4383a6d69216cbe2a32638bf90841d7e2189 /vendor/libghostty-vt/src/global.zig | |
| parent | 90e947a6e26e02f10ec7f1694c4eb5b78a86406b (diff) | |
chore: upgrade libghostty and preserve terminal behavior (#3906)
Diffstat (limited to 'vendor/libghostty-vt/src/global.zig')
| -rw-r--r-- | vendor/libghostty-vt/src/global.zig | 513 |
1 files changed, 362 insertions, 151 deletions
diff --git a/vendor/libghostty-vt/src/global.zig b/vendor/libghostty-vt/src/global.zig index 29eaf5f3..3312039a 100644 --- a/vendor/libghostty-vt/src/global.zig +++ b/vendor/libghostty-vt/src/global.zig @@ -1,6 +1,7 @@ const std = @import("std"); const builtin = @import("builtin"); const build_config = @import("build_config.zig"); +const build_options = @import("build_options"); const cli = @import("cli.zig"); const internal_os = @import("os/main.zig"); const fontconfig = @import("fontconfig"); @@ -10,26 +11,378 @@ const oni = @import("oniguruma"); const crash = @import("crash/main.zig"); const renderer = @import("renderer.zig"); const apprt = @import("apprt.zig"); +const assert = @import("quirks.zig").inlineAssert; +const allocTmpDir = @import("os/file.zig").allocTmpDir; +const freeTmpDir = @import("os/file.zig").freeTmpDir; + +// This file should only be imported for certain platforms. +comptime { + switch (@import("terminal_options").artifact) { + .ghostty => {}, + // This file is not allowed to be included in libghostty-vt + .lib => @compileError("global state cannot be used in libghostty-vt"), + } +} /// We export the xev backend we want to use so that the rest of /// Ghostty can import this once and have access to the proper /// backend. pub const xev = @import("xev").Dynamic; -/// Global process state. This is initialized in main() for exe artifacts -/// and by ghostty_init() for lib artifacts. This should ONLY be used by -/// the C API. The Zig API should NOT use any global state and should -/// rely on allocators being passed in as parameters. -pub var state: GlobalState = undefined; +/// Global process state. This is initialized in main() for exe artifacts and +/// by ghostty_init() for lib artifacts. Most other methods in this file will +/// retrieve items stored in this state. +var state: ?GlobalState = null; + +pub const InitOpts = union(enum) { + main: std.process.Init.Minimal, + + /// Same as `main` but for auxiliary tool binaries (e.g. ghostty-bench + /// and ghostty-gen) that have their own CLI action namespace. This + /// skips detection of ghostty app CLI actions, since tool actions + /// (e.g. `+terminal-stream`) are not valid app actions and would + /// otherwise cause init to fail with InvalidAction. + tool: std.process.Init.Minimal, + + c: struct { + argc: usize, + argv: [*][*:0]u8, + environ: std.process.Environ, + }, +}; + +/// Initialize the global state. +pub fn init(opts: InitOpts) !void { + // Initialize ourself to nothing so we don't have any extra state. + // IMPORTANT: this MUST be initialized before any log output because + // the log function uses the global state. + state = .{ + .io_impl = undefined, + .gpa = null, + .alloc = undefined, + .environ = switch (opts) { + .main, .tool => |m| m.environ, + .c => |c| c.environ, + }, + .args = switch (opts) { + .main, .tool => |m| m.args, + // TODO: Using the C API from Windows is unsupported at this time. + // + // When do we plan on supporting Windows, it's recommended to + // ensure that the C API can take a UNICODE_STRING (aka []16, a + // WTF-16 string) so that it can just be passed into + // std.process.Args.Vector directly. + .c => |c| .{ .vector = if (builtin.os.tag == .windows) + return error.UnsupportedOSForCApi + else + c.argv[0..c.argc] }, + }, + .tmp_dir_path = null, + .action = null, + .logging = .{}, + .rlimits = .{}, + .resources_dir = .{}, + }; + const self = &state.?; + errdefer deinit(); + + self.gpa = gpa: { + // Use the libc allocator if it is available because it is WAY + // faster than GPA. We only do this in release modes so that we + // can get easy memory leak detection in debug modes. + if (builtin.link_libc) { + if (switch (builtin.mode) { + .ReleaseSafe, .ReleaseFast => true, + + // We also use it if we can detect we're running under + // Valgrind since Valgrind only instruments the C allocator + else => std.valgrind.runningOnValgrind() > 0, + }) break :gpa null; + } + + break :gpa .init; + }; + + self.alloc = if (self.gpa) |*value| + value.allocator() + else if (builtin.link_libc) + std.heap.c_allocator + else + unreachable; + + // Set up our main I/O implementation (fully threaded w/allocator). Note + // that we cannot use any implementation supplied from main at this point, + // because there are some later initialization steps that depend on us + // mutating the environment, and thus it needs to be re-synced farther + // down. For that, we need a stable implementation that allows us to do so. + self.io_impl = .init(self.alloc, .{ + .argv0 = .init(self.args), + .environ = self.environ, + }); + + // Discover and save the temporary directory path + self.tmp_dir_path = try allocTmpDir(self.alloc, self.environ); + + // We first try to parse any action that we may be executing. + // Tool binaries (ghostty-bench, ghostty-gen) have their own action + // namespace and detect their own actions, so we skip detection here. + self.action = switch (opts) { + .main, .c => try cli.action.detectArgs( + cli.ghostty.Action, + self.alloc, + self.args, + ), + .tool => null, + }; + + // If we have an action executing, we disable logging by default + // since we write to stderr we don't want logs messing up our + // output. + if (self.action != null) self.logging.stderr = false; + + // I don't love the env var name but I don't have it in my heart + // to parse CLI args 3 times (once for actions, once for config, + // maybe once for logging) so for now this is an easy way to do + // this. Env vars are useful for logging too because they are + // easy to set. + logging: { + const v = self.environ.getAlloc(self.alloc, "GHOSTTY_LOG") catch |err| switch (err) { + error.EnvironmentVariableMissing => break :logging, + else => return err, + }; + defer self.alloc.free(v); + self.logging = cli.args.parsePackedStruct(GlobalState.Logging, v) catch .{}; + } + + // Setup our signal handlers before logging + GlobalState.initSignals(); + + // Setup our Xev backend if we're dynamic + if (comptime xev.dynamic) xev.detect() catch |err| { + std.log.warn("failed to detect xev backend, falling back to " ++ + "most compatible backend err={}", .{err}); + }; + + // Output some debug information right away + std.log.info("ghostty version={s}", .{build_config.version_string}); + std.log.info("ghostty build optimize={s}", .{build_config.mode_string}); + std.log.info("runtime={}", .{build_config.app_runtime}); + std.log.info("font_backend={}", .{build_config.font_backend}); + if (comptime build_config.font_backend.hasHarfbuzz()) { + std.log.info("dependency harfbuzz={s}", .{harfbuzz.versionString()}); + } + if (comptime build_config.font_backend.hasFontconfig()) { + std.log.info("dependency fontconfig={d}", .{fontconfig.version()}); + } + std.log.info("renderer={}", .{renderer.Renderer}); + std.log.info("libxev default backend={t}", .{xev.backend}); + + // As early as possible, initialize our resource limits. + self.rlimits = .init(); + + if (build_options.sentry) { + // Initialize our crash reporting. The environ map snapshot is + // owned by crash.init (it is freed by the init thread). + const environ_map = try self.environ.createMap(self.alloc); + crash.init(self.alloc, environ_map) catch |err| { + std.log.warn( + "sentry init failed, no crash capture available err={}", + .{err}, + ); + }; + } + + // const sentrylib = @import("sentry"); + // if (sentrylib.captureEvent(sentrylib.Value.initMessageEvent( + // .info, + // null, + // "hello, world", + // ))) |uuid| { + // std.log.warn("uuid={s}", .{uuid.string()}); + // } else std.log.warn("failed to capture event", .{}); + + // We need to make sure the process locale is set properly. Locale + // affects a lot of behaviors in a shell. + // + // We need to re-sync the environment after this completes. + try internal_os.ensureLocale(); + syncEnviron(); + + // Initialize glslang for shader compilation + try glslang.init(); + + // Initialize oniguruma for regex + try oni.init(&.{oni.Encoding.utf8}); + + // Find our resources directory once for the app so every launch + // hereafter can use this cached value. + self.resources_dir = try apprt.runtime.resourcesDir(self.alloc); + errdefer self.resources_dir.deinit(self.alloc); + + // Setup i18n + if (self.resources_dir.app()) |v| internal_os.i18n.init(v) catch |err| { + std.log.warn("failed to init i18n, translations will not be available err={}", .{err}); + }; +} + +/// Cleans up the global state. This doesn't _need_ to be called but +/// doing so in dev modes will check for memory leaks. +/// +/// Asserts that the state exists. +pub fn deinit() void { + const self = &state.?; + + self.resources_dir.deinit(self.alloc); + + // Flush our crash logs + crash.deinit(); + + // Release our tmp_dir_path if needed + if (self.tmp_dir_path) |td| freeTmpDir(self.alloc, td); + + // Release our I/O instance + self.io_impl.deinit(); + + if (self.gpa) |*value| { + // We want to ensure that we deinit the GPA because this is + // the point at which it will output if there were safety violations. + _ = value.deinit(); + } +} + +/// Helper to return either the state's I/O instance, or one from testing. +/// +/// Asserts that the global state is initialized when not running as as test. +pub fn io() std.Io { + if (builtin.is_test) return std.testing.io; + + return state.?.io(); +} + +/// Helper to return either the state's I/O instance, or one from testing. +/// +/// Asserts that the global state is initialized when not running as as test. +pub fn alloc() std.mem.Allocator { + if (builtin.is_test) return std.testing.allocator; + + return state.?.alloc; +} + +/// Helper to return either the state's environment, or one from testing. +/// +/// Asserts that the global state is initialized when not running as a test. +pub fn environ() std.process.Environ { + if (builtin.is_test) return std.testing.environ; + + return state.?.environ; +} + +/// Helper to create an environment map off of the state's environment, or one +/// from testing. The map is created off of the state allocator. +/// +/// Asserts that the global state is initialized when not running as a test. +pub fn environMap() !std.process.Environ.Map { + if (builtin.is_test) return std.testing.environ.createMap(std.testing.allocator); + + return state.?.environ.createMap(state.?.alloc); +} + +/// Re-synchronizes the global Environ (both the higher-level and I/O versions) +/// from the process. No-op on Windows, asserts libc and an initialized global +/// state on everything else. +/// +/// It is not valid to run this within any code that needs to be run through +/// tests. For any of these, re-factor the code to take an environment map +/// instead, where you can modify the environment as needed. +/// +/// NOTE: Be cognizant of where you are calling this! While the only real +/// difference between the POSIX environment and higher-level Zig `PosixBlock` +/// struct is that the latter has a length versus the former's many-item +/// pointer state, there is no concurrency control on this function (or for +/// that matter, `environ` or `environMap`. Direct modification of the system +/// environment is becoming more discouraged in the standard library as well, +/// and this should be kept in mind when resorting to lower-level `setenv` or +/// `unsetenv` - as a rule, beyond initialization, favor +/// `std.process.Environ.Map` whenever possible. +pub fn syncEnviron() void { + switch (builtin.os.tag) { + .windows => {}, + else => { + assert(builtin.link_libc); + assert(!builtin.is_test); + const new_environ: std.process.Environ = .{ .block = .{ .slice = std.c.environ[0..env_len: { + var len: usize = 0; + while (std.c.environ[len]) |_| : (len += 1) {} + break :env_len len; + } :null] } }; + state.?.environ = new_environ; + state.?.io_impl.environ = .{ .process_environ = new_environ }; + }, + } +} + +/// Helper to return either the state's args, or one from testing. +/// +/// Asserts that the global state is initialized when not running as a test. +pub fn args() std.process.Args { + if (builtin.is_test) return .{ .vector = &.{} }; + + return state.?.args; +} + +/// Returns the temporary directory discovered from the global environment +/// (saves allocation of the temporary directory where you can get at global +/// state). +/// +/// Asserts that the global state is initialized. +pub fn tmpDirPath() []const u8 { + return state.?.tmp_dir_path.?; +} + +/// Returns the global state resources_dir, or an empty one when testing. +/// +/// Asserts that the global state is initialized when not running as a test. +pub fn resourcesDir() internal_os.ResourcesDir { + if (builtin.is_test) return .{}; + + return state.?.resources_dir; +} + +/// Returns the global state rlimits, or an empty one when testing. +/// +/// Asserts that the global state is initialized when not running as a test. +pub fn rlimits() ResourceLimits { + if (builtin.is_test) return .{}; + + return state.?.rlimits; +} + +/// Returns the global state logging configuration. +/// +/// Asserts that the global state is initialized. +pub fn logging() GlobalState.Logging { + return state.?.logging; +} + +/// Returns the global state action. +/// +/// Asserts that the global state is initialized. +pub fn action() ?cli.ghostty.Action { + return state.?.action; +} /// This represents the global process state. There should only /// be one of these at any given moment. This is extracted into a dedicated /// struct because it is reused by main and the static C lib. pub const GlobalState = struct { - const GPA = std.heap.GeneralPurposeAllocator(.{}); + const GPA = std.heap.DebugAllocator(.{}); + io_impl: std.Io.Threaded, gpa: ?GPA, alloc: std.mem.Allocator, + environ: std.process.Environ, + args: std.process.Args, + tmp_dir_path: ?[]const u8, action: ?cli.ghostty.Action, logging: Logging, rlimits: ResourceLimits = .{}, @@ -48,151 +401,9 @@ pub const GlobalState = struct { macos: bool = builtin.os.tag.isDarwin(), }; - /// Initialize the global state. - pub fn init(self: *GlobalState) !void { - // const start = try std.time.Instant.now(); - // const start_micro = std.time.microTimestamp(); - // defer { - // const end = std.time.Instant.now() catch unreachable; - // // "[updateFrame critical time] <START us>\t<TIME_TAKEN us>" - // std.log.err("[global init time] start={}us duration={}ns", .{ start_micro, end.since(start) / std.time.ns_per_us }); - // } - - // Initialize ourself to nothing so we don't have any extra state. - // IMPORTANT: this MUST be initialized before any log output because - // the log function uses the global state. - self.* = .{ - .gpa = null, - .alloc = undefined, - .action = null, - .logging = .{}, - .rlimits = .{}, - .resources_dir = .{}, - }; - errdefer self.deinit(); - - self.gpa = gpa: { - // Use the libc allocator if it is available because it is WAY - // faster than GPA. We only do this in release modes so that we - // can get easy memory leak detection in debug modes. - if (builtin.link_libc) { - if (switch (builtin.mode) { - .ReleaseSafe, .ReleaseFast => true, - - // We also use it if we can detect we're running under - // Valgrind since Valgrind only instruments the C allocator - else => std.valgrind.runningOnValgrind() > 0, - }) break :gpa null; - } - - break :gpa GPA{}; - }; - - self.alloc = if (self.gpa) |*value| - value.allocator() - else if (builtin.link_libc) - std.heap.c_allocator - else - unreachable; - - // We first try to parse any action that we may be executing. - self.action = try cli.action.detectArgs( - cli.ghostty.Action, - self.alloc, - ); - - // If we have an action executing, we disable logging by default - // since we write to stderr we don't want logs messing up our - // output. - if (self.action != null) self.logging.stderr = false; - - // I don't love the env var name but I don't have it in my heart - // to parse CLI args 3 times (once for actions, once for config, - // maybe once for logging) so for now this is an easy way to do - // this. Env vars are useful for logging too because they are - // easy to set. - if ((try internal_os.getenv(self.alloc, "GHOSTTY_LOG"))) |v| { - defer v.deinit(self.alloc); - self.logging = cli.args.parsePackedStruct(Logging, v.value) catch .{}; - } - - // Setup our signal handlers before logging - initSignals(); - - // Setup our Xev backend if we're dynamic - if (comptime xev.dynamic) xev.detect() catch |err| { - std.log.warn("failed to detect xev backend, falling back to " ++ - "most compatible backend err={}", .{err}); - }; - - // Output some debug information right away - std.log.info("ghostty version={s}", .{build_config.version_string}); - std.log.info("ghostty build optimize={s}", .{build_config.mode_string}); - std.log.info("runtime={}", .{build_config.app_runtime}); - std.log.info("font_backend={}", .{build_config.font_backend}); - if (comptime build_config.font_backend.hasHarfbuzz()) { - std.log.info("dependency harfbuzz={s}", .{harfbuzz.versionString()}); - } - if (comptime build_config.font_backend.hasFontconfig()) { - std.log.info("dependency fontconfig={d}", .{fontconfig.version()}); - } - std.log.info("renderer={}", .{renderer.Renderer}); - std.log.info("libxev default backend={t}", .{xev.backend}); - - // As early as possible, initialize our resource limits. - self.rlimits = .init(); - - // Initialize our crash reporting. - crash.init(self.alloc) catch |err| { - std.log.warn( - "sentry init failed, no crash capture available err={}", - .{err}, - ); - }; - - // const sentrylib = @import("sentry"); - // if (sentrylib.captureEvent(sentrylib.Value.initMessageEvent( - // .info, - // null, - // "hello, world", - // ))) |uuid| { - // std.log.warn("uuid={s}", .{uuid.string()}); - // } else std.log.warn("failed to capture event", .{}); - - // We need to make sure the process locale is set properly. Locale - // affects a lot of behaviors in a shell. - try internal_os.ensureLocale(self.alloc); - - // Initialize glslang for shader compilation - try glslang.init(); - - // Initialize oniguruma for regex - try oni.init(&.{oni.Encoding.utf8}); - - // Find our resources directory once for the app so every launch - // hereafter can use this cached value. - self.resources_dir = try apprt.runtime.resourcesDir(self.alloc); - errdefer self.resources_dir.deinit(self.alloc); - - // Setup i18n - if (self.resources_dir.app()) |v| internal_os.i18n.init(v) catch |err| { - std.log.warn("failed to init i18n, translations will not be available err={}", .{err}); - }; - } - - /// Cleans up the global state. This doesn't _need_ to be called but - /// doing so in dev modes will check for memory leaks. - pub fn deinit(self: *GlobalState) void { - self.resources_dir.deinit(self.alloc); - - // Flush our crash logs - crash.deinit(); - - if (self.gpa) |*value| { - // We want to ensure that we deinit the GPA because this is - // the point at which it will output if there were safety violations. - _ = value.deinit(); - } + /// Asserts that `self.io_impl` has been initialized. + pub fn io(self: *GlobalState) std.Io { + return self.io_impl.io(); } fn initSignals() void { |
