feat(eww): add config

This commit is contained in:
2026-03-12 17:42:32 +01:00
parent adeb93bd24
commit c6eaad94ef
11 changed files with 1128 additions and 0 deletions

44
eww/scripts/battery.sh Executable file
View File

@@ -0,0 +1,44 @@
#!/usr/bin/env bash
# battery info as JSON for eww battery-popup
action="${1:-status}"
if [[ "$action" == "set-profile" ]]; then
powerprofilesctl set "$2"
( data=$(~/.config/eww/scripts/battery.sh); eww update bat="$data" ) &
exit 0
fi
bat_path=$(echo /sys/class/power_supply/BAT* 2>/dev/null | awk '{print $1}')
if [[ ! -d "$bat_path" ]]; then
jq -nc '{capacity:0,status:"No battery",power:"0",time:"",cycles:0,profile:"unknown"}'
exit 0
fi
capacity=$(cat "$bat_path/capacity" 2>/dev/null || echo 0)
status=$(cat "$bat_path/status" 2>/dev/null || echo "Unknown")
power_uw=$(cat "$bat_path/power_now" 2>/dev/null || echo 0)
power=$(awk -v p="$power_uw" 'BEGIN{printf "%.1f", p/1000000}')
cycles=$(cat "$bat_path/cycle_count" 2>/dev/null || echo 0)
[[ "$cycles" =~ ^[0-9]+$ ]] || cycles=0
bat_upower=$(upower -e 2>/dev/null | grep BAT | head -1)
time_str=""
if [[ -n "$bat_upower" ]]; then
time_str=$(upower -i "$bat_upower" 2>/dev/null | awk '/time to/{print $4, $5}')
fi
profile=$(powerprofilesctl get 2>/dev/null || echo "unknown")
jq -nc \
--argjson capacity "$capacity" \
--arg status "$status" \
--arg power "$power" \
--arg time "$time_str" \
--argjson cycles "${cycles:-0}" \
--arg profile "$profile" \
'{$capacity,$status,$power,$time,$cycles,$profile}'

43
eww/scripts/bluetooth.sh Executable file
View File

@@ -0,0 +1,43 @@
#!/usr/bin/env bash
# bluetooth device info as JSON for eww bluetooth-popup
action="${1:-status}"
case "$action" in
status)
powered=$(bluetoothctl show 2>/dev/null | grep -q "Powered: yes" && echo true || echo false)
devices="[]"
count=0
if [[ "$powered" == "true" ]]; then
# get connected devices in one pass
devices=$(bluetoothctl devices Connected 2>/dev/null | while read -r _ mac name; do
info=$(bluetoothctl info "$mac" 2>/dev/null)
battery=$(awk '/Battery Percentage:/{gsub(/[()]/,""); print $4}' <<< "$info")
jq -nc --arg name "$name" --arg mac "$mac" --argjson battery "${battery:--1}" \
'{$name,$mac,$battery}'
done | jq -sc '.')
[[ -z "$devices" || "$devices" == "null" ]] && devices="[]"
count=$(jq 'length' <<< "$devices" 2>/dev/null || echo 0)
fi
jq -nc \
--argjson powered "$powered" \
--argjson count "$count" \
--argjson devices "$devices" \
'{$powered,$count,$devices}'
;;
toggle-power)
if bluetoothctl show 2>/dev/null | grep -q "Powered: yes"; then
bluetoothctl power off
else
bluetoothctl power on
fi
( sleep 0.5; data=$(~/.config/eww/scripts/bluetooth.sh); eww update bt="$data" ) &
;;
disconnect)
bluetoothctl disconnect "$2"
( sleep 0.5; data=$(~/.config/eww/scripts/bluetooth.sh); eww update bt="$data" ) &
;;
esac

35
eww/scripts/keyboard.sh Executable file
View File

