From d7cb42a64c554793e42f271ccad8de1cf216cad8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Jane=C5=BEi=C4=8D?= Date: Thu, 12 Mar 2026 17:43:03 +0100 Subject: [PATCH] feat(waybar): add config --- waybar/config.jsonc | 179 ++++++++++++++++++++ waybar/scripts/mic-status.sh | 12 ++ waybar/scripts/notification-status.sh | 14 ++ waybar/scripts/ssh-session.sh | 21 +++ waybar/scripts/vpn-status.sh | 40 +++++ waybar/style.css | 232 ++++++++++++++++++++++++++ 6 files changed, 498 insertions(+) create mode 100644 waybar/config.jsonc create mode 100755 waybar/scripts/mic-status.sh create mode 100755 waybar/scripts/notification-status.sh create mode 100755 waybar/scripts/ssh-session.sh create mode 100755 waybar/scripts/vpn-status.sh create mode 100644 waybar/style.css diff --git a/waybar/config.jsonc b/waybar/config.jsonc new file mode 100644 index 0000000..8ebb038 --- /dev/null +++ b/waybar/config.jsonc @@ -0,0 +1,179 @@ +[ + { + "layer": "top", + "position": "top", + "height": 30, + "spacing": 1, + "margin": 0, + "modules-left": [ + "sway/workspaces", + "sway/mode" + ], + "modules-center": [ + "custom/ssh", + "custom/notification", + "systemd-failed-units" + ], + "modules-right": [ + "mpris", + "custom/vpn", + "cpu", + "memory", + "battery", + "wireplumber", + "custom/mic", + "backlight", + "sway/language", + "bluetooth", + "network", + "clock" + ], + + "sway/workspaces": { + "disable-scroll": true, + "format": "{name}" + }, + + "sway/mode": { + "format": "{}" + }, + + "sway/language": { + "format": "{short}", + "on-click": "swaymsg input type:keyboard xkb_switch_layout next", + "on-click-right": "~/.config/eww/scripts/popup.sh keyboard-popup" + }, + + "custom/ssh": { + "format": "{}", + "return-type": "json", + "interval": 15, + "exec": "~/.config/waybar/scripts/ssh-session.sh", + "on-click": "~/.config/waybar/scripts/ssh-session.sh disconnect" + }, + + "custom/notification": { + "format": "{}", + "return-type": "json", + "exec": "~/.config/waybar/scripts/notification-status.sh", + "on-click": "sleep 0.1 && swaync-client -t -sw", + "on-click-right": "sleep 0.1 && swaync-client -d -sw", + "escape": true + }, + + "systemd-failed-units": { + "hide-on-ok": true, + "format": "!{nr_failed}", + "format-ok": "", + "system": true, + "user": true, + "on-click": "ghostty -e sh -c 'systemctl --failed; systemctl --user --failed; read'" + }, + + // right: media + "mpris": { + "format": "{artist} - {title}", + "format-paused": "{artist} - {title} [paused]", + "format-stopped": "", + "max-length": 35, + "tooltip-format": "{player}: {artist} - {title} ({album})", + "on-click": "playerctl play-pause", + "on-click-right": "~/.config/eww/scripts/popup.sh media-popup", + "on-scroll-up": "playerctl next", + "on-scroll-down": "playerctl previous" + }, + + // right: connectivity + "network": { + "interval": 5, + "format-ethernet": "󰈀 {ipaddr}", + "format-wifi": "󰖩 {ipaddr}", + "format-linked": "󰈀 (no ip)", + "format-disconnected": "󰖪", + "tooltip-format": "{ifname} {ipaddr}/{cidr}\n{gwaddr}\n{bandwidthUpBits}up {bandwidthDownBits}down", + "on-click-right": "~/.config/eww/scripts/popup.sh network-popup" + }, + + "bluetooth": { + "format": "󰂯", + "format-connected": "󰂯 {num_connections}", + "format-connected-battery": "󰂯 {num_connections}", + "tooltip-format-connected": "{device_enumerate}", + "on-click-right": "~/.config/eww/scripts/popup.sh bluetooth-popup" + }, + + "custom/vpn": { + "format": "{}", + "return-type": "json", + "interval": 10, + "exec": "~/.config/waybar/scripts/vpn-status.sh", + "on-click-right": "~/.config/eww/scripts/popup.sh vpn-popup" + }, + + + "cpu": { + "format": "󰻠 {usage}%", + "tooltip": false, + "on-click-right": "~/.config/eww/scripts/popup.sh system-popup" + }, + + "memory": { + "interval": 10, + "format": "󰍛 {percentage}%", + "tooltip-format": "total: {total:0.2f}GiB\nused: {used:0.2f}GiB\navailable: {avail:0.2f}GiB\nswap: {swapUsed:0.2f}/{swapTotal:0.2f}GiB", + "on-click-right": "~/.config/eww/scripts/popup.sh system-popup" + }, + + // right: battery + "battery": { + "states": { + "warning": 30, + "critical": 15 + }, + "format": "󰁹 {capacity}%", + "format-charging": "󰂄 {capacity}%", + "format-plugged": "󰚥 {capacity}%", + "format-full": "󰁹 full", + "format-alt": "{time}", + "tooltip-format": "{timeTo}\n{power}W", + "on-click": "p=$(powerprofilesctl get); case $p in power-saver) n=balanced;; balanced) n=performance;; *) n=power-saver;; esac; powerprofilesctl set $n", + "on-click-right": "~/.config/eww/scripts/popup.sh battery-popup" + }, + + // right: audio + "wireplumber": { + "format": "󰕾 {volume}%", + "format-muted": "󰖁 muted", + "on-click": "pamixer -t", + "on-click-right": "~/.config/eww/scripts/popup.sh volume-popup", + "on-scroll-up": "pamixer -i 5", + "on-scroll-down": "pamixer -d 5", + "tooltip-format": "{node_name}: {volume}%" + }, + + "custom/mic": { + "format": "{}", + "return-type": "json", + "interval": 2, + "exec": "~/.config/waybar/scripts/mic-status.sh", + "on-click": "pamixer --default-source -t", + "on-click-right": "~/.config/eww/scripts/popup.sh volume-popup", + "on-scroll-up": "pamixer --default-source -i 5", + "on-scroll-down": "pamixer --default-source -d 5" + }, + + "backlight": { + "format": "󰃟 {percent}%", + "tooltip": false, + "on-click-right": "~/.config/eww/scripts/popup.sh volume-popup" + }, + + // right: clock + "clock": { + "interval": 1, + "format": "{:%d/%m %H:%M:%S}", + "tooltip-format": "{calendar}" + } + + } +] diff --git a/waybar/scripts/mic-status.sh b/waybar/scripts/mic-status.sh new file mode 100755 index 0000000..df3060c --- /dev/null +++ b/waybar/scripts/mic-status.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +# mic volume for waybar custom module + +vol=$(pamixer --default-source --get-volume 2>/dev/null || echo 0) +muted=$(pamixer --default-source --get-mute 2>/dev/null || echo false) + +if [[ "$muted" == "true" ]]; then + printf '{"text": "󰍭", "class": "muted", "tooltip": "mic muted"}\n' +else + printf '{"text": "󰍬 %d%%", "class": "", "tooltip": "mic %d%%"}\n' "$vol" "$vol" +fi diff --git a/waybar/scripts/notification-status.sh b/waybar/scripts/notification-status.sh new file mode 100755 index 0000000..65e9a33 --- /dev/null +++ b/waybar/scripts/notification-status.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +# wrap swaync subscription, hide when no notifications + +swaync-client -swb 2>/dev/null | while read -r line; do + count=$(echo "$line" | jq -r '.text // "0"') + class=$(echo "$line" | jq -r '.class // "none"') + + if [[ "$count" == "0" ]]; then + echo '{"text": "", "class": "none"}' + else + jq -nc --arg text "󰂚 $count" --arg class "$class" '{$text,$class}' + fi +done diff --git a/waybar/scripts/ssh-session.sh b/waybar/scripts/ssh-session.sh new file mode 100755 index 0000000..e6fc792 --- /dev/null +++ b/waybar/scripts/ssh-session.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +# detect remote ssh sessions and optionally disconnect them +# outputs waybar JSON; empty when no remote sessions + +if [[ "$1" == "disconnect" ]]; then + pkill -HUP -f 'sshd-session:.*@' 2>/dev/null + exit 0 +fi + +count=$(pgrep -cf 'sshd-session:.*@' 2>/dev/null || echo 0) + +if [[ "$count" -gt 0 ]]; then + # get remote session details + sessions=$(who 2>/dev/null | awk '$NF ~ /\([0-9]/ {gsub(/[()]/, "", $NF); print $1 "@" $NF}') + tooltip=${sessions:-"$count remote sessions"} + # replace newlines with \n for valid JSON + tooltip=${tooltip//$'\n'/\\n} + tooltip=${tooltip//\"/\\\"} + printf '{"text": "●", "class": "active", "tooltip": "%s"}\n' "$tooltip" +fi diff --git a/waybar/scripts/vpn-status.sh b/waybar/scripts/vpn-status.sh new file mode 100755 index 0000000..7d1c009 --- /dev/null +++ b/waybar/scripts/vpn-status.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +# check wireguard and tailscale status +# outputs waybar JSON; empty when inactive + +parts=() +tooltip_parts=() + +if command -v wg &>/dev/null; then + ifaces=$(wg show interfaces 2>/dev/null) + if [[ -n "$ifaces" ]]; then + parts+=("󰖂") + tooltip_parts+=("wireguard: $ifaces") + fi +fi + +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_ip=$(jq -r '.TailscaleIPs[0] // empty' <<<"$ts_json") + ts_name=$(jq -r '.Self.HostName // empty' <<<"$ts_json") + ts_exit=$(jq -r '.ExitNodeStatus.ID // empty' <<<"$ts_json") + parts+=("󰒒") + tip="tailscale: ${ts_name} ${ts_ip}" + if [[ -n "$ts_exit" ]]; then + tip="$tip (exit node)" + fi + tooltip_parts+=("$tip") + fi +fi + +if [[ ${#parts[@]} -gt 0 ]]; then + text="${parts[*]}" + tip=$(printf '%s\\n' "${tooltip_parts[@]}") + # strip trailing \n + tip=${tip%\\n} + jq -nc --arg text "$text" --arg tooltip "$tip" --arg class "active" \ + '{$text,$class,$tooltip}' +fi diff --git a/waybar/style.css b/waybar/style.css new file mode 100644 index 0000000..333668c --- /dev/null +++ b/waybar/style.css @@ -0,0 +1,232 @@ +/* gruvbox-material-soft-dark palette + * mapped from ghostty/themes/gruvbox-material-soft-dark + * ansi 0=#252423 1=#ea6962 2=#a9b665 3=#d8a657 + * 4=#7daea3 5=#d3869b 6=#89b482 7=#d4be98 + * bg=#32302f fg=#d4be98 selection=#45403d */ +@define-color bg #32302f; +@define-color bg_dim #252423; +@define-color bg_sel #45403d; +@define-color fg #d4be98; +@define-color red #ea6962; +@define-color green #a9b665; +@define-color yellow #d8a657; +@define-color blue #7daea3; +@define-color magenta #d3869b; +@define-color cyan #89b482; +@define-color gray #928374; + +* { + font-family: "JetBrainsMono Nerd Font"; + font-size: 14px; + font-weight: bold; +} + +/* bar: dark background, modules float inside */ +window#waybar { + background-color: @bg_dim; + color: @fg; +} + +window#waybar.hidden { + opacity: 0.2; +} + +button { + box-shadow: none; + border: none; + border-radius: 0; +} + +button:hover { + background: inherit; + box-shadow: none; +} + +/* workspaces */ +#workspaces { + background-color: @bg; + border-radius: 8px; + margin: 3px 2px; + padding: 0; +} + +#workspaces button { + padding: 0 8px; + background-color: transparent; + color: @gray; + margin: 0; + border-radius: 8px; +} + +#workspaces button:hover { + background-color: @bg_sel; + color: @fg; + box-shadow: none; +} + +#workspaces button.focused { + background-color: @bg_sel; + color: @fg; + box-shadow: none; +} + +#workspaces button.urgent { + background-color: @red; + color: @bg_dim; +} + +/* floating pill style for all modules */ +#mode, +#clock, +#battery, +#cpu, +#memory, +#backlight, +#network, +#wireplumber, +#mpris, +#bluetooth, +#language, +#custom-mic, +#custom-vpn, +#custom-ssh, +#custom-notification, +#systemd-failed-units { + background-color: @bg; + border-radius: 8px; + color: @fg; + padding: 0 10px; + margin: 3px 2px; +} + +/* center: alert zone */ +#custom-ssh.active { + color: @cyan; + font-size: 20px; +} + +#custom-notification.notification { + color: @yellow; +} + +#custom-notification.dnd-notification { + color: @red; +} + +#custom-notification.dnd-none { + color: @gray; +} + +#systemd-failed-units { + color: @red; +} + +/* right: media (hidden by default, visible only when playing/paused) */ +#mpris { + padding: 0; + margin: 0; + min-width: 0; + background-color: transparent; + color: @green; +} + +#mpris.playing { + padding: 0 10px; + margin: 3px 2px; + background-color: @bg; +} + +#mpris.paused { + padding: 0 10px; + margin: 3px 2px; + background-color: @bg; + color: @gray; +} + +/* right: connectivity */ +#network.disconnected { + color: @red; +} + +#bluetooth { + color: @blue; +} + +#custom-vpn { + color: @cyan; +} + +/* right: system */ +#cpu { + margin-right: 0; + border-radius: 8px 0 0 8px; +} + +#memory { + margin-left: 0; + border-radius: 0 8px 8px 0; +} + +/* right: battery */ +#battery.charging, +#battery.plugged { + color: @green; +} + +#battery.warning:not(.charging) { + color: @yellow; +} + +@keyframes blink { + to { + background-color: @red; + color: @bg_dim; + } +} + +#battery.critical:not(.charging) { + color: @red; + animation-name: blink; + animation-duration: 0.5s; + animation-timing-function: steps(12); + animation-iteration-count: infinite; + animation-direction: alternate; +} + +/* right: audio */ +#wireplumber.muted { + color: @red; +} + +#custom-mic.muted { + color: @red; +} + +/* right: audio group (volume + mic + brightness) */ +#wireplumber { + margin-right: 0; + border-radius: 8px 0 0 8px; +} + +#custom-mic { + margin-left: 0; + margin-right: 0; + border-radius: 0; +} + +#backlight { + margin-left: 0; + border-radius: 0 8px 8px 0; +} + +/* right: clock */ +#clock { + color: @fg; +} + +/* mode highlight */ +#mode { + color: @bg_dim; + background-color: @yellow; + padding: 0 10px; +}