ghostty/src/config/Config.zig

2851 lines
103 KiB
Zig

/// Config is the main config struct. These fields map directly to the
/// CLI flag names hence we use a lot of `@""` syntax to support hyphens.
const Config = @This();
const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const global_state = &@import("../main.zig").state;
const fontpkg = @import("../font/main.zig");
const inputpkg = @import("../input.zig");
const terminal = @import("../terminal/main.zig");
const internal_os = @import("../os/main.zig");
const cli = @import("../cli.zig");
const Command = @import("../Command.zig");
const url = @import("url.zig");
const Key = @import("key.zig").Key;
const KeyValue = @import("key.zig").Value;
const ErrorList = @import("ErrorList.zig");
const MetricModifier = fontpkg.face.Metrics.Modifier;
const log = std.log.scoped(.config);
/// Used on Unixes for some defaults.
const c = @cImport({
@cInclude("unistd.h");
});
/// The font families to use.
///
/// You can generate the list of valid values using the CLI:
/// path/to/ghostty/cli +list-fonts
///
/// This configuration can be repeated multiple times to specify
/// preferred fallback fonts when the requested codepoint is not
/// available in the primary font. This is particularly useful for
/// multiple languages, symbolic fonts, etc.
///
/// If you want to overwrite a previous set value rather than append
/// a fallback, specify the value as "" (empty string) to reset the list
/// and then set the new values. For example:
///
/// font-family = ""
/// font-family = "My Favorite Font"
///
/// Changing this configuration at runtime will only affect new terminals,
/// i.e. new windows, tabs, etc.
@"font-family": RepeatableString = .{},
@"font-family-bold": RepeatableString = .{},
@"font-family-italic": RepeatableString = .{},
@"font-family-bold-italic": RepeatableString = .{},
/// The named font style to use for each of the requested terminal font
/// styles. This looks up the style based on the font style string advertised
/// by the font itself. For example, "Iosevka Heavy" has a style of "Heavy".
///
/// You can also use these fields to completely disable a font style. If
/// you set the value of the configuration below to literal "false" then
/// that font style will be disabled. If the running program in the terminal
/// requests a disabled font style, the regular font style will be used
/// instead.
///
/// These are only valid if its corresponding font-family is also specified.
/// If no font-family is specified, then the font-style is ignored unless
/// you're disabling the font style.
@"font-style": FontStyle = .{ .default = {} },
@"font-style-bold": FontStyle = .{ .default = {} },
@"font-style-italic": FontStyle = .{ .default = {} },
@"font-style-bold-italic": FontStyle = .{ .default = {} },
/// Apply a font feature. This can be repeated multiple times to enable
/// multiple font features. You can NOT set multiple font features with
/// a single value (yet).
///
/// The font feature will apply to all fonts rendered by Ghostty. A
/// future enhancement will allow targeting specific faces.
///
/// A valid value is the name of a feature. Prefix the feature with a
/// "-" to explicitly disable it. Example: "ss20" or "-ss20".
///
/// To disable programming ligatures, use "-calt" since this is the typical
/// feature name for programming ligatures. To look into what font features
/// your font has and what they do, use a font inspection tool such as
/// fontdrop.info.
///
/// To generally disable most ligatures, use "-calt", "-liga", and "-dlig"
/// (as separate repetitive entries in your config).
@"font-feature": RepeatableString = .{},
/// Font size in points
@"font-size": u8 = switch (builtin.os.tag) {
// On Mac we default a little bigger since this tends to look better.
// This is purely subjective but this is easy to modify.
.macos => 13,
else => 12,
},
/// A repeatable configuration to set one or more font variations values
/// for a variable font. A variable font is a single font, usually
/// with a filename ending in "-VF.ttf" or "-VF.otf" that contains
/// one or more configurable axes for things such as weight, slant,
/// etc. Not all fonts support variations; only fonts that explicitly
/// state they are variable fonts will work.
///
/// The format of this is "id=value" where "id" is the axis identifier.
/// An axis identifier is always a 4 character string, such as "wght".
/// To get the list of supported axes, look at your font documentation
/// or use a font inspection tool.
///
/// Invalid ids and values are usually ignored. For example, if a font
/// only supports weights from 100 to 700, setting "wght=800" will
/// do nothing (it will not be clamped to 700). You must consult your
/// font's documentation to see what values are supported.
///
/// Common axes are: "wght" (weight), "slnt" (slant), "ital" (italic),
/// "opsz" (optical size), "wdth" (width), "GRAD" (gradient), etc.
@"font-variation": RepeatableFontVariation = .{},
@"font-variation-bold": RepeatableFontVariation = .{},
@"font-variation-italic": RepeatableFontVariation = .{},
@"font-variation-bold-italic": RepeatableFontVariation = .{},
/// Force one or a range of Unicode codepoints to map to a specific named
/// font. This is useful if you want to support special symbols or if you
/// want to use specific glyphs that render better for your specific font.
///
/// The syntax is "codepoint=fontname" where "codepoint" is either a
/// single codepoint or a range. Codepoints must be specified as full
/// Unicode hex values, such as "U+ABCD". Codepoints ranges are specified
/// as "U+ABCD-U+DEFG". You can specify multiple ranges for the same font
/// separated by commas, such as "U+ABCD-U+DEFG,U+1234-U+5678=fontname".
/// The font name is the same value as you would use for "font-family".
///
/// This configuration can be repeated multiple times to specify multiple
/// codepoint mappings.
///
/// Changing this configuration at runtime will only affect new terminals,
/// i.e. new windows, tabs, etc.
@"font-codepoint-map": RepeatableCodepointMap = .{},
/// Draw fonts with a thicker stroke, if supported. This is only supported
/// currently on macOS.
@"font-thicken": bool = false,
/// All of the configurations behavior adjust various metrics determined
/// by the font. The values can be integers (1, -1, etc.) or a percentage
/// (20%, -15%, etc.). In each case, the values represent the amount to
/// change the original value.
///
/// For example, a value of "1" increases the value by 1; it does not set
/// it to literally 1. A value of "20%" increases the value by 20%. And so
/// on.
///
/// There is little to no validation on these values so the wrong values
/// (i.e. "-100%") can cause the terminal to be unusable. Use with caution
/// and reason.
///
/// Some values are clamped to minimum or maximum values. This can make it
/// appear that certain values are ignored. For example, the underline
/// position is clamped to the height of a cell. If you set the underline
/// position so high that it extends beyond the bottom of the cell size,
/// it will be clamped to the bottom of the cell.
///
/// "adjust-cell-height" has some additional behaviors to describe:
/// - The font will be centered vertically in the cell.
/// - The cursor will remain the same size as the font.
/// - Powerline glyphs will be adjusted along with the cell height so
/// that things like status lines continue to look aligned.
///
@"adjust-cell-width": ?MetricModifier = null,
@"adjust-cell-height": ?MetricModifier = null,
@"adjust-font-baseline": ?MetricModifier = null,
@"adjust-underline-position": ?MetricModifier = null,
@"adjust-underline-thickness": ?MetricModifier = null,
@"adjust-strikethrough-position": ?MetricModifier = null,
@"adjust-strikethrough-thickness": ?MetricModifier = null,
/// The method to use for calculating the cell width of a grapheme cluster.
/// The default value is "unicode" which uses the Unicode standard to
/// determine grapheme width. This results in correct grapheme width but
/// may result in cursor-desync issues with some programs (such as shells)
/// that may use a legacy method such as "wcswidth".
///
/// Valid values are:
///
/// - "wcswidth" - Use the wcswidth function to determine grapheme width.
/// This maximizes compatibility with legacy programs but may result
/// in incorrect grapheme width for certain graphemes such as skin-tone
/// emoji, non-English characters, etc.
///
/// Note that this "wcswidth" functionality is based on the libc wcswidth,
/// not any other libraries with that name.
///
/// - "unicode" - Use the Unicode standard to determine grapheme width.
///
/// If a running program explicitly enables terminal mode 2027, then
/// "unicode" width will be forced regardless of this configuration. When
/// mode 2027 is reset, this configuration will be used again.
///
/// This configuration can be changed at runtime but will not affect existing
/// printed cells. Only new cells will use the new configuration.
@"grapheme-width-method": GraphemeWidthMethod = .unicode,
/// A named theme to use. The available themes are currently hardcoded to
/// the themes that ship with Ghostty. On macOS, this list is in the
/// `Ghostty.app/Contents/Resources/themes` directory. On Linux, this
/// list is in the `share/ghostty/themes` directory (wherever you installed
/// the Ghostty "share" directory.
///
/// To see a list of available themes, run `ghostty +list-themes`.
///
/// Any additional colors specified via background, foreground, palette,
/// etc. will override the colors specified in the theme.
///
/// This configuration can be changed at runtime, but the new theme will
/// only affect new cells. Existing colored cells will not be updated.
/// Therefore, after changing the theme, you should restart any running
/// programs to ensure they get the new colors.
///
/// A future update will allow custom themes to be installed in
/// certain directories.
theme: ?[]const u8 = null,
/// Background color for the window.
background: Color = .{ .r = 0x28, .g = 0x2C, .b = 0x34 },
/// Foreground color for the window.
foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF },
/// The foreground and background color for selection. If this is not
/// set, then the selection color is just the inverted window background
/// and foreground (note: not to be confused with the cell bg/fg).
@"selection-foreground": ?Color = null,
@"selection-background": ?Color = null,
/// Swap the foreground and background colors of cells for selection.
/// This option overrides the "selection-foreground" and "selection-background"
/// options.
///
/// If you select across cells with differing foregrounds and backgrounds,
/// the selection color will vary across the selection.
@"selection-invert-fg-bg": bool = false,
/// The minimum contrast ratio between the foreground and background
/// colors. The contrast ratio is a value between 1 and 21. A value of
/// 1 allows for no contrast (i.e. black on black). This value is
/// the contrast ratio as defined by the WCAG 2.0 specification.
///
/// If you want to avoid invisible text (same color as background),
/// a value of 1.1 is a good value. If you want to avoid text that is
/// difficult to read, a value of 3 or higher is a good value. The higher
/// the value, the more likely that text will become black or white.
///
/// This value does not apply to Emoji or images.
@"minimum-contrast": f64 = 1,
/// Color palette for the 256 color form that many terminal applications
/// use. The syntax of this configuration is "N=HEXCODE" where "n"
/// is 0 to 255 (for the 256 colors) and HEXCODE is a typical RGB
/// color code such as "#AABBCC". The 0 to 255 correspond to the
/// terminal color table.
///
/// For definitions on all the codes:
/// https://www.ditig.com/256-colors-cheat-sheet
palette: Palette = .{},
/// The color of the cursor. If this is not set, a default will be chosen.
@"cursor-color": ?Color = null,
/// The opacity level (opposite of transparency) of the cursor.
/// A value of 1 is fully opaque and a value of 0 is fully transparent.
/// A value less than 0 or greater than 1 will be clamped to the nearest
/// valid value. Note that a sufficiently small value such as 0.3 may be
/// effectively invisible and may make it difficult to find the cursor.
@"cursor-opacity": f64 = 1.0,
/// The style of the cursor. This sets the default style. A running
/// programn can still request an explicit cursor style using escape
/// sequences (such as CSI q). Shell configurations will often request
/// specific cursor styles.
///
/// Note that shell integration will automatically set the cursor to a bar
/// at a prompt, regardless of this configuration. You can disable that
/// behavior by specifying `shell-integration-features = no-cursor`
/// or disabling shell integration entirely.
@"cursor-style": terminal.Cursor.Style = .block,
/// Sets the default blinking state of the cursor. This is just the
/// default state; running programs may override the cursor style
/// using DECSCUSR (CSI q).
///
/// If this is not set, the cursor blinks by default. Note that
/// this is not the same as a "true" value, as noted below.
///
/// If this is not set at all (null), then Ghostty will respect
/// DEC Mode 12 (AT&T cursor blink) as an alternate approach to
/// turning blinking on/off. If this is set to any value other
/// than null, DEC mode 12 will be ignored but DECSCUSR will still
/// be respected.
@"cursor-style-blink": ?bool = null,
/// The color of the text under the cursor. If this is not set, a default
/// will be chosen.
@"cursor-text": ?Color = null,
/// Enables the ability to move the cursor at prompts by using alt+click
/// on Linux and option+click on macOS.
///
/// This feature requires shell integration (specifically prompt marking
/// via OSC 133) and only works in primary screen mode. Alternate screen
/// applications like vim usually have their own version of this feature
/// but this configuration doesn't control that.
///
/// It should be noted that this feature works by translating your desired
/// position into a series of synthetic arrow key movements, so some weird
/// behavior around edge cases are to be expected. This is unfortunately
/// how this feature is implemented across terminals because there isn't
/// any other way to implement it.
@"cursor-click-to-move": bool = true,
/// Hide the mouse immediately when typing. The mouse becomes visible
/// again when the mouse is used. The mouse is only hidden if the mouse
/// cursor is over the active terminal surface.
@"mouse-hide-while-typing": bool = false,
/// Determines whether running programs can detect the shift key pressed
/// with a mouse click. Typically, the shift key is used to extend mouse
/// selection.
///
/// The default value of "false" means that the shift key is not sent
/// with the mouse protocol and will extend the selection. This value
/// can be conditionally overridden by the running program with the
/// XTSHIFTESCAPE sequence.
///
/// The value "true" means that the shift key is sent with the mouse
/// protocol but the running program can override this behavior with
/// XTSHIFTESCAPE.
///
/// The value "never" is the same as "false" but the running program
/// cannot override this behavior with XTSHIFTESCAPE. The value "always"
/// is the same as "true" but the running program cannot override this
/// behavior with XTSHIFTESCAPE.
///
/// If you always want shift to extend mouse selection even if the
/// program requests otherwise, set this to "never".
@"mouse-shift-capture": MouseShiftCapture = .false,
/// The opacity level (opposite of transparency) of the background.
/// A value of 1 is fully opaque and a value of 0 is fully transparent.
/// A value less than 0 or greater than 1 will be clamped to the nearest
/// valid value.
///
/// Changing this value at runtime (and reloading config) will only
/// affect new windows, tabs, and splits.
@"background-opacity": f64 = 1.0,
/// A positive value enables blurring of the background when
/// background-opacity is less than 1. The value is the blur radius to
/// apply. A value of 20 is reasonable for a good looking blur.
/// Higher values will cause strange rendering issues as well as
/// performance issues.
///
/// This is only supported on macOS.
@"background-blur-radius": u8 = 0,
/// The opacity level (opposite of transparency) of an unfocused split.
/// Unfocused splits by default are slightly faded out to make it easier
/// to see which split is focused. To disable this feature, set this
/// value to 1.
///
/// A value of 1 is fully opaque and a value of 0 is fully transparent.
/// Because "0" is not useful (it makes the window look very weird), the
/// minimum value is 0.15. This value still looks weird but you can at least
/// see what's going on. A value outside of the range 0.15 to 1 will be
/// clamped to the nearest valid value.
@"unfocused-split-opacity": f64 = 0.7,
// The color to dim the unfocused split. Unfocused splits are dimmed by
// rendering a semi-transparent rectangle over the split. This sets
// the color of that rectangle and can be used to carefully control
// the dimming effect.
//
// This will default to the background color.
@"unfocused-split-fill": ?Color = null,
/// The command to run, usually a shell. If this is not an absolute path,
/// it'll be looked up in the PATH. If this is not set, a default will
/// be looked up from your system. The rules for the default lookup are:
///
/// - SHELL environment variable
/// - passwd entry (user information)
///
/// This can contain additional arguments to run the command with.
/// If additional arguments are provided, the command will be executed
/// using "/bin/sh -c". Ghostty does not do any shell command parsing.
///
/// If you're using the `ghostty` CLI there is also a shortcut
/// to run a command with argumens directly: you can use the `-e`
/// flag. For example: `ghostty -e fish --with --custom --args`.
command: ?[]const u8 = null,
/// The number of milliseconds of runtime below which we consider a process
/// exit to be abnormal. This is used to show an error message when the
/// process exits too quickly.
///
/// On Linux, this must be paired with a non-zero exit code. On macOS,
/// we allow any exit code because of the way shell processes are launched
/// via the login command.
@"abnormal-command-exit-runtime": u32 = 250,
/// Match a regular expression against the terminal text and associate
/// clicking it with an action. This can be used to match URLs, file paths,
/// etc. Actions can be opening using the system opener (i.e. "open" or
/// "xdg-open") or executing any arbitrary binding action.
///
/// Links that are configured earlier take precedence over links that
/// are configured later.
///
/// A default link that matches a URL and opens it in the system opener
/// always exists. This can be disabled using "link-url".
///
/// TODO: This can't currently be set!
link: RepeatableLink = .{},
/// Enable URL matching. URLs are matched on hover and open using the
/// default system application for the linked URL.
///
/// The URL matcher is always lowest priority of any configured links
/// (see "link"). If you want to customize URL matching, use "link"
/// and disable this.
@"link-url": bool = true,
/// Start new windows in fullscreen. This setting applies to new
/// windows and does not apply to tabs, splits, etc. However, this
/// setting will apply to all new windows, not just the first one.
///
/// On macOS, this always creates the window in native fullscreen.
/// Non-native fullscreen is not currently supported with this
/// setting.
fullscreen: bool = false,
/// The title Ghostty will use for the window. This will force the title
/// of the window to be this title at all times and Ghostty will ignore any
/// set title escape sequences programs (such as Neovim) may send.
title: ?[:0]const u8 = null,
/// The setting that will change the application class value.
///
/// This controls the class field of the WM_CLASS X11 property (when running
/// under X11), and the Wayland application ID (when running under Wayland).
///
/// Note that changing this value between invocations will create new, separate
/// instances, of Ghostty when running with --gtk-single-instance=true. See
/// that option for more details.
///
/// The class name must follow the GTK requirements defined here:
/// https://docs.gtk.org/gio/type_func.Application.id_is_valid.html
///
/// The default is "com.mitchellh.ghostty".
///
/// This only affects GTK builds.
class: ?[:0]const u8 = null,
/// This controls the instance name field of the WM_CLASS X11 property when
/// running under X11. It has no effect otherwise.
///
/// The default is "ghostty".
///
/// This only affects GTK builds.
@"x11-instance-name": ?[:0]const u8 = null,
/// The directory to change to after starting the command.
///
/// This setting is secondary to the "window-inherit-working-directory"
/// setting. If a previous Ghostty terminal exists in the same process,
/// "window-inherit-working-directory" will take precedence. Otherwise,
/// this setting will be used. Typically, this setting is used only
/// for the first window.
///
/// The default is "inherit" except in special scenarios listed next.
/// On macOS, if Ghostty can detect it is launched from launchd
/// (double-clicked) or `open`, then it defaults to "home".
/// On Linux with GTK, if Ghostty can detect it was launched from
/// a desktop launcher, then it defaults to "home".
///
/// The value of this must be an absolute value or one of the special
/// values below:
///
/// - "home" - The home directory of the executing user.
/// - "inherit" - The working directory of the launching process.
///
@"working-directory": ?[]const u8 = null,
/// Key bindings. The format is "trigger=action". Duplicate triggers
/// will overwrite previously set values.
///
/// Trigger: "+"-separated list of keys and modifiers. Example:
/// "ctrl+a", "ctrl+shift+b", "up". Some notes:
///
/// - modifiers cannot repeat, "ctrl+ctrl+a" is invalid.
/// - modifiers and keys can be in any order, "shift+a+ctrl" is weird,
/// but valid.
/// - only a single key input is allowed, "ctrl+a+b" is invalid.
///
/// Valid modifiers are "shift", "ctrl" (alias: "control"),
/// "alt" (alias: "opt", "option"), and "super" (alias: "cmd", "command").
/// You may use the modifier or the alias. When debugging keybinds,
/// the non-aliased modifier will always be used in output.
///
/// Action is the action to take when the trigger is satisfied. It takes
/// the format "action" or "action:param". The latter form is only valid
/// if the action requires a parameter.
///
/// - "ignore" - Do nothing, ignore the key input. This can be used to
/// black hole certain inputs to have no effect.
/// - "unbind" - Remove the binding. This makes it so the previous action
/// is removed, and the key will be sent through to the child command
/// if it is printable.
/// - "csi:text" - Send a CSI sequence. i.e. "csi:A" sends "cursor up".
/// - "esc:text" - Send an Escape sequence. i.e. "esc:d" deletes to the
/// end of the word to the right.
///
/// Some notes for the action:
///
/// - The parameter is taken as-is after the ":". Double quotes or
/// other mechanisms are included and NOT parsed. If you want to
/// send a string value that includes spaces, wrap the entire
/// trigger/action in double quotes. Example: --keybind="up=csi:A B"
///
/// There are some additional special values that can be specified for
/// keybind:
///
/// - `keybind = clear` will clear all set keybindings. Warning: this
/// removes ALL keybindings up to this point, including the default
/// keybindings.
///
keybind: Keybinds = .{},
/// Window padding. This applies padding between the terminal cells and
/// the window border. The "x" option applies to the left and right
/// padding and the "y" option is top and bottom. The value is in points,
/// meaning that it will be scaled appropriately for screen DPI.
///
/// If this value is set too large, the screen will render nothing, because
/// the grid will be completely squished by the padding. It is up to you
/// as the user to pick a reasonable value. If you pick an unreasonable
/// value, a warning will appear in the logs.
@"window-padding-x": u32 = 2,
@"window-padding-y": u32 = 2,
/// The viewport dimensions are usually not perfectly divisible by
/// the cell size. In this case, some extra padding on the end of a
/// column and the bottom of the final row may exist. If this is true,
/// then this extra padding is automatically balanced between all four
/// edges to minimize imbalance on one side. If this is false, the top
/// left grid cell will always hug the edge with zero padding other than
/// what may be specified with the other "window-padding" options.
///
/// If other "window-padding" fields are set and this is true, this will
/// still apply. The other padding is applied first and may affect how
/// many grid cells actually exist, and this is applied last in order
/// to balance the padding given a certain viewport size and grid cell size.
@"window-padding-balance": bool = false,
/// If true, new windows and tabs will inherit the working directory of
/// the previously focused window. If no window was previously focused,
/// the default working directory will be used (the "working-directory"
/// option).
@"window-inherit-working-directory": bool = true,
/// If true, new windows and tabs will inherit the font size of the previously
/// focused window. If no window was previously focused, the default
/// font size will be used. If this is false, the default font size
/// specified in the configuration "font-size" will be used.
@"window-inherit-font-size": bool = true,
/// If false, windows won't have native decorations, i.e. titlebar and
/// borders.
@"window-decoration": bool = true,
/// The theme to use for the windows. The default is "system" which
/// means that whatever the system theme is will be used. This can
/// also be set to "light" or "dark" to force a specific theme regardless
/// of the system settings.
///
/// This is currently only supported on macOS and linux.
@"window-theme": WindowTheme = .system,
/// The initial window size. This size is in terminal grid cells by default.
///
/// We don't currently support specifying a size in pixels but a future
/// change can enable that. If this isn't specified, the app runtime will
/// determine some default size.
///
/// Note that the window manager may put limits on the size or override
/// the size. For example, a tiling window manager may force the window
/// to be a certain size to fit within the grid. There is nothing Ghostty
/// will do about this, but it will make an effort.
///
/// This will not affect new tabs, splits, or other nested terminal
/// elements. This only affects the initial window size of any new window.
/// Changing this value will not affect the size of the window after
/// it has been created. This is only used for the initial size.
///
/// BUG: On Linux with GTK, the calculated window size will not properly
/// take into account window decorations. As a result, the grid dimensions
/// will not exactly match this configuration. If window decorations are
/// disabled (see window-decorations), then this will work as expected.
///
/// Windows smaller than 10 wide by 4 high are not allowed.
@"window-height": u32 = 0,
@"window-width": u32 = 0,
/// Whether to enable saving and restoring window state. Window state
/// includes their position, size, tabs, splits, etc. Some window state
/// requires shell integration, such as preserving working directories.
/// See shell-integration for more information.
///
/// There are three valid values for this configuration:
/// - "default" will use the default system behavior. On macOS, this
/// will only save state if the application is forcibly terminated
/// or if it is configured systemwide via Settings.app.
/// - "never" will never save window state.
/// - "always" will always save window state whenever Ghostty is exited.
///
/// If you change this value to "never" while Ghostty is not running,
/// the next Ghostty launch will NOT restore the window state.
///
/// If you change this value to "default" while Ghostty is not running
/// and the previous exit saved state, the next Ghostty launch will
/// still restore the window state. This is because Ghostty cannot know
/// if the previous exit was due to a forced save or not (macOS doesn't
/// provide this information).
///
/// If you change this value so that window state is saved while Ghostty
/// is not running, the previous window state will not be restored because
/// Ghostty only saves state on exit if this is enabled.
///
/// The default value is "default".
///
/// This is currently only supported on macOS. This has no effect on Linux.
@"window-save-state": WindowSaveState = .default,
/// Resize the window in discrete increments of the focused surface's
/// cell size. If this is disabled, surfaces are resized in pixel increments.
/// Currently only supported on macOS.
@"window-step-resize": bool = false,
/// When enabled, the full GTK titlebar is displayed instead of your window
/// manager's simple titlebar. The behavior of this option will vary with your
/// window manager.
///
/// This option does nothing when window-decoration is false or when running
/// under MacOS.
///
/// Changing this value at runtime and reloading the configuration will only
/// affect new windows.
@"gtk-titlebar": bool = true,
/// Whether to allow programs running in the terminal to read/write to
/// the system clipboard (OSC 52, for googling). The default is to
/// allow clipboard reading after prompting the user and allow writing
/// unconditionally.
@"clipboard-read": ClipboardAccess = .ask,
@"clipboard-write": ClipboardAccess = .allow,
/// Trims trailing whitespace on data that is copied to the clipboard.
/// This does not affect data sent to the clipboard via "clipboard-write".
@"clipboard-trim-trailing-spaces": bool = true,
/// Require confirmation before pasting text that appears unsafe. This helps
/// prevent a "copy/paste attack" where a user may accidentally execute unsafe
/// commands by pasting text with newlines.
@"clipboard-paste-protection": bool = true,
/// If true, bracketed pastes will be considered safe. By default,
/// bracketed pastes are considered safe. "Bracketed" pastes are pastes
/// while the running program has bracketed paste mode enabled (a setting
/// set by the running program, not the terminal emulator).
@"clipboard-paste-bracketed-safe": bool = true,
/// The total amount of bytes that can be used for image data (i.e.
/// the Kitty image protocol) per terminal scren. The maximum value
/// is 4,294,967,295 (4GB). The default is 320MB. If this is set to zero,
/// then all image protocols will be disabled.
///
/// This value is separate for primary and alternate screens so the
/// effective limit per surface is double.
@"image-storage-limit": u32 = 320 * 1000 * 1000,
/// Whether to automatically copy selected text to the clipboard. "true"
/// will only copy on systems that support a selection clipboard.
///
/// The value "clipboard" will copy to the system clipboard, making this
/// work on macOS. Note that middle-click will also paste from the system
/// clipboard in this case.
///
/// Note that if this is disabled, middle-click paste will also be
/// disabled.
@"copy-on-select": CopyOnSelect = .true,
/// The time in milliseconds between clicks to consider a click a repeat
/// (double, triple, etc.) or an entirely new single click. A value of
/// zero will use a platform-specific default. The default on macOS
/// is determined by the OS settings. On every other platform it is 500ms.
@"click-repeat-interval": u32 = 0,
/// Additional configuration files to read. This configuration can be repeated
/// to read multiple configuration files. Configuration files themselves can
/// load more configuration files. Paths are relative to the file containing
/// the `config-file` directive. For command-line arguments, paths are
/// relative to the current working directory.
///
/// Cycles are not allowed. If a cycle is detected, an error will be logged
/// and the configuration file will be ignored.
@"config-file": RepeatablePath = .{},
/// Confirms that a surface should be closed before closing it. This defaults
/// to true. If set to false, surfaces will close without any confirmation.
@"confirm-close-surface": bool = true,
/// Whether or not to quit after the last window is closed. This defaults
/// to false. Currently only supported on macOS. On Linux, the process always
/// exits after the last window is closed.
@"quit-after-last-window-closed": bool = false,
/// Whether to enable shell integration auto-injection or not. Shell
/// integration greatly enhances the terminal experience by enabling
/// a number of features:
///
/// * Working directory reporting so new tabs, splits inherit the
/// previous terminal's working directory.
/// * Prompt marking that enables the "jump_to_prompt" keybinding.
/// * If you're sitting at a prompt, closing a terminal will not ask
/// for confirmation.
/// * Resizing the window with a complex prompt usually paints much
/// better.
///
/// Allowable values are:
///
/// * "none" - Do not do any automatic injection. You can still manually
/// configure your shell to enable the integration.
/// * "detect" - Detect the shell based on the filename.
/// * "fish", "zsh" - Use this specific shell injection scheme.
///
/// The default value is "detect".
@"shell-integration": ShellIntegration = .detect,
/// Shell integration features to enable if shell integration itself is enabled.
/// The format of this is a list of features to enable separated by commas.
/// If you prefix a feature with "no-" then it is disabled. If you omit
/// a feature, its default value is used, so you must explicitly disable
/// features you don't want.
///
/// Available features:
///
/// - "cursor" - Set the cursor to a blinking bar at the prompt.
///
/// Example: "cursor", "no-cursor"
@"shell-integration-features": ShellIntegrationFeatures = .{},
/// Sets the reporting format for OSC sequences that request color information.
/// Ghostty currently supports OSC 10 (foreground), OSC 11 (background), and OSC
/// 4 (256 color palette) queries, and by default the reported values are
/// scaled-up RGB values, where each component are 16 bits. This is how most
/// terminals report these values. However, some legacy applications may require
/// 8-bit, unscaled, components. We also support turning off reporting
/// alltogether. The components are lowercase hex values.
///
/// Allowable values are:
///
/// * "none" - OSC 4/10/11 queries receive no reply
/// * "8-bit" - Color components are return unscaled, i.e. rr/gg/bb
/// * "16-bit" - Color components are returned scaled, e.g. rrrr/gggg/bbbb
///
/// The default value is "16-bit".
@"osc-color-report-format": OSCColorReportFormat = .@"16-bit",
/// If true, allows the "KAM" mode (ANSI mode 2) to be used within
/// the terminal. KAM disables keyboard input at the request of the
/// application. This is not a common feature and is not recommended
/// to be enabled. This will not be documented further because
/// if you know you need KAM, you know. If you don't know if you
/// need KAM, you don't need it.
@"vt-kam-allowed": bool = false,
/// Custom shaders to run after the default shaders. This is a file path
/// to a GLSL-syntax shader for all platforms.
///
/// WARNING: Invalid shaders can cause Ghostty to become unusable such as by
/// causing the window to be completely black. If this happens, you can
/// unset this configuration to disable the shader.
///
/// On Linux, this requires OpenGL 4.2. Ghostty typically only requires
/// OpenGL 3.3, but custom shaders push that requirement up to 4.2.
///
/// The shader API is identical to the Shadertoy API: you specify a `mainImage`
/// function and the available uniforms match Shadertoy. The iChannel0 uniform
/// is a texture containing the rendered terminal screen.
///
/// If the shader fails to compile, the shader will be ignored. Any errors
/// related to shader compilation will not show up as configuration errors
/// and only show up in the log, since shader compilation happens after
/// configuration loading on the dedicated render thread. For interactive
/// development, use Shadertoy.com.
///
/// This can be repeated multiple times to load multiple shaders. The shaders
/// will be run in the order they are specified.
///
/// Changing this value at runtime and reloading the configuration will only
/// affect new windows, tabs, and splits.
@"custom-shader": RepeatablePath = .{},
/// If true (default), the focused terminal surface will run an animation
/// loop when custom shaders are used. This uses slightly more CPU (generally
/// less than 10%) but allows the shader to animate. This only runs if there
/// are custom shaders.
///
/// If this is set to false, the terminal and custom shader will only render
/// when the terminal is updated. This is more efficient but the shader will
/// not animate.
///
/// This value can be changed at runtime and will affect all currently
/// open terminals.
@"custom-shader-animation": bool = true,
/// If anything other than false, fullscreen mode on macOS will not use the
/// native fullscreen, but make the window fullscreen without animations and
/// using a new space. It's faster than the native fullscreen mode since it
/// doesn't use animations.
///
/// Allowable values are:
///
/// * "visible-menu" - Use non-native macOS fullscreen, keep the menu bar visible
/// * "true" - Use non-native macOS fullscreen, hide the menu bar
/// * "false" - Use native macOS fullscreeen
@"macos-non-native-fullscreen": NonNativeFullscreen = .false,
/// If true, the Option key will be treated as Alt. This makes terminal
/// sequences expecting Alt to work properly, but will break Unicode
/// input sequences on macOS if you use them via the alt key. You may
/// set this to false to restore the macOS alt-key unicode sequences
/// but this will break terminal sequences expecting Alt to work.
///
/// Note that if an Option-sequence doesn't produce a printable
/// character, it will be treated as Alt regardless of this setting.
/// (i.e. alt+ctrl+a).
///
/// This does not work with GLFW builds.
@"macos-option-as-alt": OptionAsAlt = .false,
/// If true, the Ghostty GTK application will run in single-instance mode:
/// each new `ghostty` process launched will result in a new window if there
/// is already a running process.
///
/// If false, each new ghostty process will launch a separate application.
///
/// The default value is "desktop" which will default to "true" if Ghostty
/// detects it was launched from the .desktop file such as an app launcher.
/// If Ghostty is launched from the command line, it will default to "false".
///
/// Note that debug builds of Ghostty have a separate single-instance ID
/// so you can test single instance without conflicting with release builds.
@"gtk-single-instance": GtkSingleInstance = .desktop,
/// If true (default), then the Ghostty GTK tabs will be "wide." Wide tabs
/// are the new typical Gnome style where tabs fill their available space.
/// If you set this to false then tabs will only take up space they need,
/// which is the old style.
@"gtk-wide-tabs": bool = true,
/// If true (default), Ghostty will enable libadwaita theme support. This
/// will make `window-theme` work properly and will also allow Ghostty to
/// properly respond to system theme changes, light/dark mode changing, etc.
/// This requires a GTK4 desktop with a GTK4 theme.
///
/// If you are running GTK3 or have a GTK3 theme, you may have to set this
/// to false to get your theme picked up properly. Having this set to true
/// with GTK3 should not cause any problems, but it may not work exactly as
/// expected.
///
/// This configuration only has an effect if Ghostty was built with
/// libadwaita support.
@"gtk-adwaita": bool = true,
/// If true (default), applications running in the terminal can show desktop
/// notifications using certain escape sequences such as OSC 9 or OSC 777.
@"desktop-notifications": bool = true,
/// This will be used to set the TERM environment variable.
/// HACK: We set this with an "xterm" prefix because vim uses that to enable key
/// protocols (specifically this will enable 'modifyOtherKeys'), among other
/// features. An option exists in vim to modify this: `:set
/// keyprotocol=ghostty:kitty`, however a bug in the implementation prevents it
/// from working properly. https://github.com/vim/vim/pull/13211 fixes this.
term: []const u8 = "xterm-ghostty",
/// This is set by the CLI parser for deinit.
_arena: ?ArenaAllocator = null,
/// List of errors that occurred while loading. This can be accessed directly
/// by callers. It is only underscore-prefixed so it can't be set by the
/// configuration file.
_errors: ErrorList = .{},
/// The inputs that built up this configuration. This is used to reload
/// the configuration if we have to.
_inputs: std.ArrayListUnmanaged([]const u8) = .{},
pub fn deinit(self: *Config) void {
if (self._arena) |arena| arena.deinit();
self.* = undefined;
}
/// Load the configuration according to the default rules:
///
/// 1. Defaults
/// 2. XDG Config File
/// 3. CLI flags
/// 4. Recursively defined configuration files
///
pub fn load(alloc_gpa: Allocator) !Config {
var result = try default(alloc_gpa);
errdefer result.deinit();
// If we have a configuration file in our home directory, parse that first.
try result.loadDefaultFiles(alloc_gpa);
// Parse the config from the CLI args
try result.loadCliArgs(alloc_gpa);
// Parse the config files that were added from our file and CLI args.
try result.loadRecursiveFiles(alloc_gpa);
try result.finalize();
return result;
}
pub fn default(alloc_gpa: Allocator) Allocator.Error!Config {
// Build up our basic config
var result: Config = .{
._arena = ArenaAllocator.init(alloc_gpa),
};
errdefer result.deinit();
const alloc = result._arena.?.allocator();
// Add our default keybindings
// keybinds for opening and reloading config
try result.keybind.set.put(
alloc,
.{ .key = .comma, .mods = inputpkg.ctrlOrSuper(.{ .shift = true }) },
.{ .reload_config = {} },
);
try result.keybind.set.put(
alloc,
.{ .key = .comma, .mods = inputpkg.ctrlOrSuper(.{}) },
.{ .open_config = {} },
);
{
// On macOS we default to super but Linux ctrl+shift since
// ctrl+c is to kill the process.
const mods: inputpkg.Mods = if (builtin.target.isDarwin())
.{ .super = true }
else
.{ .ctrl = true, .shift = true };
try result.keybind.set.put(
alloc,
.{ .key = .c, .mods = mods },
.{ .copy_to_clipboard = {} },
);
try result.keybind.set.put(
alloc,
.{ .key = .v, .mods = mods },
.{ .paste_from_clipboard = {} },
);
}
// Fonts
try result.keybind.set.put(
alloc,
.{ .key = .equal, .mods = inputpkg.ctrlOrSuper(.{}) },
.{ .increase_font_size = 1 },
);
try result.keybind.set.put(
alloc,
.{ .key = .minus, .mods = inputpkg.ctrlOrSuper(.{}) },
.{ .decrease_font_size = 1 },
);
try result.keybind.set.put(
alloc,
.{ .key = .zero, .mods = inputpkg.ctrlOrSuper(.{}) },
.{ .reset_font_size = {} },
);
try result.keybind.set.put(
alloc,
.{ .key = .j, .mods = inputpkg.ctrlOrSuper(.{ .shift = true }) },
.{ .write_scrollback_file = {} },
);
// Windowing
if (comptime !builtin.target.isDarwin()) {
try result.keybind.set.put(
alloc,
.{ .key = .n, .mods = .{ .ctrl = true, .shift = true } },
.{ .new_window = {} },
);
try result.keybind.set.put(
alloc,
.{ .key = .w, .mods = .{ .ctrl = true, .shift = true } },
.{ .close_surface = {} },
);
try result.keybind.set.put(
alloc,
.{ .key = .q, .mods = .{ .ctrl = true, .shift = true } },
.{ .quit = {} },
);
try result.keybind.set.put(
alloc,
.{ .key = .f4, .mods = .{ .alt = true } },
.{ .close_window = {} },
);
try result.keybind.set.put(
alloc,
.{ .key = .t, .mods = .{ .ctrl = true, .shift = true } },
.{ .new_tab = {} },
);
try result.keybind.set.put(
alloc,
.{ .key = .left, .mods = .{ .ctrl = true, .shift = true } },
.{ .previous_tab = {} },
);
try result.keybind.set.put(
alloc,
.{ .key = .right, .mods = .{ .ctrl = true, .shift = true } },
.{ .next_tab = {} },
);
try result.keybind.set.put(
alloc,
.{ .key = .page_up, .mods = .{ .ctrl = true } },
.{ .previous_tab = {} },
);
try result.keybind.set.put(
alloc,
.{ .key = .page_down, .mods = .{ .ctrl = true } },
.{ .next_tab = {} },
);
try result.keybind.set.put(
alloc,
.{ .key = .o, .mods = .{ .ctrl = true, .shift = true } },
.{ .new_split = .right },
);
try result.keybind.set.put(
alloc,
.{ .key = .e, .mods = .{ .ctrl = true, .shift = true } },
.{ .new_split = .down },
);
try result.keybind.set.put(
alloc,
.{ .key = .left_bracket, .mods = .{ .ctrl = true, .super = true } },
.{ .goto_split = .previous },
);
try result.keybind.set.put(
alloc,
.{ .key = .right_bracket, .mods = .{ .ctrl = true, .super = true } },
.{ .goto_split = .next },
);
try result.keybind.set.put(
alloc,
.{ .key = .up, .mods = .{ .ctrl = true, .alt = true } },
.{ .goto_split = .top },
);
try result.keybind.set.put(
alloc,
.{ .key = .down, .mods = .{ .ctrl = true, .alt = true } },
.{ .goto_split = .bottom },
);
try result.keybind.set.put(
alloc,
.{ .key = .left, .mods = .{ .ctrl = true, .alt = true } },
.{ .goto_split = .left },
);
try result.keybind.set.put(
alloc,
.{ .key = .right, .mods = .{ .ctrl = true, .alt = true } },
.{ .goto_split = .right },
);
// Resizing splits
try result.keybind.set.put(
alloc,
.{ .key = .up, .mods = .{ .super = true, .ctrl = true, .shift = true } },
.{ .resize_split = .{ .up, 10 } },
);
try result.keybind.set.put(
alloc,
.{ .key = .down, .mods = .{ .super = true, .ctrl = true, .shift = true } },
.{ .resize_split = .{ .down, 10 } },
);
try result.keybind.set.put(
alloc,
.{ .key = .left, .mods = .{ .super = true, .ctrl = true, .shift = true } },
.{ .resize_split = .{ .left, 10 } },
);
try result.keybind.set.put(
alloc,
.{ .key = .right, .mods = .{ .super = true, .ctrl = true, .shift = true } },
.{ .resize_split = .{ .right, 10 } },
);
try result.keybind.set.put(
alloc,
.{ .key = .equal, .mods = .{ .super = true, .ctrl = true, .shift = true } },
.{ .equalize_splits = {} },
);
// Viewport scrolling
try result.keybind.set.put(
alloc,
.{ .key = .home, .mods = .{ .shift = true } },
.{ .scroll_to_top = {} },
);
try result.keybind.set.put(
alloc,
.{ .key = .end, .mods = .{ .shift = true } },
.{ .scroll_to_bottom = {} },
);
try result.keybind.set.put(
alloc,
.{ .key = .page_up, .mods = .{ .shift = true } },
.{ .scroll_page_up = {} },
);
try result.keybind.set.put(
alloc,
.{ .key = .page_down, .mods = .{ .shift = true } },
.{ .scroll_page_down = {} },
);
// Semantic prompts
try result.keybind.set.put(
alloc,
.{ .key = .page_up, .mods = .{ .shift = true, .ctrl = true } },
.{ .jump_to_prompt = -1 },
);
try result.keybind.set.put(
alloc,
.{ .key = .page_down, .mods = .{ .shift = true, .ctrl = true } },
.{ .jump_to_prompt = 1 },
);
// Inspector, matching Chromium
try result.keybind.set.put(
alloc,
.{ .key = .i, .mods = .{ .shift = true, .ctrl = true } },
.{ .inspector = .toggle },
);
// Terminal
try result.keybind.set.put(
alloc,
.{ .key = .a, .mods = .{ .shift = true, .ctrl = true } },
.{ .select_all = {} },
);
}
{
// Cmd+N for goto tab N
const start = @intFromEnum(inputpkg.Key.one);
const end = @intFromEnum(inputpkg.Key.nine);
var i: usize = start;
while (i <= end) : (i += 1) {
// On macOS we default to super but everywhere else
// is alt.
const mods: inputpkg.Mods = if (builtin.target.isDarwin())
.{ .super = true }
else
.{ .alt = true };
try result.keybind.set.put(
alloc,
.{
.key = @enumFromInt(i),
.mods = mods,
// On macOS, we use the physical key for tab changing so
// that this works across all keyboard layouts. This may
// want to be true on other platforms as well but this
// is definitely true on macOS so we just do it here for
// now (#817)
.physical = builtin.target.isDarwin(),
},
.{ .goto_tab = (i - start) + 1 },
);
}
}
// Toggle fullscreen
try result.keybind.set.put(
alloc,
.{ .key = .enter, .mods = inputpkg.ctrlOrSuper(.{}) },
.{ .toggle_fullscreen = {} },
);
// Toggle zoom a split
try result.keybind.set.put(
alloc,
.{ .key = .enter, .mods = inputpkg.ctrlOrSuper(.{ .shift = true }) },
.{ .toggle_split_zoom = {} },
);
// Mac-specific keyboard bindings.
if (comptime builtin.target.isDarwin()) {
try result.keybind.set.put(
alloc,
.{ .key = .q, .mods = .{ .super = true } },
.{ .quit = {} },
);
try result.keybind.set.put(
alloc,
.{ .key = .k, .mods = .{ .super = true } },
.{ .clear_screen = {} },
);
try result.keybind.set.put(
alloc,
.{ .key = .a, .mods = .{ .super = true } },
.{ .select_all = {} },
);
// Viewport scrolling
try result.keybind.set.put(
alloc,
.{ .key = .home, .mods = .{ .super = true } },
.{ .scroll_to_top = {} },
);
try result.keybind.set.put(
alloc,
.{ .key = .end, .mods = .{ .super = true } },
.{ .scroll_to_bottom = {} },
);
try result.keybind.set.put(
alloc,
.{ .key = .page_up, .mods = .{ .super = true } },
.{ .scroll_page_up = {} },
);
try result.keybind.set.put(
alloc,
.{ .key = .page_down, .mods = .{ .super = true } },
.{ .scroll_page_down = {} },
);
// Semantic prompts
try result.keybind.set.put(
alloc,
.{ .key = .up, .mods = .{ .super = true, .shift = true } },
.{ .jump_to_prompt = -1 },
);
try result.keybind.set.put(
alloc,
.{ .key = .down, .mods = .{ .super = true, .shift = true } },
.{ .jump_to_prompt = 1 },
);
// Mac windowing
try result.keybind.set.put(
alloc,
.{ .key = .n, .mods = .{ .super = true } },
.{ .new_window = {} },
);
try result.keybind.set.put(
alloc,
.{ .key = .w, .mods = .{ .super = true } },
.{ .close_surface = {} },
);
try result.keybind.set.put(
alloc,
.{ .key = .w, .mods = .{ .super = true, .shift = true } },
.{ .close_window = {} },
);
try result.keybind.set.put(
alloc,
.{ .key = .w, .mods = .{ .super = true, .shift = true, .alt = true } },
.{ .close_all_windows = {} },
);
try result.keybind.set.put(
alloc,
.{ .key = .t, .mods = .{ .super = true } },
.{ .new_tab = {} },
);
try result.keybind.set.put(
alloc,
.{ .key = .left_bracket, .mods = .{ .super = true, .shift = true } },
.{ .previous_tab = {} },
);
try result.keybind.set.put(
alloc,
.{ .key = .right_bracket, .mods = .{ .super = true, .shift = true } },
.{ .next_tab = {} },
);
try result.keybind.set.put(
alloc,
.{ .key = .d, .mods = .{ .super = true } },
.{ .new_split = .right },
);
try result.keybind.set.put(
alloc,
.{ .key = .d, .mods = .{ .super = true, .shift = true } },
.{ .new_split = .down },
);
try result.keybind.set.put(
alloc,
.{ .key = .left_bracket, .mods = .{ .super = true } },
.{ .goto_split = .previous },
);
try result.keybind.set.put(
alloc,
.{ .key = .right_bracket, .mods = .{ .super = true } },
.{ .goto_split = .next },
);
try result.keybind.set.put(
alloc,
.{ .key = .up, .mods = .{ .super = true, .alt = true } },
.{ .goto_split = .top },
);
try result.keybind.set.put(
alloc,
.{ .key = .down, .mods = .{ .super = true, .alt = true } },
.{ .goto_split = .bottom },
);
try result.keybind.set.put(
alloc,
.{ .key = .left, .mods = .{ .super = true, .alt = true } },
.{ .goto_split = .left },
);
try result.keybind.set.put(
alloc,
.{ .key = .right, .mods = .{ .super = true, .alt = true } },
.{ .goto_split = .right },
);
try result.keybind.set.put(
alloc,
.{ .key = .up, .mods = .{ .super = true, .ctrl = true } },
.{ .resize_split = .{ .up, 10 } },
);
try result.keybind.set.put(
alloc,
.{ .key = .down, .mods = .{ .super = true, .ctrl = true } },
.{ .resize_split = .{ .down, 10 } },
);
try result.keybind.set.put(
alloc,
.{ .key = .left, .mods = .{ .super = true, .ctrl = true } },
.{ .resize_split = .{ .left, 10 } },
);
try result.keybind.set.put(
alloc,
.{ .key = .right, .mods = .{ .super = true, .ctrl = true } },
.{ .resize_split = .{ .right, 10 } },
);
try result.keybind.set.put(
alloc,
.{ .key = .equal, .mods = .{ .shift = true, .alt = true } },
.{ .equalize_splits = {} },
);
// Inspector, matching Chromium
try result.keybind.set.put(
alloc,
.{ .key = .i, .mods = .{ .alt = true, .super = true } },
.{ .inspector = .toggle },
);
// Alternate keybind, common to Mac programs
try result.keybind.set.put(
alloc,
.{ .key = .f, .mods = .{ .super = true, .ctrl = true } },
.{ .toggle_fullscreen = {} },
);
}
// Add our default link for URL detection
try result.link.links.append(alloc, .{
.regex = url.regex,
.action = .{ .open = {} },
.highlight = .{ .hover = {} },
});
return result;
}
/// Load configuration from an iterator that yields values that look like
/// command-line arguments, i.e. `--key=value`.
pub fn loadIter(
self: *Config,
alloc: Allocator,
iter: anytype,
) !void {
try cli.args.parse(Config, alloc, self, iter);
}
/// Load the configuration from the default configuration file. The default
/// configuration file is at `$XDG_CONFIG_HOME/ghostty/config`.
pub fn loadDefaultFiles(self: *Config, alloc: Allocator) !void {
const config_path = try internal_os.xdg.config(alloc, .{ .subdir = "ghostty/config" });
defer alloc.free(config_path);
const cwd = std.fs.cwd();
if (cwd.openFile(config_path, .{})) |file| {
defer file.close();
std.log.info("reading configuration file path={s}", .{config_path});
var buf_reader = std.io.bufferedReader(file.reader());
var iter = cli.args.lineIterator(buf_reader.reader());
try self.loadIter(alloc, &iter);
try self.expandPaths(std.fs.path.dirname(config_path).?);
} else |err| switch (err) {
error.FileNotFound => std.log.info(
"homedir config not found, not loading path={s}",
.{config_path},
),
else => std.log.warn(
"error reading config file, not loading err={} path={s}",
.{ err, config_path },
),
}
}
/// Load and parse the CLI args.
pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void {
switch (builtin.os.tag) {
.windows => {},
// Fast-path if we are non-Windows and no args, do nothing.
else => if (std.os.argv.len <= 1) return,
}
// On Linux, we have a special case where if the executing
// program is "xdg-terminal-exec" then we treat all CLI
// args as if they are a command to execute.
if (comptime builtin.os.tag == .linux) xdg: {
if (!std.mem.eql(
u8,
std.fs.path.basename(std.mem.sliceTo(std.os.argv[0], 0)),
"xdg-terminal-exec",
)) break :xdg;
const arena_alloc = self._arena.?.allocator();
// First, we add an artificial "-e" so that if we
// replay the inputs to rebuild the config (i.e. if
// a theme is set) then we will get the same behavior.
try self._inputs.append(arena_alloc, "-e");
// Next, take all remaining args and use that to build up
// a command to execute.
var command = std.ArrayList(u8).init(arena_alloc);
errdefer command.deinit();
for (std.os.argv[1..]) |arg_raw| {
const arg = std.mem.sliceTo(arg_raw, 0);
try self._inputs.append(arena_alloc, try arena_alloc.dupe(u8, arg));
try command.appendSlice(arg);
try command.append(' ');
}
self.command = command.items[0 .. command.items.len - 1];
return;
}
// Parse the config from the CLI args
var iter = try std.process.argsWithAllocator(alloc_gpa);
defer iter.deinit();
try self.loadIter(alloc_gpa, &iter);
// Config files loaded from the CLI args are relative to pwd
if (self.@"config-file".value.list.items.len > 0) {
var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
try self.expandPaths(try std.fs.cwd().realpath(".", &buf));
}
}
/// Load and parse the config files that were added in the "config-file" key.
pub fn loadRecursiveFiles(self: *Config, alloc_gpa: Allocator) !void {
if (self.@"config-file".value.list.items.len == 0) return;
const arena_alloc = self._arena.?.allocator();
// Keeps track of loaded files to prevent cycles.
var loaded = std.StringHashMap(void).init(alloc_gpa);
defer loaded.deinit();
const cwd = std.fs.cwd();
var i: usize = 0;
while (i < self.@"config-file".value.list.items.len) : (i += 1) {
const path = self.@"config-file".value.list.items[i];
// Error paths
if (path.len == 0) continue;
// All paths should already be absolute at this point because
// they're fixed up after each load.
assert(std.fs.path.isAbsolute(path));
// We must only load a unique file once
if (try loaded.fetchPut(path, {}) != null) {
try self._errors.add(arena_alloc, .{
.message = try std.fmt.allocPrintZ(
arena_alloc,
"config-file {s}: cycle detected",
.{path},
),
});
continue;
}
var file = cwd.openFile(path, .{}) catch |err| {
try self._errors.add(arena_alloc, .{
.message = try std.fmt.allocPrintZ(
arena_alloc,
"error opening config-file {s}: {}",
.{ path, err },
),
});
continue;
};
defer file.close();
log.info("loading config-file path={s}", .{path});
var buf_reader = std.io.bufferedReader(file.reader());
var iter = cli.args.lineIterator(buf_reader.reader());
try self.loadIter(alloc_gpa, &iter);
try self.expandPaths(std.fs.path.dirname(path).?);
}
}
/// Expand the relative paths in config-files to be absolute paths
/// relative to the base directory.
fn expandPaths(self: *Config, base: []const u8) !void {
const arena_alloc = self._arena.?.allocator();
inline for (@typeInfo(Config).Struct.fields) |field| {
if (field.type == RepeatablePath) {
try @field(self, field.name).expand(
arena_alloc,
base,
&self._errors,
);
}
}
}
fn loadTheme(self: *Config, theme: []const u8) !void {
const alloc = self._arena.?.allocator();
const resources_dir = global_state.resources_dir orelse {
try self._errors.add(alloc, .{
.message = "no resources directory found, themes will not work",
});
return;
};
const path = try std.fs.path.join(alloc, &.{
resources_dir,
"themes",
theme,
});
const cwd = std.fs.cwd();
var file = cwd.openFile(path, .{}) catch |err| {
switch (err) {
error.FileNotFound => try self._errors.add(alloc, .{
.message = try std.fmt.allocPrintZ(
alloc,
"theme \"{s}\" not found, path={s}",
.{ theme, path },
),
}),
else => try self._errors.add(alloc, .{
.message = try std.fmt.allocPrintZ(
alloc,
"failed to load theme \"{s}\": {}",
.{ theme, err },
),
}),
}
return;
};
defer file.close();
// From this point onwards, we load the theme and do a bit of a dance
// to achive two separate goals:
//
// (1) We want the theme to be loaded and our existing config to
// override the theme. So we need to load the theme and apply
// our config on top of it.
//
// (2) We want to free existing memory that we aren't using anymore
// as a result of reloading the configuration.
//
// Point 2 is strictly a result of aur approach to point 1.
// Keep track of our input length prior ot loading the theme
// so that we can replay the previous config to override values.
const input_len = self._inputs.items.len;
// Load into a new configuration so that we can free the existing memory.
const alloc_gpa = self._arena.?.child_allocator;
var new_config = try default(alloc_gpa);
errdefer new_config.deinit();
// Load our theme
var buf_reader = std.io.bufferedReader(file.reader());
var iter = cli.args.lineIterator(buf_reader.reader());
try new_config.loadIter(alloc_gpa, &iter);
// Replay our previous inputs so that we can override values
// from the theme.
var slice_it = cli.args.sliceIterator(self._inputs.items[0..input_len]);
try new_config.loadIter(alloc_gpa, &slice_it);
// Success, swap our new config in and free the old.
self.deinit();
self.* = new_config;
}
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| try self.loadTheme(theme);
// If we have a font-family set and don't set the others, default
// the others to the font family. This way, if someone does
// --font-family=foo, then we try to get the stylized versions of
// "foo" as well.
if (self.@"font-family".count() > 0) {
const fields = &[_][]const u8{
"font-family-bold",
"font-family-italic",
"font-family-bold-italic",
};
inline for (fields) |field| {
if (@field(self, field).count() == 0) {
@field(self, field) = try self.@"font-family".clone(alloc);
}
}
}
// Prevent setting TERM to an empty string
if (self.term.len == 0) {
// HACK: See comment above at definition
self.term = "xterm-ghostty";
}
// The default for the working directory depends on the system.
const wd = self.@"working-directory" orelse wd: {
// If we have no working directory set, our default depends on
// whether we were launched from the desktop or CLI.
if (internal_os.launchedFromDesktop()) {
break :wd "home";
}
break :wd "inherit";
};
// If we are missing either a command or home directory, we need
// to look up defaults which is kind of expensive. We only do this
// on desktop.
const wd_home = std.mem.eql(u8, "home", wd);
if (comptime !builtin.target.isWasm()) {
if (self.command == null or wd_home) command: {
// First look up the command using the SHELL env var if needed.
// We don't do this in flatpak because SHELL in Flatpak is always
// set to /bin/sh.
if (self.command) |cmd|
log.info("shell src=config value={s}", .{cmd})
else {
if (!internal_os.isFlatpak()) {
if (std.process.getEnvVarOwned(alloc, "SHELL")) |value| {
log.info("default shell source=env value={s}", .{value});
self.command = value;
// If we don't need the working directory, then we can exit now.
if (!wd_home) break :command;
} else |_| {}
}
}
switch (builtin.os.tag) {
.windows => {
if (self.command == null) {
log.warn("no default shell found, will default to using cmd", .{});
self.command = "cmd.exe";
}
if (wd_home) {
var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
if (try internal_os.home(&buf)) |home| {
self.@"working-directory" = try alloc.dupe(u8, home);
}
}
},
else => {
// We need the passwd entry for the remainder
const pw = try internal_os.passwd.get(alloc);
if (self.command == null) {
if (pw.shell) |sh| {
log.info("default shell src=passwd value={s}", .{sh});
self.command = sh;
}
}
if (wd_home) {
if (pw.home) |home| {
log.info("default working directory src=passwd value={s}", .{home});
self.@"working-directory" = home;
}
}
if (self.command == null) {
log.warn("no default shell found, will default to using sh", .{});
}
},
}
}
}
// If we have the special value "inherit" then set it to null which
// does the same. In the future we should change to a tagged union.
if (std.mem.eql(u8, wd, "inherit")) self.@"working-directory" = null;
// Default our click interval
if (self.@"click-repeat-interval" == 0) {
self.@"click-repeat-interval" = internal_os.clickInterval() orelse 500;
}
// Clamp our split opacity
self.@"unfocused-split-opacity" = @min(1.0, @max(0.15, self.@"unfocused-split-opacity"));
// Clamp our contrast
self.@"minimum-contrast" = @min(21, @max(1, self.@"minimum-contrast"));
// Minimmum window size
if (self.@"window-width" > 0) self.@"window-width" = @max(10, self.@"window-width");
if (self.@"window-height" > 0) self.@"window-height" = @max(4, self.@"window-height");
// If URLs are disabled, cut off the first link. The first link is
// always the URL matcher.
if (!self.@"link-url") self.link.links.items = self.link.links.items[1..];
}
/// Callback for src/cli/args.zig to allow us to handle special cases
/// like `--help` or `-e`. Returns "false" if the CLI parsing should halt.
pub fn parseManuallyHook(self: *Config, alloc: Allocator, arg: []const u8, iter: anytype) !bool {
if (std.mem.eql(u8, arg, "-e")) {
// Build up the command. We don't clean this up because we take
// ownership in our allocator.
var command = std.ArrayList(u8).init(alloc);
errdefer command.deinit();
while (iter.next()) |param| {
try self._inputs.append(alloc, try alloc.dupe(u8, param));
try command.appendSlice(param);
try command.append(' ');
}
if (command.items.len == 0) {
try self._errors.add(alloc, .{
.message = try std.fmt.allocPrintZ(
alloc,
"missing command after {s}",
.{arg},
),
});
return false;
}
self.command = command.items[0 .. command.items.len - 1];
// Do not continue, we consumed everything.
return false;
}
// If we didn't find a special case, continue parsing normally
return true;
}
/// Create a shallow copy of this config. This will share all the memory
/// allocated with the previous config but will have a new arena for
/// any changes or new allocations. The config should have `deinit`
/// called when it is complete.
///
/// Beware: these shallow clones are not meant for a long lifetime,
/// they are just meant to exist temporarily for the duration of some
/// modifications. It is very important that the original config not
/// be deallocated while shallow clones exist.
pub fn shallowClone(self: *const Config, alloc_gpa: Allocator) Config {
var result = self.*;
result._arena = ArenaAllocator.init(alloc_gpa);
return result;
}
/// Create a copy of this configuration. This is useful as a starting
/// point for modifying a configuration since a config can NOT be
/// modified once it is in use by an app or surface.
pub fn clone(self: *const Config, alloc_gpa: Allocator) !Config {
// Start with an empty config with a new arena we're going
// to use for all our copies.
var result: Config = .{
._arena = ArenaAllocator.init(alloc_gpa),
};
errdefer result.deinit();
const alloc = result._arena.?.allocator();
inline for (@typeInfo(Config).Struct.fields) |field| {
if (!@hasField(Key, field.name)) continue;
@field(result, field.name) = try cloneValue(
alloc,
field.type,
@field(self, field.name),
);
}
return result;
}
fn cloneValue(alloc: Allocator, comptime T: type, src: T) !T {
// Do known named types first
switch (T) {
[]const u8 => return try alloc.dupe(u8, src),
[:0]const u8 => return try alloc.dupeZ(u8, src),
else => {},
}
// If we're a type that can have decls and we have clone, then
// call clone and be done.
const t = @typeInfo(T);
if (t == .Struct or t == .Enum or t == .Union) {
if (@hasDecl(T, "clone")) return try src.clone(alloc);
}
// Back into types of types
switch (t) {
inline .Bool,
.Int,
.Float,
.Enum,
.Union,
=> return src,
.Optional => |info| return try cloneValue(
alloc,
info.child,
src orelse return null,
),
.Struct => |info| {
// Packed structs we can return directly as copies.
assert(info.layout == .Packed);
return src;
},
else => {
@compileLog(T);
@compileError("unsupported field type");
},
}
}
/// Returns an iterator that goes through each changed field from
/// old to new. The order of old or new do not matter.
pub fn changeIterator(old: *const Config, new: *const Config) ChangeIterator {
return .{
.old = old,
.new = new,
};
}
/// Returns true if the given key has changed from old to new. This
/// requires the key to be comptime known to make this more efficient.
pub fn changed(self: *const Config, new: *const Config, comptime key: Key) bool {
// Get the field at comptime
const field = comptime field: {
const fields = std.meta.fields(Config);
for (fields) |field| {
if (@field(Key, field.name) == key) {
break :field field;
}
}
unreachable;
};
const old_value = @field(self, field.name);
const new_value = @field(new, field.name);
return !equalField(field.type, old_value, new_value);
}
/// This yields a key for every changed field between old and new.
pub const ChangeIterator = struct {
old: *const Config,
new: *const Config,
i: usize = 0,
pub fn next(self: *ChangeIterator) ?Key {
const fields = comptime std.meta.fields(Key);
while (self.i < fields.len) {
switch (self.i) {
inline 0...(fields.len - 1) => |i| {
const field = fields[i];
const key = @field(Key, field.name);
self.i += 1;
if (self.old.changed(self.new, key)) return key;
},
else => unreachable,
}
}
return null;
}
};
const TestIterator = struct {
data: []const []const u8,
i: usize = 0,
pub fn next(self: *TestIterator) ?[]const u8 {
if (self.i >= self.data.len) return null;
const result = self.data[self.i];
self.i += 1;
return result;
}
};
test "parse hook: invalid command" {
const testing = std.testing;
var cfg = try Config.default(testing.allocator);
defer cfg.deinit();
const alloc = cfg._arena.?.allocator();
var it: TestIterator = .{ .data = &.{"foo"} };
try testing.expect(try cfg.parseManuallyHook(alloc, "--command", &it));
try testing.expect(cfg.command == null);
}
test "parse e: command only" {
const testing = std.testing;
var cfg = try Config.default(testing.allocator);
defer cfg.deinit();
const alloc = cfg._arena.?.allocator();
var it: TestIterator = .{ .data = &.{"foo"} };
try testing.expect(!try cfg.parseManuallyHook(alloc, "-e", &it));
try testing.expectEqualStrings("foo", cfg.command.?);
}
test "parse e: command and args" {
const testing = std.testing;
var cfg = try Config.default(testing.allocator);
defer cfg.deinit();
const alloc = cfg._arena.?.allocator();
var it: TestIterator = .{ .data = &.{ "echo", "foo", "bar baz" } };
try testing.expect(!try cfg.parseManuallyHook(alloc, "-e", &it));
try testing.expectEqualStrings("echo foo bar baz", cfg.command.?);
}
test "clone default" {
const testing = std.testing;
const alloc = testing.allocator;
var source = try Config.default(alloc);
defer source.deinit();
var dest = try source.clone(alloc);
defer dest.deinit();
// Should have no changes
var it = source.changeIterator(&dest);
try testing.expectEqual(@as(?Key, null), it.next());
// I want to do this but this doesn't work (the API doesn't work)
// try testing.expectEqualDeep(dest, source);
}
test "changed" {
const testing = std.testing;
const alloc = testing.allocator;
var source = try Config.default(alloc);
defer source.deinit();
var dest = try source.clone(alloc);
defer dest.deinit();
dest.@"font-thicken" = true;
try testing.expect(source.changed(&dest, .@"font-thicken"));
try testing.expect(!source.changed(&dest, .@"font-size"));
}
/// A config-specific helper to determine if two values of the same
/// type are equal. This isn't the same as std.mem.eql or std.testing.equals
/// because we expect structs to implement their own equality.
///
/// This also doesn't support ALL Zig types, because we only add to it
/// as we need types for the config.
fn equalField(comptime T: type, old: T, new: T) bool {
// Do known named types first
switch (T) {
inline []const u8,
[:0]const u8,
=> return std.mem.eql(u8, old, new),
else => {},
}
// Back into types of types
switch (@typeInfo(T)) {
.Void => return true,
inline .Bool,
.Int,
.Float,
.Enum,
=> return old == new,
.Optional => |info| {
if (old == null and new == null) return true;
if (old == null or new == null) return false;
return equalField(info.child, old.?, new.?);
},
.Struct => |info| {
if (@hasDecl(T, "equal")) return old.equal(new);
// If a struct doesn't declare an "equal" function, we fall back
// to a recursive field-by-field compare.
inline for (info.fields) |field_info| {
if (!equalField(
field_info.type,
@field(old, field_info.name),
@field(new, field_info.name),
)) return false;
}
return true;
},
.Union => |info| {
const tag_type = info.tag_type.?;
const old_tag = std.meta.activeTag(old);
const new_tag = std.meta.activeTag(new);
if (old_tag != new_tag) return false;
inline for (info.fields) |field_info| {
if (@field(tag_type, field_info.name) == old_tag) {
return equalField(
field_info.type,
@field(old, field_info.name),
@field(new, field_info.name),
);
}
}
unreachable;
},
else => {
@compileLog(T);
@compileError("unsupported field type");
},
}
}
/// Valid values for macos-non-native-fullscreen
/// c_int because it needs to be extern compatible
/// If this is changed, you must also update ghostty.h
pub const NonNativeFullscreen = enum(c_int) {
false,
true,
@"visible-menu",
};
/// Valid values for macos-option-as-alt.
pub const OptionAsAlt = enum {
false,
true,
left,
right,
};
/// Color represents a color using RGB.
///
/// This is a packed struct so that the C API to read color values just
/// works by setting it to a C integer.
pub const Color = packed struct(u24) {
r: u8,
g: u8,
b: u8,
/// Convert this to the terminal RGB struct
pub fn toTerminalRGB(self: Color) terminal.color.RGB {
return .{ .r = self.r, .g = self.g, .b = self.b };
}
pub fn parseCLI(input: ?[]const u8) !Color {
return fromHex(input orelse return error.ValueRequired);
}
/// Deep copy of the struct. Required by Config.
pub fn clone(self: Color, _: Allocator) !Color {
return self;
}
/// Compare if two of our value are requal. Required by Config.
pub fn equal(self: Color, other: Color) bool {
return std.meta.eql(self, other);
}
/// fromHex parses a color from a hex value such as #RRGGBB. The "#"
/// is optional.
pub fn fromHex(input: []const u8) !Color {
// Trim the beginning '#' if it exists
const trimmed = if (input.len != 0 and input[0] == '#') input[1..] else input;
// We expect exactly 6 for RRGGBB
if (trimmed.len != 6) return error.InvalidValue;
// Parse the colors two at a time.
var result: Color = undefined;
comptime var i: usize = 0;
inline while (i < 6) : (i += 2) {
const v: u8 =
((try std.fmt.charToDigit(trimmed[i], 16)) * 16) +
try std.fmt.charToDigit(trimmed[i + 1], 16);
@field(result, switch (i) {
0 => "r",
2 => "g",
4 => "b",
else => unreachable,
}) = v;
}
return result;
}
test "fromHex" {
const testing = std.testing;
try testing.expectEqual(Color{ .r = 0, .g = 0, .b = 0 }, try Color.fromHex("#000000"));
try testing.expectEqual(Color{ .r = 10, .g = 11, .b = 12 }, try Color.fromHex("#0A0B0C"));
try testing.expectEqual(Color{ .r = 10, .g = 11, .b = 12 }, try Color.fromHex("0A0B0C"));
try testing.expectEqual(Color{ .r = 255, .g = 255, .b = 255 }, try Color.fromHex("FFFFFF"));
}
};
/// Palette is the 256 color palette for 256-color mode. This is still
/// used by many terminal applications.
pub const Palette = struct {
const Self = @This();
/// The actual value that is updated as we parse.
value: terminal.color.Palette = terminal.color.default,
pub fn parseCLI(
self: *Self,
input: ?[]const u8,
) !void {
const value = input orelse return error.ValueRequired;
const eqlIdx = std.mem.indexOf(u8, value, "=") orelse
return error.InvalidValue;
const key = try std.fmt.parseInt(u8, value[0..eqlIdx], 10);
const rgb = try Color.parseCLI(value[eqlIdx + 1 ..]);
self.value[key] = .{ .r = rgb.r, .g = rgb.g, .b = rgb.b };
}
/// Deep copy of the struct. Required by Config.
pub fn clone(self: Self, _: Allocator) !Self {
return self;
}
/// Compare if two of our value are requal. Required by Config.
pub fn equal(self: Self, other: Self) bool {
return std.meta.eql(self, other);
}
test "parseCLI" {
const testing = std.testing;
var p: Self = .{};
try p.parseCLI("0=#AABBCC");
try testing.expect(p.value[0].r == 0xAA);
try testing.expect(p.value[0].g == 0xBB);
try testing.expect(p.value[0].b == 0xCC);
}
test "parseCLI overflow" {
const testing = std.testing;
var p: Self = .{};
try testing.expectError(error.Overflow, p.parseCLI("256=#AABBCC"));
}
};
/// RepeatableString is a string value that can be repeated to accumulate
/// a list of strings. This isn't called "StringList" because I find that
/// sometimes leads to confusion that it _accepts_ a list such as
/// comma-separated values.
pub const RepeatableString = struct {
const Self = @This();
// Allocator for the list is the arena for the parent config.
list: std.ArrayListUnmanaged([:0]const u8) = .{},
pub fn parseCLI(self: *Self, alloc: Allocator, input: ?[]const u8) !void {
const value = input orelse return error.ValueRequired;
// Empty value resets the list
if (value.len == 0) {
self.list.clearRetainingCapacity();
return;
}
const copy = try alloc.dupeZ(u8, value);
try self.list.append(alloc, copy);
}
/// Deep copy of the struct. Required by Config.
pub fn clone(self: *const Self, alloc: Allocator) !Self {
return .{
.list = try self.list.clone(alloc),
};
}
/// The number of itemsin the list
pub fn count(self: Self) usize {
return self.list.items.len;
}
/// Compare if two of our value are requal. Required by Config.
pub fn equal(self: Self, other: Self) bool {
const itemsA = self.list.items;
const itemsB = other.list.items;
if (itemsA.len != itemsB.len) return false;
for (itemsA, itemsB) |a, b| {
if (!std.mem.eql(u8, a, b)) return false;
} else return true;
}
test "parseCLI" {
const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var list: Self = .{};
try list.parseCLI(alloc, "A");
try list.parseCLI(alloc, "B");
try testing.expectEqual(@as(usize, 2), list.list.items.len);
try list.parseCLI(alloc, "");
try testing.expectEqual(@as(usize, 0), list.list.items.len);
}
};
/// RepeatablePath is like repeatable string but represents a path value.
/// The difference is that when loading the configuration any values for
/// this will be automatically expanded relative to the path of the config
/// file.
pub const RepeatablePath = struct {
const Self = @This();
value: RepeatableString = .{},
pub fn parseCLI(self: *Self, alloc: Allocator, input: ?[]const u8) !void {
return self.value.parseCLI(alloc, input);
}
/// Deep copy of the struct. Required by Config.
pub fn clone(self: *const Self, alloc: Allocator) !Self {
return .{
.value = try self.value.clone(alloc),
};
}
/// Compare if two of our value are requal. Required by Config.
pub fn equal(self: Self, other: Self) bool {
return self.value.equal(other.value);
}
/// Expand all the paths relative to the base directory.
pub fn expand(
self: *Self,
alloc: Allocator,
base: []const u8,
errors: *ErrorList,
) !void {
assert(std.fs.path.isAbsolute(base));
var dir = try std.fs.cwd().openDir(base, .{});
defer dir.close();
for (self.value.list.items, 0..) |path, i| {
// If it is already absolute we can ignore it.
if (path.len == 0 or std.fs.path.isAbsolute(path)) continue;
// If it isn't absolute, we need to make it absolute relative
// to the base.
var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
const abs = dir.realpath(path, &buf) catch |err| {
try errors.add(alloc, .{
.message = try std.fmt.allocPrintZ(
alloc,
"error resolving config-file {s}: {}",
.{ path, err },
),
});
self.value.list.items[i] = "";
continue;
};
log.debug(
"expanding config-file path relative={s} abs={s}",
.{ path, abs },
);
self.value.list.items[i] = try alloc.dupeZ(u8, abs);
}
}
};
/// FontVariation is a repeatable configuration value that sets a single
/// font variation value. Font variations are configurations for what
/// are often called "variable fonts." The font files usually end in
/// "-VF.ttf."
///
/// The value for this is in the format of `id=value` where `id` is the
/// 4-character font variation axis identifier and `value` is the
/// floating point value for that axis. For more details on font variations
/// see the MDN font-variation-settings documentation since this copies that
/// behavior almost exactly:
///
/// https://developer.mozilla.org/en-US/docs/Web/CSS/font-variation-settings
pub const RepeatableFontVariation = struct {
const Self = @This();
// Allocator for the list is the arena for the parent config.
list: std.ArrayListUnmanaged(fontpkg.face.Variation) = .{},
pub fn parseCLI(self: *Self, alloc: Allocator, input_: ?[]const u8) !void {
const input = input_ orelse return error.ValueRequired;
const eql_idx = std.mem.indexOf(u8, input, "=") orelse return error.InvalidValue;
const whitespace = " \t";
const key = std.mem.trim(u8, input[0..eql_idx], whitespace);
const value = std.mem.trim(u8, input[eql_idx + 1 ..], whitespace);
if (key.len != 4) return error.InvalidValue;
try self.list.append(alloc, .{
.id = fontpkg.face.Variation.Id.init(@ptrCast(key.ptr)),
.value = std.fmt.parseFloat(f64, value) catch return error.InvalidValue,
});
}
/// Deep copy of the struct. Required by Config.
pub fn clone(self: *const Self, alloc: Allocator) !Self {
return .{
.list = try self.list.clone(alloc),
};
}
/// Compare if two of our value are requal. Required by Config.
pub fn equal(self: Self, other: Self) bool {
const itemsA = self.list.items;
const itemsB = other.list.items;
if (itemsA.len != itemsB.len) return false;
for (itemsA, itemsB) |a, b| {
if (!std.meta.eql(a, b)) return false;
} else return true;
}
test "parseCLI" {
const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var list: Self = .{};
try list.parseCLI(alloc, "wght=200");
try list.parseCLI(alloc, "slnt=-15");
try testing.expectEqual(@as(usize, 2), list.list.items.len);
try testing.expectEqual(fontpkg.face.Variation{
.id = fontpkg.face.Variation.Id.init("wght"),
.value = 200,
}, list.list.items[0]);
try testing.expectEqual(fontpkg.face.Variation{
.id = fontpkg.face.Variation.Id.init("slnt"),
.value = -15,
}, list.list.items[1]);
}
test "parseCLI with whitespace" {
const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var list: Self = .{};
try list.parseCLI(alloc, "wght =200");
try list.parseCLI(alloc, "slnt= -15");
try testing.expectEqual(@as(usize, 2), list.list.items.len);
try testing.expectEqual(fontpkg.face.Variation{
.id = fontpkg.face.Variation.Id.init("wght"),
.value = 200,
}, list.list.items[0]);
try testing.expectEqual(fontpkg.face.Variation{
.id = fontpkg.face.Variation.Id.init("slnt"),
.value = -15,
}, list.list.items[1]);
}
};
/// Stores a set of keybinds.
pub const Keybinds = struct {
set: inputpkg.Binding.Set = .{},
pub fn parseCLI(self: *Keybinds, alloc: Allocator, input: ?[]const u8) !void {
var copy: ?[]u8 = null;
const value = value: {
const value = input orelse return error.ValueRequired;
// If we don't have a colon, use the value as-is, no copy
if (std.mem.indexOf(u8, value, ":") == null)
break :value value;
// If we have a colon, we copy the whole value for now. We could
// do this more efficiently later if we wanted to.
const buf = try alloc.alloc(u8, value.len);
copy = buf;
@memcpy(buf, value);
break :value buf;
};
errdefer if (copy) |v| alloc.free(v);
// Check for special values
if (std.mem.eql(u8, value, "clear")) {
// We don't clear the memory because its in the arena and unlikely
// to be free-able anyways (since arenas can only clear the last
// allocated value). This isn't a memory leak because the arena
// will be freed when the config is freed.
log.info("config has 'keybind = clear', all keybinds cleared", .{});
self.set = .{};
return;
}
const binding = try inputpkg.Binding.parse(value);
switch (binding.action) {
.unbind => self.set.remove(binding.trigger),
else => if (binding.consumed) {
try self.set.put(alloc, binding.trigger, binding.action);
} else {
try self.set.putUnconsumed(alloc, binding.trigger, binding.action);
},
}
}
/// Deep copy of the struct. Required by Config.
pub fn clone(self: *const Keybinds, alloc: Allocator) !Keybinds {
return .{
.set = .{
.bindings = try self.set.bindings.clone(alloc),
.reverse = try self.set.reverse.clone(alloc),
.unconsumed = try self.set.unconsumed.clone(alloc),
},
};
}
/// Compare if two of our value are requal. Required by Config.
pub fn equal(self: Keybinds, other: Keybinds) bool {
const self_map = self.set.bindings;
const other_map = other.set.bindings;
if (self_map.count() != other_map.count()) return false;
var it = self_map.iterator();
while (it.next()) |self_entry| {
const other_entry = other_map.getEntry(self_entry.key_ptr.*) orelse
return false;
if (!equalField(
inputpkg.Binding.Action,
self_entry.value_ptr.*,
other_entry.value_ptr.*,
)) return false;
}
return true;
}
test "parseCLI" {
const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var set: Keybinds = .{};
try set.parseCLI(alloc, "shift+a=copy_to_clipboard");
try set.parseCLI(alloc, "shift+a=csi:hello");
}
};
/// See "font-codepoint-map" for documentation.
pub const RepeatableCodepointMap = struct {
const Self = @This();
map: fontpkg.CodepointMap = .{},
pub fn parseCLI(self: *Self, alloc: Allocator, input_: ?[]const u8) !void {
const input = input_ orelse return error.ValueRequired;
const eql_idx = std.mem.indexOf(u8, input, "=") orelse return error.InvalidValue;
const whitespace = " \t";
const key = std.mem.trim(u8, input[0..eql_idx], whitespace);
const value = std.mem.trim(u8, input[eql_idx + 1 ..], whitespace);
const valueZ = try alloc.dupeZ(u8, value);
var p: UnicodeRangeParser = .{ .input = key };
while (try p.next()) |range| {
try self.map.add(alloc, .{
.range = range,
.descriptor = .{
.family = valueZ,
.monospace = false, // we allow any font
},
});
}
}
/// Deep copy of the struct. Required by Config.
pub fn clone(self: *const Self, alloc: Allocator) !Self {
return .{
.map = .{ .list = try self.map.list.clone(alloc) },
};
}
/// Compare if two of our value are requal. Required by Config.
pub fn equal(self: Self, other: Self) bool {
const itemsA = self.map.list.slice();
const itemsB = other.map.list.slice();
if (itemsA.len != itemsB.len) return false;
for (0..itemsA.len) |i| {
const a = itemsA.get(i);
const b = itemsB.get(i);
if (!std.meta.eql(a, b)) return false;
} else return true;
}
/// Parses the list of Unicode codepoint ranges. Valid syntax:
///
/// "" (empty returns null)
/// U+1234
/// U+1234-5678
/// U+1234,U+5678
/// U+1234-5678,U+5678
/// U+1234,U+5678-U+9ABC
///
/// etc.
const UnicodeRangeParser = struct {
input: []const u8,
i: usize = 0,
pub fn next(self: *UnicodeRangeParser) !?[2]u21 {
// Once we're EOF then we're done without an error.
if (self.eof()) return null;
// One codepoint no matter what
const start = try self.parseCodepoint();
if (self.eof()) return .{ start, start };
// We're allowed to have any whitespace here
self.consumeWhitespace();
// Otherwise we expect either a range or a comma
switch (self.input[self.i]) {
// Comma means we have another codepoint but in a different
// range so we return our current codepoint.
',' => {
self.advance();
self.consumeWhitespace();
if (self.eof()) return error.InvalidValue;
return .{ start, start };
},
// Hyphen means we have a range.
'-' => {
self.advance();
self.consumeWhitespace();
if (self.eof()) return error.InvalidValue;
const end = try self.parseCodepoint();
self.consumeWhitespace();
if (!self.eof() and self.input[self.i] != ',') return error.InvalidValue;
self.advance();
self.consumeWhitespace();
if (start > end) return error.InvalidValue;
return .{ start, end };
},
else => return error.InvalidValue,
}
}
fn consumeWhitespace(self: *UnicodeRangeParser) void {
while (!self.eof()) {
switch (self.input[self.i]) {
' ', '\t' => self.advance(),
else => return,
}
}
}
fn parseCodepoint(self: *UnicodeRangeParser) !u21 {
if (self.input[self.i] != 'U') return error.InvalidValue;
self.advance();
if (self.eof()) return error.InvalidValue;
if (self.input[self.i] != '+') return error.InvalidValue;
self.advance();
if (self.eof()) return error.InvalidValue;
const start_i = self.i;
while (true) {
const current = self.input[self.i];
const is_hex = (current >= '0' and current <= '9') or
(current >= 'A' and current <= 'F') or
(current >= 'a' and current <= 'f');
if (!is_hex) break;
// Advance but break on EOF
self.advance();
if (self.eof()) break;
}
// If we didn't consume a single character, we have an error.
if (start_i == self.i) return error.InvalidValue;
return std.fmt.parseInt(u21, self.input[start_i..self.i], 16) catch
return error.InvalidValue;
}
fn advance(self: *UnicodeRangeParser) void {
self.i += 1;
}
fn eof(self: *const UnicodeRangeParser) bool {
return self.i >= self.input.len;
}
};
test "parseCLI" {
const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var list: Self = .{};
try list.parseCLI(alloc, "U+ABCD=Comic Sans");
try list.parseCLI(alloc, "U+0001 - U+0005=Verdana");
try list.parseCLI(alloc, "U+0006-U+0009, U+ABCD=Courier");
try testing.expectEqual(@as(usize, 4), list.map.list.len);
{
const entry = list.map.list.get(0);
try testing.expectEqual([2]u21{ 0xABCD, 0xABCD }, entry.range);
try testing.expectEqualStrings("Comic Sans", entry.descriptor.family.?);
}
{
const entry = list.map.list.get(1);
try testing.expectEqual([2]u21{ 1, 5 }, entry.range);
try testing.expectEqualStrings("Verdana", entry.descriptor.family.?);
}
{
const entry = list.map.list.get(2);
try testing.expectEqual([2]u21{ 6, 9 }, entry.range);
try testing.expectEqualStrings("Courier", entry.descriptor.family.?);
}
{
const entry = list.map.list.get(3);
try testing.expectEqual([2]u21{ 0xABCD, 0xABCD }, entry.range);
try testing.expectEqualStrings("Courier", entry.descriptor.family.?);
}
}
};
pub const FontStyle = union(enum) {
const Self = @This();
/// Use the default font style that font discovery finds.
default: void,
/// Disable this font style completely. This will fall back to using
/// the regular font when this style is encountered.
false: void,
/// A specific named font style to use for this style.
name: [:0]const u8,
pub fn parseCLI(self: *Self, alloc: Allocator, input: ?[]const u8) !void {
const value = input orelse return error.ValueRequired;
if (std.mem.eql(u8, value, "default")) {
self.* = .{ .default = {} };
return;
}
if (std.mem.eql(u8, value, "false")) {
self.* = .{ .false = {} };
return;
}
const nameZ = try alloc.dupeZ(u8, value);
self.* = .{ .name = nameZ };
}
/// Returns the string name value that can be used with a font
/// descriptor.
pub fn nameValue(self: Self) ?[:0]const u8 {
return switch (self) {
.default, .false => null,
.name => self.name,
};
}
test "parseCLI" {
const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var p: Self = .{ .default = {} };
try p.parseCLI(alloc, "default");
try testing.expectEqual(Self{ .default = {} }, p);
try p.parseCLI(alloc, "false");
try testing.expectEqual(Self{ .false = {} }, p);
try p.parseCLI(alloc, "bold");
try testing.expectEqualStrings("bold", p.name);
}
};
/// See "link" for documentation.
pub const RepeatableLink = struct {
const Self = @This();
links: std.ArrayListUnmanaged(inputpkg.Link) = .{},
pub fn parseCLI(self: *Self, alloc: Allocator, input_: ?[]const u8) !void {
_ = self;
_ = alloc;
_ = input_;
return error.NotImplemented;
}
/// Deep copy of the struct. Required by Config.
pub fn clone(self: *const Self, alloc: Allocator) !Self {
_ = self;
_ = alloc;
return .{};
}
/// Compare if two of our value are requal. Required by Config.
pub fn equal(self: Self, other: Self) bool {
_ = self;
_ = other;
return true;
}
};
/// Options for copy on select behavior.
pub const CopyOnSelect = enum {
/// Disables copy on select entirely.
false,
/// 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,
/// Copy on select is enabled and goes to the system clipboard.
clipboard,
};
/// Shell integration values
pub const ShellIntegration = enum {
none,
detect,
fish,
zsh,
};
/// Shell integration features
pub const ShellIntegrationFeatures = packed struct {
cursor: bool = true,
};
/// OSC 4, 10, 11, and 12 default color reporting format.
pub const OSCColorReportFormat = enum {
none,
@"8-bit",
@"16-bit",
};
/// The default window theme.
pub const WindowTheme = enum {
system,
light,
dark,
};
/// See gtk-single-instance
pub const GtkSingleInstance = enum {
desktop,
false,
true,
};
/// See mouse-shift-capture
pub const MouseShiftCapture = enum {
false,
true,
always,
never,
};
/// How to treat requests to write to or read from the clipboard
pub const ClipboardAccess = enum {
allow,
deny,
ask,
};
/// See window-save-state
pub const WindowSaveState = enum {
default,
never,
always,
};
/// See grapheme-width-method
pub const GraphemeWidthMethod = enum {
wcswidth,
unicode,
};