@@ -0,0 +1,35 @@
#!/usr/bin/env bash
# keyboard layout info as JSON for eww keyboard-popup
action="${1:-status}"
case "$action" in
status)
inputs=$(swaymsg -t get_inputs 2>/dev/null)
current=$(jq -r '[.[] | select(.type == "keyboard")] | .[0].xkb_active_layout_name // "unknown"' <<< "$inputs")
layouts=$(jq -c '[.[] | select(.type == "keyboard")] | .[0].xkb_layout_names // []' <<< "$inputs")
layout_count=$(jq 'length' <<< "$layouts" 2>/dev/null || echo 0)
keyboards=$(jq -c '[.[] | select(.type == "keyboard") |
{id: .identifier, name: .name, layout: .xkb_active_layout_name}] | unique_by(.name)' <<< "$inputs")
kb_count=$(jq 'length' <<< "$keyboards" 2>/dev/null || echo 0)
jq -nc \
--arg current "${current:-unknown}" \
--argjson layouts "${layouts:-[]}" \
--argjson layout_count "${layout_count:-0}" \
--argjson keyboards "${keyboards:-[]}" \
--argjson kb_count "${kb_count:-0}" \
'{$current,$layouts,$layout_count,$keyboards,$kb_count}'
;;
switch)
swaymsg input type:keyboard xkb_switch_layout next 2>/dev/null
( sleep 0.3; data=$(~/.config/eww/scripts/keyboard.sh); eww update kbd="$data" ) &
;;
set-layout)
swaymsg input type:keyboard xkb_switch_layout "$2" 2>/dev/null
( sleep 0.3; data=$(~/.config/eww/scripts/keyboard.sh); eww update kbd="$data" ) &
;;
esac

46
eww/scripts/media.sh Executable file
View File

@@ -0,0 +1,46 @@
#!/usr/bin/env bash
# media player info as JSON for eww media-popup
action="${1:-status}"
case "$action" in
status)
players=$(playerctl -l 2>/dev/null | head -10)
if [[ -z "$players" ]]; then
jq -nc '{count:0,players:[]}'
exit 0
fi
result="[]"
while IFS= read -r name; do
status=$(playerctl -p "$name" status 2>/dev/null || echo "Stopped")
artist=$(playerctl -p "$name" metadata artist 2>/dev/null || echo "")
title=$(playerctl -p "$name" metadata title 2>/dev/null || echo "")
album=$(playerctl -p "$name" metadata album 2>/dev/null || echo "")
# clean up player name for display
display=${name%%.*}
result=$(jq -c --arg name "$name" --arg display "$display" \
--arg status "$status" --arg artist "$artist" \
--arg title "$title" --arg album "$album" \
'. + [{name:$name, display:$display, status:$status, artist:$artist, title:$title, album:$album}]' <<< "$result")
done <<< "$players"
count=$(jq 'length' <<< "$result")
jq -nc --argjson count "$count" --argjson players "$result" '{$count,$players}'
;;
play-pause)
playerctl -p "$2" play-pause 2>/dev/null
( sleep 0.3; data=$(~/.config/eww/scripts/media.sh); eww update media="$data" ) &
;;
next)
playerctl -p "$2" next 2>/dev/null
( sleep 0.5; data=$(~/.config/eww/scripts/media.sh); eww update media="$data" ) &
;;
prev)
playerctl -p "$2" previous 2>/dev/null
( sleep 0.5; data=$(~/.config/eww/scripts/media.sh); eww update media="$data" ) &
;;
esac

80
eww/scripts/network.sh Executable file
View File

