From 28b8fcb492fe8190cca0e65b01968b894b522775 Mon Sep 17 00:00:00 2001 From: Jonas Haugesen Date: Tue, 4 Aug 2026 12:14:29 +0200 Subject: [PATCH] generic eww+sway --- .gitignore | 1 + eww/.config/eww/eww.yuck | 93 +++++++++++++++++++--- eww/.config/eww/eww.yuck.backup | 92 --------------------- eww/.config/eww/scripts/vpn-toggle | 9 ++- sway/.config/sway/monitors | 29 +++---- sway/.config/sway/scripts/eww-bars.sh | 64 ++++++++++----- sway/.config/sway/scripts/hdmi-setup.sh | 68 ++++++++++++---- sway/.config/sway/scripts/watch-outputs.sh | 5 +- sway/.config/sway/windowrules | 10 +-- 9 files changed, 204 insertions(+), 167 deletions(-) delete mode 100644 eww/.config/eww/eww.yuck.backup diff --git a/.gitignore b/.gitignore index 7d68aa2..efccee9 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ pi/.pi/agent/themes /claude/.claude/settings.json /.claude/settings.json /sway/.config/sway/input +.pi/ diff --git a/eww/.config/eww/eww.yuck b/eww/.config/eww/eww.yuck index 816f8e4..eae5128 100644 --- a/eww/.config/eww/eww.yuck +++ b/eww/.config/eww/eww.yuck @@ -14,6 +14,22 @@ (defvar gap 15) (defvar gap-tight 4) +;; --------------------------------------------------------------------------- +;; Bar monitor slots +;; --------------------------------------------------------------------------- +;; Which output each bar slot lives on is machine-specific, so no connector +;; names are hardcoded here. eww-bars.sh computes the mapping at runtime from +;; sway's active outputs (sorted left-to-right), updates the defvars below +;; (`eww update barN-monitor=`) and opens each bar with +;; `eww open barN --screen `. The defwindow `:monitor ""` placeholders +;; are never honored in normal use (bars are always opened with --screen); they +;; only satisfy eww's requirement that the attribute is present. + +(defvar bar0-monitor "") +(defvar bar1-monitor "") +(defvar bar2-monitor "") +(defvar bar3-monitor "") + ;; --------------------------------------------------------------------------- ;; Workspaces ;; --------------------------------------------------------------------------- @@ -82,10 +98,10 @@ :initial '{"icon":"󰪎","vpn":false,"vpnname":"asahi","vpnstate":"missing","wifi":false,"ssid":"","wifidev":"","signal":0,"wired":false,"wireddev":"","wiredconn":"","online":false,"tooltip":"No connection"}' "bash ~/.config/eww/scripts/net-info") -;; Bar entry. Click opens the network popup for this monitor. -(defwidget internet [popup] +;; Bar entry. Click opens the network popup on this bar's monitor. +(defwidget internet [popup monitor] (eventbox :class "net-eventbox" - :onclick "eww open --toggle ${popup}" + :onclick "eww open --toggle ${popup} --screen ${monitor}" :tooltip {net.tooltip} (label :text {net.icon} :class "net-icon ${net.vpn ? 'net-vpn-on' : (net.online ? 'net-online' : 'net-offline')}"))) @@ -183,10 +199,10 @@ ;; Bar ;; --------------------------------------------------------------------------- -(defwidget system [popup] +(defwidget system [popup monitor] (box :class "system" :orientation "h" :halign "end" :space-evenly false :spacing gap (signal-widget) - (internet :popup popup) + (internet :popup popup :monitor monitor) (brightness-widget) (volume-widget) (battery-widget) @@ -197,10 +213,10 @@ (box :class "bar" :orientation "h" :space-evenly false (workspaces :monitor monitor) (box :hexpand true) - (system :popup popup))) + (system :popup popup :monitor monitor))) (defwindow bar0 - :monitor "HDMI-A-1" + :monitor "" :geometry (geometry :x "0%" :y "0px" :width "100%" @@ -211,10 +227,10 @@ :reserve (struts :distance "40px" :side "top") :windowtype "dock" :wm-ignore false - (bar :monitor "HDMI-A-1" :popup "net-popup0")) + (bar :monitor bar0-monitor :popup "net-popup0")) (defwindow bar1 - :monitor "eDP-1" + :monitor "" :geometry (geometry :x "0%" :y "0px" :width "100%" @@ -225,12 +241,41 @@ :reserve (struts :distance "40px" :side "top") :windowtype "dock" :wm-ignore false - (bar :monitor "eDP-1" :popup "net-popup1")) + (bar :monitor bar1-monitor :popup "net-popup1")) + +(defwindow bar2 + :monitor "" + :geometry (geometry :x "0%" + :y "0px" + :width "100%" + :height "30px" + :anchor "top center") + :stacking "fg" + :exclusive true + :reserve (struts :distance "40px" :side "top") + :windowtype "dock" + :wm-ignore false + (bar :monitor bar2-monitor :popup "net-popup2")) + +(defwindow bar3 + :monitor "" + :geometry (geometry :x "0%" + :y "0px" + :width "100%" + :height "30px" + :anchor "top center") + :stacking "fg" + :exclusive true + :reserve (struts :distance "40px" :side "top") + :windowtype "dock" + :wm-ignore false + (bar :monitor bar3-monitor :popup "net-popup3")) ;; Popups sit under the bar on the same monitor. The bar reserves 40px at the -;; top, so anchoring at "top right" already lands below it. +;; top, so anchoring at "top right" already lands below it. Opened from the +;; network widget's click handler with `--screen ${barN-monitor}`. (defwindow net-popup0 - :monitor "HDMI-A-1" + :monitor "" :geometry (geometry :x "10px" :y "0px" :width "330px" @@ -241,7 +286,29 @@ (net-popup-content)) (defwindow net-popup1 - :monitor "eDP-1" + :monitor "" + :geometry (geometry :x "10px" + :y "0px" + :width "330px" + :anchor "top right") + :stacking "overlay" + :exclusive false + :focusable false + (net-popup-content)) + +(defwindow net-popup2 + :monitor "" + :geometry (geometry :x "10px" + :y "0px" + :width "330px" + :anchor "top right") + :stacking "overlay" + :exclusive false + :focusable false + (net-popup-content)) + +(defwindow net-popup3 + :monitor "" :geometry (geometry :x "10px" :y "0px" :width "330px" diff --git a/eww/.config/eww/eww.yuck.backup b/eww/.config/eww/eww.yuck.backup deleted file mode 100644 index a734ad7..0000000 --- a/eww/.config/eww/eww.yuck.backup +++ /dev/null @@ -1,92 +0,0 @@ -(deflisten workspaces :initial "[]" "bash ~/.config/eww/scripts/get-workspaces") - -(deflisten current_workspace :initial "1" "bash ~/.config/eww/scripts/get-active-workspace") - -(defwidget workspaces [monitor] - (eventbox :onscroll "bash ~/.config/eww/scripts/change-active-workspace {} ${current_workspace}" :class "workspaces-widget" - (box :spacing 10 :orientation "h" :halign "start" :space-evenly false - (label :text "${workspaces}${current_workspace}" :visible false) - (for workspace in workspaces - (box - :visible {workspace.output == monitor} - (eventbox :onclick "swaymsg workspace number ${workspace.id}" - (box :class "workspace-entry ${workspace.focused ? 'current' : ''} ${workspace.urgent ? 'urgent' : ''}" - (label :text "${workspace.id}")))))))) - - -(defpoll time :interval "1s" - "date '+%H:%M'") - - -(defwidget time-widget [] - (label - :text {time} - :class "time-label")) - -(deflisten BATPERC :initial "" "bash ~/.config/eww/scripts/battery-perc-monitor") -(deflisten BATICON :initial "" "bash ~/.config/eww/scripts/battery-icon-monitor") - -(defwidget battery-widget [] - (box :orientation "horizontal" :spacing 0 - (label :text {BATICON} :class "battery-icon") - (label :text {BATPERC} :class "battery-percent"))) - -(defwidget wifi [] - (label :text wifi-icon - :class "wifi-icon")) -(deflisten wifi-icon :initial "󰪎" "bash ~/.config/eww/scripts/wifi-monitor") -(deflisten wifi-name :initial "" "bash ~/.config/eww/scripts/wifi-name-monitor") - -(deflisten brightness :initial "50" "bash ~/.config/eww/scripts/brightness") - -(defwidget brightness-widget [] - (eventbox :onscroll "[ {} = up ] && brightnessctl set 5%+ || brightnessctl set 5%-" - (box :orientation "h" :spacing 0 - (label :text "󰌵" :class "brightness-icon") - (scale :min 0 - :max 100 - :value brightness - :class "brightness-bar" - :onchange "brightnessctl set {}%")))) - -(deflisten volume :initial "50" "bash ~/.config/eww/scripts/volume") - -(defwidget volume-widget [] - (box :orientation "h" :spacing 0 - (label :text "" :class "volume-icon") - (scale :min 0 - :max 100 - :value volume - :class "volume-bar" - :onchange "pactl set-sink-volume @DEFAULT_SINK@ {}%"))) - -(defwidget system [] - (box :orientation "h" :halign "end" :spacing 0 - (brightness-widget) - (volume-widget) - (wifi) - (battery-widget) - (time-widget))) - - -(defwidget bar [monitor] - (box :orientation "h" :space-evenly false - (workspaces :monitor monitor) - (box :hexpand true) - (system))) - -(defwindow bar0 - :monitor 0 - :geometry (geometry :x "0%" - :y "0px" - :width "100%" - :height "30px" - :anchor "top center") - :stacking "fg" - :exclusive true - :reserve (struts :distance "40px" :side "top") - :windowtype "dock" - :wm-ignore false - (bar :monitor "eDP-1")) - - diff --git a/eww/.config/eww/scripts/vpn-toggle b/eww/.config/eww/scripts/vpn-toggle index aab8b4b..9978e83 100755 --- a/eww/.config/eww/scripts/vpn-toggle +++ b/eww/.config/eww/scripts/vpn-toggle @@ -1,10 +1,13 @@ #!/bin/bash -# Toggle the "asahi" WireGuard connection in NetworkManager. +# Toggle a WireGuard connection in NetworkManager. # Usage: vpn-toggle [toggle|up|down|status] +# The connection name is machine-specific - override with VPN_CONN, e.g.: +# VPN_CONN=myvpn vpn-toggle +# Defaults to "asahi" for backwards compatibility. # Requires the connection to be imported first: -# sudo nmcli connection import type wireguard file /etc/wireguard/asahi.conf +# sudo nmcli connection import type wireguard file /etc/wireguard/${CONN}.conf -CONN="asahi" +CONN="${VPN_CONN:-asahi}" is_up() { nmcli -t -f NAME connection show --active 2>/dev/null | grep -qx "$CONN" diff --git a/sway/.config/sway/monitors b/sway/.config/sway/monitors index d60ff1a..21b64ba 100644 --- a/sway/.config/sway/monitors +++ b/sway/.config/sway/monitors @@ -1,18 +1,15 @@ -# MACHINE-SPECIFIC: output names and resolutions vary per device. -# Run `swaymsg -t get_outputs` to find your output names. -set $res_x 3024 -set $res_y 1890 -set $resolution 3024x1890 -set $scale 1.5 - -output eDP-1 resolution $resolution scale $scale -output DP-1 resolution $resolution scale $scale +# Output rules (connector names, resolution, scale, position) vary from machine +# to machine, so none are tracked here. Put them in local.d/monitors on each +# machine - that directory is gitignored (see local.d/.gitignore) and included +# at the end of the sway config, so its `output` lines override anything here. +# +# Find your connector names with: swaymsg -t get_outputs +# +# Example for a laptop with an external 4K on the dock: +# output eDP-1 scale 1.5 +# output HDMI-A-1 position 0 0 scale 1.5 +# exec_always ~/.config/sway/scripts/hdmi-setup.sh # auto-arrange on hotplug +# Generic: when one external display is connected, put it at (0,0) and the +# internal panel below it. No-op otherwise (see the script for details). exec_always ~/.config/sway/scripts/hdmi-setup.sh - -output DVI-I-2 position -2560,0 scale 1.5 -output DVI-I-1 position -5120,0 scale 1.5 - - - -# You can get the names of your outputs by running: swaymsg -t get_outputs diff --git a/sway/.config/sway/scripts/eww-bars.sh b/sway/.config/sway/scripts/eww-bars.sh index d28939f..bf4d3aa 100755 --- a/sway/.config/sway/scripts/eww-bars.sh +++ b/sway/.config/sway/scripts/eww-bars.sh @@ -1,12 +1,21 @@ #!/usr/bin/env bash # Reconcile the eww bars with the currently-connected sway outputs. # -# Mapping: eww window -> output (connector name) it must live on. -# Each defwindow in ~/.config/eww/eww.yuck is bound to the SAME name via -# `:monitor ""`, so a bar's workspace list and its physical placement -# can never drift apart. (The old numeric `:monitor 0/1` indices were not -# stable across sway reloads / display hotplug, which is what mixed up the -# bars: e.g. the internal panel showed the external display's workspaces.) +# Mapping: bar slot barN -> Nth active output (sorted left-to-right, then +# alphabetically for ties, so the order is stable when outputs share an x +# position, e.g. stacked vertically). The mapping is computed from +# `swaymsg -t get_outputs` at runtime - no connector names are hardcoded, so +# the same dotfiles work on any machine. +# +# For each open bar the `barN-monitor` defvar in eww.yuck is set via +# `eww update` (this drives the workspace filter inside the bar) and the window +# is placed with `eww open barN --screen `, so the defwindow's +# `:monitor` placeholder in eww.yuck is never honored. +# +# To pin bars to specific outputs on one machine (e.g. no bar on a dock +# monitor), set EWW_BAR_OUTPUTS to a space-separated list of output names, e.g. +# from ~/.profile or a sway `exec` line: +# EWW_BAR_OUTPUTS="HDMI-A-1 eDP-1" sway-bar-script... # # Usage: # eww-bars.sh Open bars whose monitor is connected, close the rest. @@ -14,33 +23,44 @@ # eww-bars.sh --force Also re-place already-open bars (close + reopen). # Use right after an external display is (dis)connected, # so a stale layer-surface can't keep a bar hidden. -# -# To support another monitor, just add a `window -> connector` pair below and -# a matching `defwindow` in eww.yuck. set -u -declare -A BARS=( - [bar0]="HDMI-A-1" # external - [bar1]="eDP-1" # internal / built-in -) +N_SLOTS=4 # must match the defwindows in eww.yuck (bar0..bar3) force=0 [[ "${1:-}" == "--force" ]] && force=1 eww daemon >/dev/null 2>&1 || true +# Give the daemon a moment to come up (it is started asynchronously). +for _ in {1..10}; do + eww ping >/dev/null 2>&1 && break + sleep 0.2 +done -output_active() { - swaymsg -t get_outputs 2>/dev/null \ - | jq -e --arg n "$1" 'any(.[]; .name == $n and .active)' >/dev/null 2>&1 +# Active outputs, left-to-right. Overridable per machine via EWW_BAR_OUTPUTS. +if [[ -n "${EWW_BAR_OUTPUTS:-}" ]]; then + IFS=' ' read -r -a OUTS <<< "$EWW_BAR_OUTPUTS" +else + mapfile -t OUTS < <( + swaymsg -t get_outputs 2>/dev/null \ + | jq -r '[.[] | select(.active)] | sort_by(.rect.x, .name) | .[].name' + ) +fi + +open_slot() { + local slot="$1" out="$2" + eww update "bar${slot}-monitor=$out" >/dev/null 2>&1 + if (( force )); then + eww close "bar$slot" >/dev/null 2>&1 + fi + eww open "bar$slot" --screen "$out" >/dev/null 2>&1 || true } -for win in "${!BARS[@]}"; do - out="${BARS[$win]}" - if output_active "$out"; then - (( force )) && eww close "$win" >/dev/null 2>&1 - eww open "$win" >/dev/null 2>&1 || true +for (( i = 0; i < N_SLOTS; i++ )); do + if [[ -n "${OUTS[$i]:-}" ]]; then + open_slot "$i" "${OUTS[$i]}" else - eww close "$win" >/dev/null 2>&1 || true + eww close "bar$i" >/dev/null 2>&1 || true fi done diff --git a/sway/.config/sway/scripts/hdmi-setup.sh b/sway/.config/sway/scripts/hdmi-setup.sh index c4ac236..10b5ffc 100755 --- a/sway/.config/sway/scripts/hdmi-setup.sh +++ b/sway/.config/sway/scripts/hdmi-setup.sh @@ -1,23 +1,61 @@ #!/bin/bash +# Arrange the built-in panel and one external display (if connected) so they +# never overlap: the external goes to (0,0), the internal panel below it. +# +# Everything is read from sway IPC at runtime - no output names, resolutions +# or scales are hardcoded, so this works on any machine. The external's scale +# follows one heuristic: 4K -> 1.5, otherwise 1.0. Per-machine scale/resolution +# preferences belong in ~/.config/sway/local.d/monitors (e.g. `output eDP-1 +# scale 1.5`), which is applied before this script runs. +# +# If more than one external is connected, do nothing: a multi-monitor +# arrangement is machine-specific and belongs in local.d/monitors as static +# `output ... position ...` rules (or a machine-local script). -OUTPUTS=$(swaymsg -t get_outputs) -HDMI_CONNECTED=$(echo "$OUTPUTS" | jq '[.[] | select(.name == "HDMI-A-1")] | length > 0') +set -euo pipefail -if [ "$HDMI_CONNECTED" = "true" ]; then - IS_4K=$(echo "$OUTPUTS" | jq '[.[] | select(.name == "HDMI-A-1") | .modes[].width] | max >= 3840') +# Built-in (internal) panels use these connector name prefixes. +INTERNAL_RE='^(eDP|LVDS|DSI)' - if [ "$IS_4K" = "true" ]; then - RES_H=2160 - SCALE=1.5 - swaymsg output HDMI-A-1 resolution 3840x2160 scale $SCALE pos 0 0 - else - RES_H=1440 - SCALE=1.0 - swaymsg output HDMI-A-1 resolution 2560x1440 scale $SCALE pos 0 0 +outputs=$(swaymsg -t get_outputs) || exit 1 + +# All active outputs. +mapfile -t active < <(jq -r '.[] | select(.active) | .name' <<<"$outputs") +(( ${#active[@]} )) || exit 0 + +# The internal panel, if any. With no built-in panel this is a desktop: +# leave the whole arrangement to local.d/monitors. +internal="" +for o in "${active[@]}"; do + if [[ "$o" =~ $INTERNAL_RE ]]; then + internal="$o" + break fi +done +[[ -n "$internal" ]] || exit 0 - HDMI_LOGICAL_H=$(echo "$RES_H / $SCALE" | bc) - swaymsg output eDP-1 resolution 3024x1890 scale 1.5 pos 0 $HDMI_LOGICAL_H +# Active external outputs, left-to-right. +mapfile -t externals < <( + jq -r '[.[] | select(.active) | select(.name | test("^(eDP|LVDS|DSI)") | not)] + | sort_by(.rect.x, .name) | .[].name' <<<"$outputs" +) +(( ${#externals[@]} )) || exit 0 # only the internal panel: nothing to arrange +(( ${#externals[@]} == 1 )) || exit 0 # multi-monitor rig: leave to local.d + +ext="${externals[0]}" +ext_w=$(jq -r --arg n "$ext" '.[] | select(.name == $n) | .current_mode.width' <<<"$outputs") +ext_h=$(jq -r --arg n "$ext" '.[] | select(.name == $n) | .current_mode.height' <<<"$outputs") + +# Scale heuristic: 4K external -> 1.5, otherwise 1.0. +if (( ext_w >= 3840 )); then + ext_scale=1.5 else - swaymsg output eDP-1 resolution 3024x1890 scale 1.5 pos 0 0 + ext_scale=1.0 +fi +ext_logical_h=$(awk -v h="$ext_h" -v s="$ext_scale" 'BEGIN { printf "%d", h / s }') + +swaymsg output "$ext" scale "$ext_scale" pos 0 0 + +if [[ -n "$internal" ]]; then + swaymsg output "$internal" pos 0 "$ext_logical_h" fi diff --git a/sway/.config/sway/scripts/watch-outputs.sh b/sway/.config/sway/scripts/watch-outputs.sh index 9a59bb8..130b7ea 100755 --- a/sway/.config/sway/scripts/watch-outputs.sh +++ b/sway/.config/sway/scripts/watch-outputs.sh @@ -17,6 +17,9 @@ flock -n 9 || exit 0 # another instance is already running BARS_SCRIPT="$HOME/.config/sway/scripts/eww-bars.sh" +# Built-in (internal) panels use these connector name prefixes. +is_internal() { [[ "$1" == eDP* || "$1" == LVDS* || "$1" == DSI* ]]; } + declare -A known_outputs list_outputs() { swaymsg -t get_outputs | jq -r '.[].name'; } @@ -42,7 +45,7 @@ on_output_event() { changed=1 # Skip built-in display - [[ "$name" == eDP* ]] && continue + is_internal "$name" && continue # External display connected — claim workspaces 1-3 and reload so the # per-output position/scale rules (hdmi-setup.sh) are reapplied. diff --git a/sway/.config/sway/windowrules b/sway/.config/sway/windowrules index 20c5377..d598736 100644 --- a/sway/.config/sway/windowrules +++ b/sway/.config/sway/windowrules @@ -13,10 +13,10 @@ for_window [class="destinations"] floating enable for_window [app_id="evidence-quest"] floating enable # Workaround for wezterm fractional-scaling bug (wezterm#5875 / #6463): on a -# fractional-scaled output (DP-1 @ 1.5x) a newly mapped wezterm window renders its -# terminal area as a small rectangle until it receives a configure event. The -# window is held transparent from its first frame and revealed by the nudge script -# only after it has been forced to re-configure (resize nudge, or float-toggle when -# alone). See scripts/wezterm-nudge.sh for details. +# fractional-scaled output (e.g. a 4K display at 1.5x) a newly mapped wezterm +# window renders its terminal area as a small rectangle until it receives a +# configure event. The window is held transparent from its first frame and +# revealed by the nudge script only after it has been forced to re-configure +# (resize nudge, or float-toggle when alone). See scripts/wezterm-nudge.sh. for_window [app_id="org.wezfurlong.wezterm"] opacity 0 for_window [app_id="org.wezfurlong.wezterm"] exec ~/.config/sway/scripts/wezterm-nudge.sh