hyprland: cap workspaces and add empty-workspace helpers

- Introduce HYPR_MAX_WORKSPACE (default 9) and enforce it in scripts
- Replace 'workspace empty' bindings with scripts that pick an empty id
- Add scroll-to-workspace helper for mouse wheel binds
This commit is contained in:
2026-02-05 12:03:16 -08:00
committed by Kat Huang
parent 9dc6aacb25
commit 0c6363c793
9 changed files with 185 additions and 17 deletions

View File

@@ -27,6 +27,8 @@ $runMenu = rofi -show run
# ============================================================================= # =============================================================================
env = XCURSOR_SIZE,24 env = XCURSOR_SIZE,24
env = QT_QPA_PLATFORMTHEME,qt5ct env = QT_QPA_PLATFORMTHEME,qt5ct
# Used by ~/.config/hypr/scripts/* to keep workspace IDs bounded.
env = HYPR_MAX_WORKSPACE,9
# ============================================================================= # =============================================================================
# INPUT CONFIGURATION # INPUT CONFIGURATION
@@ -405,7 +407,7 @@ bind = $modAlt, Return, togglespecialworkspace, minimized
# WORKSPACE CONTROL # WORKSPACE CONTROL
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Switch workspaces (1-9, 0=10) # Switch workspaces (1-9 only)
bind = $mainMod, 1, workspace, 1 bind = $mainMod, 1, workspace, 1
bind = $mainMod, 2, workspace, 2 bind = $mainMod, 2, workspace, 2
bind = $mainMod, 3, workspace, 3 bind = $mainMod, 3, workspace, 3
@@ -415,7 +417,6 @@ bind = $mainMod, 6, workspace, 6
bind = $mainMod, 7, workspace, 7 bind = $mainMod, 7, workspace, 7
bind = $mainMod, 8, workspace, 8 bind = $mainMod, 8, workspace, 8
bind = $mainMod, 9, workspace, 9 bind = $mainMod, 9, workspace, 9
bind = $mainMod, 0, workspace, 10
# Move window to workspace # Move window to workspace
bind = $mainMod SHIFT, 1, movetoworkspace, 1 bind = $mainMod SHIFT, 1, movetoworkspace, 1
@@ -427,7 +428,6 @@ bind = $mainMod SHIFT, 6, movetoworkspace, 6
bind = $mainMod SHIFT, 7, movetoworkspace, 7 bind = $mainMod SHIFT, 7, movetoworkspace, 7
bind = $mainMod SHIFT, 8, movetoworkspace, 8 bind = $mainMod SHIFT, 8, movetoworkspace, 8
bind = $mainMod SHIFT, 9, movetoworkspace, 9 bind = $mainMod SHIFT, 9, movetoworkspace, 9
bind = $mainMod SHIFT, 0, movetoworkspace, 10
# Move and follow to workspace (like XMonad's shiftThenView) # Move and follow to workspace (like XMonad's shiftThenView)
bind = $mainMod CTRL, 1, movetoworkspacesilent, 1 bind = $mainMod CTRL, 1, movetoworkspacesilent, 1
@@ -448,8 +448,6 @@ bind = $mainMod CTRL, 8, movetoworkspacesilent, 8
bind = $mainMod CTRL, 8, workspace, 8 bind = $mainMod CTRL, 8, workspace, 8
bind = $mainMod CTRL, 9, movetoworkspacesilent, 9 bind = $mainMod CTRL, 9, movetoworkspacesilent, 9
bind = $mainMod CTRL, 9, workspace, 9 bind = $mainMod CTRL, 9, workspace, 9
bind = $mainMod CTRL, 0, movetoworkspacesilent, 10
bind = $mainMod CTRL, 0, workspace, 10
# Workspace cycling (like XMonad's cycleWorkspaceOnCurrentScreen) # Workspace cycling (like XMonad's cycleWorkspaceOnCurrentScreen)
bind = $mainMod, backslash, exec, ~/.config/hypr/scripts/workspace-back.sh bind = $mainMod, backslash, exec, ~/.config/hypr/scripts/workspace-back.sh
@@ -458,14 +456,14 @@ bind = $mainMod, backslash, exec, ~/.config/hypr/scripts/workspace-back.sh
bind = $hyper, 5, exec, ~/.config/hypr/scripts/swap-workspaces.sh bind = $hyper, 5, exec, ~/.config/hypr/scripts/swap-workspaces.sh
# Go to next empty workspace (like XMonad's moveTo Next emptyWS) # Go to next empty workspace (like XMonad's moveTo Next emptyWS)
bind = $hyper, E, workspace, empty bind = $hyper, E, exec, ~/.config/hypr/scripts/workspace-goto-empty.sh
# Move to next screen (like XMonad's shiftToNextScreenX) # Move to next screen (like XMonad's shiftToNextScreenX)
bind = $mainMod, Z, focusmonitor, +1 bind = $mainMod, Z, focusmonitor, +1
bind = $mainMod SHIFT, Z, movewindow, mon:+1 bind = $mainMod SHIFT, Z, movewindow, mon:+1
# Shift to empty workspace and view (like XMonad's shiftToEmptyAndView) # Shift to empty workspace and view (like XMonad's shiftToEmptyAndView)
bind = $mainMod SHIFT, H, movetoworkspace, empty bind = $mainMod SHIFT, H, exec, ~/.config/hypr/scripts/workspace-move-to-empty.sh
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# WINDOW MANAGEMENT # WINDOW MANAGEMENT
@@ -543,8 +541,8 @@ bindm = $mainMod, mouse:272, movewindow
bindm = $mainMod, mouse:273, resizewindow bindm = $mainMod, mouse:273, resizewindow
# Scroll through workspaces # Scroll through workspaces
bind = $mainMod, mouse_down, workspace, e+1 bind = $mainMod, mouse_down, exec, ~/.config/hypr/scripts/workspace-scroll.sh +1
bind = $mainMod, mouse_up, workspace, e-1 bind = $mainMod, mouse_up, exec, ~/.config/hypr/scripts/workspace-scroll.sh -1
# ============================================================================= # =============================================================================
# AUTOSTART # AUTOSTART