@@ -0,0 +1,80 @@
#!/usr/bin/env bash
# network info as JSON for eww network-popup
action="${1:-status}"
case "$action" in
status)
active=$(nmcli -t -f DEVICE,TYPE,STATE,CONNECTION device status 2>/dev/null \
| grep ':connected:' | head -1)
iface=$(echo "$active" | cut -d: -f1)
conn_type=$(echo "$active" | cut -d: -f2)
conn_name=$(echo "$active" | cut -d: -f4-)
ip=$(ip -4 -o addr show "$iface" 2>/dev/null | awk '{print $4}' | cut -d/ -f1)
gateway=$(ip route show default dev "$iface" 2>/dev/null | awk '{print $3}')
networks="[]"
net_count=0
# always check active wifi regardless of primary connection type
ssid=$(nmcli -t -f active,ssid dev wifi 2>/dev/null | grep '^yes' | cut -d: -f2-)
signal=$(nmcli -t -f active,signal dev wifi 2>/dev/null | grep '^yes' | cut -d: -f2-)
signal=${signal:-0}
# scan nearby wifi
saved=$(nmcli -t -f NAME connection show 2>/dev/null | sort -u)
# replace last 3 colons with tabs to handle SSIDs containing colons
all_wifi=$(nmcli -t -f SSID,SIGNAL,SECURITY,IN-USE dev wifi list --rescan no 2>/dev/null \
| sed 's/:\([^:]*\):\([^:]*\):\([^:]*\)$/\t\1\t\2\t\3/' \
| awk -F'\t' 'NF>=3 && $1!=""' \
| sort -t$'\t' -k4,4r -k2,2rn \
| awk -F'\t' '!seen[$1]++')
# known networks nearby
networks=$(echo "$all_wifi" \
| while IFS=$'\t' read -r s sig sec use; do
echo "$saved" | grep -qxF "$s" && printf '%s\t%s\n' "$s" "$sig"
done \
| head -10 \
| jq -Rnc --arg active "$ssid" '[inputs | split("\t") |
{ssid:.[0], signal:(.[1]|tonumber), active:(.[0] == $active)}]')
net_count=$(jq 'length' <<< "$networks" 2>/dev/null || echo 0)
# unknown networks nearby
unknown=$(echo "$all_wifi" \
| while IFS=$'\t' read -r s sig sec use; do
echo "$saved" | grep -qxF "$s" || printf '%s\t%s\t%s\n' "$s" "$sig" "$sec"
done \
| head -5 \
| jq -Rnc '[inputs | split("\t") |
{ssid:.[0], signal:(.[1]|tonumber), security:.[2]}]')
unknown_count=$(jq 'length' <<< "$unknown" 2>/dev/null || echo 0)
jq -nc \
--arg type "${conn_type:-none}" \
--arg iface "${iface:-none}" \
--arg ip "${ip:-none}" \
--arg gateway "${gateway:-none}" \
--arg ssid "$ssid" \
--argjson signal "${signal:-0}" \
--arg conn_name "$conn_name" \
--argjson count "${net_count:-0}" \
--argjson networks "${networks:-[]}" \
--argjson unknown_count "${unknown_count:-0}" \
--argjson unknown "${unknown:-[]}" \
'{$type,$iface,$ip,$gateway,$ssid,$signal,$conn_name,$count,$networks,$unknown_count,$unknown}'
;;
connect)
( nmcli dev wifi connect "$2" 2>/dev/null; sleep 1; data=$(~/.config/eww/scripts/network.sh); eww update net="$data" ) &
;;
disconnect)
( nmcli connection down "$2" 2>/dev/null; sleep 1; data=$(~/.config/eww/scripts/network.sh); eww update net="$data" ) &
;;
connect-new)
ssid="$2"
pass=$(zenity --entry --hide-text --title="WiFi" --text="Password for $ssid" 2>/dev/null)
( [[ -n "$pass" ]] && nmcli dev wifi connect "$ssid" password "$pass" 2>/dev/null; sleep 1; data=$(~/.config/eww/scripts/network.sh); eww update net="$data" ) &
;;
esac

29
eww/scripts/popup.sh Executable file
View File

@@ -0,0 +1,29 @@
#!/usr/bin/env bash
# toggle an eww popup
# opens on the currently focused monitor
POPUPS=(system-popup battery-popup network-popup vpn-popup volume-popup bluetooth-popup keyboard-popup media-popup)
target="$1"
if [[ -z "$target" ]]; then
echo "usage: popup.sh <popup-name|close-all>" >&2
exit 1
fi
if [[ "$target" == "close-all" ]]; then
eww close "${POPUPS[@]}" 2>/dev/null
exit 0
fi
# check if target is already open
if eww active-windows 2>/dev/null | grep -q "$target"; then
eww close "$target" 2>/dev/null
else
# close others, open popup
screen=$(swaymsg -t get_outputs 2>/dev/null \
| jq '[.[] | .focused] | index(true) // 0')
eww close "${POPUPS[@]}" 2>/dev/null
eww open --screen "${screen:-0}" "$target"
fi

40
eww/scripts/system.sh Executable file
View File

