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/config | |
| parent | 90e947a6e26e02f10ec7f1694c4eb5b78a86406b (diff) | |
chore: upgrade libghostty and preserve terminal behavior (#3906)
Diffstat (limited to 'vendor/libghostty-vt/src/config')
| -rw-r--r-- | vendor/libghostty-vt/src/config/CApi.zig | 26 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/config/Config.zig | 692 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/config/c_get.zig | 2 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/config/command.zig | 2 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/config/conditional.zig | 20 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/config/edit.zig | 76 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/config/file_load.zig | 42 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/config/formatter_file.zig | 2 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/config/io.zig | 2 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/config/key.zig | 27 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/config/limit.zig | 115 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/config/path.zig | 58 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/config/string.zig | 24 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/config/theme.zig | 28 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/config/url.zig | 16 |
15 files changed, 843 insertions, 289 deletions
diff --git a/vendor/libghostty-vt/src/config/CApi.zig b/vendor/libghostty-vt/src/config/CApi.zig index 7d3366c9..f69f48ea 100644 --- a/vendor/libghostty-vt/src/config/CApi.zig +++ b/vendor/libghostty-vt/src/config/CApi.zig @@ -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; }; diff --git a/vendor/libghostty-vt/src/config/Config.zig b/vendor/libghostty-vt/src/config/Config.zig index d6a038d9..877c4372 100644 --- a/vendor/libghostty-vt/src/config/Config.zig +++ b/vendor/libghostty-vt/src/config/Config.zig @@ -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"); @@ -41,6 +41,7 @@ const ClipboardCodepointMap = @import("ClipboardCodepointMap.zig"); const KeyRemapSet = @import("../input/key_mods.zig").RemapSet; pub const WindowPaddingBalance = @import("../renderer/size.zig").PaddingBalance; const string = @import("string.zig"); +const Limit = @import("limit.zig").Limit; // We do this instead of importing all of terminal/main.zig to // limit the dependency graph. This is important because some things @@ -95,6 +96,16 @@ pub const compatibility = std.StaticStringMap( // Ghostty 1.3 rename the "window" option to "new-window". // See: https://github.com/ghostty-org/ghostty/pull/9764 .{ "macos-dock-drop-behavior", compatMacOSDockDropBehavior }, + + // Ghostty 1.4 renamed `scrollback-limit` to `scrollback-limit-bytes` + // when `scrollback-limit-lines` was added so the units are explicit. + .{ "scrollback-limit", cli.compatibilityRenamed(Config, "scrollback-limit-bytes") }, + + // Ghostty 1.4 updated "copy-on-select", allow copying to the selection + // clipboard (on supported operating systems), the system clipboard, or + // both. The semantics also changed but this is the correct mapping. + // See: https://github.com/ghostty-org/ghostty/pull/12604 + .{ "copy-on-select", compatCopyOnSelect }, }); /// Set Ghostty's graphical user interface language to a language other than the @@ -903,10 +914,10 @@ palette: Palette = .{}, /// Enables the ability to move the cursor at prompts by clicking on a /// location in the prompt text. /// -/// This feature requires shell integration, specifically prompt marking -/// via `OSC 133`. Some shells like Fish (v4) and Nu (0.111+) natively -/// support this while others may require additional configuration or -/// Ghostty's shell integration features to be enabled. +/// This feature requires prompt marking via `OSC 133`. Some shells like Fish +/// (v4.1+) and Nu (0.111+) natively support this while others may require +/// additional configuration or Ghostty's shell integration features to be +/// enabled. /// /// Depending on the shell, this works either by translating your click /// position into a series of synthetic arrow key movements or by sending @@ -1036,27 +1047,15 @@ palette: Palette = .{}, /// If the macOS values are set, then this implies `background-blur = true` /// on non-macOS platforms. /// -/// Supported on macOS and on some Linux desktop environments, including: -/// -/// * KDE Plasma (Wayland and X11) +/// On Linux, the exact blur intensity is ignored and any positive integer +/// or `true` value will enable background blur. On Wayland, Ghostty uses +/// the [`ext-background-effect-v1`](https://wayland.app/protocols/ext-background-effect-v1) +/// protocol to enable the blur effect, but this is not always supported +/// on all compositors. Check the documentation of your compositor or +/// desktop environment for blur support and how to configure it. /// -/// Warning: the exact blur intensity is _ignored_ under KDE Plasma, and setting -/// this setting to either `true` or any positive blur intensity value would -/// achieve the same effect. The reason is that KWin, the window compositor -/// powering Plasma, only has one global blur setting and does not allow -/// applications to specify individual blur settings. -/// -/// To configure KWin's global blur setting, open System Settings and go to -/// "Apps & Windows" > "Window Management" > "Desktop Effects" and select the -/// "Blur" plugin. If disabled, enable it by ticking the checkbox to the left. -/// Then click on the "Configure" button and there will be two sliders that -/// allow you to set background blur and noise intensities for all apps, -/// including Ghostty. -/// -/// All other Linux desktop environments are as of now unsupported. Users may -/// need to set environment-specific settings and/or install third-party plugins -/// in order to support background blur, as there isn't a unified interface for -/// doing so. +/// On X11, blur can only be enabled when using the KWin compositor +/// as a part of KDE Plasma. @"background-blur": BackgroundBlur = .false, /// The opacity level (opposite of transparency) of an unfocused split. @@ -1387,11 +1386,32 @@ input: RepeatableReadableIO = .{}, /// /// This size is per terminal surface, not for the entire application. /// -/// It is not currently possible to set an unlimited scrollback buffer. -/// This is a future planned feature. +/// A separate maximum can be set with `scrollback-limit-lines`; if both limits +/// are set, then the first one reached will determine when scrollback is +/// removed. +/// +/// The default is 50 MB. Set this to `unlimited` to remove the byte limit. /// /// This can be changed at runtime but will only affect new terminal surfaces. -@"scrollback-limit": usize = 50_000_000, // 50MB +@"scrollback-limit-bytes": Limit(usize, 50_000_000) = .default, + +/// The maximum number of lines of scrollback to retain. This excludes +/// the active screen. Soft-wrapped lines count as multiple lines. +/// +/// This limit is an estimate. Internally, Ghostty will only trim lines +/// up to the minimum allocation unit that is used internally (called a +/// "page"). The size of a page depends on how many styles, graphemes, etc. +/// take up the screen. In practice, this can be anywhere from a handful to +/// a couple hundred lines. Importantly, memory is capped either way. +/// This means that the actual limited lines will likely be slightly +/// higher in practice. +/// +/// The default is `unlimited`. A separate maximum can be set with +/// `scrollback-limit-bytes`; if both limits are set, then the first one reached +/// will determine when scrollback is removed. +/// +/// This can be changed at runtime but will only affect new terminal surfaces. +@"scrollback-limit-lines": Limit(usize, std.math.maxInt(usize)) = .default, /// Whether to compress scrollback pages while the terminal is idle. /// @@ -1424,9 +1444,9 @@ input: RepeatableReadableIO = .{}, /// Valid values: /// /// * `system` - Respect the system settings for when to show scrollbars. -/// For example, on macOS, this will respect the "Scrollbar behavior" -/// system setting which by default usually only shows scrollbars while -/// actively scrolling or hovering the gutter. +/// On macOS, we only show scrollbars while actively scrolling or hovering +/// the gutter. If the system setting is set to "Always", the scrollbar +/// will be shown when the mouse is over the gutter area. /// /// * `never` - Never show a scrollbar. You can still scroll using the mouse, /// keybind actions, etc. but you will not have a visual UI widget showing @@ -1455,6 +1475,14 @@ link: RepeatableLink = .{}, /// `link`). If you want to customize URL matching, use `link` and disable this. @"link-url": bool = true, +/// Enable hyperlinks created with the OSC 8 escape sequence. When disabled, +/// OSC 8 hyperlinks are not highlighted, previewed, copied, or opened. +/// +/// This does not affect URL matching controlled by `link-url`. +/// +/// Available since: 1.4.0 +@"link-osc8": bool = true, + /// Show link previews for a matched URL. /// /// When true, link previews are shown for all matched URLs. When false, link @@ -1781,8 +1809,6 @@ class: ?[:0]const u8 = null, /// `global:unconsumed:ctrl+a=reload_config` will make the keybind global /// and not consume the input to reload the config. /// -/// Note: `global:` is only supported on macOS and certain Linux platforms. -/// /// On macOS, this feature requires accessibility permissions to be granted /// to Ghostty. When a `global:` keybind is specified and Ghostty is launched /// or reloaded, Ghostty will attempt to request these permissions. @@ -1790,24 +1816,40 @@ class: ?[:0]const u8 = null, /// you can find these permissions in System Preferences -> Privacy & Security /// -> Accessibility. /// -/// On Linux, you need a desktop environment that implements the -/// [Global Shortcuts](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GlobalShortcuts.html) -/// protocol as a part of its XDG desktop protocol implementation. -/// Desktop environments that are known to support (or not support) -/// global shortcuts include: +/// Since Ghostty 1.4.0, global shortcuts on Linux are primarily supported +/// through the [`vicinae-hotkey-v1`](https://github.com/vicinaehq/vicinae-wayland-protocols/tree/main/staging/vicinae-hotkey) +/// protocol, available out-of-the-box with the following Wayland compositors +/// and desktop environments: +/// +/// - Hyprland since version 0.56.0 +/// +/// On X11 or when your Wayland compositor/desktop environment does not +/// support `vicinae-hotkey-v1`, the much more widely-used +/// [XDG Global Shortcuts](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GlobalShortcuts.html) +/// protocol is used instead. +/// +/// Note: XDG Global Shortcuts rely on a functional XDG Desktop Portal +/// implementation **that is designed to be used with your desktop environment +/// or compositor**. Please make sure that you have the correct one installed. /// -/// - Users using KDE Plasma (since [5.27](https://kde.org/announcements/plasma/5/5.27.0/#wayland)) -/// and GNOME (since [48](https://release.gnome.org/48/#and-thats-not-all)) should be able -/// to use global shortcuts with little to no configuration. +/// Desktop environments that are known to support the XDG Global Shortcuts +/// protocol include: /// -/// - Some manual configuration is required on Hyprland. Consult the steps -/// outlined on the [Hyprland Wiki](https://wiki.hyprland.org/Configuring/Binds/#dbus-global-shortcuts) -/// to set up global shortcuts correctly. -/// (Important: [`xdg-desktop-portal-hyprland`](https://wiki.hyprland.org/Hypr-Ecosystem/xdg-desktop-portal-hyprland/) -/// must also be installed!) +/// - KDE Plasma since [5.27](https://kde.org/announcements/plasma/5/5.27.0/#wayland) +/// with `xdg-desktop-portal-kde` installed /// -/// - Notably, global shortcuts have not been implemented on wlroots-based -/// compositors like Sway (see [upstream issue](https://github.com/emersion/xdg-desktop-portal-wlr/issues/240)). +/// - GNOME since [48](https://release.gnome.org/48/#and-thats-not-all) with +/// `xdg-desktop-portal-gnome` installed +/// +/// - Hyprland before version 0.56.0, with `xdg-desktop-portal-hyprland` +/// installed. **Some manual configuration is required.** Consult the steps +/// outlined on the [Hyprland Wiki](https://wiki.hypr.land/Configuring/Basics/Binds/#dbus-global-shortcuts) +/// to set up D-Bus-based global shortcuts correctly. +/// +/// Desktop environments and compositors that don't implement either protocol +/// do not support global shortcuts, like wlroots-based compositors e.g. Sway +/// (see [upstream issue](https://github.com/emersion/xdg-desktop-portal-wlr/issues/240)) +/// and COSMIC. /// /// ## Chained Actions /// @@ -2309,6 +2351,33 @@ keybind: Keybinds = .{}, /// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color. @"window-titlebar-foreground": ?Color = null, +/// Controls when drag handles are shown over splits, +/// allowing splits to be rearranged with mouse controls. +/// +/// Valid values: +/// +/// - `always` +/// +/// Always display the drag handle, even when there's only one split. +/// +/// - `auto` *(default)* +/// +/// Automatically show and hide the drag handle. +/// +/// On Linux, the handle is only shown when there are two or +/// more splits present. +/// +/// On macOS, the handle is only hidden when there's one split +/// in **fullscreen** mode, otherwise it's shown when hovered. +/// +/// - `never` +/// +/// Never show the drag handle. Splits then cannot be rearranged with +/// mouse controls. +/// +/// Available since: 1.4.0. +@"drag-handle": DragHandle = .auto, + /// This controls when resize overlays are shown. Resize overlays are a /// transient popup that shows the size of the terminal while the surfaces are /// being resized. The possible options are: @@ -2391,6 +2460,32 @@ keybind: Keybinds = .{}, @"clipboard-read": ClipboardAccess = .ask, @"clipboard-write": ClipboardAccess = .allow, +/// The maximum size in bytes of a single clipboard write by a program +/// running in the terminal via the Kitty clipboard protocol (OSC 5522). +/// This doesn't apply to OSC 52, which is limited by the maximum length +/// of an escape sequence hardcoded into Ghostty for now. +/// +/// Data beyond the limit fails the entire write with an `EFBIG` status, +/// discards the transaction, and leaves the clipboard untouched. Later +/// write-related packets are ignored until a new write begins. +/// +/// The data is buffered in memory while the write is in progress, so +/// this limit bounds how much memory a program can make Ghostty +/// allocate per write. A future improvement will attempt to spool large +/// writes to disk. +/// +/// The default is 64 MiB, the minimum a conforming implementation must +/// accept. Set this to `unlimited` to remove the limit, allowing writes +/// bounded only by available memory. A value of `0` rejects every non-empty +/// write. To reject clipboard writes entirely, use `clipboard-write = deny` +/// instead. +/// +/// This can be changed at runtime and applies to writes that begin +/// after the change. +/// +/// Available since: 1.4.0 +@"clipboard-write-limit-bytes": Limit(usize, 64 * 1024 * 1024) = .default, + /// Trims trailing whitespace on data that is copied to the clipboard. This does /// not affect data sent to the clipboard via `clipboard-write`. This only /// applies to trailing whitespace on lines that have other characters. @@ -2428,25 +2523,28 @@ keybind: Keybinds = .{}, /// limit per surface is double. @"image-storage-limit": u32 = 320 * 1000 * 1000, -/// Whether to automatically copy selected text to the clipboard. `true` -/// will prefer to copy to the selection clipboard, otherwise it will copy to -/// the system clipboard. +/// Whether to automatically copy selected text to the clipboard. +/// +/// Valid values: +/// +/// * `none` - Do not copy selected text automatically. +/// +/// * `primary` - On Linux, copy to the selection clipboard only. This has no +/// effect on macOS. (Available since: 1.4.0) /// -/// The value `clipboard` will always copy text to the selection clipboard -/// as well as the system clipboard. +/// * `clipboard` - Copy text to the system clipboard only. /// -/// Middle-click primary paste (see `middle-click-action`) is enabled by -/// default even if this is `false`. The clipboard it pastes from follows -/// this setting: with `true` (or `false`) it reads from the selection -/// clipboard (falling back to the system clipboard on platforms without a -/// selection clipboard); with `clipboard` it reads from the system -/// clipboard. +/// * `both` - Copy to both clipboards on Linux, and only the system clipboard +/// on macOS. (Available since: 1.4.0) /// -/// The default value is true on Linux and macOS. +/// For backward compatibility and convenience, a value of `true` is the same as +/// `primary` on Linux and `clipboard` on macOS, and `false` is an alias for +/// `none`. +/// +/// The default value is `primary` on Linux and `none` otherwise. @"copy-on-select": CopyOnSelect = switch (builtin.os.tag) { - .linux => .true, - .macos => .true, - else => .false, + .linux => .primary, + else => .none, }, /// The action to take when the user right-clicks on the terminal surface. @@ -2465,8 +2563,9 @@ keybind: Keybinds = .{}, /// The action to take when the user middle-clicks on the terminal surface. /// /// Valid values: -/// * `primary-paste` - Paste from the selection (or system) clipboard per -/// `copy-on-select`. +/// * `primary-paste` - Paste from the selection clipboard on Linux. +/// Does nothing on macOS. +/// * `clipboard-paste` - Paste from the system clipboard. /// * `ignore` - Do nothing, ignore the middle click. /// /// The default value is `primary-paste`. @@ -2868,10 +2967,9 @@ keybind: Keybinds = .{}, /// (Available since: 1.2.0) /// /// * `ssh-terminfo` - Enable automatic terminfo installation on remote hosts. -/// Attempts to install Ghostty's terminfo entry using `infocmp` and `tic` when -/// connecting to hosts that lack it. Requires `infocmp` to be available locally -/// and `tic` to be available on remote hosts. Once terminfo is installed on a -/// remote host, it will be automatically "cached" to avoid repeat installations. +/// Attempts to install Ghostty's embedded terminfo entry using `tic` on local +/// cache misses. Requires `tic` to be available on remote hosts. Successful +/// installations are cached locally to avoid repeat installations. /// If desired, the `+ssh-cache` CLI action can be used to manage the installation /// cache manually using various arguments. /// (Available since: 1.2.0) @@ -3860,7 +3958,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, @@ -3934,7 +4032,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", @@ -3945,16 +4043,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); } @@ -4047,17 +4145,18 @@ 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"), .{ .path = path }, ); + try writer.flush(); } /// Load configurations from the default configuration files. The default @@ -4146,7 +4245,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. @@ -4165,7 +4264,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 @@ -4215,7 +4314,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); @@ -4241,7 +4340,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. @@ -4304,7 +4407,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( @@ -4320,9 +4423,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| { @@ -4450,6 +4553,29 @@ fn expandPaths(self: *Config, base: []const u8) !void { } } +/// Expand tilde paths to absolute paths to the user's home directory. +/// If expansion fails, an error is logged and the original path is returned. +fn expandHome(path: []const u8, buf: []u8) []const u8 { + if (!std.mem.startsWith(u8, path, "~/")) + return path; + + var environ_map = global.environMap() catch |err| { + log.warn("failed to get environment map for path \"{s}\": {}", .{ path, err }); + return path; + }; + defer environ_map.deinit(); + + return internal_os.expandHome( + global.io(), + &environ_map, + path, + buf, + ) catch |err| { + log.warn("failed to expand home directory in path \"{s}\": {}", .{ path, err }); + return path; + }; +} + fn loadTheme(self: *Config, theme: Theme) !void { // Load the correct theme depending on the conditional state. // Dark/light themes were programmed prior to conditional configuration @@ -4467,7 +4593,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: @@ -4489,7 +4615,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); @@ -4549,14 +4675,17 @@ fn loadTheme(self: *Config, theme: Theme) !void { /// Call this once after you are done setting configuration. This /// is idempotent but will waste memory if called multiple times. pub fn finalize(self: *Config) !void { + const alloc = self._arena.?.allocator(); + // We always load the theme first because it may set other fields // in our config. - if (self.theme) |theme| { + if (self.theme) |*theme| { + try theme.finalize(alloc); const different = !std.mem.eql(u8, theme.light, theme.dark); // Warning: loadTheme will deinit our existing config and replace // it so all memory from self prior to this point will be freed. - try self.loadTheme(theme); + try self.loadTheme(theme.*); // If we have different light vs dark mode themes, disable // window-theme = auto since that breaks it. @@ -4570,8 +4699,6 @@ pub fn finalize(self: *Config) !void { } } - const alloc = self._arena.?.allocator(); - // Used for a variety of defaults. See the function docs as well the // specific variable use sites for more details. const probable_cli = probableCliEnvironment(); @@ -4626,15 +4753,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) { @@ -4645,8 +4776,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(global.io(), &environ_map, &buf)) |home| { wd = .{ .path = try alloc.dupe(u8, home) }; } else { wd = .inherit; @@ -4924,6 +5057,31 @@ fn compatMacOSDockDropBehavior( return false; } +fn compatCopyOnSelect( + self: *Config, + alloc: Allocator, + key: []const u8, + value: ?[]const u8, +) bool { + _ = alloc; + assert(std.mem.eql(u8, key, "copy-on-select")); + + if (std.mem.eql(u8, value orelse "", "true")) { + self.@"copy-on-select" = switch (builtin.os.tag) { + .linux, .freebsd => .primary, + else => .clipboard, + }; + return true; + } + + if (std.mem.eql(u8, value orelse "", "false")) { + self.@"copy-on-select" = .none; + return true; + } + + return false; +} + /// Add a diagnostic message to the config with the given string. /// This is always added with a location of "none". pub fn addDiagnosticFmt( @@ -5135,14 +5293,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; @@ -5367,16 +5528,8 @@ pub const WorkingDirectory = union(enum) { else => return, }; - 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| { - log.warn( - "error expanding home directory for working-directory path={s}: {}", - .{ path, err }, - ); - return; - }; + const expanded = expandHome(path, &buf); if (std.mem.eql(u8, expanded, path)) return; self.* = .{ .path = try alloc.dupe(u8, expanded) }; @@ -5429,6 +5582,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" }; @@ -5436,6 +5591,8 @@ pub const WorkingDirectory = union(enum) { var buf: [std.fs.max_path_bytes]u8 = undefined; const expected = internal_os.expandHome( + testing.io, + &environ_map, "~/projects/ghostty", &buf, ) catch "~/projects/ghostty"; @@ -5670,8 +5827,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 { @@ -5984,7 +6141,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 @@ -6274,7 +6431,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; @@ -6435,7 +6592,7 @@ pub const Keybinds = struct { try self.set.put( alloc, .{ .key = .{ .unicode = ',' }, .mods = inputpkg.ctrlOrSuper(.{}) }, - .{ .open_config = {} }, + .{ .open_config = .default }, ); { @@ -8557,7 +8714,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; @@ -8606,16 +8763,20 @@ pub const RepeatableLink = struct { /// Options for copy on select behavior. pub const CopyOnSelect = enum { - /// Disables copy on select entirely. - false, + /// Disables copy on select entirely. This is the default on macOS. + none, /// Copy on select is enabled, but goes to the selection clipboard. - /// This is not supported on platforms such as macOS. This is the default. - true, + /// This is not supported on platforms such as macOS. This is the default + /// on Linux. + primary, + + /// Copy on select is enabled and goes to the system clipboard. + clipboard, /// Copy on select is enabled and goes to both the system clipboard /// and the selection clipboard (for Linux). - clipboard, + both, }; /// Options for right-click actions. @@ -8639,8 +8800,10 @@ pub const RightClickAction = enum { /// Options for middle-click actions. pub const MiddleClickAction = enum { - /// Paste from the selection/standard clipboard per `copy-on-select`. + /// Paste from the selection clipboard. @"primary-paste", + /// Paste from the standard clipboard. + @"clipboard-paste", /// No action is taken on middle click. ignore, @@ -8697,8 +8860,20 @@ pub const RepeatableCommand = struct { self.value.deinit(alloc); self.value_c.deinit(alloc); } - try self.value.appendSlice(alloc, inputpkg.command.defaults); - try self.value_c.appendSlice(alloc, inputpkg.command.defaultsC); + try self.value.ensureUnusedCapacity(alloc, inputpkg.command.defaults.len); + try self.value_c.ensureUnusedCapacity(alloc, inputpkg.command.defaults.len); + for (inputpkg.command.defaults) |cmd| { + // Translation is currently a GTK-only feature. In particular, + // translating these shared strings for the embedded runtime gives + // the macOS app a localized command palette in an otherwise + // unlocalized UI. + const localized = if (comptime build_config.app_runtime == .gtk) + cmd.translated() + else + cmd; + self.value.appendAssumeCapacity(localized); + self.value_c.appendAssumeCapacity(try localized.cval(alloc)); + } } pub fn parseCLI( @@ -8743,9 +8918,19 @@ pub const RepeatableCommand = struct { item.* = try item.clone(alloc); } + // Cloning value_c directly would copy Command.C structs + // whose string pointers still reference the source config's + // memory — the clone must stay valid after the source is + // freed. + var value_c: std.ArrayListUnmanaged(inputpkg.Command.C) = .empty; + try value_c.ensureTotalCapacityPrecise(alloc, value.items.len); + for (value.items) |item| { + value_c.appendAssumeCapacity(try item.cval(alloc)); + } + return .{ .value = value, - .value_c = try self.value_c.clone(alloc), + .value_c = value_c, }; } @@ -8834,6 +9019,26 @@ pub const RepeatableCommand = struct { try testing.expectEqual(inputpkg.command.defaults.len, list.value.items.len); } + test "RepeatableCommand clone rebuilds the C mirror" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var list: RepeatableCommand = .{}; + try list.parseCLI(alloc, "title:Foo,description:bar,action:new_tab"); + + const copy = try list.clone(alloc); + try testing.expectEqual(list.value_c.items.len, copy.value_c.items.len); + // The clone's C strings must not alias the source's — the + // source config can be freed while the clone lives on. + try testing.expect(list.value_c.items[0].title != copy.value_c.items[0].title); + try testing.expectEqualStrings( + std.mem.span(list.value_c.items[0].title), + std.mem.span(copy.value_c.items[0].title), + ); + } + test "RepeatableCommand formatConfig empty" { const testing = std.testing; var buf: std.Io.Writer.Allocating = .init(testing.allocator); @@ -9228,6 +9433,13 @@ pub const WindowShowTabBar = enum { never, }; +/// See drag-handle +pub const DragHandle = enum { + always, + auto, + never, +}; + /// See resize-overlay pub const ResizeOverlay = enum { always, @@ -9876,6 +10088,19 @@ pub const Theme = struct { }; } + /// Expand tilde paths in light/dark theme values. + pub fn finalize(self: *Theme, alloc: Allocator) Allocator.Error!void { + var buf: [std.fs.max_path_bytes]u8 = undefined; + + const light = expandHome(self.light, &buf); + if (!std.mem.eql(u8, light, self.light)) + self.light = try alloc.dupeZ(u8, light); + + const dark = expandHome(self.dark, &buf); + if (!std.mem.eql(u8, dark, self.dark)) + self.dark = try alloc.dupeZ(u8, dark); + } + /// Deep copy of the struct. Required by Config. pub fn clone(self: *const Theme, alloc: Allocator) Allocator.Error!Theme { return .{ @@ -9932,6 +10157,34 @@ pub const Theme = struct { try testing.expectEqualStrings("bar", v.dark); } + // Expand tilde to home + { + var environ_map = try testing.environ.createMap(alloc); + defer environ_map.deinit(); + + var home_buf: [std.fs.max_path_bytes]u8 = undefined; + const home = try internal_os.expandHome( + testing.io, + &environ_map, + "~/", + &home_buf, + ); + + var v: Theme = undefined; + try v.parseCLI(alloc, "light:~/foo, dark:~/bar"); + try v.finalize(alloc); + + var expected_buf: [std.fs.max_path_bytes]u8 = undefined; + try testing.expectEqualStrings( + try std.fmt.bufPrint(&expected_buf, "{s}foo", .{home}), + v.light, + ); + try testing.expectEqualStrings( + try std.fmt.bufPrint(&expected_buf, "{s}bar", .{home}), + v.dark, + ); + } + var v: Theme = undefined; try testing.expectError(error.ValueRequired, v.parseCLI(alloc, null)); try testing.expectError(error.ValueRequired, v.parseCLI(alloc, "")); @@ -10424,23 +10677,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(); @@ -10501,7 +10754,10 @@ test "clone preserves conditional set" { test "working-directory expands tilde" { const testing = std.testing; + const io = testing.io; 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(); @@ -10513,6 +10769,8 @@ test "working-directory expands tilde" { var buf: [std.fs.max_path_bytes]u8 = undefined; const expected = internal_os.expandHome( + io, + &environ_map, "~/projects/ghostty", &buf, ) catch "~/projects/ghostty"; @@ -10583,14 +10841,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(); @@ -10622,14 +10880,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(); @@ -10655,14 +10913,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(); @@ -10692,23 +10950,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 { @@ -10814,6 +11072,126 @@ test "theme specifying light/dark sets theme usage in conditional state" { } } +test "scrollback limits" { + const testing = std.testing; + const alloc = testing.allocator; + + var cfg = try Config.default(alloc); + defer cfg.deinit(); + try testing.expectEqual( + @as(usize, 50_000_000), + cfg.@"scrollback-limit-bytes".value, + ); + try testing.expectEqual( + std.math.maxInt(usize), + cfg.@"scrollback-limit-lines".value, + ); + + var it: TestIterator = .{ .data = &.{ + "--scrollback-limit-bytes=1234", + "--scrollback-limit-lines=567", + } }; + try cfg.loadIter(alloc, &it); + + try testing.expectEqual( + @as(usize, 1234), + cfg.@"scrollback-limit-bytes".value, + ); + try testing.expectEqual( + @as(usize, 567), + cfg.@"scrollback-limit-lines".value, + ); + + var unlimited_it: TestIterator = .{ .data = &.{ + "--scrollback-limit-bytes=unlimited", + "--scrollback-limit-lines=unlimited", + } }; + try cfg.loadIter(alloc, &unlimited_it); + + try testing.expectEqual( + std.math.maxInt(usize), + cfg.@"scrollback-limit-bytes".value, + ); + try testing.expectEqual( + std.math.maxInt(usize), + cfg.@"scrollback-limit-lines".value, + ); + + var reset_it: TestIterator = .{ .data = &.{ + "--scrollback-limit-bytes=", + "--scrollback-limit-lines=", + } }; + try cfg.loadIter(alloc, &reset_it); + + try testing.expectEqual( + @as(usize, 50_000_000), + cfg.@"scrollback-limit-bytes".value, + ); + try testing.expectEqual( + std.math.maxInt(usize), + cfg.@"scrollback-limit-lines".value, + ); +} + +test "clipboard write limit" { + const testing = std.testing; + const alloc = testing.allocator; + + var cfg = try Config.default(alloc); + defer cfg.deinit(); + try testing.expectEqual( + @as(usize, 64 * 1024 * 1024), + cfg.@"clipboard-write-limit-bytes".value, + ); + + var it: TestIterator = .{ .data = &.{ + "--clipboard-write-limit-bytes=1234", + } }; + try cfg.loadIter(alloc, &it); + + try testing.expectEqual( + @as(usize, 1234), + cfg.@"clipboard-write-limit-bytes".value, + ); + + var unlimited_it: TestIterator = .{ .data = &.{ + "--clipboard-write-limit-bytes=unlimited", + } }; + try cfg.loadIter(alloc, &unlimited_it); + + try testing.expectEqual( + std.math.maxInt(usize), + cfg.@"clipboard-write-limit-bytes".value, + ); +} + +test "compatibility: scrollback-limit renamed to bytes" { + const testing = std.testing; + const alloc = testing.allocator; + + var cfg = try Config.default(alloc); + defer cfg.deinit(); + var it: TestIterator = .{ .data = &.{ + "--scrollback-limit=1234", + } }; + try cfg.loadIter(alloc, &it); + + try testing.expectEqual( + @as(usize, 1234), + cfg.@"scrollback-limit-bytes".value, + ); + + var unlimited_it: TestIterator = .{ .data = &.{ + "--scrollback-limit=unlimited", + } }; + try cfg.loadIter(alloc, &unlimited_it); + + try testing.expectEqual( + std.math.maxInt(usize), + cfg.@"scrollback-limit-bytes".value, + ); +} + test "compatibility: gtk-single-instance desktop" { const testing = std.testing; const alloc = testing.allocator; diff --git a/vendor/libghostty-vt/src/config/c_get.zig b/vendor/libghostty-vt/src/config/c_get.zig index dcfdc671..9bcb8d98 100644 --- a/vendor/libghostty-vt/src/config/c_get.zig +++ b/vendor/libghostty-vt/src/config/c_get.zig @@ -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)); diff --git a/vendor/libghostty-vt/src/config/command.zig b/vendor/libghostty-vt/src/config/command.zig index 7cd70acb..b48bee18 100644 --- a/vendor/libghostty-vt/src/config/command.zig +++ b/vendor/libghostty-vt/src/config/command.zig @@ -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, diff --git a/vendor/libghostty-vt/src/config/conditional.zig b/vendor/libghostty-vt/src/config/conditional.zig index fdc285a2..e1a78792 100644 --- a/vendor/libghostty-vt/src/config/conditional.zig +++ b/vendor/libghostty-vt/src/config/conditional.zig @@ -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. diff --git a/vendor/libghostty-vt/src/config/edit.zig b/vendor/libghostty-vt/src/config/edit.zig index 8cedc47a..113ec8b2 100644 --- a/vendor/libghostty-vt/src/config/edit.zig +++ b/vendor/libghostty-vt/src/config/edit.zig @@ -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. /// @@ -26,30 +27,52 @@ pub fn openPath(alloc_gpa: Allocator) ![:0]const u8 { // Get the path we should open const config_path = try configPath(alloc_arena); - // Create config directory recursively. - if (std.fs.path.dirname(config_path)) |config_dir| { - try std.fs.cwd().makePath(config_dir); - } - - // Try to create file and go on if it already exists - _ = std.fs.createFileAbsolute( - config_path, - .{ .exclusive = true }, - ) catch |err| { - switch (err) { - error.PathAlreadyExists => {}, - else => return err, + if (!config_path.exists) { + if (std.fs.path.dirname(config_path.name)) |config_dir| check_dir: { + // Check to see if dir exists. + const dir = std.Io.Dir.cwd().openDir(global.io(), config_dir, .{ .follow_symlinks = true }) catch |err| { + switch (err) { + error.FileNotFound => { + // Create config directory recursively. Note that this does not + // allow intermediate symlinks by design, see + // std.Io.Threaded.dirCreateDirPath for why. If some sort of + // complex symlink structure is needed, it will need to be created + // manually. + try std.Io.Dir.cwd().createDirPath(global.io(), config_dir); + break :check_dir; + }, + else => return err, + } + }; + dir.close(global.io()); } - }; - return try alloc_gpa.dupeZ(u8, config_path); + // Try to create file and go on if it already exists + _ = std.Io.Dir.createFileAbsolute( + global.io(), + config_path.name, + .{ .exclusive = true }, + ) catch |err| { + switch (err) { + error.PathAlreadyExists => {}, + else => return err, + } + }; + } + + return try alloc_gpa.dupeZ(u8, config_path.name); } +const ConfigPathResult = struct { + name: []const u8, + exists: bool, +}; + /// Returns the config path to use for open for the current OS. /// /// The allocator must be an arena allocator. No memory is freed by this /// function and the resulting path is not all the memory that is allocated. -fn configPath(alloc_arena: Allocator) ![]const u8 { +fn configPath(alloc_arena: Allocator) !ConfigPathResult { const paths: []const []const u8 = try configPathCandidates(alloc_arena); assert(paths.len > 0); @@ -58,7 +81,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,23 +90,32 @@ 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; + if (stat.size > 0) return .{ + .name = path, + .exists = true, + }; // If the file is empty, remember it exists. if (exists == null) exists = path; } // No paths are non-empty, return the first path that exists. - if (exists) |v| return v; + if (exists) |v| return .{ + .name = v, + .exists = true, + }; // No paths are non-empty or exist, return the first path. - return paths[0]; + return .{ + .name = paths[0], + .exists = false, + }; } /// Returns a const list of possible paths the main config file could be diff --git a/vendor/libghostty-vt/src/config/file_load.zig b/vendor/libghostty-vt/src/config/file_load.zig index 7885de32..7495e899 100644 --- a/vendor/libghostty-vt/src/config/file_load.zig +++ b/vendor/libghostty-vt/src/config/file_load.zig @@ -3,14 +3,19 @@ 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( + global.io(), alloc, + &environ_map, .{ .subdir = "ghostty/config.ghostty" }, ); } @@ -18,8 +23,12 @@ 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( + global.io(), alloc, + &environ_map, .{ .subdir = "ghostty/config" }, ); } @@ -29,16 +38,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 +75,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 +108,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 +139,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 +156,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, diff --git a/vendor/libghostty-vt/src/config/formatter_file.zig b/vendor/libghostty-vt/src/config/formatter_file.zig index 37beb7b5..71e8004e 100644 --- a/vendor/libghostty-vt/src/config/formatter_file.zig +++ b/vendor/libghostty-vt/src/config/formatter_file.zig @@ -41,7 +41,7 @@ pub const FileFormatter = struct { if (field.name[0] == '_') continue; const value = @field(self.config, field.name); - const do_format = if (default) |d| format: { + const do_format = if (default) |*d| format: { const key = @field(Key, field.name); break :format d.changed(self.config, key); } else true; diff --git a/vendor/libghostty-vt/src/config/io.zig b/vendor/libghostty-vt/src/config/io.zig index a1e433b6..056dc874 100644 --- a/vendor/libghostty-vt/src/config/io.zig +++ b/vendor/libghostty-vt/src/config/io.zig @@ -156,7 +156,7 @@ pub const RepeatableReadableIO = struct { const Self = @This(); // Allocator for the list is the arena for the parent config. - list: std.ArrayListUnmanaged(ReadableIO) = .{}, + list: std.ArrayList(ReadableIO) = .empty, pub fn parseCLI( self: *Self, diff --git a/vendor/libghostty-vt/src/config/key.zig b/vendor/libghostty-vt/src/config/key.zig index 5709e207..1d15c937 100644 --- a/vendor/libghostty-vt/src/config/key.zig +++ b/vendor/libghostty-vt/src/config/key.zig @@ -6,29 +6,26 @@ const Config = @import("Config.zig"); /// amongst other things. pub const Key = key: { const field_infos = std.meta.fields(Config); - var enumFields: [field_infos.len]std.builtin.Type.EnumField = undefined; + var names: [field_infos.len][]const u8 = undefined; + var raw_values: [field_infos.len]comptime_int = undefined; var i: usize = 0; - for (field_infos) |field| { + for (field_infos, &names, &raw_values) |field, *name, *raw| { // Ignore fields starting with "_" since they're internal and // not copied ever. if (field.name[0] == '_') continue; - enumFields[i] = .{ - .name = field.name, - .value = i, - }; + name.* = field.name; + raw.* = i; i += 1; } - var decls = [_]std.builtin.Type.Declaration{}; - break :key @Type(.{ - .@"enum" = .{ - .tag_type = std.math.IntFittingRange(0, field_infos.len - 1), - .fields = enumFields[0..i], - .decls = &decls, - .is_exhaustive = true, - }, - }); + const TagInt = std.math.IntFittingRange(0, field_infos.len - 1); + var values: [i]TagInt = undefined; + for (raw_values[0..i], &values) |raw, *val| { + val.* = raw; + } + + break :key @Enum(TagInt, .exhaustive, names[0..i], &values); }; /// Returns the value type for a key diff --git a/vendor/libghostty-vt/src/config/limit.zig b/vendor/libghostty-vt/src/config/limit.zig new file mode 100644 index 00000000..ace78676 --- /dev/null +++ b/vendor/libghostty-vt/src/config/limit.zig @@ -0,0 +1,115 @@ +const std = @import("std"); +const formatterpkg = @import("formatter.zig"); + +/// A configurable integer limit with a declared default and support for the +/// special value `unlimited`. +/// +/// Unlimited is stored as the maximum value of the integer type so the value +/// remains directly comparable without optional unwrapping. +pub fn Limit(comptime T: type, comptime default_value: T) type { + comptime std.debug.assert(@typeInfo(T) == .int); + + return struct { + const Self = @This(); + + value: T = default_value, + + /// The limit initialized with its declared default value. + pub const default: Self = .{}; + + /// Parses an integer limit or the special value "unlimited". + pub fn parseCLI(input_: ?[]const u8) !Self { + const input = input_ orelse return error.ValueRequired; + if (std.mem.eql(u8, input, "unlimited")) { + return .{ .value = std.math.maxInt(T) }; + } + + return .{ + .value = std.fmt.parseInt(T, input, 0) catch + return error.InvalidValue, + }; + } + + /// Formats the maximum integer value as "unlimited" and all other + /// values as integers. + pub fn formatEntry( + self: Self, + formatter: formatterpkg.EntryFormatter, + ) !void { + if (self.value == std.math.maxInt(T)) { + try formatter.formatEntry([]const u8, "unlimited"); + } else { + try formatter.formatEntry(T, self.value); + } + } + + /// Returns the configured value, or null for the unlimited sentinel. + pub fn optional(self: Self) ?T { + if (self.value == std.math.maxInt(T)) return null; + return self.value; + } + + /// Returns an independent copy of this value for Config cloning. + pub fn clone( + self: Self, + _: std.mem.Allocator, + ) std.mem.Allocator.Error!Self { + return self; + } + }; +} + +test "Limit default and parsing" { + const testing = std.testing; + const TestLimit = Limit(usize, 42); + + try testing.expectEqual(@as(usize, 42), TestLimit.default.value); + try testing.expectEqual( + @as(usize, 123), + (try TestLimit.parseCLI("123")).value, + ); + try testing.expectEqual( + std.math.maxInt(usize), + (try TestLimit.parseCLI("unlimited")).value, + ); + try testing.expectError(error.InvalidValue, TestLimit.parseCLI("invalid")); +} + +test "Limit optional" { + const testing = std.testing; + const TestLimit = Limit(u16, 42); + + try testing.expectEqual(@as(?u16, 42), (TestLimit{}).optional()); + try testing.expectEqual( + @as(?u16, null), + (TestLimit{ .value = std.math.maxInt(u16) }).optional(), + ); +} + +test "Limit formatting" { + const testing = std.testing; + const TestLimit = Limit(usize, 42); + + { + var buf: std.Io.Writer.Allocating = .init(testing.allocator); + defer buf.deinit(); + + try (TestLimit{ .value = 123 }).formatEntry( + formatterpkg.entryFormatter("limit", &buf.writer), + ); + try testing.expectEqualStrings("limit = 123\n", buf.written()); + } + + { + var buf: std.Io.Writer.Allocating = .init(testing.allocator); + defer buf.deinit(); + + try (TestLimit{ .value = std.math.maxInt(usize) }).formatEntry( + formatterpkg.entryFormatter("limit", &buf.writer), + ); + try testing.expectEqualStrings( + "limit = unlimited\n", + buf.written(), + ); + } +} diff --git a/vendor/libghostty-vt/src/config/path.zig b/vendor/libghostty-vt/src/config/path.zig index 793cf184..5fd88f33 100644 --- a/vendor/libghostty-vt/src/config/path.zig +++ b/vendor/libghostty-vt/src/config/path.zig @@ -7,6 +7,7 @@ const ArenaAllocator = std.heap.ArenaAllocator; const cli = @import("../cli.zig"); const internal_os = @import("../os/main.zig"); const formatterpkg = @import("formatter.zig"); +const global = @import("../global.zig"); const log = std.log.scoped(.config); @@ -163,7 +164,12 @@ pub const Path = union(enum) { // Windows isn't supported yet if (comptime builtin.os.tag == .windows) break :expand; + var environ_map = try global.environMap(); + defer environ_map.deinit(); + const expanded: []const u8 = internal_os.expandHome( + global.io(), + &environ_map, path, &buf, ) catch |err| { @@ -195,32 +201,36 @@ pub const Path = union(enum) { return; } - var dir = try std.fs.openDirAbsolute(base, .{}); - defer dir.close(); - - const abs = dir.realpath(path, &buf) catch |err| abs: { - if (err == error.FileNotFound) { - // The file doesn't exist. Try to resolve the relative path - // another way. - const resolved = try std.fs.path.resolve(arena_alloc, &.{ base, path }); - defer arena_alloc.free(resolved); - @memcpy(buf[0..resolved.len], resolved); - break :abs buf[0..resolved.len]; - } + var dir = try std.Io.Dir.openDirAbsolute(global.io(), base, .{}); + defer dir.close(global.io()); - try diags.append(arena_alloc, .{ - .message = try std.fmt.allocPrintSentinel( - arena_alloc, - "error resolving file path {s}: {}", - .{ path, err }, - 0, - ), - }); + const abs = abs: { + const abs_len = dir.realPathFile(global.io(), path, &buf) catch |err| { + if (err == error.FileNotFound) { + // The file doesn't exist. Try to resolve the relative path + // another way. + const resolved = try std.fs.path.resolve(arena_alloc, &.{ base, path }); + defer arena_alloc.free(resolved); + @memcpy(buf[0..resolved.len], resolved); + break :abs buf[0..resolved.len]; + } - // Blank this path so that we don't attempt to resolve it again - self.* = .{ .required = "" }; + try diags.append(arena_alloc, .{ + .message = try std.fmt.allocPrintSentinel( + arena_alloc, + "error resolving file path {s}: {}", + .{ path, err }, + 0, + ), + }); - return; + // Blank this path so that we don't attempt to resolve it again + self.* = .{ .required = "" }; + + return; + }; + + break :abs buf[0..abs_len]; }; log.debug( @@ -357,7 +367,7 @@ pub const Path = union(enum) { /// be automatically expanded relative to the path of the config file (or the home /// directory). pub const RepeatablePath = struct { - value: std.ArrayListUnmanaged(Path) = .{}, + value: std.ArrayList(Path) = .empty, pub fn parseCLI(self: *RepeatablePath, alloc: Allocator, input: ?[]const u8) ParseError!void { const item = try Path.parse(alloc, input) orelse { diff --git a/vendor/libghostty-vt/src/config/string.zig b/vendor/libghostty-vt/src/config/string.zig index 45079937..8d4fc7b4 100644 --- a/vendor/libghostty-vt/src/config/string.zig +++ b/vendor/libghostty-vt/src/config/string.zig @@ -20,16 +20,23 @@ pub fn parse(out: []u8, bytes: []const u8) ![]u8 { continue; } + // Zig's \xNN syntax represents a single byte, while the other + // escape sequences represent Unicode codepoints. + const is_byte_escape = src_i + 1 < bytes.len and + bytes[src_i + 1] == 'x'; + // Parse the escape sequence switch (std.zig.string_literal.parseEscapeSequence( bytes, &src_i, )) { .failure => return error.InvalidString, - .success => |cp| dst_i += try std.unicode.utf8Encode( - cp, - out[dst_i..], - ), + .success => |cp| if (is_byte_escape) { + out[dst_i] = @intCast(cp); + dst_i += 1; + } else { + dst_i += try std.unicode.utf8Encode(cp, out[dst_i..]); + }, } } @@ -100,6 +107,15 @@ test "parse: escapes" { } } +test "parse: hex escapes are bytes" { + var buf: [128]u8 = undefined; + const result = try parse( + &buf, + "\\xe6\\x97\\xa5\\xe6\\x9c\\xac\\xe8\\xaa\\x9e", + ); + try std.testing.expectEqualStrings("日本語", result); +} + test "codepointIterator: empty" { var it = codepointIterator(""); try std.testing.expectEqual(null, try it.next()); diff --git a/vendor/libghostty-vt/src/config/theme.zig b/vendor/libghostty-vt/src/config/theme.zig index 8776fb1b..a8c39cad 100644 --- a/vendor/libghostty-vt/src/config/theme.zig +++ b/vendor/libghostty-vt/src/config/theme.zig @@ -1,9 +1,9 @@ const std = @import("std"); const builtin = @import("builtin"); const Allocator = std.mem.Allocator; -const global_state = &@import("../global.zig").state; const internal_os = @import("../os/main.zig"); const cli = @import("../cli.zig"); +const global = @import("../global.zig"); /// Location of possible themes. The order of this enum matters because it /// defines the priority of theme search (from top to bottom). @@ -23,7 +23,9 @@ pub const Location = enum { pub fn dir( self: Location, arena_alloc: Allocator, - ) error{OutOfMemory}!?[]const u8 { + ) error{ OutOfMemory, Unexpected }!?[]const u8 { + var environ_map = try global.environMap(); + defer environ_map.deinit(); return switch (self) { .user => user: { const subdir = std.fs.path.join(arena_alloc, &.{ @@ -31,7 +33,9 @@ pub const Location = enum { }) catch return error.OutOfMemory; break :user internal_os.xdg.config( + global.io(), arena_alloc, + &environ_map, .{ .subdir = subdir }, ) catch |err| { // We need to do some comptime tricks to get the right @@ -55,7 +59,7 @@ pub const Location = enum { }, .resources => try std.fs.path.join(arena_alloc, &.{ - global_state.resources_dir.app() orelse return null, + global.resourcesDir().app() orelse return null, "themes", }), }; @@ -111,18 +115,18 @@ pub fn open( arena_alloc: Allocator, theme: []const u8, diags: *cli.DiagnosticList, -) error{OutOfMemory}!?struct { +) error{ OutOfMemory, Unexpected }!?struct { path: []const u8, - file: std.fs.File, + file: std.Io.File, } { // Absolute themes are loaded a different path. if (std.fs.path.isAbsolute(theme)) { - const file: std.fs.File = try openAbsolute( + const file: std.Io.File = try openAbsolute( arena_alloc, theme, diags, ) orelse return null; - const stat = file.stat() catch |err| { + const stat = file.stat(global.io()) catch |err| { try diags.append(arena_alloc, .{ .message = try std.fmt.allocPrintSentinel( arena_alloc, @@ -166,11 +170,11 @@ pub fn open( // Iterate over the possible locations to try to find the // one that exists. var it: LocationIterator = .{ .arena_alloc = arena_alloc }; - const cwd = std.fs.cwd(); + const cwd = std.Io.Dir.cwd(); while (try it.next()) |loc| { const path = try std.fs.path.join(arena_alloc, &.{ loc.dir, theme }); - if (cwd.openFile(path, .{})) |file| { - const stat = file.stat() catch |err| { + if (cwd.openFile(global.io(), path, .{})) |file| { + const stat = file.stat(global.io()) catch |err| { try diags.append(arena_alloc, .{ .message = try std.fmt.allocPrintSentinel( arena_alloc, @@ -251,8 +255,8 @@ pub fn openAbsolute( arena_alloc: Allocator, theme: []const u8, diags: *cli.DiagnosticList, -) error{OutOfMemory}!?std.fs.File { - return std.fs.openFileAbsolute(theme, .{}) catch |err| { +) error{OutOfMemory}!?std.Io.File { + return std.Io.Dir.openFileAbsolute(global.io(), theme, .{}) catch |err| { switch (err) { error.FileNotFound => try diags.append(arena_alloc, .{ .message = try std.fmt.allocPrintSentinel( diff --git a/vendor/libghostty-vt/src/config/url.zig b/vendor/libghostty-vt/src/config/url.zig index 2621898e..463ea9bf 100644 --- a/vendor/libghostty-vt/src/config/url.zig +++ b/vendor/libghostty-vt/src/config/url.zig @@ -52,10 +52,6 @@ const no_trailing_colon = \\(?<!:) ; -const trailing_spaces_at_eol = - \\(?: +(?= *$))? -; - const dotted_path_lookahead = \\(?=[\w\-.~:\/?#@!$&*+;=%]*\.) ; @@ -92,13 +88,11 @@ const rooted_or_relative_path_branch = path_chars ++ "+" ++ dotted_path_space_segments ++ no_trailing_colon ++ - trailing_spaces_at_eol ++ "|" ++ non_dotted_path_lookahead ++ path_chars ++ "+" ++ any_path_space_segments ++ no_trailing_colon ++ - trailing_spaces_at_eol ++ ")"; // Branch 3: Bare relative paths such as src/config/url.zig. @@ -110,8 +104,7 @@ const bare_relative_path_branch = dotted_path_lookahead ++ bare_relative_path_prefix ++ path_chars ++ "+" ++ - no_trailing_colon ++ - trailing_spaces_at_eol; + no_trailing_colon; pub const regex = scheme_url_branch ++ @@ -282,7 +275,7 @@ test "url regex" { }, .{ .input = "../example.py ", - .expect = "../example.py ", + .expect = "../example.py", }, .{ .input = "first time ../example.py contributor ", @@ -341,11 +334,12 @@ test "url regex" { .input = "IPv6 in markdown [link](http://[2001:db8::1]/docs)", .expect = "http://[2001:db8::1]/docs", }, - // File paths with spaces + // Trailing whitespace isn't part of a detected file path. .{ .input = "./spaces-end. ", - .expect = "./spaces-end. ", + .expect = "./spaces-end.", }, + // File paths with internal spaces .{ .input = "./space middle", .expect = "./space middle", |
