hyprland: cycle workspaces per monitor

This commit is contained in:
2026-03-30 02:06:12 -07:00
committed by Kat Huang
parent c7c4ff9df3
commit 91d22f053d
4 changed files with 221 additions and 61 deletions

View File

@@ -444,8 +444,10 @@ 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
# Workspace cycling (like XMonad's cycleWorkspaceOnCurrentScreen) # Workspace cycling with monitor-local history and commit-on-release semantics.
bind = $mainMod, backslash, exec, ~/.config/hypr/scripts/workspace-back.sh bind = $mainMod, backslash, exec, ~/.config/hypr/scripts/workspace-back.sh cycle
bindr = , SUPER_L, exec, ~/.config/hypr/scripts/workspace-back.sh finalize
bindr = , SUPER_R, exec, ~/.config/hypr/scripts/workspace-back.sh finalize
# Swap current workspace with another (like XMonad's swapWithCurrent) # Swap current workspace with another (like XMonad's swapWithCurrent)
bind = $hyper, 5, exec, ~/.config/hypr/scripts/swap-workspaces.sh bind = $hyper, 5, exec, ~/.config/hypr/scripts/swap-workspaces.sh

View File

@@ -1,19 +1,71 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
max_ws="${HYPR_MAX_WORKSPACE:-9}" script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=workspace-history-common.sh
# shellcheck source-path=SCRIPTDIR
source "${script_dir}/workspace-history-common.sh"
runtime_dir="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}" action="${1:-cycle}"
state_dir="${runtime_dir}/hypr"
prev_file="${state_dir}/prev-workspace"
prev="$(cat "${prev_file}" 2>/dev/null || true)" exec 9>"${lock_file}"
if [[ -z "${prev}" ]]; then flock 9
state="$(wh_load_state)"
monitors_json="$(wh_monitors_json)"
state="$(wh_refresh_state_json "${state}" "${monitors_json}")"
focused_monitor="$(
jq -r '.[] | select(.focused == true) | .name // empty' <<<"${monitors_json}" | head -n 1
)"
if [[ -z "${focused_monitor}" ]]; then
wh_save_state "${state}"
exit 0 exit 0
fi fi
if [[ "${prev}" =~ ^[0-9]+$ ]] && (( prev < 1 || prev > max_ws )); then current_workspace="$(jq -r --arg monitor "${focused_monitor}" '.monitorCurrent[$monitor] // empty' <<<"${state}")"
exit 0
fi
hyprctl dispatch workspace "${prev}" >/dev/null 2>&1 || true case "${action}" in
cycle)
next_workspace="$(
jq -r \
--arg monitor "${focused_monitor}" \
--arg current "${current_workspace}" \
'
(.monitorHistory[$monitor] // []) as $history
| if ($history | length) < 2 then
""
else
($history | index($current)) as $idx
| if $idx == null then
""
else
$history[(($idx + 1) % ($history | length))]
end
end
' <<<"${state}"
)"
if [[ -z "${next_workspace}" || "${next_workspace}" == "${current_workspace}" ]]; then
wh_save_state "${state}"
exit 0
fi
state="$(
jq -c \
--arg monitor "${focused_monitor}" \
'.cycle = {"active": true, "monitor": $monitor}' <<<"${state}"
)"
wh_save_state "${state}"
hyprctl dispatch workspace "${next_workspace}" >/dev/null 2>&1 || true
;;
finalize)
state="$(wh_finalize_cycle_json "${state}")"
wh_save_state "${state}"
;;
*)
printf 'Unknown action: %s\n' "${action}" >&2
exit 1
;;
esac

View File