View File

@@ -0,0 +1,72 @@
#!/usr/bin/env bash
set -euo pipefail
# Print an "empty" workspace id within 1..$HYPR_MAX_WORKSPACE (default 9).
#
# Preference order (lowest id wins within each tier):
# 1. Workspace exists on the target monitor and has 0 windows
# 2. Workspace id does not exist at all (will be created on dispatch)
# 3. Workspace exists (elsewhere) and has 0 windows
#
# Usage:
# find-empty-workspace.sh [monitor] [exclude_id]
max_ws="${HYPR_MAX_WORKSPACE:-9}"
monitor="${1:-}"
exclude_id="${2:-}"
if [[ -z "${monitor}" ]]; then
monitor="$(hyprctl activeworkspace -j | jq -r '.monitor' 2>/dev/null || true)"
fi
if [[ -z "${monitor}" || "${monitor}" == "null" ]]; then
exit 1
fi
workspaces_json="$(hyprctl workspaces -j 2>/dev/null || echo '[]')"
unused_candidate=""
elsewhere_empty_candidate=""
for i in $(seq 1 "${max_ws}"); do
if [[ -n "${exclude_id}" && "${i}" == "${exclude_id}" ]]; then
continue
fi
exists="$(jq -r --argjson id "${i}" '[.[] | select(.id == $id)] | length' <<<"${workspaces_json}")"
if [[ "${exists}" == "0" ]]; then
if [[ -z "${unused_candidate}" ]]; then
unused_candidate="${i}"
fi
continue
fi
windows="$(jq -r --argjson id "${i}" '([.[] | select(.id == $id) | .windows] | .[0]) // 0' <<<"${workspaces_json}")"
if [[ "${windows}" != "0" ]]; then
continue
fi
ws_monitor="$(jq -r --argjson id "${i}" '([.[] | select(.id == $id) | .monitor] | .[0]) // ""' <<<"${workspaces_json}")"
if [[ "${ws_monitor}" == "${monitor}" ]]; then
printf '%s\n' "${i}"
exit 0
fi
if [[ -z "${elsewhere_empty_candidate}" ]]; then
elsewhere_empty_candidate="${i}"
fi
done
if [[ -n "${unused_candidate}" ]]; then
printf '%s\n' "${unused_candidate}"
exit 0
fi
if [[ -n "${elsewhere_empty_candidate}" ]]; then
printf '%s\n' "${elsewhere_empty_candidate}"
exit 0
fi
exit 1

