mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-13 23:36:09 +03:00
cli/list-keybinds: output chorded keybinds (#5357)
Print chorded/sequenced keybinds in `+list-keybinds`. Recursively traverses the binding sets of sequenced keybinds and builds a singly-linked list of triggers for each leaf. Also adapted the current sorting criteria to work for multiple triggers per keybind. Chorded keybinds are already output when not printing to a tty so that code path is unchanged. Closes #4505
This commit is contained in:
@ -68,7 +68,9 @@ pub fn run(alloc: Allocator) !u8 {
|
|||||||
|
|
||||||
// Despite being under the posix namespace, this also works on Windows as of zig 0.13.0
|
// Despite being under the posix namespace, this also works on Windows as of zig 0.13.0
|
||||||
if (tui.can_pretty_print and !opts.plain and std.posix.isatty(stdout.handle)) {
|
if (tui.can_pretty_print and !opts.plain and std.posix.isatty(stdout.handle)) {
|
||||||
return prettyPrint(alloc, config.keybind);
|
var arena = std.heap.ArenaAllocator.init(alloc);
|
||||||
|
defer arena.deinit();
|
||||||
|
return prettyPrint(arena.allocator(), config.keybind);
|
||||||
} else {
|
} else {
|
||||||
try config.keybind.formatEntryDocs(
|
try config.keybind.formatEntryDocs(
|
||||||
configpkg.entryFormatter("keybind", stdout.writer()),
|
configpkg.entryFormatter("keybind", stdout.writer()),
|
||||||
@ -79,6 +81,111 @@ pub fn run(alloc: Allocator) !u8 {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TriggerList = std.SinglyLinkedList(Binding.Trigger);
|
||||||
|
|
||||||
|
const ChordBinding = struct {
|
||||||
|
triggers: TriggerList,
|
||||||
|
action: Binding.Action,
|
||||||
|
|
||||||
|
// Order keybinds based on various properties
|
||||||
|
// 1. Longest chord sequence
|
||||||
|
// 2. Most active modifiers
|
||||||
|
// 3. Alphabetically by active modifiers
|
||||||
|
// 4. Trigger key order
|
||||||
|
// These properties propagate through chorded keypresses
|
||||||
|
//
|
||||||
|
// Adapted from Binding.lessThan
|
||||||
|
pub fn lessThan(_: void, lhs: ChordBinding, rhs: ChordBinding) bool {
|
||||||
|
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) |trigger| : (maybe_trigger = trigger.next) {
|
||||||
|
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) |trigger| : (maybe_trigger = trigger.next) {
|
||||||
|
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 = l_trigger.?.data.mods.int();
|
||||||
|
const r_int = 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) {
|
||||||
|
const lhs_key: c_int = blk: {
|
||||||
|
switch (l_trigger.?.data.key) {
|
||||||
|
.translated => |key| break :blk @intFromEnum(key),
|
||||||
|
.physical => |key| break :blk @intFromEnum(key),
|
||||||
|
.unicode => |key| break :blk @intCast(key),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const rhs_key: c_int = blk: {
|
||||||
|
switch (r_trigger.?.data.key) {
|
||||||
|
.translated => |key| break :blk @intFromEnum(key),
|
||||||
|
.physical => |key| break :blk @intFromEnum(key),
|
||||||
|
.unicode => |key| break :blk @intCast(key),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 {
|
fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 {
|
||||||
// Set up vaxis
|
// Set up vaxis
|
||||||
var tty = try vaxis.Tty.init();
|
var tty = try vaxis.Tty.init();
|
||||||
@ -111,26 +218,11 @@ fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 {
|
|||||||
|
|
||||||
const win = vx.window();
|
const win = vx.window();
|
||||||
|
|
||||||
// Get all of our keybinds into a list. We also search for the longest printed keyname so we can
|
// Generate a list of bindings, recursively traversing chorded keybindings
|
||||||
// align things nicely
|
|
||||||
var iter = keybinds.set.bindings.iterator();
|
var iter = keybinds.set.bindings.iterator();
|
||||||
var bindings = std.ArrayList(Binding).init(alloc);
|
const bindings, const widest_chord = try iterateBindings(alloc, &iter, &win);
|
||||||
var widest_key: u16 = 0;
|
|
||||||
var buf: [64]u8 = undefined;
|
std.mem.sort(ChordBinding, bindings, {}, ChordBinding.lessThan);
|
||||||
while (iter.next()) |bind| {
|
|
||||||
const action = switch (bind.value_ptr.*) {
|
|
||||||
.leader => continue, // TODO: support this
|
|
||||||
.leaf => |leaf| leaf.action,
|
|
||||||
};
|
|
||||||
const key = switch (bind.key_ptr.key) {
|
|
||||||
.translated => |k| try std.fmt.bufPrint(&buf, "{s}", .{@tagName(k)}),
|
|
||||||
.physical => |k| try std.fmt.bufPrint(&buf, "physical:{s}", .{@tagName(k)}),
|
|
||||||
.unicode => |c| try std.fmt.bufPrint(&buf, "{u}", .{c}),
|
|
||||||
};
|
|
||||||
widest_key = @max(widest_key, win.gwidth(key));
|
|
||||||
try bindings.append(.{ .trigger = bind.key_ptr.*, .action = action });
|
|
||||||
}
|
|
||||||
std.mem.sort(Binding, bindings.items, {}, Binding.lessThan);
|
|
||||||
|
|
||||||
// Set up styles for each modifier
|
// Set up styles for each modifier
|
||||||
const super_style: vaxis.Style = .{ .fg = .{ .index = 1 } };
|
const super_style: vaxis.Style = .{ .fg = .{ .index = 1 } };
|
||||||
@ -138,41 +230,41 @@ fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 {
|
|||||||
const alt_style: vaxis.Style = .{ .fg = .{ .index = 3 } };
|
const alt_style: vaxis.Style = .{ .fg = .{ .index = 3 } };
|
||||||
const shift_style: vaxis.Style = .{ .fg = .{ .index = 4 } };
|
const shift_style: vaxis.Style = .{ .fg = .{ .index = 4 } };
|
||||||
|
|
||||||
var longest_col: u16 = 0;
|
|
||||||
|
|
||||||
// Print the list
|
// Print the list
|
||||||
for (bindings.items) |bind| {
|
for (bindings) |bind| {
|
||||||
win.clear();
|
win.clear();
|
||||||
|
|
||||||
var result: vaxis.Window.PrintResult = .{ .col = 0, .row = 0, .overflow = false };
|
var result: vaxis.Window.PrintResult = .{ .col = 0, .row = 0, .overflow = false };
|
||||||
const trigger = bind.trigger;
|
var maybe_trigger = bind.triggers.first;
|
||||||
if (trigger.mods.super) {
|
while (maybe_trigger) |trigger| : (maybe_trigger = trigger.next) {
|
||||||
result = win.printSegment(.{ .text = "super", .style = super_style }, .{ .col_offset = result.col });
|
if (trigger.data.mods.super) {
|
||||||
result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
|
result = win.printSegment(.{ .text = "super", .style = super_style }, .{ .col_offset = result.col });
|
||||||
}
|
result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
|
||||||
if (trigger.mods.ctrl) {
|
}
|
||||||
result = win.printSegment(.{ .text = "ctrl ", .style = ctrl_style }, .{ .col_offset = result.col });
|
if (trigger.data.mods.ctrl) {
|
||||||
result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
|
result = win.printSegment(.{ .text = "ctrl ", .style = ctrl_style }, .{ .col_offset = result.col });
|
||||||
}
|
result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
|
||||||
if (trigger.mods.alt) {
|
}
|
||||||
result = win.printSegment(.{ .text = "alt ", .style = alt_style }, .{ .col_offset = result.col });
|
if (trigger.data.mods.alt) {
|
||||||
result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
|
result = win.printSegment(.{ .text = "alt ", .style = alt_style }, .{ .col_offset = result.col });
|
||||||
}
|
result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
|
||||||
if (trigger.mods.shift) {
|
}
|
||||||
result = win.printSegment(.{ .text = "shift", .style = shift_style }, .{ .col_offset = result.col });
|
if (trigger.data.mods.shift) {
|
||||||
result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
|
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) {
|
||||||
|
.translated => |k| try std.fmt.allocPrint(alloc, "{s}", .{@tagName(k)}),
|
||||||
|
.physical => |k| try std.fmt.allocPrint(alloc, "physical:{s}", .{@tagName(k)}),
|
||||||
|
.unicode => |c| try std.fmt.allocPrint(alloc, "{u}", .{c}),
|
||||||
|
};
|
||||||
|
result = win.printSegment(.{ .text = key }, .{ .col_offset = result.col });
|
||||||
|
|
||||||
const key = switch (trigger.key) {
|
// Print a separator between chorded keys
|
||||||
.translated => |k| try std.fmt.allocPrint(alloc, "{s}", .{@tagName(k)}),
|
if (trigger.next != null) {
|
||||||
.physical => |k| try std.fmt.allocPrint(alloc, "physical:{s}", .{@tagName(k)}),
|
result = win.printSegment(.{ .text = " > ", .style = .{ .bold = true, .fg = .{ .index = 6 } } }, .{ .col_offset = result.col });
|
||||||
.unicode => |c| try std.fmt.allocPrint(alloc, "{u}", .{c}),
|
}
|
||||||
};
|
}
|
||||||
// We don't track the key print because we index the action off the *widest* key so we get
|
|
||||||
// nice alignment no matter what was printed for mods
|
|
||||||
_ = win.printSegment(.{ .text = key }, .{ .col_offset = result.col });
|
|
||||||
|
|
||||||
if (longest_col < result.col) longest_col = result.col;
|
|
||||||
|
|
||||||
const action = try std.fmt.allocPrint(alloc, "{}", .{bind.action});
|
const action = try std.fmt.allocPrint(alloc, "{}", .{bind.action});
|
||||||
// If our action has an argument, we print the argument in a different color
|
// If our action has an argument, we print the argument in a different color
|
||||||
@ -181,12 +273,69 @@ fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 {
|
|||||||
.{ .text = action[0..idx] },
|
.{ .text = action[0..idx] },
|
||||||
.{ .text = action[idx .. idx + 1], .style = .{ .dim = true } },
|
.{ .text = action[idx .. idx + 1], .style = .{ .dim = true } },
|
||||||
.{ .text = action[idx + 1 ..], .style = .{ .fg = .{ .index = 5 } } },
|
.{ .text = action[idx + 1 ..], .style = .{ .fg = .{ .index = 5 } } },
|
||||||
}, .{ .col_offset = longest_col + widest_key + 2 });
|
}, .{ .col_offset = widest_chord + 3 });
|
||||||
} else {
|
} else {
|
||||||
_ = win.printSegment(.{ .text = action }, .{ .col_offset = longest_col + widest_key + 2 });
|
_ = win.printSegment(.{ .text = action }, .{ .col_offset = widest_chord + 3 });
|
||||||
}
|
}
|
||||||
try vx.prettyPrint(writer);
|
try vx.prettyPrint(writer);
|
||||||
}
|
}
|
||||||
try buf_writer.flush();
|
try buf_writer.flush();
|
||||||
return 0;
|
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).init(alloc);
|
||||||
|
while (iter.next()) |bind| {
|
||||||
|
const width = blk: {
|
||||||
|
var buf = std.ArrayList(u8).init(alloc);
|
||||||
|
const t = bind.key_ptr.*;
|
||||||
|
|
||||||
|
if (t.mods.super) try std.fmt.format(buf.writer(), "super + ", .{});
|
||||||
|
if (t.mods.ctrl) try std.fmt.format(buf.writer(), "ctrl + ", .{});
|
||||||
|
if (t.mods.alt) try std.fmt.format(buf.writer(), "alt + ", .{});
|
||||||
|
if (t.mods.shift) try std.fmt.format(buf.writer(), "shift + ", .{});
|
||||||
|
|
||||||
|
switch (t.key) {
|
||||||
|
.translated => |k| try std.fmt.format(buf.writer(), "{s}", .{@tagName(k)}),
|
||||||
|
.physical => |k| try std.fmt.format(buf.writer(), "physical:{s}", .{@tagName(k)}),
|
||||||
|
.unicode => |c| try std.fmt.format(buf.writer(), "{u}", .{c}),
|
||||||
|
}
|
||||||
|
|
||||||
|
break :blk win.gwidth(buf.items);
|
||||||
|
};
|
||||||
|
|
||||||
|
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(TriggerList.Node);
|
||||||
|
prepend_node.* = TriggerList.Node{ .data = bind.key_ptr.* };
|
||||||
|
nb.triggers.prepend(prepend_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(sub_bindings);
|
||||||
|
},
|
||||||
|
.leaf => |leaf| {
|
||||||
|
const node = try alloc.create(TriggerList.Node);
|
||||||
|
node.* = TriggerList.Node{ .data = bind.key_ptr.* };
|
||||||
|
const triggers = TriggerList{
|
||||||
|
.first = node,
|
||||||
|
};
|
||||||
|
|
||||||
|
widest_chord = @max(widest_chord, width);
|
||||||
|
try bindings.append(.{ .triggers = triggers, .action = leaf.action });
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return .{ try bindings.toOwnedSlice(), widest_chord };
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user