@@ -0,0 +1,40 @@
#!/usr/bin/env bash
# system metrics as JSON for eww system-popup
# delta-based cpu: sample /proc/stat twice 0.5s apart
read -r _ u1 n1 s1 i1 _ < /proc/stat
sleep 0.5
read -r _ u2 n2 s2 i2 _ < /proc/stat
cpu=$(( (u2+n2+s2 - u1-n1-s1) * 100 / (u2+n2+s2+i2 - u1-n1-s1-i1) ))
ram_info=$(free -b | awk '/^Mem:/{printf "%.0f %.1f %.1f", $3/$2*100, $3/1073741824, $2/1073741824}')
ram_percent=$(awk '{print $1}' <<< "$ram_info")
ram_used=$(awk '{printf "%.1f", $2}' <<< "$ram_info")
ram_total=$(awk '{printf "%.1f", $3}' <<< "$ram_info")
temp=$(cat /sys/class/thermal/thermal_zone*/temp 2>/dev/null | sort -rn | head -1)
temp=$(( ${temp:-0} / 1000 ))
disk_info=$(df -h / | awk 'NR==2{gsub(/%/,""); printf "%s %s %s", $5, $3, $2}')
disk_percent=$(awk '{print $1}' <<< "$disk_info")
disk_used=$(awk '{print $2}' <<< "$disk_info")
disk_total=$(awk '{print $3}' <<< "$disk_info")
swap=$(free -h | awk '/^Swap:/{printf "%s/%s", $3, $2}')
load=$(awk '{print $1, $2, $3}' /proc/loadavg)
uptime_str=$(uptime -p 2>/dev/null | sed 's/up //' || echo "n/a")
jq -nc \
--argjson cpu "${cpu:-0}" \
--argjson ram_percent "${ram_percent:-0}" \
--arg ram_used "${ram_used:-0}Gi" \
--arg ram_total "${ram_total:-0}Gi" \
--argjson temp "${temp:-0}" \
--argjson disk_percent "${disk_percent:-0}" \
--arg disk_used "$disk_used" \
--arg disk_total "$disk_total" \
--arg swap "$swap" \
--arg load "$load" \
--arg uptime "$uptime_str" \
'{$cpu,$ram_percent,$ram_used,$ram_total,$temp,$disk_percent,$disk_used,$disk_total,$swap,$load,$uptime}'

62
eww/scripts/volume.sh Executable file
View File

@@ -0,0 +1,62 @@
#!/usr/bin/env bash
# audio + brightness info for eww volume-popup
action="${1:-status}"
case "$action" in
status)
default_sink=$(pactl get-default-sink 2>/dev/null)
default_source=$(pactl get-default-source 2>/dev/null)
# get all sink+source info in one pactl call, extract volume/mute + device lists
all_data=$(pactl --format=json list sinks 2>/dev/null)
sinks=$(jq -c --arg d "$default_sink" \
'[.[] | {name: .description, sink_name: .name, active: (.name == $d)}]' <<< "$all_data" 2>/dev/null || echo '[]')
# extract default sink volume+mute
volume=$(jq --arg d "$default_sink" \
'[.[] | select(.name == $d)][0] | .volume | to_entries[0].value.value_percent | rtrimstr("%") | tonumber' <<< "$all_data" 2>/dev/null || echo 0)
muted=$(jq --arg d "$default_sink" \
'[.[] | select(.name == $d)][0].mute' <<< "$all_data" 2>/dev/null || echo false)
all_sources=$(pactl --format=json list sources 2>/dev/null)
sources=$(jq -c --arg d "$default_source" \
'[.[] | select(.name | test("monitor$") | not) | {name: .description, source_name: .name, active: (.name == $d)}]' <<< "$all_sources" 2>/dev/null || echo '[]')
mic_volume=$(jq --arg d "$default_source" \
'[.[] | select(.name == $d)][0] | .volume | to_entries[0].value.value_percent | rtrimstr("%") | tonumber' <<< "$all_sources" 2>/dev/null || echo 0)
mic_muted=$(jq --arg d "$default_source" \
'[.[] | select(.name == $d)][0].mute' <<< "$all_sources" 2>/dev/null || echo false)
brightness=$(brightnessctl -m 2>/dev/null | cut -d, -f5 | tr -d '%')
sink_count=$(jq 'length' <<< "$sinks" 2>/dev/null || echo 0)
source_count=$(jq 'length' <<< "$sources" 2>/dev/null || echo 0)
jq -nc \
--argjson volume "${volume:-0}" \
--argjson muted "${muted:-false}" \
--argjson mic_volume "${mic_volume:-0}" \
--argjson mic_muted "${mic_muted:-false}" \
--argjson brightness "${brightness:-0}" \
--argjson sinks "${sinks:-[]}" \
--argjson sources "${sources:-[]}" \
--argjson sink_count "${sink_count:-0}" \
--argjson source_count "${source_count:-0}" \
'{$volume,$muted,$mic_volume,$mic_muted,$brightness,$sinks,$sources,$sink_count,$source_count}'
;;
set-sink)
pactl set-default-sink "$2"
( data=$(~/.config/eww/scripts/volume.sh); eww update vol="$data" ) &
;;
set-source)
pactl set-default-source "$2"
( data=$(~/.config/eww/scripts/volume.sh); eww update vol="$data" ) &
;;
toggle-mute)
pamixer -t
( data=$(~/.config/eww/scripts/volume.sh); eww update vol="$data" ) &
;;
toggle-mic)
pamixer --default-source -t
( data=$(~/.config/eww/scripts/volume.sh); eww update vol="$data" ) &
;;
esac