View File

@@ -6,6 +6,7 @@
set -euo pipefail set -euo pipefail
DIRECTION="$1" DIRECTION="$1"
max_ws="${HYPR_MAX_WORKSPACE:-9}"
# Track the current monitor so we can return # Track the current monitor so we can return
ORIG_MONITOR=$(hyprctl activeworkspace -j | jq -r '.monitor') ORIG_MONITOR=$(hyprctl activeworkspace -j | jq -r '.monitor')
@@ -21,14 +22,17 @@ if [ "$MONITOR" = "$ORIG_MONITOR" ]; then
exit 0 exit 0
fi fi
# Find an empty workspace or create one # Find an empty workspace within 1..$HYPR_MAX_WORKSPACE.
# First check if there's an empty workspace on this monitor EMPTY_WS="$(~/.config/hypr/scripts/find-empty-workspace.sh "${MONITOR}" 2>/dev/null || true)"
EMPTY_WS=$(hyprctl workspaces -j | jq -r ".[] | select(.windows == 0 and .monitor == \"$MONITOR\") | .id" | head -1) if [[ -z "${EMPTY_WS}" ]]; then
# No empty workspace available within the cap; restore focus and bail.
hyprctl dispatch focusmonitor "$ORIG_MONITOR"
exit 0
fi
if [ -z "$EMPTY_WS" ]; then if (( EMPTY_WS < 1 || EMPTY_WS > max_ws )); then
# No empty workspace, find next available workspace number hyprctl dispatch focusmonitor "$ORIG_MONITOR"
MAX_WS=$(hyprctl workspaces -j | jq -r 'map(.id) | max') exit 0
EMPTY_WS=$((MAX_WS + 1))
fi fi
# Ensure the workspace exists on the target monitor # Ensure the workspace exists on the target monitor

View File

@@ -4,6 +4,8 @@
set -euo pipefail set -euo pipefail
max_ws="${HYPR_MAX_WORKSPACE:-9}"
CURRENT_WS="$(hyprctl activeworkspace -j | jq -r '.id')" CURRENT_WS="$(hyprctl activeworkspace -j | jq -r '.id')"
if [[ -z "${CURRENT_WS}" || "${CURRENT_WS}" == "null" ]]; then if [[ -z "${CURRENT_WS}" || "${CURRENT_WS}" == "null" ]]; then
exit 0 exit 0
@@ -13,7 +15,7 @@ TARGET_WS="${1:-}"
if [[ -z "${TARGET_WS}" ]]; then if [[ -z "${TARGET_WS}" ]]; then
WS_LIST="$({ WS_LIST="$({
seq 1 10 seq 1 "${max_ws}"
hyprctl workspaces -j | jq -r '.[].id' 2>/dev/null || true hyprctl workspaces -j | jq -r '.[].id' 2>/dev/null || true
} | awk 'NF {print $1}' | awk '!seen[$0]++' | sort -n)" } | awk 'NF {print $1}' | awk '!seen[$0]++' | sort -n)"
@@ -33,6 +35,11 @@ if ! [[ "${TARGET_WS}" =~ ^-?[0-9]+$ ]]; then
exit 1 exit 1
fi fi
if (( TARGET_WS < 1 || TARGET_WS > max_ws )); then
notify-send "Swap Workspace" "Workspace out of range (1-${max_ws}): ${TARGET_WS}"
exit 1
fi
WINDOWS_CURRENT="$(hyprctl clients -j | jq -r --arg ws "${CURRENT_WS}" '.[] | select((.workspace.id|tostring) == $ws) | .address')" WINDOWS_CURRENT="$(hyprctl clients -j | jq -r --arg ws "${CURRENT_WS}" '.[] | select((.workspace.id|tostring) == $ws) | .address')"
WINDOWS_TARGET="$(hyprctl clients -j | jq -r --arg ws "${TARGET_WS}" '.[] | select((.workspace.id|tostring) == $ws) | .address')" WINDOWS_TARGET="$(hyprctl clients -j | jq -r --arg ws "${TARGET_WS}" '.[] | select((.workspace.id|tostring) == $ws) | .address')"

