mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-12-31 18:52:12 +00:00
These are just different from macOS. I've looked at various Linux terminals and there seems to be some general consensus around this.
888 lines
33 KiB
Zig
888 lines
33 KiB
Zig
const std = @import("std");
|
|
const builtin = @import("builtin");
|
|
const Allocator = std.mem.Allocator;
|
|
const ArenaAllocator = std.heap.ArenaAllocator;
|
|
const inputpkg = @import("input.zig");
|
|
const passwd = @import("passwd.zig");
|
|
const terminal = @import("terminal/main.zig");
|
|
const internal_os = @import("os/main.zig");
|
|
const xdg = @import("xdg.zig");
|
|
const cli_args = @import("cli_args.zig");
|
|
|
|
const log = std.log.scoped(.config);
|
|
|
|
/// Used on Unixes for some defaults.
|
|
const c = @cImport({
|
|
@cInclude("unistd.h");
|
|
});
|
|
|
|
/// 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.
|
|
pub const Config = struct {
|
|
/// The font families to use.
|
|
@"font-family": ?[:0]const u8 = null,
|
|
@"font-family-bold": ?[:0]const u8 = null,
|
|
@"font-family-italic": ?[:0]const u8 = null,
|
|
@"font-family-bold-italic": ?[:0]const u8 = null,
|
|
|
|
/// 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,
|
|
},
|
|
|
|
/// 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,
|
|
|
|
/// 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 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)
|
|
///
|
|
command: ?[]const u8 = null,
|
|
|
|
/// The directory to change to after starting the command.
|
|
///
|
|
/// The default is "inherit" except in special scenarios listed next.
|
|
/// If ghostty can detect it is launched on macOS from launchd
|
|
/// (double-clicked), 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.
|
|
/// - modifers and key scan be in any order, "shift+a+ctrl" is weird,
|
|
/// but valid.
|
|
/// - only a single key input is allowed, "ctrl+a+b" is invalid.
|
|
///
|
|
/// 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".
|
|
///
|
|
/// 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"
|
|
///
|
|
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 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,
|
|
|
|
/// Whether to allow programs running in the terminal to read/write to
|
|
/// the system clipboard (OSC 52, for googling). The default is to
|
|
/// disallow clipboard reading but allow writing.
|
|
@"clipboard-read": bool = false,
|
|
@"clipboard-write": bool = true,
|
|
|
|
/// 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,
|
|
|
|
/// 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.
|
|
@"config-file": RepeatableString = .{},
|
|
|
|
/// This is set by the CLI parser for deinit.
|
|
_arena: ?ArenaAllocator = null,
|
|
|
|
pub fn deinit(self: *Config) void {
|
|
if (self._arena) |arena| arena.deinit();
|
|
self.* = undefined;
|
|
}
|
|
|
|
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
|
|
try result.keybind.set.put(
|
|
alloc,
|
|
.{ .key = .c, .mods = ctrlOrSuper(.{}) },
|
|
.{ .copy_to_clipboard = {} },
|
|
);
|
|
try result.keybind.set.put(
|
|
alloc,
|
|
.{ .key = .v, .mods = ctrlOrSuper(.{}) },
|
|
.{ .paste_from_clipboard = {} },
|
|
);
|
|
|
|
// Some control keys
|
|
try result.keybind.set.put(alloc, .{ .key = .up }, .{ .cursor_key = .{
|
|
.normal = "\x1b[A",
|
|
.application = "\x1bOA",
|
|
} });
|
|
try result.keybind.set.put(alloc, .{ .key = .down }, .{ .cursor_key = .{
|
|
.normal = "\x1b[B",
|
|
.application = "\x1bOB",
|
|
} });
|
|
try result.keybind.set.put(alloc, .{ .key = .right }, .{ .cursor_key = .{
|
|
.normal = "\x1b[C",
|
|
.application = "\x1bOC",
|
|
} });
|
|
try result.keybind.set.put(alloc, .{ .key = .left }, .{ .cursor_key = .{
|
|
.normal = "\x1b[D",
|
|
.application = "\x1bOD",
|
|
} });
|
|
try result.keybind.set.put(alloc, .{ .key = .home }, .{ .cursor_key = .{
|
|
.normal = "\x1b[H",
|
|
.application = "\x1bOH",
|
|
} });
|
|
try result.keybind.set.put(alloc, .{ .key = .end }, .{ .cursor_key = .{
|
|
.normal = "\x1b[F",
|
|
.application = "\x1bOF",
|
|
} });
|
|
|
|
try result.keybind.set.put(alloc, .{ .key = .page_up }, .{ .csi = "5~" });
|
|
try result.keybind.set.put(alloc, .{ .key = .page_down }, .{ .csi = "6~" });
|
|
|
|
// From xterm:
|
|
// Note that F1 through F4 are prefixed with SS3 , while the other keys are
|
|
// prefixed with CSI . Older versions of xterm implement different escape
|
|
// sequences for F1 through F4, with a CSI prefix. These can be activated
|
|
// by setting the oldXtermFKeys resource. However, since they do not
|
|
// correspond to any hardware terminal, they have been deprecated. (The
|
|
// DEC VT220 reserves F1 through F5 for local functions such as Setup).
|
|
try result.keybind.set.put(alloc, .{ .key = .f1 }, .{ .csi = "11~" });
|
|
try result.keybind.set.put(alloc, .{ .key = .f2 }, .{ .csi = "12~" });
|
|
try result.keybind.set.put(alloc, .{ .key = .f3 }, .{ .csi = "13~" });
|
|
try result.keybind.set.put(alloc, .{ .key = .f4 }, .{ .csi = "14~" });
|
|
try result.keybind.set.put(alloc, .{ .key = .f5 }, .{ .csi = "15~" });
|
|
try result.keybind.set.put(alloc, .{ .key = .f6 }, .{ .csi = "17~" });
|
|
try result.keybind.set.put(alloc, .{ .key = .f7 }, .{ .csi = "18~" });
|
|
try result.keybind.set.put(alloc, .{ .key = .f8 }, .{ .csi = "19~" });
|
|
try result.keybind.set.put(alloc, .{ .key = .f9 }, .{ .csi = "20~" });
|
|
try result.keybind.set.put(alloc, .{ .key = .f10 }, .{ .csi = "21~" });
|
|
try result.keybind.set.put(alloc, .{ .key = .f11 }, .{ .csi = "23~" });
|
|
try result.keybind.set.put(alloc, .{ .key = .f12 }, .{ .csi = "24~" });
|
|
|
|
// Fonts
|
|
try result.keybind.set.put(
|
|
alloc,
|
|
.{ .key = .equal, .mods = ctrlOrSuper(.{}) },
|
|
.{ .increase_font_size = 1 },
|
|
);
|
|
try result.keybind.set.put(
|
|
alloc,
|
|
.{ .key = .minus, .mods = ctrlOrSuper(.{}) },
|
|
.{ .decrease_font_size = 1 },
|
|
);
|
|
try result.keybind.set.put(
|
|
alloc,
|
|
.{ .key = .zero, .mods = ctrlOrSuper(.{}) },
|
|
.{ .reset_font_size = {} },
|
|
);
|
|
|
|
// Dev Mode
|
|
try result.keybind.set.put(
|
|
alloc,
|
|
.{ .key = .down, .mods = .{ .shift = true, .super = true } },
|
|
.{ .toggle_dev_mode = {} },
|
|
);
|
|
|
|
// 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 = .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 = .d, .mods = .{ .ctrl = true } },
|
|
.{ .new_split = .right },
|
|
);
|
|
try result.keybind.set.put(
|
|
alloc,
|
|
.{ .key = .d, .mods = .{ .ctrl = true, .shift = true } },
|
|
.{ .new_split = .down },
|
|
);
|
|
try result.keybind.set.put(
|
|
alloc,
|
|
.{ .key = .left_bracket, .mods = .{ .ctrl = true } },
|
|
.{ .goto_split = .previous },
|
|
);
|
|
try result.keybind.set.put(
|
|
alloc,
|
|
.{ .key = .right_bracket, .mods = .{ .ctrl = 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 },
|
|
);
|
|
}
|
|
{
|
|
// Cmd+N for goto tab N
|
|
const start = @enumToInt(inputpkg.Key.one);
|
|
const end = @enumToInt(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 = @intToEnum(inputpkg.Key, i), .mods = mods },
|
|
.{ .goto_tab = (i - start) + 1 },
|
|
);
|
|
}
|
|
}
|
|
|
|
// 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 = {} },
|
|
);
|
|
|
|
// 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 = .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 },
|
|
);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// This sets either "ctrl" or "super" to true (but not both)
|
|
/// on mods depending on if the build target is Mac or not. On
|
|
/// Mac, we default to super (i.e. super+c for copy) and on
|
|
/// non-Mac we default to ctrl (i.e. ctrl+c for copy).
|
|
fn ctrlOrSuper(mods: inputpkg.Mods) inputpkg.Mods {
|
|
var copy = mods;
|
|
if (comptime builtin.target.isDarwin()) {
|
|
copy.super = true;
|
|
} else {
|
|
copy.ctrl = true;
|
|
}
|
|
|
|
return copy;
|
|
}
|
|
|
|
/// Load the configuration from the default file locations. Currently,
|
|
/// this loads from $XDG_CONFIG_HOME/ghostty/config.
|
|
pub fn loadDefaultFiles(self: *Config, alloc: Allocator) !void {
|
|
const home_config_path = try xdg.config(alloc, .{ .subdir = "ghostty/config" });
|
|
defer alloc.free(home_config_path);
|
|
|
|
const cwd = std.fs.cwd();
|
|
if (cwd.openFile(home_config_path, .{})) |file| {
|
|
defer file.close();
|
|
std.log.info("reading configuration file path={s}", .{home_config_path});
|
|
|
|
var buf_reader = std.io.bufferedReader(file.reader());
|
|
var iter = cli_args.lineIterator(buf_reader.reader());
|
|
try cli_args.parse(Config, alloc, self, &iter);
|
|
} else |err| switch (err) {
|
|
error.FileNotFound => std.log.info(
|
|
"homedir config not found, not loading path={s}",
|
|
.{home_config_path},
|
|
),
|
|
|
|
else => std.log.warn(
|
|
"error reading homedir config file, not loading err={} path={s}",
|
|
.{ err, home_config_path },
|
|
),
|
|
}
|
|
}
|
|
|
|
/// Load and parse the config files that were added in the "config-file" key.
|
|
pub fn loadRecursiveFiles(self: *Config, alloc: Allocator) !void {
|
|
// TODO(mitchellh): we should parse the files form the homedir first
|
|
// TODO(mitchellh): support nesting (config-file in a config file)
|
|
// TODO(mitchellh): detect cycles when nesting
|
|
|
|
if (self.@"config-file".list.items.len == 0) return;
|
|
|
|
const cwd = std.fs.cwd();
|
|
const len = self.@"config-file".list.items.len;
|
|
for (self.@"config-file".list.items) |path| {
|
|
var file = try cwd.openFile(path, .{});
|
|
defer file.close();
|
|
|
|
var buf_reader = std.io.bufferedReader(file.reader());
|
|
var iter = cli_args.lineIterator(buf_reader.reader());
|
|
try cli_args.parse(Config, alloc, self, &iter);
|
|
|
|
// We don't currently support adding more config files to load
|
|
// from within a loaded config file. This can be supported
|
|
// later.
|
|
if (self.@"config-file".list.items.len > len)
|
|
return error.ConfigFileInConfigFile;
|
|
}
|
|
}
|
|
|
|
pub fn finalize(self: *Config) !void {
|
|
// 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") |family| {
|
|
const fields = &[_][]const u8{
|
|
"font-family-bold",
|
|
"font-family-italic",
|
|
"font-family-bold-italic",
|
|
};
|
|
inline for (fields) |field| {
|
|
if (@field(self, field) == null) {
|
|
@field(self, field) = family;
|
|
}
|
|
}
|
|
}
|
|
|
|
// The default for the working directory depends on the system.
|
|
const wd = self.@"working-directory" orelse switch (builtin.os.tag) {
|
|
.macos => if (c.getppid() == 1) "home" else "inherit",
|
|
else => "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: {
|
|
const alloc = self._arena.?.allocator();
|
|
|
|
// We don't do this in flatpak because SHELL in Flatpak is
|
|
// always set to /bin/sh
|
|
if (!internal_os.isFlatpak()) {
|
|
// First look up the command using the SHELL env var.
|
|
if (std.process.getEnvVarOwned(alloc, "SHELL")) |value| {
|
|
log.debug("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 |_| {}
|
|
}
|
|
|
|
// We need the passwd entry for the remainder
|
|
const pw = try passwd.get(alloc);
|
|
if (self.command == null) {
|
|
if (pw.shell) |sh| {
|
|
log.debug("default shell src=passwd value={s}", .{sh});
|
|
self.command = sh;
|
|
}
|
|
}
|
|
|
|
if (wd_home) {
|
|
if (pw.home) |home| {
|
|
log.debug("default working directory src=passwd value={s}", .{home});
|
|
self.@"working-directory" = home;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
};
|
|
|
|
/// Color represents a color using RGB.
|
|
pub const Color = struct {
|
|
r: u8,
|
|
g: u8,
|
|
b: u8,
|
|
|
|
pub const Error = error{
|
|
InvalidFormat,
|
|
};
|
|
|
|
/// 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);
|
|
}
|
|
|
|
/// 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.InvalidFormat;
|
|
|
|
// 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 const Error = error{
|
|
InvalidFormat,
|
|
};
|
|
|
|
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.InvalidFormat;
|
|
|
|
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 };
|
|
}
|
|
|
|
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([]const u8) = .{},
|
|
|
|
pub fn parseCLI(self: *Self, alloc: Allocator, input: ?[]const u8) !void {
|
|
const value = input orelse return error.ValueRequired;
|
|
try self.list.append(alloc, value);
|
|
}
|
|
|
|
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);
|
|
}
|
|
};
|
|
|
|
/// 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;
|
|
var 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;
|
|
|
|
std.mem.copy(u8, buf, value);
|
|
break :value buf;
|
|
};
|
|
errdefer if (copy) |v| alloc.free(v);
|
|
|
|
const binding = try inputpkg.Binding.parse(value);
|
|
switch (binding.action) {
|
|
.unbind => self.set.remove(binding.trigger),
|
|
else => try self.set.put(alloc, binding.trigger, binding.action),
|
|
}
|
|
}
|
|
|
|
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");
|
|
}
|
|
};
|
|
|
|
// Wasm API.
|
|
pub const Wasm = if (!builtin.target.isWasm()) struct {} else struct {
|
|
const wasm = @import("os/wasm.zig");
|
|
const alloc = wasm.alloc;
|
|
|
|
/// Create a new configuration filled with the initial default values.
|
|
export fn config_new() ?*Config {
|
|
const result = alloc.create(Config) catch |err| {
|
|
log.err("error allocating config err={}", .{err});
|
|
return null;
|
|
};
|
|
|
|
result.* = Config.default(alloc) catch |err| {
|
|
log.err("error creating config err={}", .{err});
|
|
return null;
|
|
};
|
|
|
|
return result;
|
|
}
|
|
|
|
export fn config_free(ptr: ?*Config) void {
|
|
if (ptr) |v| {
|
|
v.deinit();
|
|
alloc.destroy(v);
|
|
}
|
|
}
|
|
|
|
/// Load the configuration from a string in the same format as
|
|
/// the file-based syntax for the desktop version of the terminal.
|
|
export fn config_load_string(
|
|
self: *Config,
|
|
str: [*]const u8,
|
|
len: usize,
|
|
) void {
|
|
config_load_string_(self, str[0..len]) catch |err| {
|
|
log.err("error loading config err={}", .{err});
|
|
};
|
|
}
|
|
|
|
fn config_load_string_(self: *Config, str: []const u8) !void {
|
|
var fbs = std.io.fixedBufferStream(str);
|
|
var iter = cli_args.lineIterator(fbs.reader());
|
|
try cli_args.parse(Config, alloc, self, &iter);
|
|
}
|
|
|
|
export fn config_finalize(self: *Config) void {
|
|
self.finalize() catch |err| {
|
|
log.err("error finalizing config err={}", .{err});
|
|
};
|
|
}
|
|
};
|
|
|
|
// C API.
|
|
pub const CAPI = struct {
|
|
const global = &@import("main.zig").state;
|
|
|
|
/// Create a new configuration filled with the initial default values.
|
|
export fn ghostty_config_new() ?*Config {
|
|
const result = global.alloc.create(Config) catch |err| {
|
|
log.err("error allocating config err={}", .{err});
|
|
return null;
|
|
};
|
|
|
|
result.* = Config.default(global.alloc) catch |err| {
|
|
log.err("error creating config err={}", .{err});
|
|
return null;
|
|
};
|
|
|
|
return result;
|
|
}
|
|
|
|
export fn ghostty_config_free(ptr: ?*Config) void {
|
|
if (ptr) |v| {
|
|
v.deinit();
|
|
global.alloc.destroy(v);
|
|
}
|
|
}
|
|
|
|
/// Load the configuration from a string in the same format as
|
|
/// the file-based syntax for the desktop version of the terminal.
|
|
export fn ghostty_config_load_string(
|
|
self: *Config,
|
|
str: [*]const u8,
|
|
len: usize,
|
|
) void {
|
|
config_load_string_(self, str[0..len]) catch |err| {
|
|
log.err("error loading config err={}", .{err});
|
|
};
|
|
}
|
|
|
|
fn config_load_string_(self: *Config, str: []const u8) !void {
|
|
var fbs = std.io.fixedBufferStream(str);
|
|
var iter = cli_args.lineIterator(fbs.reader());
|
|
try cli_args.parse(Config, global.alloc, self, &iter);
|
|
}
|
|
|
|
/// Load the configuration from the default file locations. This
|
|
/// is usually done first. The default file locations are locations
|
|
/// such as the home directory.
|
|
export fn ghostty_config_load_default_files(self: *Config) void {
|
|
self.loadDefaultFiles(global.alloc) catch |err| {
|
|
log.err("error loading config err={}", .{err});
|
|
};
|
|
}
|
|
|
|
/// Load the configuration from the user-specified configuration
|
|
/// file locations in the previously loaded configuration. This will
|
|
/// recursively continue to load up to a built-in limit.
|
|
export fn ghostty_config_load_recursive_files(self: *Config) void {
|
|
self.loadRecursiveFiles(global.alloc) catch |err| {
|
|
log.err("error loading config err={}", .{err});
|
|
};
|
|
}
|
|
|
|
export fn ghostty_config_finalize(self: *Config) void {
|
|
self.finalize() catch |err| {
|
|
log.err("error finalizing config err={}", .{err});
|
|
};
|
|
}
|
|
};
|
|
|
|
test {
|
|
std.testing.refAllDecls(@This());
|
|
}
|