/ de-omarchy / bin / omarchy-menu-herdr-keybindings
swadhinbiswas Initial de-omarchy: Omarchy UI layer for Arch-based distros · 8/22/2026
9f393b2
232 lines · 5.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
#!/bin/bash

# omarchy:summary=Display annotated Herdr keybindings using an interactive search menu.
# omarchy:args=[--print|-p] [--config <path>]

print_only=false
config_file="${HERDR_CONFIG_PATH:-${XDG_CONFIG_HOME:-$HOME/.config}/herdr/config.toml}"

while (($#)); do
  case "$1" in
    --print|-p)
      print_only=true
      ;;
    --config)
      shift
      config_file="$1"
      ;;
    *)
      config_file="$1"
      ;;
  esac

  shift
done

# With no config file, Herdr runs on its own built-in defaults. Substituting the
# Omarchy seed config here would describe bindings Herdr never loaded.
[[ -f $config_file ]] || config_file="/dev/null"

# Herdr has no CLI that dumps resolved keybindings, so the action list and its
# defaults come from `herdr --default-config`, where every action appears as a
# commented `# action = "binding"` line, and the user config overrides those.
output_keybindings() (
  set -o pipefail
  herdr --default-config | awk -v config_file="$config_file" '
# TOML literal strings are as valid as basic ones, so both quote characters
# open and close a value here.
function strip_comment(value,   i, ch, quote, out) {
  quote = ""
  out = ""

  for (i = 1; i <= length(value); i++) {
    ch = substr(value, i, 1)

    if (quote == "" && (ch == "\"" || ch == "'\''")) quote = ch
    else if (ch == quote) quote = ""
    if (ch == "#" && quote == "") break

    out = out ch
  }

  return out
}

# Removes every quoted run, leaving only what surrounds the strings. An
# unterminated quote returns a sentinel no valid value can produce.
function without_quoted(value,   i, ch, quote, out) {
  quote = ""
  out = ""

  for (i = 1; i <= length(value); i++) {
    ch = substr(value, i, 1)

    if (quote == "") {
      if (ch == "\"" || ch == "'"'"'") quote = ch
      else out = out ch
    } else if (ch == quote) {
      quote = ""
    }
  }

  return quote == "" ? out : "\001"
}

# Prose in the default config can read like an assignment, as in
# `# type = "popup" opens a session-modal terminal`, so a value counts only when
# it is a bare string or an array of them.
function is_binding_value(value,   rest) {
  value = strip_comment(value)
  sub(/^[[:space:]]+/, "", value)
  sub(/[[:space:]]+$/, "", value)
  if (value == "") return 0

  rest = without_quoted(value)

  # A bare string leaves nothing behind; an array leaves only its brackets,
  # commas, and whitespace.
  return rest == "" || rest ~ /^\[[[:space:],]*\]$/
}

function key_text(key,   count, parts, i, text) {
  count = split(key, parts, "+")
  text = ""

  for (i = 1; i <= count; i++) {
    text = text (text == "" ? "" : " + ") toupper(parts[i])
  }

  return text
}

# Renders every alternate binding for one action as "A / B".
function combo_text(value,   i, ch, quote, part, text) {
  value = strip_comment(value)
  quote = ""
  part = ""
  text = ""

  for (i = 1; i <= length(value); i++) {
    ch = substr(value, i, 1)

    if (quote == "") {
      if (ch == "\"" || ch == "'"'"'") { quote = ch; part = "" }
      continue
    }

    if (ch == quote) {
      quote = ""
      if (part != "") text = text (text == "" ? "" : " / ") key_text(part)
      continue
    }

    part = part ch
  }

  return text
}

function describe(action,   text) {
  sub(/^navigate_/, "", action)
  gsub(/_/, " ", action)
  text = toupper(substr(action, 1, 1)) substr(action, 2)

  return text
}

function remember(action) {
  if (action in seen) return
  seen[action] = 1
  order[++count] = action
}

function bind(action, value,   combo) {
  combo = combo_text(value)
  if (combo == "") return

  if (action ~ /^navigate_/) combo = "NAVIGATE + " combo
  remember(action)
  combos[action] = combo
}

# Pass 1: default config on stdin, for the action order and default bindings.
# Every line there is commented out, so section markers are too.
FNR == NR {
  line = $0
  sub(/^[[:space:]]*#[[:space:]]*/, "", line)

  if (line ~ /^\[\[keys\.command\]\]/) { in_keys = 0; next }
  if (line ~ /^\[/) { in_keys = (line ~ /^\[keys\]/); next }
  if (!in_keys) next
  if (line !~ /^[a-z_]+[[:space:]]*=/) next

  action = line
  sub(/[[:space:]]*=.*$/, "", action)
  value = line
  sub(/^[^=]*=[[:space:]]*/, "", value)
  if (!is_binding_value(value)) next

  # Actions Herdr leaves unbound still hold their place in the listing order,
  # for when the user config binds them.
  remember(action)
  bind(action, value)
  next
}

# Pass 2: the user config, which overrides the defaults it sets.
{
  if ($0 ~ /^\[\[keys\.command\]\]/) { in_user_keys = 0; next }
  if ($0 ~ /^\[/) { in_user_keys = ($0 ~ /^\[keys\]/); next }
  if (!in_user_keys) next
  if ($0 ~ /^[[:space:]]*#/) next
  if ($0 !~ /^[[:space:]]*[a-z_]+[[:space:]]*=/) next

  line = $0
  sub(/^[[:space:]]*/, "", line)
  action = line
  sub(/[[:space:]]*=.*$/, "", action)
  value = line
  sub(/^[^=]*=[[:space:]]*/, "", value)
  if (!is_binding_value(value)) next

  # An action bound to nothing in the user config is unbound, not defaulted.
  if (combo_text(value) == "") {
    delete combos[action]
    next
  }

  bind(action, value)
}

END {
  if ("prefix" in combos) {
    printf "%-32s → %s\n", "PREFIX", combos["prefix"]
  }

  for (i = 1; i <= count; i++) {
    action = order[i]
    if (action == "prefix") continue
    if (!(action in combos)) continue

    printf "%-32s → %s\n", combos[action], describe(action)
  }
}
' - "$config_file"
)

if [[ $print_only == "true" ]]; then
  output_keybindings
  exit 0
fi

records=$(output_keybindings)

monitor_height=$(hyprctl monitors -j 2>/dev/null | jq -r '.[] | select(.focused == true) | .height' 2>/dev/null)

if [[ ! $monitor_height =~ ^[0-9]+$ ]] || ((monitor_height <= 0)); then
  monitor_height=900
fi

menu_height=$((monitor_height * 40 / 100))
printf '%s\n' "$records" | omarchy-menu-select 'Herdr keybindings' -- --width 800 --height "$menu_height" >/dev/null || true