89
eww/scripts/vpn.sh Executable file
View File

@@ -0,0 +1,89 @@
#!/usr/bin/env bash
# vpn status as JSON for eww vpn-popup
action="${1:-status}"
case "$action" in
status)
ts_running=false
ts_ip=""
ts_hostname=""
ts_login=""
ts_exit_nodes="[]"
ts_exit_count=0
ts_peers="[]"
ts_peer_count=0
if command -v tailscale &>/dev/null; then
ts_json=$(tailscale status --json 2>/dev/null)
state=$(jq -r '.BackendState // empty' <<< "$ts_json")
if [[ "$state" == "Running" ]]; then
ts_running=true
ts_ip=$(jq -r '.TailscaleIPs[0] // empty' <<< "$ts_json")
ts_hostname=$(jq -r '.Self.HostName // empty' <<< "$ts_json")
ts_login=$(jq -r '.CurrentTailnet.Name // empty' <<< "$ts_json")
ts_exit_nodes=$(jq -c '[.Peer | to_entries[]? |
select(.value.ExitNodeOption) |
{id: .key, name: .value.HostName, ip: (.value.TailscaleIPs[0] // ""), active: (.value.ExitNode // false)}
] // []' <<< "$ts_json" 2>/dev/null || echo '[]')
ts_exit_count=$(jq 'length' <<< "$ts_exit_nodes" 2>/dev/null || echo 0)
ts_peers=$(jq -c '[.Peer | to_entries[]? |
{name: .value.HostName, ip: (.value.TailscaleIPs[0] // ""), online: .value.Online}
] // []' <<< "$ts_json" 2>/dev/null || echo '[]')
ts_peer_count=$(jq 'length' <<< "$ts_peers" 2>/dev/null || echo 0)
fi
fi
wg_active=false
wg_iface=""
if command -v wg &>/dev/null; then
wg_iface=$(wg show interfaces 2>/dev/null | head -1)
[[ -n "$wg_iface" ]] && wg_active=true
fi
jq -nc \
--argjson ts_running "$ts_running" \
--arg ts_ip "$ts_ip" \
--arg ts_hostname "$ts_hostname" \
--arg ts_login "$ts_login" \
--argjson ts_exit_nodes "$ts_exit_nodes" \
--argjson ts_exit_count "$ts_exit_count" \
--argjson ts_peers "$ts_peers" \
--argjson ts_peer_count "$ts_peer_count" \
--argjson wg_active "$wg_active" \
--arg wg_iface "$wg_iface" \
'{tailscale:{running:$ts_running,ip:$ts_ip,hostname:$ts_hostname,login:$ts_login,
exit_nodes:$ts_exit_nodes,exit_count:$ts_exit_count,
peers:$ts_peers,peer_count:$ts_peer_count},
wireguard:{active:$wg_active,iface:$wg_iface}}'
;;
ts-up)
tailscale up 2>/dev/null
( sleep 1; data=$(~/.config/eww/scripts/vpn.sh); eww update vpn_data="$data" ) &
;;
ts-down)
tailscale down 2>/dev/null
( sleep 1; data=$(~/.config/eww/scripts/vpn.sh); eww update vpn_data="$data" ) &
;;
ts-exit)
if [[ -n "$2" ]]; then
tailscale set --exit-node="$2" 2>/dev/null
else
tailscale set --exit-node="" 2>/dev/null
fi
( sleep 0.5; data=$(~/.config/eww/scripts/vpn.sh); eww update vpn_data="$data" ) &
;;
wg-up)
sudo wg-quick up "${2:-wg0}" 2>/dev/null
( sleep 1; data=$(~/.config/eww/scripts/vpn.sh); eww update vpn_data="$data" ) &
;;
wg-down)
iface="${2:-$(wg show interfaces 2>/dev/null | head -1)}"
sudo wg-quick down "${iface:-wg0}" 2>/dev/null
( sleep 1; data=$(~/.config/eww/scripts/vpn.sh); eww update vpn_data="$data" ) &
;;
esac