diff --git a/src/cli/list_keybinds.zig b/src/cli/list_keybinds.zig index 13c69d9709..6cd989201c 100644 --- a/src/cli/list_keybinds.zig +++ b/src/cli/list_keybinds.zig @@ -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()), @@ -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(); @@ -111,26 +218,11 @@ 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 } }; @@ -138,41 +230,41 @@ fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 { 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 @@ -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 }; +}