config: add `command-palette-entry` config option

pull/7688/head
Leah Amelia Chen 2025-06-25 18:49:57 +02:00 committed by Mitchell Hashimoto
parent d419e5c922
commit dbe6035da0
3 changed files with 177 additions and 2 deletions

View File

@ -104,7 +104,7 @@ pub fn updateConfig(self: *CommandPalette, config: *const configpkg.Config) !voi
_ = self.arena.reset(.retain_capacity);
// TODO: Allow user-configured palette entries
for (inputpkg.command.defaults) |command| {
for (config.@"command-palette-entry".value.items) |command| {
// Filter out actions that are not implemented
// or don't make sense for GTK
switch (command.action) {

View File

@ -1975,6 +1975,28 @@ keybind: Keybinds = .{},
/// Example: `cursor`, `no-cursor`, `sudo`, `no-sudo`, `title`, `no-title`
@"shell-integration-features": ShellIntegrationFeatures = .{},
/// Custom entries into the command palette.
///
/// Each entry requires the title, the corresponding action, and an optional
/// description. Each field should be prefixed with the field name, a colon
/// (`:`), and then the specified value. The syntax for actions is identical
/// to the one for keybind actions. Whitespace in between fields is ignored.
///
/// ```ini
/// command-palette-entry = title:Reset Font Style, action:csi:0m
/// command-palette-entry = title:Crash on Main Thread,description:Causes a crash on the main (UI) thread.,action:crash:main
/// ```
///
/// By default, the command palette is preloaded with most actions that might
/// be useful in an interactive setting yet do not have easily accessible or
/// memorizable shortcuts. The default entries can be cleared by setting this
/// setting to an empty value:
///
/// ```ini
/// command-palette-entry =
/// ```
@"command-palette-entry": RepeatableCommand = .{},
/// 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
@ -2784,6 +2806,9 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config {
// Add our default keybindings
try result.keybind.init(alloc);
// Add our default command palette entries
try result.@"command-palette-entry".init(alloc);
// Add our default link for URL detection
try result.link.links.append(alloc, .{
.regex = url.regex,
@ -6114,6 +6139,141 @@ pub const ShellIntegrationFeatures = packed struct {
title: bool = true,
};
pub const RepeatableCommand = struct {
value: std.ArrayListUnmanaged(inputpkg.Command) = .empty,
pub fn init(self: *RepeatableCommand, alloc: Allocator) !void {
self.value = .empty;
try self.value.appendSlice(alloc, inputpkg.command.defaults);
}
pub fn parseCLI(self: *RepeatableCommand, alloc: Allocator, input: ?[]const u8) !void {
const input_ = input orelse {
self.value.clearRetainingCapacity();
return;
};
const cmd = try cli.args.parseAutoStruct(inputpkg.Command, alloc, input_);
try self.value.append(alloc, cmd);
}
/// Deep copy of the struct. Required by Config.
pub fn clone(self: *const RepeatableCommand, alloc: Allocator) Allocator.Error!RepeatableCommand {
const value = try self.value.clone(alloc);
for (value.items) |*item| {
item.* = try item.clone(alloc);
}
return .{ .value = value };
}
/// Compare if two of our value are equal. Required by Config.
pub fn equal(self: RepeatableCommand, other: RepeatableCommand) bool {
if (self.value.items.len != other.value.items.len) return false;
for (self.value.items, other.value.items) |a, b| {
if (!a.equal(b)) return false;
}
return true;
}
/// Used by Formatter
pub fn formatEntry(self: RepeatableCommand, formatter: anytype) !void {
if (self.value.items.len == 0) {
try formatter.formatEntry(void, {});
return;
}
var buf: [4096]u8 = undefined;
for (self.value.items) |item| {
const str = if (item.description.len > 0) std.fmt.bufPrint(
&buf,
"title:{s},description:{s},action:{}",
.{ item.title, item.description, item.action },
) else std.fmt.bufPrint(
&buf,
"title:{s},action:{}",
.{ item.title, item.action },
);
try formatter.formatEntry([]const u8, str catch return error.OutOfMemory);
}
}
test "RepeatableCommand parseCLI" {
const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var list: RepeatableCommand = .{};
try list.parseCLI(alloc, "title:Foo,action:ignore");
try list.parseCLI(alloc, "title:Bar,description:bobr,action:text:ale bydle");
try list.parseCLI(alloc, "title:Quux,description:boo,action:increase_font_size:2.5");
try testing.expectEqual(@as(usize, 3), list.value.items.len);
try testing.expectEqual(inputpkg.Binding.Action.ignore, list.value.items[0].action);
try testing.expectEqualStrings("Foo", list.value.items[0].title);
try testing.expectEqual(
inputpkg.Binding.Action{ .text = "ale bydle" },
list.value.items[0].action,
);
try testing.expectEqualStrings("Bar", list.value.items[1].title);
try testing.expectEqualStrings("bobr", list.value.items[1].description);
try testing.expectEqual(
inputpkg.Binding.Action{ .increase_font_size = 2.5 },
list.value.items[0].action,
);
try testing.expectEqualStrings("Quux", list.value.items[2].title);
try testing.expectEqualStrings("boo", list.value.items[2].description);
try list.parseCLI(alloc, "");
try testing.expectEqual(@as(usize, 0), list.value.items.len);
}
test "RepeatableCommand formatConfig empty" {
const testing = std.testing;
var buf = std.ArrayList(u8).init(testing.allocator);
defer buf.deinit();
var list: RepeatableCommand = .{};
try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer()));
try std.testing.expectEqualSlices(u8, "a = \n", buf.items);
}
test "RepeatableCommand formatConfig single item" {
const testing = std.testing;
var buf = std.ArrayList(u8).init(testing.allocator);
defer buf.deinit();
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var list: RepeatableCommand = .{};
try list.parseCLI(alloc, "title:Bobr, action:text:Bober");
try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer()));
try std.testing.expectEqualSlices(u8, "a = title:Bobr,action:text:Bober\n", buf.items);
}
test "RepeatableCommand formatConfig multiple items" {
const testing = std.testing;
var buf = std.ArrayList(u8).init(testing.allocator);
defer buf.deinit();
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var list: RepeatableCommand = .{};
try list.parseCLI(alloc, "title:Bobr, action:text:kurwa");
try list.parseCLI(alloc, "title:Ja, description: pierdole, action:text:jakie bydle");
try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer()));
try std.testing.expectEqualSlices(u8, "a = title:bobr,action:text:kurwa\na = title:Ja,description:pierdole,action:text:jakie bydle\n", buf.items);
}
};
/// OSC 4, 10, 11, and 12 default color reporting format.
pub const OSCColorReportFormat = enum {
none,

View File

@ -18,7 +18,7 @@ const Action = @import("Binding.zig").Action;
pub const Command = struct {
action: Action,
title: [:0]const u8,
description: [:0]const u8,
description: [:0]const u8 = "",
/// ghostty_command_s
pub const C = extern struct {
@ -28,6 +28,21 @@ pub const Command = struct {
description: [*:0]const u8,
};
pub fn clone(self: *const Command, alloc: Allocator) Allocator.Error!Command {
return .{
.action = try self.action.clone(alloc),
.title = try alloc.dupeZ(u8, self.title),
.description = try alloc.dupeZ(u8, self.description),
};
}
pub fn equal(self: Command, other: Command) bool {
if (self.action.hash() != other.action.hash()) return false;
if (!std.mem.eql(u8, self.title, other.title)) return false;
if (!std.mem.eql(u8, self.description, other.description)) return false;
return true;
}
/// Convert this command to a C struct.
pub fn comptimeCval(self: Command) C {
assert(@inComptime());