aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/cli/list_keybinds.zig
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-04-04 19:22:04 +0300
committerCan Celik <ogulcancelik@gmail.com>2026-04-05 01:23:06 +0300
commit41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch)
tree3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/cli/list_keybinds.zig
parent55c138263e190a1e79d4b997bfb9397306c0ec1f (diff)
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/cli/list_keybinds.zig')
-rw-r--r--vendor/libghostty-vt/src/cli/list_keybinds.zig435
1 files changed, 435 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/cli/list_keybinds.zig b/vendor/libghostty-vt/src/cli/list_keybinds.zig
new file mode 100644
index 00000000..61050d0c
--- /dev/null
+++ b/vendor/libghostty-vt/src/cli/list_keybinds.zig
@@ -0,0 +1,435 @@
+const 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 };
+}