generic eww+sway

This commit is contained in:
2026-08-04 12:14:29 +02:00
parent 36087d1b4b
commit 28b8fcb492
9 changed files with 204 additions and 167 deletions

1
.gitignore vendored
View File

@@ -24,3 +24,4 @@ pi/.pi/agent/themes
/claude/.claude/settings.json
/.claude/settings.json
/sway/.config/sway/input
.pi/

View File

@@ -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=<output>`) and opens each bar with
;; `eww open barN --screen <output>`. 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"

View File

@@ -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"))

View File

@@ -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"

View File

@@ -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

View File

@@ -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 "<name>"`, 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 <output>`, 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

View File

@@ -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

View File

@@ -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.

View File

@@ -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