From cb3fb6db1e52b5edd20edc98c024db78c73136d2 Mon Sep 17 00:00:00 2001 From: Mathias Scheider Date: Sun, 20 Sep 2026 21:40:14 +0200 Subject: [PATCH] hyprland: show keybinds --- hypr/.config/hypr/hyprland.lua | 120 +++++++++++--------- hypr/.config/hypr/scripts/show-keybinds.sh | 123 +++++++++++++++++++++ 2 files changed, 193 insertions(+), 50 deletions(-) create mode 100755 hypr/.config/hypr/scripts/show-keybinds.sh diff --git a/hypr/.config/hypr/hyprland.lua b/hypr/.config/hypr/hyprland.lua index a9c97fd..fe26c34 100644 --- a/hypr/.config/hypr/hyprland.lua +++ b/hypr/.config/hypr/hyprland.lua @@ -370,68 +370,70 @@ hl.device({ -- See https://wiki.hypr.land/Configuring/Keywords/ -- Example binds, see https://wiki.hypr.land/Configuring/Binds/ for more -hl.bind(var_mainMod .. " + RETURN", hl.dsp.exec_cmd(var_terminal)) -hl.bind(var_mainMod .. " + Q", hl.dsp.window.close()) -hl.bind(var_mainMod .. " + ESCAPE", hl.dsp.exec_cmd("hyprshutdown")) -hl.bind(var_mainMod .. " + SHIFT + ESCAPE", hl.dsp.exec_cmd("command -v hyprshutdown >/dev/null 2>&1 && hyprshutdown || hyprctl dispatch exit")) -hl.bind(var_mainMod .. " + CTRL + ESCAPE", hl.dsp.exec_cmd("hyprshutdown -t 'Shutting down...' --post-cmd 'shutdown -P 0'")) -hl.bind(var_mainMod .. " + ALT + ESCAPE", hl.dsp.exec_cmd("hyprshutdown -t 'Restarting...' --post-cmd 'reboot'")) -hl.bind(var_mainMod .. " + E", hl.dsp.exec_cmd(var_fileManager)) -hl.bind(var_mainMod .. " + V", hl.dsp.window.float({ action = "toggle" })) -hl.bind(var_mainMod .. " + SPACE", hl.dsp.exec_cmd(var_menu)) -hl.bind(var_mainMod .. " + SHIFT + SPACE", hl.dsp.exec_cmd("nwg-drawer")) -hl.bind(var_mainMod .. " + P", hl.dsp.window.pseudo()) -hl.bind(var_mainMod .. " + SHIFT + J", hl.dsp.layout("togglesplit")) -hl.bind(var_mainMod .. " + I", hl.dsp.exec_cmd("firefox &")) -hl.bind(var_mainMod .. " + n", hl.dsp.exec_cmd("~/.local/bin/hyprltm-net")) +hl.bind(var_mainMod .. " + RETURN", hl.dsp.exec_cmd(var_terminal), { description = "Open terminal (kitty)" }) +hl.bind(var_mainMod .. " + Q", hl.dsp.window.close(), { description = "Close focused window" }) +hl.bind(var_mainMod .. " + ESCAPE", hl.dsp.exec_cmd("hyprshutdown"), { description = "Shutdown Hyprland" }) +hl.bind(var_mainMod .. " + SHIFT + ESCAPE", hl.dsp.exec_cmd("command -v hyprshutdown >/dev/null 2>&1 && hyprshutdown || hyprctl dispatch exit"), { description = "Exit Hyprland" }) +hl.bind(var_mainMod .. " + CTRL + ESCAPE", hl.dsp.exec_cmd("hyprshutdown -t 'Shutting down...' --post-cmd 'shutdown -P 0'"), { description = "Power off the system" }) +hl.bind(var_mainMod .. " + ALT + ESCAPE", hl.dsp.exec_cmd("hyprshutdown -t 'Restarting...' --post-cmd 'reboot'"), { description = "Reboot the system" }) +hl.bind(var_mainMod .. " + E", hl.dsp.exec_cmd(var_fileManager), { description = "Open file manager (dolphin)" }) +hl.bind(var_mainMod .. " + V", hl.dsp.window.float({ action = "toggle" }), { description = "Toggle floating window" }) +hl.bind(var_mainMod .. " + SPACE", hl.dsp.exec_cmd(var_menu), { description = "Open app launcher (wofi)" }) +hl.bind(var_mainMod .. " + SHIFT + SPACE", hl.dsp.exec_cmd("nwg-drawer"), { description = "Open app drawer (nwg-drawer)" }) +hl.bind(var_mainMod .. " + P", hl.dsp.window.pseudo(), { description = "Toggle pseudo-tiling" }) +hl.bind(var_mainMod .. " + SHIFT + J", hl.dsp.layout("togglesplit"), { description = "Toggle split layout" }) +hl.bind(var_mainMod .. " + I", hl.dsp.exec_cmd("firefox &"), { description = "Open Firefox" }) +hl.bind(var_mainMod .. " + n", hl.dsp.exec_cmd("~/.local/bin/hyprltm-net"), { description = "Network manager (hyprltm)" }) -- Move focus with mainMod or Vim-style -hl.bind(var_mainMod .. " + CTRL + h", hl.dsp.focus({ direction = "left" })) -hl.bind(var_mainMod .. " + CTRL + l", hl.dsp.focus({ direction = "right" })) -hl.bind(var_mainMod .. " + CTRL + k", hl.dsp.focus({ direction = "up" })) -hl.bind(var_mainMod .. " + CTRL + j", hl.dsp.focus({ direction = "down" })) +hl.bind(var_mainMod .. " + CTRL + h", hl.dsp.focus({ direction = "left" }), { description = "Focus window left" }) +hl.bind(var_mainMod .. " + CTRL + l", hl.dsp.focus({ direction = "right" }), { description = "Focus window right" }) +hl.bind(var_mainMod .. " + CTRL + k", hl.dsp.focus({ direction = "up" }), { description = "Focus window up" }) +hl.bind(var_mainMod .. " + CTRL + j", hl.dsp.focus({ direction = "down" }), { description = "Focus window down" }) -- Move windows with mainMod or Vim-style -hl.bind(var_mainMod .. " + SHIFT + h", hl.dsp.window.move({ direction = "left" })) -hl.bind(var_mainMod .. " + SHIFT + l", hl.dsp.window.move({ direction = "right" })) -hl.bind(var_mainMod .. " + SHIFT + k", hl.dsp.window.move({ direction = "up" })) -hl.bind(var_mainMod .. " + SHIFT + j", hl.dsp.window.move({ direction = "down" })) +hl.bind(var_mainMod .. " + SHIFT + h", hl.dsp.window.move({ direction = "left" }), { description = "Move window left" }) +hl.bind(var_mainMod .. " + SHIFT + l", hl.dsp.window.move({ direction = "right" }), { description = "Move window right" }) +hl.bind(var_mainMod .. " + SHIFT + k", hl.dsp.window.move({ direction = "up" }), { description = "Move window up" }) +hl.bind(var_mainMod .. " + SHIFT + j", hl.dsp.window.move({ direction = "down" }), { description = "Move window down" }) -- Resize focused window with mainMod + Arrows -hl.bind(var_mainMod .. " + left", hl.dsp.window.resize({ x = -10, y = 0, relative = true })) -hl.bind(var_mainMod .. " + right", hl.dsp.window.resize({ x = 10, y = 0, relative = true })) -hl.bind(var_mainMod .. " + up", hl.dsp.window.resize({ x = 0, y = -10, relative = true })) -hl.bind(var_mainMod .. " + down", hl.dsp.window.resize({ x = 0, y = 10, relative = true })) +hl.bind(var_mainMod .. " + left", hl.dsp.window.resize({ x = -10, y = 0, relative = true }), { description = "Resize window left" }) +hl.bind(var_mainMod .. " + right", hl.dsp.window.resize({ x = 10, y = 0, relative = true }), { description = "Resize window right" }) +hl.bind(var_mainMod .. " + up", hl.dsp.window.resize({ x = 0, y = -10, relative = true }), { description = "Resize window up" }) +hl.bind(var_mainMod .. " + down", hl.dsp.window.resize({ x = 0, y = 10, relative = true }), { description = "Resize window down" }) -- Switch workspaces with mainMod + [0-9] -hl.bind(var_mainMod .. " + 1", hl.dsp.focus({ workspace = 1 })) -hl.bind(var_mainMod .. " + 2", hl.dsp.focus({ workspace = 2 })) -hl.bind(var_mainMod .. " + 3", hl.dsp.focus({ workspace = 3 })) -hl.bind(var_mainMod .. " + 4", hl.dsp.focus({ workspace = 4 })) -hl.bind(var_mainMod .. " + 5", hl.dsp.focus({ workspace = 5 })) -hl.bind(var_mainMod .. " + 6", hl.dsp.focus({ workspace = 6 })) -hl.bind(var_mainMod .. " + 7", hl.dsp.focus({ workspace = 7 })) -hl.bind(var_mainMod .. " + 8", hl.dsp.focus({ workspace = 8 })) -hl.bind(var_mainMod .. " + 9", hl.dsp.focus({ workspace = 9 })) -hl.bind(var_mainMod .. " + 0", hl.dsp.focus({ workspace = 10 })) +hl.bind(var_mainMod .. " + 1", hl.dsp.focus({ workspace = 1 }), { description = "Switch to workspace 1" }) +hl.bind(var_mainMod .. " + 2", hl.dsp.focus({ workspace = 2 }), { description = "Switch to workspace 2" }) +hl.bind(var_mainMod .. " + 3", hl.dsp.focus({ workspace = 3 }), { description = "Switch to workspace 3" }) +hl.bind(var_mainMod .. " + 4", hl.dsp.focus({ workspace = 4 }), { description = "Switch to workspace 4" }) +hl.bind(var_mainMod .. " + 5", hl.dsp.focus({ workspace = 5 }), { description = "Switch to workspace 5" }) +hl.bind(var_mainMod .. " + 6", hl.dsp.focus({ workspace = 6 }), { description = "Switch to workspace 6" }) +hl.bind(var_mainMod .. " + 7", hl.dsp.focus({ workspace = 7 }), { description = "Switch to workspace 7" }) +hl.bind(var_mainMod .. " + 8", hl.dsp.focus({ workspace = 8 }), { description = "Switch to workspace 8" }) +hl.bind(var_mainMod .. " + 9", hl.dsp.focus({ workspace = 9 }), { description = "Switch to workspace 9" }) +hl.bind(var_mainMod .. " + 0", hl.dsp.focus({ workspace = 10 }), { description = "Switch to workspace 10" }) hl.bind("XF86AudioRaiseVolume", hl.dsp.exec_cmd("wpctl set-volume -l 1.5 @DEFAULT_AUDIO_SINK@ 5%+"), { repeating = true, + description = "Volume up", }) hl.bind("XF86AudioLowerVolume", hl.dsp.exec_cmd("wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-"), { locked = true, + description = "Volume down", }) -- Move active window to a workspace with mainMod + SHIFT + [0-9] -hl.bind(var_mainMod .. " + SHIFT + 1", hl.dsp.window.move({ workspace = 1 })) -hl.bind(var_mainMod .. " + SHIFT + 2", hl.dsp.window.move({ workspace = 2 })) -hl.bind(var_mainMod .. " + SHIFT + 3", hl.dsp.window.move({ workspace = 3 })) -hl.bind(var_mainMod .. " + SHIFT + 4", hl.dsp.window.move({ workspace = 4 })) -hl.bind(var_mainMod .. " + SHIFT + 5", hl.dsp.window.move({ workspace = 5 })) -hl.bind(var_mainMod .. " + SHIFT + 6", hl.dsp.window.move({ workspace = 6 })) -hl.bind(var_mainMod .. " + SHIFT + 7", hl.dsp.window.move({ workspace = 7 })) -hl.bind(var_mainMod .. " + SHIFT + 8", hl.dsp.window.move({ workspace = 8 })) -hl.bind(var_mainMod .. " + SHIFT + 9", hl.dsp.window.move({ workspace = 9 })) -hl.bind(var_mainMod .. " + SHIFT + 0", hl.dsp.window.move({ workspace = 10 })) +hl.bind(var_mainMod .. " + SHIFT + 1", hl.dsp.window.move({ workspace = 1 }), { description = "Move window to workspace 1" }) +hl.bind(var_mainMod .. " + SHIFT + 2", hl.dsp.window.move({ workspace = 2 }), { description = "Move window to workspace 2" }) +hl.bind(var_mainMod .. " + SHIFT + 3", hl.dsp.window.move({ workspace = 3 }), { description = "Move window to workspace 3" }) +hl.bind(var_mainMod .. " + SHIFT + 4", hl.dsp.window.move({ workspace = 4 }), { description = "Move window to workspace 4" }) +hl.bind(var_mainMod .. " + SHIFT + 5", hl.dsp.window.move({ workspace = 5 }), { description = "Move window to workspace 5" }) +hl.bind(var_mainMod .. " + SHIFT + 6", hl.dsp.window.move({ workspace = 6 }), { description = "Move window to workspace 6" }) +hl.bind(var_mainMod .. " + SHIFT + 7", hl.dsp.window.move({ workspace = 7 }), { description = "Move window to workspace 7" }) +hl.bind(var_mainMod .. " + SHIFT + 8", hl.dsp.window.move({ workspace = 8 }), { description = "Move window to workspace 8" }) +hl.bind(var_mainMod .. " + SHIFT + 9", hl.dsp.window.move({ workspace = 9 }), { description = "Move window to workspace 9" }) +hl.bind(var_mainMod .. " + SHIFT + 0", hl.dsp.window.move({ workspace = 10 }), { description = "Move window to workspace 10" }) -- # Example special workspace (scratchpad) @@ -440,71 +442,89 @@ hl.bind(var_mainMod .. " + SHIFT + 0", hl.dsp.window.move({ workspace = 10 })) -- bind = $mainMod SHIFT, S, movetoworkspace, special:magic -- Scroll through existing workspaces with mainMod + scroll -hl.bind(var_mainMod .. " + mouse_down", hl.dsp.focus({ workspace = "e+1" })) -hl.bind(var_mainMod .. " + mouse_up", hl.dsp.focus({ workspace = "e-1" })) +hl.bind(var_mainMod .. " + mouse_down", hl.dsp.focus({ workspace = "e+1" }), { description = "Next workspace (scroll down)" }) +hl.bind(var_mainMod .. " + mouse_up", hl.dsp.focus({ workspace = "e-1" }), { description = "Previous workspace (scroll up)" }) -- Move/resize windows with mainMod + LMB/RMB and dragging hl.bind(var_mainMod .. " + mouse:272", hl.dsp.window.drag(), { mouse = true, + description = "Move window with mouse", }) hl.bind(var_mainMod .. " + mouse:273", hl.dsp.window.resize(), { mouse = true, + description = "Resize window with mouse", }) -- Laptop multimedia keys for volume and LCD brightness hl.bind("XF86AudioRaiseVolume", hl.dsp.exec_cmd("wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 5%+"), { repeating = true, locked = true, + description = "Volume up (repeat)", }) hl.bind("XF86AudioLowerVolume", hl.dsp.exec_cmd("wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-"), { repeating = true, locked = true, + description = "Volume down (repeat)", }) hl.bind("XF86AudioMute", hl.dsp.exec_cmd("wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle"), { repeating = true, locked = true, + description = "Mute audio", }) hl.bind("XF86AudioMicMute", hl.dsp.exec_cmd("wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle"), { repeating = true, locked = true, + description = "Mute microphone", }) hl.bind("XF86MonBrightnessUp", hl.dsp.exec_cmd("brightnessctl -e4 -n2 set 5%+"), { repeating = true, locked = true, + description = "Brightness up", }) hl.bind("XF86MonBrightnessDown", hl.dsp.exec_cmd("brightnessctl -e4 -n2 set 5%-"), { repeating = true, locked = true, + description = "Brightness down", }) -- Requires playerctl hl.bind("XF86AudioNext", hl.dsp.exec_cmd("playerctl next"), { locked = true, + description = "Next track", }) hl.bind("XF86AudioPause", hl.dsp.exec_cmd("playerctl play-pause"), { locked = true, + description = "Play/Pause", }) hl.bind("XF86AudioPlay", hl.dsp.exec_cmd("playerctl play-pause"), { locked = true, + description = "Play/Pause", }) hl.bind("XF86AudioPrev", hl.dsp.exec_cmd("playerctl previous"), { locked = true, + description = "Previous track", }) -- Screenshots hl.bind("PRINT", hl.dsp.exec_cmd("hyprshot -m region --output-folder " .. var_screenshot_folder), { locked = true, + description = "Screenshot region", }) hl.bind("CTRL + PRINT", hl.dsp.exec_cmd("hyprshot -m window --output-folder " .. var_screenshot_folder), { locked = true, + description = "Screenshot window", }) hl.bind("SHIFT + PRINT", hl.dsp.exec_cmd("hyprshot -m active --output-folder " .. var_screenshot_folder), { locked = true, + description = "Screenshot active monitor", }) -- Screenlock -hl.bind("CTRL + ALT + L", hl.dsp.exec_cmd("hyprlock"), {locked = true,}) -hl.bind(var_mainMod .. " + L", hl.dsp.exec_cmd("hyprlock"), {locked = true,}) +hl.bind("CTRL + ALT + L", hl.dsp.exec_cmd("hyprlock"), {locked = true, description = "Lock screen"}) +hl.bind(var_mainMod .. " + L", hl.dsp.exec_cmd("hyprlock"), {locked = true, description = "Lock screen"}) + +-- Keybind overview popup +hl.bind(var_mainMod .. " + K", hl.dsp.exec_cmd("sh ~/.config/hypr/scripts/show-keybinds.sh"), { description = "Show keybind overview" }) -- ############################# -- ## WINDOWS AND WORKSPACES ### diff --git a/hypr/.config/hypr/scripts/show-keybinds.sh b/hypr/.config/hypr/scripts/show-keybinds.sh new file mode 100755 index 0000000..8991250 --- /dev/null +++ b/hypr/.config/hypr/scripts/show-keybinds.sh @@ -0,0 +1,123 @@ +#!/usr/bin/env bash +# show-keybinds.sh — floating keybind overview popup. +# +# Reads the live keybind list from `hyprctl binds -j` and shows it in a wofi +# popup (single column, sorted A-Z, fuzzy-searchable, closes on Esc). +# Pressing Enter on a row executes that keybind's action via the compositor's +# Lua state (hl.dispatch on the bind's stored dispatcher). +# +# Note: this config uses the Lua API (hl.bind), so `hyprctl binds -j` reports +# key + modmask + an opaque Lua registry reference instead of a readable +# dispatcher. The human-readable action comes from the `description` option +# added to each hl.bind(...) call - keep those descriptions up to date when +# you change a bind. +# +# Usage: show-keybinds.sh [--print] +# --print print the formatted list to stdout instead of opening wofi +# (handy for debugging / testing the formatting pipeline) + +set -euo pipefail + +# modmask bits reported by hyprctl (XKB modifiers), pairwise bit:name in +# display order. See https://wiki.hypr.land/Configuring/Binds/ +readonly MOD_ORDER=(64 SUPER 8 ALT 4 CTRL 1 SHIFT 2 CAPS 16 MOD2 32 MOD3 128 MOD5) + +# chord -> Lua registry ref of the dispatcher to run on selection +declare -A dispatch_for + +if ! hyprctl binds -j >/dev/null 2>&1; then + echo "hyprctl binds failed - are you inside a Hyprland session?" >&2 + exit 1 +fi + +rows=() + +# modmask\x1fkey\x1fdescription\x1fsubmap\x1farg +# Fields are joined with the unit separator (0x1f): tabs are IFS-whitespace +# and `read` would collapse consecutive tabs, eating empty middle fields +# like the (usually empty) submap. \x1f is not whitespace, so every field +# keeps its position. +while IFS=$'\x1f' read -r modmask key description submap arg; do + raw_key="$key" + + # Humanize mouse-button binds. + case "$key" in + mouse:272) key="LMB" ;; + mouse:273) key="RMB" ;; + esac + + # Build the chord, e.g. "SUPER + SHIFT + Q". + chord="" + i=0 + while (( i < ${#MOD_ORDER[@]} )); do + bit=${MOD_ORDER[i]} + name=${MOD_ORDER[i + 1]} + if (( modmask & bit )); then + chord+="${name} + " + fi + i=$((i + 2)) + done + chord+="$key" + + if [[ -n "$submap" ]]; then + chord="[$submap] $chord" + fi + + if [[ -n "$description" ]]; then + rows+=("$(printf '%-26s %s' "$chord" "$description")") + else + rows+=("$(printf '%-26s %s' "$chord" "(no description)")") + fi + + # Remember how to run this row. Mouse drag/resize need a real pointer + # gesture, so they stay display-only. First occurrence of a chord wins + # (media keys are often bound twice with different flags). + case "$raw_key" in + mouse:272 | mouse:273) ;; + *) + if [[ -n "$arg" && -z "${dispatch_for[$chord]+x}" ]]; then + dispatch_for[$chord]="$arg" + fi + ;; + esac +done < <(hyprctl binds -j | jq -r '.[] | [(.modmask | tostring), .key, .description, .submap, .arg] | join("\u001f")') + +if (( ${#rows[@]} == 0 )); then + echo "No keybinds found." >&2 + exit 1 +fi + +# Drop duplicate chords (e.g. media keys bound twice with different flags), +# then sort alphabetically (A-Z). +mapfile -t rows < <(printf '%s\n' "${rows[@]}" | awk '{ c = $0; sub(/ {2,}.*/, "", c); if (!seen[c]++) print }' | LC_ALL=C sort -f) + +if [[ "${1:-}" == "--print" ]]; then + printf '%s\n' "${rows[@]}" + exit 0 +fi + +if ! command -v wofi >/dev/null 2>&1; then + printf '%s\n' "${rows[@]}" + exit 0 +fi + +sel="" +if ! sel=$(printf '%s\n' "${rows[@]}" | wofi \ + --dmenu \ + --prompt "Keybinds" \ + --location center \ + --width 860 \ + --lines 22 \ + --cache-file /dev/null \ + --insensitive \ + --no-custom-entry \ + --hide-scroll); then + exit 0 # popup dismissed (Esc) - not an error +fi + +# Re-derive the chord from the selected row (everything before the first +# double space) and run its stored dispatcher in the compositor's Lua state. +sel_chord="${sel%% *}" +if [[ -n "${dispatch_for[$sel_chord]+x}" ]]; then + hyprctl eval "hl.dispatch(debug.getregistry()[${dispatch_for[$sel_chord]}])" >/dev/null 2>&1 || true +fi \ No newline at end of file