@@ -0,0 +1,127 @@
#!/usr/bin/env bash
max_ws="${HYPR_MAX_WORKSPACE:-9}"
runtime_dir="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
state_dir="${HYPR_WORKSPACE_STATE_DIR:-${runtime_dir}/hypr}"
state_file="${state_dir}/workspace-history.json"
# shellcheck disable=SC2034 # Sourced by sibling scripts that coordinate updates.
lock_file="${state_dir}/workspace-history.lock"
mkdir -p "${state_dir}"
wh_default_state() {
cat <<'EOF'
{"monitorCurrent":{},"monitorHistory":{},"cycle":{"active":false}}
EOF
}
wh_load_state() {
local state
if [[ -s "${state_file}" ]] && state="$(jq -c '.' "${state_file}" 2>/dev/null)"; then
printf '%s\n' "${state}"
return
fi
wh_default_state
}
wh_save_state() {
local state="$1"
local tmp_file
tmp_file="$(mktemp "${state_file}.XXXXXX")"
printf '%s\n' "${state}" >"${tmp_file}"
mv "${tmp_file}" "${state_file}"
}
wh_normalize_workspace() {
local ws_id="${1:-}"
local ws_name="${2:-}"
if [[ -n "${ws_name}" && "${ws_name}" != "null" && "${ws_name}" != special:* ]]; then
printf '%s\n' "${ws_name}"
return
fi
if [[ -z "${ws_id}" || "${ws_id}" == "null" || "${ws_id}" =~ ^- ]]; then
return
fi
if [[ "${ws_id}" =~ ^[0-9]+$ ]] && ((ws_id >= 1 && ws_id <= max_ws)); then
printf '%s\n' "${ws_id}"
fi
}
wh_monitors_json() {
hyprctl -j monitors 2>/dev/null || printf '[]\n'
}
wh_refresh_state_json() {
local state="$1"
local monitors_json="$2"
local row
local monitor
local ws_id
local ws_name
local workspace
while IFS= read -r row; do
[[ -z "${row}" ]] && continue
monitor="$(jq -r '.name // empty' <<<"${row}")"
ws_id="$(jq -r '.activeWorkspace.id // empty' <<<"${row}")"
ws_name="$(jq -r '.activeWorkspace.name // empty' <<<"${row}")"
workspace="$(wh_normalize_workspace "${ws_id}" "${ws_name}")"
[[ -z "${monitor}" || -z "${workspace}" ]] && continue
state="$(
jq -c \
--arg monitor "${monitor}" \
--arg workspace "${workspace}" \
'
.monitorCurrent[$monitor] = $workspace
| if .cycle.active == true and .cycle.monitor == $monitor then
.
else
.monitorHistory[$monitor] =
([$workspace] + ((.monitorHistory[$monitor] // []) | map(select(. != $workspace))))
end
' <<<"${state}"
)"
done < <(jq -c '.[]' <<<"${monitors_json}")
printf '%s\n' "${state}"
}
wh_finalize_cycle_json() {
local state="$1"
local monitor
local current
if [[ "$(jq -r '.cycle.active // false' <<<"${state}")" != "true" ]]; then
printf '%s\n' "${state}"
return
fi
monitor="$(jq -r '.cycle.monitor // empty' <<<"${state}")"
current="$(jq -r --arg monitor "${monitor}" '.monitorCurrent[$monitor] // empty' <<<"${state}")"
if [[ -n "${monitor}" && -n "${current}" ]]; then
state="$(
jq -c \
--arg monitor "${monitor}" \
--arg current "${current}" \
'
.monitorHistory[$monitor] =
([$current] + ((.monitorHistory[$monitor] // []) | map(select(. != $current))))
| .cycle = {"active": false}
' <<<"${state}"
)"
else
state="$(jq -c '.cycle = {"active": false}' <<<"${state}")"
fi
printf '%s\n' "${state}"
}

View File

@@ -1,7 +1,10 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
max_ws="${HYPR_MAX_WORKSPACE:-9}" script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=workspace-history-common.sh
# shellcheck source-path=SCRIPTDIR
source "${script_dir}/workspace-history-common.sh"
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:-}"
@@ -10,19 +13,24 @@ if [[ -z "$sig" ]]; then
fi fi
sock="${runtime_dir}/hypr/${sig}/.socket2.sock" sock="${runtime_dir}/hypr/${sig}/.socket2.sock"
state_dir="${runtime_dir}/hypr"
last_file="${state_dir}/last-workspace"
prev_file="${state_dir}/prev-workspace"
mkdir -p "${state_dir}" with_history_lock() {
exec 9>"${lock_file}"
flock 9
"$@"
}
# Initialize current workspace to avoid empty state. refresh_history_state() {
if command -v hyprctl >/dev/null 2>&1; then local state
cur_id="$(hyprctl activeworkspace -j | jq -r '.id' 2>/dev/null || true)" local monitors_json
if [[ -n "${cur_id}" && "${cur_id}" != "null" ]]; then
echo "${cur_id}" > "${last_file}" state="$(wh_load_state)"
fi monitors_json="$(wh_monitors_json)"
fi state="$(wh_refresh_state_json "${state}" "${monitors_json}")"
wh_save_state "${state}"
}
with_history_lock refresh_history_state
# Wait for the event socket to be ready. # Wait for the event socket to be ready.
while [[ ! -S "${sock}" ]]; do while [[ ! -S "${sock}" ]]; do
@@ -32,36 +40,7 @@ done
nc -U "${sock}" | while read -r line; do nc -U "${sock}" | while read -r line; do
case "${line}" in case "${line}" in
workspace*">>"*) workspace*">>"*)
payload="${line#*>>}" with_history_lock refresh_history_state
# Handle workspacev2 payloads: id,name
if [[ "${payload}" == *","* ]]; then
ws_id="${payload%%,*}"
ws_name="${payload#*,}"
else
ws_id="${payload}"
ws_name="${payload}"
fi
# Ignore special/negative workspaces.
if [[ "${ws_id}" =~ ^- ]] || [[ "${ws_name}" == special:* ]]; then
continue
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}"
if [[ -z "${ws_ident}" ]]; then
ws_ident="${ws_id}"
fi
prev="$(cat "${last_file}" 2>/dev/null || true)"
if [[ -n "${prev}" && "${ws_ident}" != "${prev}" ]]; then
echo "${prev}" > "${prev_file}"
fi
echo "${ws_ident}" > "${last_file}"
;; ;;
esac esac
done done