diff --git a/config/sway/conf.d/10-appearance.conf b/config/sway/conf.d/10-appearance.conf index 2cb9536..4c00a21 100644 --- a/config/sway/conf.d/10-appearance.conf +++ b/config/sway/conf.d/10-appearance.conf @@ -16,11 +16,15 @@ output * bg ~/.config/sway/wallpapers/blue-horizon.svg fill # Desktop-only dual-monitor arrangement: DP-3 is the centered primary # display, DP-2 sits immediately to its left. Sway silently ignores # `output` directives for names that don't exist, so this is a no-op on -# the single-monitor laptop (its internal panel, e.g. eDP-1, is untouched -# and simply gets everything). Workspace-to-output routing is not pinned -# here; scripts/select-workspace.sh assigns workspaces to whichever -# outputs are actually connected, in left-to-right order, so it adapts -# automatically on both machines without any per-host config. +# the laptop, where scripts/apply-outputs.sh instead positions whatever +# external display gets plugged in dynamically (see that script and +# scripts/lib-outputs.sh for the shared laptop/desktop detection logic). +# +# Workspace-to-output routing isn't pinned here either way: +# scripts/select-workspace.sh (shared with apply-outputs.sh) assigns +# workspaces 1-10/11-20/... to whichever outputs are connected, ranked +# primary-first, so both keybinds and monitor placement always agree on +# which output is "first" without any per-host config. output DP-3 position 0 0 output DP-2 position -2560 0 diff --git a/config/sway/conf.d/40-autostart.conf b/config/sway/conf.d/40-autostart.conf index 57da1de..dc96c75 100644 --- a/config/sway/conf.d/40-autostart.conf +++ b/config/sway/conf.d/40-autostart.conf @@ -1,4 +1,5 @@ exec_always --no-startup-id sh -c 'pkill waybar; exec waybar' +exec_always --no-startup-id sh -c 'pkill -f apply-outputs.sh; exec ~/.config/sway/scripts/apply-outputs.sh' exec --no-startup-id mako exec --no-startup-id nm-applet --indicator exec --no-startup-id /usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1 diff --git a/config/sway/scripts/apply-outputs.sh b/config/sway/scripts/apply-outputs.sh new file mode 100755 index 0000000..a473a5d --- /dev/null +++ b/config/sway/scripts/apply-outputs.sh @@ -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 diff --git a/config/sway/scripts/lib-outputs.sh b/config/sway/scripts/lib-outputs.sh new file mode 100644 index 0000000..779ab01 --- /dev/null +++ b/config/sway/scripts/lib-outputs.sh @@ -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[@]}" +} diff --git a/config/sway/scripts/select-workspace.sh b/config/sway/scripts/select-workspace.sh index c57c808..ea012aa 100755 --- a/config/sway/scripts/select-workspace.sh +++ b/config/sway/scripts/select-workspace.sh @@ -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