From b148fcc25613f8c66aa5150f23cb74289c48f39b Mon Sep 17 00:00:00 2001 From: Samuel O'Neal Date: Mon, 7 Sep 2026 18:14:27 -0600 Subject: [PATCH] hyprland: add SUPER+arrow workspace cycling and overview binds SUPER+Left/Right cycle existing workspaces on the active monitor (m+1/m-1 with cycling enabled), SUPER+Up opens the hyprshell overview scoped to the current monitor (create/manage workspaces there), and SUPER+Down closes the overview again. Adds binds-help.sh cheat sheet (SHIFT+SUPER+H) and overview-close.sh helper. --- hyprland/.config/hypr/binds.lua | 94 +++++++++++++++----------- hyprland/.config/hypr/hyprland.lua | 8 +++ hyprland/.local/bin/binds-help.sh | 31 +++++++++ hyprland/.local/bin/overview-close.sh | 16 +++++ hyprshell/.config/hyprshell/config.ron | 17 +++-- 5 files changed, 119 insertions(+), 47 deletions(-) create mode 100755 hyprland/.local/bin/binds-help.sh create mode 100755 hyprland/.local/bin/overview-close.sh diff --git a/hyprland/.config/hypr/binds.lua b/hyprland/.config/hypr/binds.lua index e05a2dd..8f3bba3 100644 --- a/hyprland/.config/hypr/binds.lua +++ b/hyprland/.config/hypr/binds.lua @@ -1,73 +1,87 @@ -- See https://wiki.hypr.land/Configuring/Basics/Binds/ for more -- Sound through pactl -hl.bind("XF86AudioRaiseVolume", hl.dsp.exec_cmd("pactl set-sink-volume @DEFAULT_SINK@ +10%")) -hl.bind("XF86AudioLowerVolume", hl.dsp.exec_cmd("pactl set-sink-volume @DEFAULT_SINK@ -10%")) -hl.bind("XF86AudioMute", hl.dsp.exec_cmd("pactl set-sink-mute @DEFAULT_SINK@ toggle")) -hl.bind("XF86AudioPlay", hl.dsp.exec_cmd("playerctl play-pause")) -hl.bind("XF86AudioPrev", hl.dsp.exec_cmd("playerctl previous")) -hl.bind("XF86AudioNext", hl.dsp.exec_cmd("playerctl next")) +hl.bind("XF86AudioRaiseVolume", hl.dsp.exec_cmd("pactl set-sink-volume @DEFAULT_SINK@ +10%"), { desc = "Raise volume" }) +hl.bind("XF86AudioLowerVolume", hl.dsp.exec_cmd("pactl set-sink-volume @DEFAULT_SINK@ -10%"), { desc = "Lower volume" }) +hl.bind("XF86AudioMute", hl.dsp.exec_cmd("pactl set-sink-mute @DEFAULT_SINK@ toggle"), { desc = "Mute audio" }) +hl.bind("XF86AudioPlay", hl.dsp.exec_cmd("playerctl play-pause"), { desc = "Play/pause media" }) +hl.bind("XF86AudioPrev", hl.dsp.exec_cmd("playerctl previous"), { desc = "Previous track" }) +hl.bind("XF86AudioNext", hl.dsp.exec_cmd("playerctl next"), { desc = "Next track" }) -- Brightness through brightnessctl -hl.bind("XF86MonBrightnessUp", hl.dsp.exec_cmd("brightnessctl set +5%")) -hl.bind("XF86MonBrightnessDown", hl.dsp.exec_cmd("brightnessctl set 5%-")) +hl.bind("XF86MonBrightnessUp", hl.dsp.exec_cmd("brightnessctl set +5%"), { desc = "Increase brightness" }) +hl.bind("XF86MonBrightnessDown", hl.dsp.exec_cmd("brightnessctl set 5%-"), { desc = "Decrease brightness" }) -- Main modifier binds -hl.bind(mainMod .. " + T", hl.dsp.exec_cmd(terminal)) -hl.bind(mainMod .. " + B", hl.dsp.exec_cmd(browser)) -hl.bind(mainMod .. " + F", hl.dsp.exec_cmd(fileManager)) -hl.bind(mainMod .. " + C", hl.dsp.window.close()) -hl.bind(mainMod .. " + Q", hl.dsp.exec_cmd("hyprctl dispatch closewindow \"class:$(hyprctl activewindow -j | jq -r .class)\"")) -hl.bind(mainMod .. " + M", hl.dsp.exit()) -hl.bind(mainMod .. " + V", hl.dsp.window.float({ action = "toggle" })) -hl.bind(mainMod .. " + space", hl.dsp.exec_cmd(menu)) -hl.bind(SUPER_SHIFT .. " + L", hl.dsp.exec_cmd("hyprlock")) -hl.bind(SUPER_SHIFT .. " + R", hl.dsp.exec_cmd("hyprctl reload")) +hl.bind(mainMod .. " + T", hl.dsp.exec_cmd(terminal), { desc = "Open terminal" }) +hl.bind(mainMod .. " + B", hl.dsp.exec_cmd(browser), { desc = "Open browser" }) +hl.bind(mainMod .. " + F", hl.dsp.exec_cmd(fileManager), { desc = "Open file manager" }) +hl.bind(mainMod .. " + C", hl.dsp.window.close(), { desc = "Close active window" }) +hl.bind(mainMod .. " + Q", hl.dsp.exec_cmd("hyprctl dispatch closewindow \"class:$(hyprctl activewindow -j | jq -r .class)\""), { desc = "Close active window (by class)" }) +hl.bind(mainMod .. " + M", hl.dsp.exit(), { desc = "Exit Hyprland" }) +hl.bind(mainMod .. " + V", hl.dsp.window.float({ action = "toggle" }), { desc = "Toggle floating" }) +hl.bind(mainMod .. " + space", hl.dsp.exec_cmd(menu), { desc = "Open app menu" }) +hl.bind(SUPER_SHIFT .. " + L", hl.dsp.exec_cmd("hyprlock"), { desc = "Lock screen" }) +hl.bind(SUPER_SHIFT .. " + R", hl.dsp.exec_cmd("hyprctl reload"), { desc = "Reload config" }) -- Monique: GUI monitor arrangement/display settings -hl.bind(SUPER_SHIFT .. " + M", hl.dsp.exec_cmd("monique")) -hl.bind(SUPER_SHIFT .. " + D", hl.dsp.exec_cmd("hyprctl keyword monitor eDP-1, disable")) -hl.bind(SUPER_SHIFT .. " + F", hl.dsp.exec_cmd("hyprctl keyword monitor eDP-1, enable")) +hl.bind(SUPER_SHIFT .. " + M", hl.dsp.exec_cmd("monique"), { desc = "Open monique (monitor settings)" }) +hl.bind(SUPER_SHIFT .. " + D", hl.dsp.exec_cmd("hyprctl keyword monitor eDP-1, disable"), { desc = "Disable eDP-1 (clamshell)" }) +hl.bind(SUPER_SHIFT .. " + F", hl.dsp.exec_cmd("hyprctl keyword monitor eDP-1, enable"), { desc = "Enable eDP-1" }) -- Screenshots -hl.bind("Print", hl.dsp.exec_cmd("grim -c -o \"$(hyprctl activeworkspace -j | jq -r '.monitor')\" $(xdg-user-dir PICTURES)/Screenshots/$(date +'%s_grim.png')")) -hl.bind("SHIFT + Print", hl.dsp.exec_cmd("grim -g \"$(slurp)\" $(xdg-user-dir PICTURES)/Screenshots/$(date +'%s_grim.png')")) +hl.bind("Print", hl.dsp.exec_cmd("grim -c -o \"$(hyprctl activeworkspace -j | jq -r '.monitor')\" $(xdg-user-dir PICTURES)/Screenshots/$(date +'%s_grim.png')"), { desc = "Screenshot active monitor" }) +hl.bind("SHIFT + Print", hl.dsp.exec_cmd("grim -g \"$(slurp)\" $(xdg-user-dir PICTURES)/Screenshots/$(date +'%s_grim.png')"), { desc = "Screenshot region" }) -- Dwindle layout -hl.bind(mainMod .. " + P", hl.dsp.window.pseudo()) -- dwindle -hl.bind(mainMod .. " + E", hl.dsp.layout("togglesplit")) -- dwindle -hl.bind(mainMod .. " + CTRL + F", hl.dsp.window.fullscreen({ action = "toggle" })) -- dwindle -hl.bind(mainMod .. " + W", hl.dsp.group.toggle()) -- dwindle +hl.bind(mainMod .. " + P", hl.dsp.window.pseudo(), { desc = "Toggle pseudo tiling" }) -- dwindle +hl.bind(mainMod .. " + E", hl.dsp.layout("togglesplit"), { desc = "Toggle split" }) -- dwindle +hl.bind(mainMod .. " + CTRL + F", hl.dsp.window.fullscreen({ action = "toggle" }), { desc = "Toggle fullscreen" }) -- dwindle +hl.bind(mainMod .. " + W", hl.dsp.group.toggle(), { desc = "Toggle window group" }) -- dwindle -- Move focus with mainMod + vim keys -hl.bind(mainMod .. " + H", hl.dsp.focus({ direction = "l" })) -hl.bind(mainMod .. " + L", hl.dsp.focus({ direction = "r" })) -hl.bind(mainMod .. " + K", hl.dsp.focus({ direction = "u" })) -hl.bind(mainMod .. " + J", hl.dsp.focus({ direction = "d" })) +hl.bind(mainMod .. " + H", hl.dsp.focus({ direction = "l" }), { desc = "Focus left" }) +hl.bind(mainMod .. " + L", hl.dsp.focus({ direction = "r" }), { desc = "Focus right" }) +hl.bind(mainMod .. " + K", hl.dsp.focus({ direction = "u" }), { desc = "Focus up" }) +hl.bind(mainMod .. " + J", hl.dsp.focus({ direction = "d" }), { desc = "Focus down" }) + +-- Cycle workspaces on the active monitor with mainMod + arrows (m+1/m-1 stay +-- on the current monitor; plain +1/-1 would follow the workspace if it moves +-- to another monitor). These only visit non-empty workspaces. +hl.bind(mainMod .. " + Left", hl.dsp.focus({ workspace = "m-1" }), { desc = "Previous workspace (active monitor)" }) +hl.bind(mainMod .. " + Right", hl.dsp.focus({ workspace = "m+1" }), { desc = "Next workspace (active monitor)" }) -- Switch workspaces with mainMod + [0-9] for i = 1, 10 do local key = i % 10 -- 10 maps to key 0 - hl.bind(mainMod .. " + " .. key, hl.dsp.focus({ workspace = i })) + hl.bind(mainMod .. " + " .. key, hl.dsp.focus({ workspace = i }), { desc = "Go to workspace " .. i }) end -- Move active window to a workspace with mainMod + SHIFT + [0-9] for i = 1, 10 do local key = i % 10 -- 10 maps to key 0 - hl.bind(mainMod .. " + SHIFT + " .. key, hl.dsp.window.move({ workspace = i })) + hl.bind(mainMod .. " + SHIFT + " .. key, hl.dsp.window.move({ workspace = i }), { desc = "Move window to workspace " .. i }) end -- Example special workspace (scratchpad) -hl.bind(mainMod .. " + S", hl.dsp.workspace.toggle_special("magic")) -hl.bind(mainMod .. " + SHIFT + S", hl.dsp.window.move({ workspace = "special:magic" })) +hl.bind(mainMod .. " + S", hl.dsp.workspace.toggle_special("magic"), { desc = "Toggle scratchpad" }) +hl.bind(mainMod .. " + SHIFT + S", hl.dsp.window.move({ workspace = "special:magic" }), { desc = "Move window to scratchpad" }) -- Scroll through existing workspaces with mainMod + scroll -hl.bind(mainMod .. " + mouse_down", hl.dsp.focus({ workspace = "e+1" })) -hl.bind(mainMod .. " + mouse_up", hl.dsp.focus({ workspace = "e-1" })) +hl.bind(mainMod .. " + mouse_down", hl.dsp.focus({ workspace = "e+1" }), { desc = "Next workspace (scroll)" }) +hl.bind(mainMod .. " + mouse_up", hl.dsp.focus({ workspace = "e-1" }), { desc = "Previous workspace (scroll)" }) + +-- Keybind cheat sheet +hl.bind(SUPER_SHIFT .. " + H", hl.dsp.exec_cmd("binds-help.sh"), { desc = "Keybind cheat sheet" }) + +-- Workspace overview (macOS Mission Control style). SUPER + Up is registered by +-- the hyprshell daemon itself ("Open Overview with Super + Up"); SUPER + Down +-- closes the overview again without switching. +hl.bind(mainMod .. " + Down", hl.dsp.exec_cmd("overview-close.sh"), { desc = "Close workspace overview" }) -- Move/resize windows with mainMod + LMB/RMB and dragging -hl.bind(mainMod .. " + mouse:272", hl.dsp.window.drag(), { mouse = true }) -hl.bind(mainMod .. " + mouse:273", hl.dsp.window.resize(), { mouse = true }) +hl.bind(mainMod .. " + mouse:272", hl.dsp.window.drag(), { mouse = true, desc = "Drag window" }) +hl.bind(mainMod .. " + mouse:273", hl.dsp.window.resize(), { mouse = true, desc = "Resize window" }) -- Laptop lid is handled by hypr-lid-power.service (delayed lock/suspend when -- closed undocked; display/clamshell handling is done by moniqued) instead of --- these binds; see .local/bin/hypr-lid-power.sh. +-- these binds; see .local/bin/hypr-lid-power.sh. \ No newline at end of file diff --git a/hyprland/.config/hypr/hyprland.lua b/hyprland/.config/hypr/hyprland.lua index ec0e98d..514a2a0 100644 --- a/hyprland/.config/hypr/hyprland.lua +++ b/hyprland/.config/hypr/hyprland.lua @@ -39,6 +39,14 @@ hl.config({ }, }) +hl.config({ + binds = { + -- Let SUPER+Left/Right (and scroll) wrap around instead of stopping at the + -- edge when cycling workspaces on the active monitor. + allow_workspace_cycles = true, + }, +}) + hl.config({ decoration = { rounding = 4, diff --git a/hyprland/.local/bin/binds-help.sh b/hyprland/.local/bin/binds-help.sh new file mode 100755 index 0000000..cfac958 --- /dev/null +++ b/hyprland/.local/bin/binds-help.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# Searchable cheat sheet of Hyprland keybindings, shown in wofi. +# +# The descriptions shown here come from the `desc` option passed to hl.bind() +# in ~/.config/hypr/binds.lua, read back at runtime via `hyprctl binds -j`, +# so the cheat sheet always matches the live config. +set -u + +mods_from_mask() { + local mask="$1" out="" + (( mask & 1 )) && out="${out:+$out + }SHIFT" + (( mask & 4 )) && out="${out:+$out + }CTRL" + (( mask & 8 )) && out="${out:+$out + }ALT" + (( mask & 64 )) && out="${out:+$out + }SUPER" + (( mask & 128 )) && out="${out:+$out + }MOD5" + printf "%s" "$out" +} + +hyprctl binds -j | + jq -rc '.[] | select(.has_description) | select(.description != "") | [.modmask, .key, .description] | @tsv' | + while IFS=$'\t' read -r mask key desc; do + if (( mask )); then + mods="$(mods_from_mask "$mask")" + printf '%s + %s\t%s\n' "$mods" "$key" "$desc" + else + printf '%s\t%s\n' "$key" "$desc" + fi + done | + sort -k1,1 | + column -t -s $'\t' | + wofi --dmenu --prompt "Hyprland keybinds" >/dev/null \ No newline at end of file diff --git a/hyprland/.local/bin/overview-close.sh b/hyprland/.local/bin/overview-close.sh new file mode 100755 index 0000000..6055bf9 --- /dev/null +++ b/hyprland/.local/bin/overview-close.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# macOS-style SUPER+Down: close the hyprshell workspace overview if it is open, +# returning to the current workspace without switching. +# +# hyprshell's overview is a gtk4-layer-shell surface whose namespace is +# "hyprshell_overview", and the OpenOverview IPC message toggles the overview +# closed when it is already open (see OverviewRootInput::OpenOverview). +set -u + +if ! hyprctl layers -j | jq -e ' + .. | objects | .namespace? // empty | select(. == "hyprshell_overview") +' >/dev/null; then + exit 0 +fi + +hyprshell socat '"OpenOverview"' \ No newline at end of file diff --git a/hyprshell/.config/hyprshell/config.ron b/hyprshell/.config/hyprshell/config.ron index 707dc46..3f6b25b 100644 --- a/hyprshell/.config/hyprshell/config.ron +++ b/hyprshell/.config/hyprshell/config.ron @@ -1,6 +1,6 @@ // Edit with `hyprshell config edit` ( - version: 3, + version: 4, windows: ( scale: 8.5, items_per_row: 5, @@ -69,11 +69,13 @@ ), ), ), - key: "Tab", - modifier: "alt", - filter_by: [], - hide_filtered: false, - exclude_special_workspaces: "", + key: "Up", + top_offset: 430, + modifier: "super", + filter_by: [ + current_monitor, + ], + exclude_workspaces: "special:.*", ), switch: ( modifier: "alt", @@ -82,7 +84,8 @@ current_monitor, ], switch_workspaces: false, - exclude_special_workspaces: "", + exclude_workspaces: "", + kill_key: 'q', ), switch_2: None, ),