aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/config
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-09-10 21:51:57 +0300
committerGitHub <noreply@github.com>2026-09-10 21:51:57 +0300
commit425c86179791cd32e2d4ce0ae7f269940b4b0663 (patch)
treee52c4383a6d69216cbe2a32638bf90841d7e2189 /vendor/libghostty-vt/src/config
parent90e947a6e26e02f10ec7f1694c4eb5b78a86406b (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.zig26
-rw-r--r--vendor/libghostty-vt/src/config/Config.zig692
-rw-r--r--vendor/libghostty-vt/src/config/c_get.zig2
-rw-r--r--vendor/libghostty-vt/src/config/command.zig2
-rw-r--r--vendor/libghostty-vt/src/config/conditional.zig20
-rw-r--r--vendor/libghostty-vt/src/config/edit.zig76
-rw-r--r--vendor/libghostty-vt/src/config/file_load.zig42
-rw-r--r--vendor/libghostty-vt/src/config/formatter_file.zig2
-rw-r--r--vendor/libghostty-vt/src/config/io.zig2
-rw-r--r--vendor/libghostty-vt/src/config/key.zig27
-rw-r--r--vendor/libghostty-vt/src/config/limit.zig115
-rw-r--r--vendor/libghostty-vt/src/config/path.zig58
-rw-r--r--vendor/libghostty-vt/src/config/string.zig24
-rw-r--r--vendor/libghostty-vt/src/config/theme.zig28
-rw-r--r--vendor/libghostty-vt/src/config/url.zig16
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",