Fix multi-monitor workspace routing regression, add laptop auto-detect

- Restore desktop DP-3=primary/DP-2=secondary workspace mapping that
  broke when workspace routing switched from name-matching to raw
  geometric ordering.
- Extract shared laptop/desktop detection + output ranking into
  lib-outputs.sh (battery presence selects the profile automatically),
  sourced by both select-workspace.sh and the new apply-outputs.sh, so
  keybind routing and monitor placement can never disagree.
- Add apply-outputs.sh: on the laptop, automatically positions whatever
  external display is plugged in (and re-runs on hotplug via swaymsg
  -t subscribe), making it primary. No-op on the desktop, which keeps
  its static fixed-arrangement config.
This commit is contained in:
2026-08-21 23:08:01 -05:00
parent 5c57ee20f7
commit f1275e7870
5 changed files with 106 additions and 12 deletions
+29
View File
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
# Laptop-only dynamic monitor placement: whenever a display is plugged or
# unplugged, arrange active outputs left-to-right in output_order() order
# (external display(s) first, internal panel last), so a newly connected
# external monitor gets a sensible position automatically without any
# manual config. On the desktop this exits immediately — that monitor
# pair has a fixed physical arrangement set statically in
# conf.d/10-appearance.conf.
set -Eeuo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/lib-outputs.sh"
is_laptop || exit 0
apply_layout() {
local outputs_json x=0 name width
outputs_json=$(swaymsg -t get_outputs -r)
mapfile -t ordered < <(output_order)
for name in "${ordered[@]}"; do
width=$(jq -r --arg n "$name" '.[] | select(.name==$n) | .rect.width' <<<"$outputs_json")
swaymsg output "$name" position "$x" 0 >/dev/null 2>&1 || true
x=$((x + width))
done
}
apply_layout
swaymsg -t subscribe -m '["output"]' | while read -r _; do
apply_layout
done
+55
View File
@@ -0,0 +1,55 @@
# Shared helpers for arranging outputs and routing workspaces across both
# machines: the laptop (whatever external display happens to be plugged
# in, if any) and the desktop (a fixed DP-3/DP-2 pair). Sourced by
# apply-outputs.sh and select-workspace.sh so both always agree on which
# output is "primary" and in what order the rest follow.
is_laptop() {
local supply
for supply in /sys/class/power_supply/*/; do
[[ -r "${supply}type" ]] || continue
[[ "$(<"${supply}type")" == Battery ]] && return 0
done
return 1
}
is_internal_panel() {
case "$1" in
eDP-*|LVDS-*|DSI-*) return 0 ;;
*) return 1 ;;
esac
}
# Prints the names of currently active outputs, one per line, ordered
# primary-first:
# - laptop: any external display(s), in the order sway reports them,
# then the internal panel last (so a docked external is
# always primary; undocked, the panel is the only entry).
# - desktop: DP-3, then DP-2, then anything else alphabetically as a
# fallback for an unexpected output.
output_order() {
local outputs_json name
outputs_json=$(swaymsg -t get_outputs -r)
mapfile -t active < <(jq -r '.[] | select(.active) | .name' <<<"$outputs_json")
local ordered=()
if is_laptop; then
local externals=() internals=()
for name in "${active[@]}"; do
if is_internal_panel "$name"; then internals+=("$name"); else externals+=("$name"); fi
done
ordered=("${externals[@]}" "${internals[@]}")
else
local preferred=(DP-3 DP-2) p seen
for p in "${preferred[@]}"; do
for name in "${active[@]}"; do [[ "$name" == "$p" ]] && ordered+=("$name"); done
done
for name in "${active[@]}"; do
seen=false
local o
for o in "${ordered[@]}"; do [[ "$o" == "$name" ]] && seen=true; done
"$seen" || ordered+=("$name")
done
fi
printf '%s\n' "${ordered[@]}"
}
+12 -7
View File
@@ -1,5 +1,7 @@
#!/usr/bin/env bash
set -Eeuo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/lib-outputs.sh"
slot=${1:-}
action=${2:-select}
@@ -9,15 +11,18 @@ if [[ ! "$slot" =~ ^([1-9]|10)$ ]]; then
exit 2
fi
# Outputs are ordered left-to-right by position, not by name, so this works
# unmodified whether there is one output (laptop) or several (desktop):
# the focused output's rank in that order selects its block of 10 workspaces.
outputs=$(swaymsg -t get_outputs -r | jq -c 'sort_by(.rect.x)')
index=$(jq -r '[.[] | .focused] | index(true)' <<<"$outputs")
if [[ "$index" == null ]]; then
printf 'No focused output found\n' >&2
mapfile -t ordered < <(output_order)
focused=$(swaymsg -t get_outputs -r | jq -r '.[] | select(.focused) | .name')
index=-1
for i in "${!ordered[@]}"; do
[[ "${ordered[$i]}" == "$focused" ]] && index=$i && break
done
if ((index < 0)); then
printf 'Focused output %s is not currently tracked\n' "$focused" >&2
exit 1
fi
workspace=$((slot + index * 10))
case "$action" in