mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-01-09 06:43:25 +00:00
macOS: command-palette-entry shows up in command palette (#10050)
Fixes #7158 This has worked on GTK for awhile. This exposes custom command palette entries to the macOS app now.
This commit is contained in:
@@ -454,6 +454,12 @@ typedef struct {
|
||||
size_t len;
|
||||
} ghostty_config_color_list_s;
|
||||
|
||||
// config.RepeatableCommand
|
||||
typedef struct {
|
||||
const ghostty_command_s* commands;
|
||||
size_t len;
|
||||
} ghostty_config_command_list_s;
|
||||
|
||||
// config.Palette
|
||||
typedef struct {
|
||||
ghostty_config_color_s colors[256];
|
||||
|
||||
@@ -137,7 +137,6 @@
|
||||
Features/Update/UpdateSimulator.swift,
|
||||
Features/Update/UpdateViewModel.swift,
|
||||
"Ghostty/FullscreenMode+Extension.swift",
|
||||
Ghostty/Ghostty.Command.swift,
|
||||
Ghostty/Ghostty.Error.swift,
|
||||
Ghostty/Ghostty.Event.swift,
|
||||
Ghostty/Ghostty.Input.swift,
|
||||
|
||||
@@ -64,7 +64,7 @@ struct TerminalCommandPaletteView: View {
|
||||
// Sort the rest. We replace ":" with a character that sorts before space
|
||||
// so that "Foo:" sorts before "Foo Bar:". Use sortKey as a tie-breaker
|
||||
// for stable ordering when titles are equal.
|
||||
options.append(contentsOf: (jumpOptions + terminalOptions).sorted { a, b in
|
||||
options.append(contentsOf: (jumpOptions + terminalOptions + customEntries).sorted { a, b in
|
||||
let aNormalized = a.title.replacingOccurrences(of: ":", with: "\t")
|
||||
let bNormalized = b.title.replacingOccurrences(of: ":", with: "\t")
|
||||
let comparison = aNormalized.localizedCaseInsensitiveCompare(bNormalized)
|
||||
@@ -135,6 +135,19 @@ struct TerminalCommandPaletteView: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// Custom commands from the command-palette-entry configuration.
|
||||
private var customEntries: [CommandOption] {
|
||||
guard let appDelegate = NSApp.delegate as? AppDelegate else { return [] }
|
||||
return appDelegate.ghostty.config.commandPaletteEntries.map { c in
|
||||
CommandOption(
|
||||
title: c.title,
|
||||
description: c.description
|
||||
) {
|
||||
onAction(c.action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Commands for jumping to other terminal surfaces.
|
||||
private var jumpOptions: [CommandOption] {
|
||||
TerminalController.all.flatMap { controller -> [CommandOption] in
|
||||
|
||||
@@ -622,6 +622,16 @@ extension Ghostty {
|
||||
let str = String(cString: ptr)
|
||||
return Scrollbar(rawValue: str) ?? defaultValue
|
||||
}
|
||||
|
||||
var commandPaletteEntries: [Ghostty.Command] {
|
||||
guard let config = self.config else { return [] }
|
||||
var v: ghostty_config_command_list_s = .init()
|
||||
let key = "command-palette-entry"
|
||||
guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return [] }
|
||||
guard v.len > 0 else { return [] }
|
||||
let buffer = UnsafeBufferPointer(start: v.commands, count: v.len)
|
||||
return buffer.map { Ghostty.Command(cValue: $0) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8041,15 +8041,37 @@ pub const SplitPreserveZoom = packed struct {
|
||||
};
|
||||
|
||||
pub const RepeatableCommand = struct {
|
||||
value: std.ArrayListUnmanaged(inputpkg.Command) = .empty,
|
||||
const Self = @This();
|
||||
|
||||
pub fn init(self: *RepeatableCommand, alloc: Allocator) !void {
|
||||
value: std.ArrayListUnmanaged(inputpkg.Command) = .empty,
|
||||
value_c: std.ArrayListUnmanaged(inputpkg.Command.C) = .empty,
|
||||
|
||||
/// ghostty_config_command_list_s
|
||||
pub const C = extern struct {
|
||||
commands: [*]inputpkg.Command.C,
|
||||
len: usize,
|
||||
};
|
||||
|
||||
pub fn cval(self: *const Self) C {
|
||||
return .{
|
||||
.commands = self.value_c.items.ptr,
|
||||
.len = self.value_c.items.len,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn init(self: *Self, alloc: Allocator) !void {
|
||||
self.value = .empty;
|
||||
self.value_c = .empty;
|
||||
errdefer {
|
||||
self.value.deinit(alloc);
|
||||
self.value_c.deinit(alloc);
|
||||
}
|
||||
try self.value.appendSlice(alloc, inputpkg.command.defaults);
|
||||
try self.value_c.appendSlice(alloc, inputpkg.command.defaultsC);
|
||||
}
|
||||
|
||||
pub fn parseCLI(
|
||||
self: *RepeatableCommand,
|
||||
self: *Self,
|
||||
alloc: Allocator,
|
||||
input_: ?[]const u8,
|
||||
) !void {
|
||||
@@ -8057,26 +8079,36 @@ pub const RepeatableCommand = struct {
|
||||
const input = input_ orelse "";
|
||||
if (input.len == 0) {
|
||||
self.value.clearRetainingCapacity();
|
||||
self.value_c.clearRetainingCapacity();
|
||||
return;
|
||||
}
|
||||
|
||||
// Reserve space in our lists
|
||||
try self.value.ensureUnusedCapacity(alloc, 1);
|
||||
try self.value_c.ensureUnusedCapacity(alloc, 1);
|
||||
|
||||
const cmd = try cli.args.parseAutoStruct(
|
||||
inputpkg.Command,
|
||||
alloc,
|
||||
input,
|
||||
null,
|
||||
);
|
||||
try self.value.append(alloc, cmd);
|
||||
const cmd_c = try cmd.cval(alloc);
|
||||
self.value.appendAssumeCapacity(cmd);
|
||||
self.value_c.appendAssumeCapacity(cmd_c);
|
||||
}
|
||||
|
||||
/// Deep copy of the struct. Required by Config.
|
||||
pub fn clone(self: *const RepeatableCommand, alloc: Allocator) Allocator.Error!RepeatableCommand {
|
||||
pub fn clone(self: *const Self, alloc: Allocator) Allocator.Error!Self {
|
||||
const value = try self.value.clone(alloc);
|
||||
for (value.items) |*item| {
|
||||
item.* = try item.clone(alloc);
|
||||
}
|
||||
|
||||
return .{ .value = value };
|
||||
return .{
|
||||
.value = value,
|
||||
.value_c = try self.value_c.clone(alloc),
|
||||
};
|
||||
}
|
||||
|
||||
/// Compare if two of our value are equal. Required by Config.
|
||||
@@ -8232,6 +8264,50 @@ pub const RepeatableCommand = struct {
|
||||
try testing.expectEqualStrings("kurwa", item.action.text);
|
||||
}
|
||||
}
|
||||
|
||||
test "RepeatableCommand cval" {
|
||||
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 testing.expectEqual(@as(usize, 2), list.value.items.len);
|
||||
try testing.expectEqual(@as(usize, 2), list.value_c.items.len);
|
||||
|
||||
const cv = list.cval();
|
||||
try testing.expectEqual(@as(usize, 2), cv.len);
|
||||
|
||||
// First entry
|
||||
try testing.expectEqualStrings("Foo", std.mem.sliceTo(cv.commands[0].title, 0));
|
||||
try testing.expectEqualStrings("ignore", std.mem.sliceTo(cv.commands[0].action_key, 0));
|
||||
try testing.expectEqualStrings("ignore", std.mem.sliceTo(cv.commands[0].action, 0));
|
||||
|
||||
// Second entry
|
||||
try testing.expectEqualStrings("Bar", std.mem.sliceTo(cv.commands[1].title, 0));
|
||||
try testing.expectEqualStrings("bobr", std.mem.sliceTo(cv.commands[1].description, 0));
|
||||
try testing.expectEqualStrings("text", std.mem.sliceTo(cv.commands[1].action_key, 0));
|
||||
try testing.expectEqualStrings("text:ale bydle", std.mem.sliceTo(cv.commands[1].action, 0));
|
||||
}
|
||||
|
||||
test "RepeatableCommand cval cleared" {
|
||||
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 testing.expectEqual(@as(usize, 1), list.cval().len);
|
||||
|
||||
try list.parseCLI(alloc, "");
|
||||
try testing.expectEqual(@as(usize, 0), list.cval().len);
|
||||
}
|
||||
};
|
||||
|
||||
/// OSC 4, 10, 11, and 12 default color reporting format.
|
||||
|
||||
@@ -43,7 +43,7 @@ pub const Command = struct {
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Convert this command to a C struct.
|
||||
/// Convert this command to a C struct at comptime.
|
||||
pub fn comptimeCval(self: Command) C {
|
||||
assert(@inComptime());
|
||||
|
||||
@@ -55,6 +55,27 @@ pub const Command = struct {
|
||||
};
|
||||
}
|
||||
|
||||
/// Convert this command to a C struct at runtime.
|
||||
///
|
||||
/// This shares memory with the original command.
|
||||
///
|
||||
/// The action string is allocated using the provided allocator. You can
|
||||
/// free the slice directly if you need to but we recommend an arena
|
||||
/// for this.
|
||||
pub fn cval(self: Command, alloc: Allocator) Allocator.Error!C {
|
||||
var buf: std.Io.Writer.Allocating = .init(alloc);
|
||||
defer buf.deinit();
|
||||
self.action.format(&buf.writer) catch return error.OutOfMemory;
|
||||
const action = try buf.toOwnedSliceSentinel(0);
|
||||
|
||||
return .{
|
||||
.action_key = @tagName(self.action),
|
||||
.action = action.ptr,
|
||||
.title = self.title,
|
||||
.description = self.description,
|
||||
};
|
||||
}
|
||||
|
||||
/// Implements a comparison function for std.mem.sortUnstable
|
||||
/// and similar functions. The sorting is defined by Ghostty
|
||||
/// to be what we prefer. If a caller wants some other sorting,
|
||||
|
||||
Reference in New Issue
Block a user