aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/config/Config.zig
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/Config.zig
parent90e947a6e26e02f10ec7f1694c4eb5b78a86406b (diff)
chore: upgrade libghostty and preserve terminal behavior (#3906)
Diffstat (limited to 'vendor/libghostty-vt/src/config/Config.zig')
-rw-r--r--vendor/libghostty-vt/src/config/Config.zig692
1 files changed, 535 insertions, 157 deletions
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;