mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 16:56:09 +03:00
227 lines
7.8 KiB
Zig
227 lines
7.8 KiB
Zig
const std = @import("std");
|
|
const Allocator = std.mem.Allocator;
|
|
const ArenaAllocator = std.heap.ArenaAllocator;
|
|
const inputpkg = @import("input.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.
|
|
pub const Config = struct {
|
|
/// Font size in points
|
|
@"font-size": u8 = 12,
|
|
|
|
/// Background color for the window.
|
|
background: Color = .{ .r = 0, .g = 0, .b = 0 },
|
|
|
|
/// Foreground color for the window.
|
|
foreground: Color = .{ .r = 0xFF, .g = 0xA5, .b = 0 },
|
|
|
|
/// The command to run, usually a shell. If this is not an absolute path,
|
|
/// it'll be looked up in the PATH.
|
|
command: ?[]const u8,
|
|
|
|
/// 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 = .{},
|
|
|
|
/// 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 {
|
|
var arena = ArenaAllocator.init(alloc_gpa);
|
|
errdefer arena.deinit();
|
|
const alloc = arena.allocator();
|
|
|
|
// Build up our basic config
|
|
var result: Config = .{
|
|
._arena = arena,
|
|
.command = "sh",
|
|
};
|
|
|
|
// Add our default keybindings
|
|
try result.keybind.set.put(
|
|
alloc,
|
|
.{ .key = .c, .mods = .{ .super = true } },
|
|
.{ .copy_to_clipboard = 0 },
|
|
);
|
|
|
|
try result.keybind.set.put(
|
|
alloc,
|
|
.{ .key = .v, .mods = .{ .super = true } },
|
|
.{ .paste_from_clipboard = 0 },
|
|
);
|
|
|
|
try result.keybind.set.put(alloc, .{ .key = .up }, .{ .csi = "A" });
|
|
try result.keybind.set.put(alloc, .{ .key = .down }, .{ .csi = "B" });
|
|
try result.keybind.set.put(alloc, .{ .key = .right }, .{ .csi = "C" });
|
|
try result.keybind.set.put(alloc, .{ .key = .left }, .{ .csi = "D" });
|
|
try result.keybind.set.put(alloc, .{ .key = .home }, .{ .csi = "H" });
|
|
try result.keybind.set.put(alloc, .{ .key = .end }, .{ .csi = "F" });
|
|
try result.keybind.set.put(alloc, .{ .key = .page_up }, .{ .csi = "5~" });
|
|
try result.keybind.set.put(alloc, .{ .key = .page_down }, .{ .csi = "6~" });
|
|
|
|
return result;
|
|
}
|
|
};
|
|
|
|
/// Color represents a color using RGB.
|
|
pub const Color = struct {
|
|
r: u8,
|
|
g: u8,
|
|
b: u8,
|
|
|
|
pub const Error = error{
|
|
InvalidFormat,
|
|
};
|
|
|
|
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"));
|
|
}
|
|
};
|
|
|
|
/// 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");
|
|
}
|
|
};
|
|
|
|
test {
|
|
std.testing.refAllDecls(@This());
|
|
}
|