Skip to content

Commit

Permalink
cli/list-keybinds: output chorded keybinds (#5357)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
mitchellh authored Jan 24, 2025
2 parents 136d6e9 + 5ad2ec8 commit a88e301
Showing 1 changed file with 201 additions and 52 deletions.
253 changes: 201 additions & 52 deletions src/cli/list_keybinds.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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
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 {
try config.keybind.formatEntryDocs(
configpkg.entryFormatter("keybind", stdout.writer()),
Expand All @@ -79,6 +81,111 @@ pub fn run(alloc: Allocator) !u8 {
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 {
// Set up vaxis
var tty = try vaxis.Tty.init();
Expand Down Expand Up @@ -111,68 +218,53 @@ fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 {

const win = vx.window();

// Get all of our keybinds into a list. We also search for the longest printed keyname so we can
// align things nicely
// Generate a list of bindings, recursively traversing chorded keybindings
var iter = keybinds.set.bindings.iterator();
var bindings = std.ArrayList(Binding).init(alloc);
var widest_key: u16 = 0;
var buf: [64]u8 = undefined;
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);
const bindings, const widest_chord = try iterateBindings(alloc, &iter, &win);

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 } };

var longest_col: u16 = 0;

// Print the list
for (bindings.items) |bind| {
for (bindings) |bind| {
win.clear();

var result: vaxis.Window.PrintResult = .{ .col = 0, .row = 0, .overflow = false };
const trigger = bind.trigger;
if (trigger.mods.super) {
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 });
result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
}
if (trigger.mods.alt) {
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 });
result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col });
}

const key = switch (trigger.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}),
};
// 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 });
var maybe_trigger = bind.triggers.first;
while (maybe_trigger) |trigger| : (maybe_trigger = trigger.next) {
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) {
.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 });

if (longest_col < result.col) longest_col = result.col;
// Print a separator between chorded keys
if (trigger.next != null) {
result = win.printSegment(.{ .text = " > ", .style = .{ .bold = true, .fg = .{ .index = 6 } } }, .{ .col_offset = result.col });
}
}

const action = try std.fmt.allocPrint(alloc, "{}", .{bind.action});
// If our action has an argument, we print the argument in a different color
Expand All @@ -181,12 +273,69 @@ fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 {
.{ .text = action[0..idx] },
.{ .text = action[idx .. idx + 1], .style = .{ .dim = true } },
.{ .text = action[idx + 1 ..], .style = .{ .fg = .{ .index = 5 } } },
}, .{ .col_offset = longest_col + widest_key + 2 });
}, .{ .col_offset = widest_chord + 3 });
} 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 buf_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).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 };
}

0 comments on commit a88e301

Please sign in to comment.