Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436const std = @import("std");const builtin = @import("builtin");const args = @import("args.zig");const Action = @import("ghostty.zig").Action;const Arena = std.heap.ArenaAllocator;const Allocator = std.mem.Allocator;const configpkg = @import("../config.zig");const Config = configpkg.Config;const vaxis = @import("vaxis");const input = @import("../input.zig");const tui = @import("tui.zig");const Binding = input.Binding;
pub const Options = struct { /// If `true`, print out the default keybinds instead of the ones configured /// in the config file. default: bool = false,
/// If `true`, print out documentation about the action associated with the /// keybinds. docs: bool = false,
/// If `true`, print without formatting even if printing to a tty plain: bool = false,
pub fn deinit(self: Options) void { _ = self; }
/// Enables `-h` and `--help` to work. pub fn help(self: Options) !void { _ = self; return Action.help_error; }};
/// The `list-keybinds` command is used to list all the available keybinds for/// Ghostty.////// When executed without any arguments this will list the current keybinds/// loaded by the config file. If no config file is found or there aren't any/// changes to the keybinds it will print out the default ones configured for/// Ghostty////// Flags:////// * `--default`: will print out all the default keybinds////// * `--docs`: currently does nothing, intended to print out documentation/// about the action associated with the keybinds////// * `--plain`: will disable formatting and make the output more/// friendly for Unix tooling. This is default when not printing to a tty.pub fn run(alloc: Allocator) !u8 { var opts: Options = .{}; defer opts.deinit();
{ var iter = try args.argsIterator(alloc); defer iter.deinit(); try args.parse(Options, alloc, &opts, &iter); }
var config = if (opts.default) try Config.default(alloc) else try Config.load(alloc); defer config.deinit();
var buffer: [1024]u8 = undefined; const stdout: std.fs.File = .stdout(); var stdout_writer = stdout.writer(&buffer); const writer = &stdout_writer.interface;
if (tui.can_pretty_print and !opts.plain and stdout.isTty()) { var arena = std.heap.ArenaAllocator.init(alloc); defer arena.deinit(); return prettyPrint(arena.allocator(), config.keybind); } else { try config.keybind.formatEntryDocs( configpkg.entryFormatter("keybind", writer), opts.docs, ); }
// Don't forget to flush! try writer.flush(); return 0;}
const TriggerNode = struct { data: Binding.Trigger, node: std.SinglyLinkedList.Node = .{},
pub fn get(node: *std.SinglyLinkedList.Node) *TriggerNode { return @fieldParentPtr("node", node); }};
const ChordBinding = struct { table_name: ?[]const u8 = null, triggers: std.SinglyLinkedList, actions: []const Binding.Action,
// Order keybinds based on various properties // 1. Default bindings before table bindings (tables grouped at end) // 2. Longest chord sequence // 3. Most active modifiers // 4. Alphabetically by active modifiers // 5. Trigger key order // 6. Within tables, sort by table name // These properties propagate through chorded keypresses // // Adapted from Binding.lessThan pub fn lessThan(_: void, lhs: ChordBinding, rhs: ChordBinding) bool { const lhs_has_table = lhs.table_name != null; const rhs_has_table = rhs.table_name != null;
if (lhs_has_table != rhs_has_table) { return !lhs_has_table; }
if (lhs_has_table) { const table_cmp = std.mem.order(u8, lhs.table_name.?, rhs.table_name.?); if (table_cmp != .eq) { return table_cmp == .lt; } }
const lhs_len = lhs.triggers.len(); const rhs_len = rhs.triggers.len();
std.debug.assert(lhs_len != 0); std.debug.assert(rhs_len != 0);
if (lhs_len != rhs_len) { return lhs_len > rhs_len; }
const lhs_count: usize = blk: { var count: usize = 0; var maybe_trigger = lhs.triggers.first; while (maybe_trigger) |node| : (maybe_trigger = node.next) { const trigger: *TriggerNode = .get(node); if (trigger.data.mods.super) count += 1; if (trigger.data.mods.ctrl) count += 1; if (trigger.data.mods.shift) count += 1; if (trigger.data.mods.alt) count += 1; } break :blk count; }; const rhs_count: usize = blk: { var count: usize = 0; var maybe_trigger = rhs.triggers.first; while (maybe_trigger) |node| : (maybe_trigger = node.next) { const trigger: *TriggerNode = .get(node); if (trigger.data.mods.super) count += 1; if (trigger.data.mods.ctrl) count += 1; if (trigger.data.mods.shift) count += 1; if (trigger.data.mods.alt) count += 1; }
break :blk count; };
if (lhs_count != rhs_count) return lhs_count > rhs_count;
{ var l_trigger = lhs.triggers.first; var r_trigger = rhs.triggers.first; while (l_trigger != null and r_trigger != null) { const l_int = TriggerNode.get(l_trigger.?).data.mods.int(); const r_int = TriggerNode.get(r_trigger.?).data.mods.int();
if (l_int != r_int) { return l_int > r_int; }
l_trigger = l_trigger.?.next; r_trigger = r_trigger.?.next; } }
var l_trigger = lhs.triggers.first; var r_trigger = rhs.triggers.first;
while (l_trigger != null and r_trigger != null) { // We want catch_all to sort last. const lhs_key: c_int = blk: { switch (TriggerNode.get(l_trigger.?).data.key) { .physical => |key| break :blk @intFromEnum(key), .unicode => |key| break :blk @intCast(key), .catch_all => break :blk std.math.maxInt(c_int), } }; const rhs_key: c_int = blk: { switch (TriggerNode.get(r_trigger.?).data.key) { .physical => |key| break :blk @intFromEnum(key), .unicode => |key| break :blk @intCast(key), .catch_all => break :blk std.math.maxInt(c_int), } };
l_trigger = l_trigger.?.next; r_trigger = r_trigger.?.next;
if (l_trigger == null or r_trigger == null) { return lhs_key < rhs_key; }
if (lhs_key != rhs_key) { return lhs_key < rhs_key; } }
// The previous loop will always return something on its final iteration so we cannot // reach this point unreachable; }};
fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 { // Set up vaxis var buf: [1024]u8 = undefined; var tty = try vaxis.Tty.init(&buf); defer tty.deinit(); var vx = try vaxis.init(alloc, .{}); const writer = tty.writer(); defer vx.deinit(alloc, writer);
// We know we are ghostty, so let's enable mode 2027. Vaxis normally does this but you need an // event loop to auto-enable it. vx.caps.unicode = .unicode; try writer.writeAll(vaxis.ctlseqs.unicode_set); defer writer.writeAll(vaxis.ctlseqs.unicode_reset) catch {};
const winsize: vaxis.Winsize = switch (builtin.os.tag) { // We use some default, it doesn't really matter for what // we're doing because we don't do any wrapping. .windows => .{ .rows = 24, .cols = 120, .x_pixel = 1024, .y_pixel = 768, },
else => try vaxis.Tty.getWinsize(tty.fd), }; try vx.resize(alloc, writer, winsize);
const win = vx.window();
// Collect default bindings, recursively flattening chords var iter = keybinds.set.bindings.iterator(); const default_bindings, var widest_chord = try iterateBindings(alloc, &iter, &win);
var bindings_list: std.ArrayList(ChordBinding) = .empty; try bindings_list.appendSlice(alloc, default_bindings);
// Collect key table bindings var widest_table_prefix: u16 = 0; var table_iter = keybinds.tables.iterator(); while (table_iter.next()) |table_entry| { const table_name = table_entry.key_ptr.*; var binding_iter = table_entry.value_ptr.bindings.iterator(); const table_bindings, const table_width = try iterateBindings(alloc, &binding_iter, &win); for (table_bindings) |*b| { b.table_name = table_name; }
try bindings_list.appendSlice(alloc, table_bindings); widest_chord = @max(widest_chord, table_width); widest_table_prefix = @max(widest_table_prefix, @as(u16, @intCast(win.gwidth(table_name) + win.gwidth("/")))); }
const bindings = bindings_list.items; std.mem.sort(ChordBinding, bindings, {}, ChordBinding.lessThan);
// Set up styles for each modifier const super_style: vaxis.Style = .{ .fg = .{ .index = 1 } }; const ctrl_style: vaxis.Style = .{ .fg = .{ .index = 2 } }; const alt_style: vaxis.Style = .{ .fg = .{ .index = 3 } }; const shift_style: vaxis.Style = .{ .fg = .{ .index = 4 } }; const table_style: vaxis.Style = .{ .fg = .{ .index = 8 } };
// Print the list for (bindings) |bind| { win.clear();
var result: vaxis.Window.PrintResult = .{ .col = 0, .row = 0, .overflow = false };
if (bind.table_name) |name| { result = win.printSegment( .{ .text = name, .style = table_style }, .{ .col_offset = result.col }, ); result = win.printSegment(.{ .text = "/", .style = table_style }, .{ .col_offset = result.col }); }
var maybe_trigger = bind.triggers.first; while (maybe_trigger) |node| : (maybe_trigger = node.next) { const trigger: *TriggerNode = .get(node);
if (trigger.data.mods.super) { result = win.printSegment(.{ .text = "super", .style = super_style }, .{ .col_offset = result.col }); result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col }); } if (trigger.data.mods.ctrl) { result = win.printSegment(.{ .text = "ctrl ", .style = ctrl_style }, .{ .col_offset = result.col }); result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col }); } if (trigger.data.mods.alt) { result = win.printSegment(.{ .text = "alt ", .style = alt_style }, .{ .col_offset = result.col }); result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col }); } if (trigger.data.mods.shift) { result = win.printSegment(.{ .text = "shift", .style = shift_style }, .{ .col_offset = result.col }); result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col }); } const key = switch (trigger.data.key) { .physical => |k| try std.fmt.allocPrint(alloc, "{t}", .{k}), .unicode => |c| try std.fmt.allocPrint(alloc, "{u}", .{c}), .catch_all => "catch_all", }; result = win.printSegment(.{ .text = key }, .{ .col_offset = result.col });
// Print a separator between chorded keys if (trigger.node.next != null) { result = win.printSegment(.{ .text = " > ", .style = .{ .bold = true, .fg = .{ .index = 6 } } }, .{ .col_offset = result.col }); } }
var action_col: u16 = widest_table_prefix + widest_chord + 3; for (bind.actions, 0..) |act, i| { if (i > 0) { const chain_result = win.printSegment( .{ .text = ", ", .style = .{ .dim = true } }, .{ .col_offset = action_col }, ); action_col = chain_result.col; }
const action = try std.fmt.allocPrint(alloc, "{f}", .{act}); // If our action has an argument, we print the argument in a different color if (std.mem.indexOfScalar(u8, action, ':')) |idx| { const print_result = win.print(&.{ .{ .text = action[0..idx] }, .{ .text = action[idx .. idx + 1], .style = .{ .dim = true } }, .{ .text = action[idx + 1 ..], .style = .{ .fg = .{ .index = 5 } } }, }, .{ .col_offset = action_col }); action_col = print_result.col; } else { const print_result = win.printSegment( .{ .text = action }, .{ .col_offset = action_col }, ); action_col = print_result.col; } } try vx.prettyPrint(writer); } try writer.flush(); return 0;}
fn iterateBindings( alloc: Allocator, iter: anytype, win: *const vaxis.Window,) !struct { []ChordBinding, u16 } { var widest_chord: u16 = 0; var bindings: std.ArrayList(ChordBinding) = .empty; while (iter.next()) |bind| { const width = blk: { var buf: std.Io.Writer.Allocating = .init(alloc); const t = bind.key_ptr.*;
if (t.mods.super) try buf.writer.print("super + ", .{}); if (t.mods.ctrl) try buf.writer.print("ctrl + ", .{}); if (t.mods.alt) try buf.writer.print("alt + ", .{}); if (t.mods.shift) try buf.writer.print("shift + ", .{});
switch (t.key) { .physical => |k| try buf.writer.print("{t}", .{k}), .unicode => |c| try buf.writer.print("{u}", .{c}), .catch_all => try buf.writer.print("catch_all", .{}), }
break :blk win.gwidth(buf.written()); };
switch (bind.value_ptr.*) { .leader => |leader| { // Recursively iterate on the set of bindings for this leader key var n_iter = leader.bindings.iterator(); const sub_bindings, const max_width = try iterateBindings(alloc, &n_iter, win);
// Prepend the current keybind onto the list of sub-binds for (sub_bindings) |*nb| { const prepend_node = try alloc.create(TriggerNode); prepend_node.* = .{ .data = bind.key_ptr.* }; nb.triggers.prepend(&prepend_node.node); }
// Add the longest sub-bind width to the current bind width along with a padding // of 5 for the ' > ' spacer widest_chord = @max(widest_chord, width + max_width + 5); try bindings.appendSlice(alloc, sub_bindings); }, .leaf => |leaf| { const node = try alloc.create(TriggerNode); node.* = .{ .data = bind.key_ptr.* };
const actions = try alloc.alloc(Binding.Action, 1); actions[0] = leaf.action;
widest_chord = @max(widest_chord, width); try bindings.append(alloc, .{ .triggers = .{ .first = &node.node }, .actions = actions, }); }, .leaf_chained => |leaf| { const node = try alloc.create(TriggerNode); node.* = .{ .data = bind.key_ptr.* };
widest_chord = @max(widest_chord, width); try bindings.append(alloc, .{ .triggers = .{ .first = &node.node }, .actions = leaf.actions.items, }); }, } }
return .{ try bindings.toOwnedSlice(alloc), widest_chord };}