View File

@@ -1,6 +1,8 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
max_ws="${HYPR_MAX_WORKSPACE:-9}"
runtime_dir="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}" runtime_dir="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
state_dir="${runtime_dir}/hypr" state_dir="${runtime_dir}/hypr"
prev_file="${state_dir}/prev-workspace" prev_file="${state_dir}/prev-workspace"
@@ -10,4 +12,8 @@ if [[ -z "${prev}" ]]; then
exit 0 exit 0
fi fi
if [[ "${prev}" =~ ^[0-9]+$ ]] && (( prev < 1 || prev > max_ws )); then
exit 0
fi
hyprctl dispatch workspace "${prev}" >/dev/null 2>&1 || true hyprctl dispatch workspace "${prev}" >/dev/null 2>&1 || true

View File

@@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -euo pipefail
cur_ws="$(hyprctl activeworkspace -j | jq -r '.id' 2>/dev/null || true)"
monitor="$(hyprctl activeworkspace -j | jq -r '.monitor' 2>/dev/null || true)"
ws="$(
~/.config/hypr/scripts/find-empty-workspace.sh "${monitor}" "${cur_ws}" 2>/dev/null || true
)"
if [[ -z "${ws}" ]]; then
exit 0
fi
hyprctl dispatch workspace "${ws}" >/dev/null 2>&1 || true

View File

@@ -1,6 +1,8 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
max_ws="${HYPR_MAX_WORKSPACE:-9}"
runtime_dir="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}" runtime_dir="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
sig="${HYPRLAND_INSTANCE_SIGNATURE:-}" sig="${HYPRLAND_INSTANCE_SIGNATURE:-}"
if [[ -z "$sig" ]]; then if [[ -z "$sig" ]]; then
@@ -45,6 +47,11 @@ nc -U "${sock}" | while read -r line; do
continue continue
fi fi
# Ignore workspaces outside the configured cap.
if [[ "${ws_id}" =~ ^[0-9]+$ ]] && (( ws_id < 1 || ws_id > max_ws )); then
continue
fi
ws_ident="${ws_name}" ws_ident="${ws_name}"
if [[ -z "${ws_ident}" ]]; then if [[ -z "${ws_ident}" ]]; then
ws_ident="${ws_id}" ws_ident="${ws_id}"

View File

@@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -euo pipefail
cur_ws="$(hyprctl activeworkspace -j | jq -r '.id' 2>/dev/null || true)"
monitor="$(hyprctl activeworkspace -j | jq -r '.monitor' 2>/dev/null || true)"
ws="$(
~/.config/hypr/scripts/find-empty-workspace.sh "${monitor}" "${cur_ws}" 2>/dev/null || true
)"
if [[ -z "${ws}" ]]; then
exit 0
fi
hyprctl dispatch movetoworkspace "${ws}" >/dev/null 2>&1 || true

View File

@@ -0,0 +1,42 @@
#!/usr/bin/env bash
set -euo pipefail
max_ws="${HYPR_MAX_WORKSPACE:-9}"
delta="${1:-}"
case "${delta}" in
+1|-1) ;;
next) delta="+1" ;;
prev) delta="-1" ;;
*)
exit 2
;;
esac
cur="$(hyprctl activeworkspace -j | jq -r '.id' 2>/dev/null || true)"
if ! [[ "${cur}" =~ ^[0-9]+$ ]]; then
exit 0
fi
if (( cur < 1 )); then
cur=1
elif (( cur > max_ws )); then
cur="${max_ws}"
fi
if [[ "${delta}" == "+1" ]]; then
if (( cur >= max_ws )); then
nxt=1
else
nxt=$((cur + 1))
fi
else
if (( cur <= 1 )); then
nxt="${max_ws}"
else
nxt=$((cur - 1))
fi
fi
hyprctl dispatch workspace "${nxt}" >/dev/null 2>&1 || true