From 1f192dbba4018eab5ec732fb29f66f6f0c76d892 Mon Sep 17 00:00:00 2001 From: Christopher Boone Date: Sun, 8 Feb 2026 21:07:57 -0500 Subject: [PATCH 1/2] feat: sort bindings by key text within each group Sort bindings alphabetically by binding text (case-sensitive) using insertion sort in the AWK parser. Set LC_ALL=C for consistent byte-order comparison regardless of the user's locale. --- scripts/parse-bindings.awk | 22 ++++++++++++++++++++++ scripts/popup.sh | 2 +- tests/parse-bindings.scrut | 16 ++++++++-------- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/scripts/parse-bindings.awk b/scripts/parse-bindings.awk index e8b52ed..4272cc4 100755 --- a/scripts/parse-bindings.awk +++ b/scripts/parse-bindings.awk @@ -166,11 +166,32 @@ function is_mouse_key(k, bare) { return (bare ~ /^(Mouse|Wheel|DoubleClick|TripleClick)/) } +# Insertion sort: order bindings within a table by key text (case-sensitive). +function sort_bindings(t, i, j, n, tmp_key, tmp_cmd, a, b) { + n = count[t] + for (i = 2; i <= n; i++) { + tmp_key = keys[t, i] + tmp_cmd = cmds[t, i] + a = tmp_key + j = i - 1 + while (j >= 1) { + b = keys[t, j] + if (b <= a) break + keys[t, j + 1] = keys[t, j] + cmds[t, j + 1] = cmds[t, j] + j-- + } + keys[t, j + 1] = tmp_key + cmds[t, j + 1] = tmp_cmd + } +} + END { for (i = 1; i <= order_count; i++) { t = order_arr[i] if (t ~ /^mouse:/) continue if (count[t] == 0) continue + sort_bindings(t) label = table_label[t] if (label == "") label = t printf "GROUP\t%s (%d)\n", label, count[t] @@ -181,6 +202,7 @@ END { t = order_arr[i] if (t !~ /^mouse:/) continue if (count[t] == 0) continue + sort_bindings(t) label = table_label[t] if (label == "") label = t printf "GROUP\t%s (%d)\n", label, count[t] diff --git a/scripts/popup.sh b/scripts/popup.sh index 1d519f9..b9b1f26 100755 --- a/scripts/popup.sh +++ b/scripts/popup.sh @@ -7,7 +7,7 @@ CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" main() { local data - data="$(tmux list-keys | awk -f "$CURRENT_DIR/parse-bindings.awk")" + data="$(tmux list-keys | LC_ALL=C awk -f "$CURRENT_DIR/parse-bindings.awk")" if [[ -z "$data" ]]; then tmux display-message "tmux-binding-help: no bindings found" diff --git a/tests/parse-bindings.scrut b/tests/parse-bindings.scrut index 4aa7bcf..15f8a08 100644 --- a/tests/parse-bindings.scrut +++ b/tests/parse-bindings.scrut @@ -4,10 +4,10 @@ $ printf '%s\n' \ > 'bind-key -T prefix c new-window' \ > 'bind-key -r -T prefix Up select-pane -U' \ -> | awk -f scripts/parse-bindings.awk | tr '\t' '|' +> | LC_ALL=C awk -f scripts/parse-bindings.awk | tr '\t' '|' GROUP|Prefix (2) -BIND|c|new-window BIND|Up (repeat)|select-pane -U +BIND|c|new-window ``` # Handles -N and -F options before key @@ -16,7 +16,7 @@ BIND|Up (repeat)|select-pane -U $ printf '%s\n' \ > 'bind-key -N "pane split" -T prefix % split-window -h' \ > 'bind-key -F "#{pane_id}" -T root MouseDown1Pane select-pane -t = && display-menu -T "Pane #{pane_index}"' \ -> | awk -f scripts/parse-bindings.awk | tr '\t' '|' +> | LC_ALL=C awk -f scripts/parse-bindings.awk | tr '\t' '|' GROUP|Prefix (1) BIND|%|split-window -h GROUP|Mouse (root) (1) @@ -30,11 +30,11 @@ $ printf '%s\n' \ > 'bind-key -T prefix C-\\ display-message hi' \ > 'bind-key -T prefix \# display-message hash' \ > 'bind-key -T prefix \; display-message semi' \ -> | awk -f scripts/parse-bindings.awk | tr '\t' '|' +> | LC_ALL=C awk -f scripts/parse-bindings.awk | tr '\t' '|' GROUP|Prefix (3) -BIND|C-\|display-message hi BIND|#|display-message hash BIND|;|display-message semi +BIND|C-\|display-message hi ``` # Appends custom tables after built-in order @@ -44,7 +44,7 @@ $ printf '%s\n' \ > 'bind-key -T custom-table x display-message custom' \ > 'bind-key -T root c new-window' \ > 'bind-key -T prefix n next-window' \ -> | awk -f scripts/parse-bindings.awk | tr '\t' '|' +> | LC_ALL=C awk -f scripts/parse-bindings.awk | tr '\t' '|' GROUP|Prefix (1) BIND|n|next-window GROUP|Root (no prefix) (1) @@ -63,15 +63,15 @@ $ printf '%s\n' \ > 'bind-key -T copy-mode-vi MouseDrag1Pane begin-selection' \ > 'bind-key -T copy-mode-vi v send-keys -X begin-selection' \ > 'bind-key -T root M-DoubleClick1Pane resize-pane -Z' \ -> | awk -f scripts/parse-bindings.awk | tr '\t' '|' +> | LC_ALL=C awk -f scripts/parse-bindings.awk | tr '\t' '|' GROUP|Root (no prefix) (1) BIND|n|next-window GROUP|Copy Mode (vi) (1) BIND|v|send-keys -X begin-selection GROUP|Mouse (root) (3) +BIND|M-DoubleClick1Pane|resize-pane -Z BIND|MouseDown1Pane|select-pane BIND|WheelUpPane|scroll-up -BIND|M-DoubleClick1Pane|resize-pane -Z GROUP|Mouse (copy-mode-vi) (1) BIND|MouseDrag1Pane|begin-selection ``` From f097ebf21fd71d20e7a6bd7d145a4791b7fae65d Mon Sep 17 00:00:00 2001 From: Christopher Boone Date: Sun, 8 Feb 2026 21:32:15 -0500 Subject: [PATCH 2/2] docs: add Copilot instruction for awk string comparison in sort --- .github/awk.instructions.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/awk.instructions.md b/.github/awk.instructions.md index 972e315..d657556 100644 --- a/.github/awk.instructions.md +++ b/.github/awk.instructions.md @@ -3,3 +3,4 @@ applyTo: "**/*.awk" --- - **Virtual `mouse:` table prefix**: The `mouse:` prefix on table names is an internal routing convention within the AWK parser to separate mouse bindings from keyboard bindings. These are virtual names that never leave the script -- tmux itself never sees them. Tmux key table names cannot contain colons, so there is no collision risk with real tables. Do not flag this prefix as a potential namespace collision. +- **String comparison in sort**: The `sort_bindings` function uses `<=` for lexicographic comparison. Key values stored via `keys[table, idx] = key repeat` are string-typed because the concatenation with the `repeat` variable converts strnum field values to strings. Do not flag this as a numeric comparison issue.