diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-09-10 21:51:57 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-10 21:51:57 +0300 |
| commit | 425c86179791cd32e2d4ce0ae7f269940b4b0663 (patch) | |
| tree | e52c4383a6d69216cbe2a32638bf90841d7e2189 /vendor/libghostty-vt/src/config/Config.zig | |
| parent | 90e947a6e26e02f10ec7f1694c4eb5b78a86406b (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.zig | 692 |
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; |
