merge: custom shell

This commit is contained in:
2026-03-12 17:45:25 +01:00
26 changed files with 1948 additions and 22 deletions

40
.gitignore vendored
View File

@@ -63,13 +63,51 @@ swayidle/*
waybar/*
!waybar/config.jsonc
!waybar/style.css
!waybar/cider.sh
!waybar/scripts
waybar/scripts/*
!waybar/scripts/ssh-session.sh
!waybar/scripts/vpn-status.sh
!waybar/scripts/notification-status.sh
!waybar/scripts/mic-status.sh
# eww
!eww
eww/*
!eww/eww.yuck
!eww/eww.scss
!eww/scripts
eww/scripts/*
!eww/scripts/popup.sh
!eww/scripts/system.sh
!eww/scripts/battery.sh
!eww/scripts/volume.sh
!eww/scripts/bluetooth.sh
!eww/scripts/network.sh
!eww/scripts/vpn.sh
!eww/scripts/keyboard.sh
!eww/scripts/media.sh
# sway scripts
!sway/scripts
sway/scripts/*
!sway/scripts/power-menu.sh
# bin
!bin
bin/*
!bin/ssh-menu
# swaync
!swaync
swaync/*
!swaync/config.json
!swaync/style.css
# fuzzel
!fuzzel
fuzzel/*
!fuzzel/fuzzel.ini
# flameshot
!flameshot
flameshot/*

239
eww/eww.scss Normal file
View File

@@ -0,0 +1,239 @@
// gruvbox-material-soft-dark palette
$bg: #32302f;
$bg_dim: #252423;
$bg_sel: #45403d;
$fg: #d4be98;
$red: #ea6962;
$green: #a9b665;
$yellow: #d8a657;
$blue: #7daea3;
$magenta: #d3869b;
$cyan: #89b482;
$gray: #928374;
// base
* {
all: unset;
font-family: "JetBrainsMono Nerd Font";
font-size: 13px;
}
// transparent backdrop for click-away dismiss
.backdrop {
background-color: transparent;
}
// popup container
.popup {
background-color: $bg_dim;
border: 1px solid $bg_sel;
border-radius: 12px;
padding: 16px;
}
// section titles
.title {
color: $fg;
font-size: 13px;
font-weight: bold;
margin-bottom: 4px;
}
.subtitle {
color: $gray;
font-size: 12px;
margin-bottom: 2px;
}
// labels and values
.label {
color: $gray;
}
.value {
color: $fg;
}
.value.dim {
color: $gray;
font-size: 12px;
margin-left: 8px;
}
// detail lines (smaller, dimmer)
.details {
margin-top: 4px;
padding-top: 8px;
border-top: 1px solid $bg_sel;
}
.detail {
color: $gray;
font-size: 12px;
}
// rows
.row {
padding: 2px 0;
}
// progress bars
progressbar {
border-radius: 3px;
trough {
min-height: 4px;
border-radius: 3px;
background-color: $bg_sel;
progress {
min-height: 4px;
border-radius: 3px;
background-color: $green;
}
}
}
progressbar.warning trough progress {
background-color: $yellow;
}
progressbar.critical trough progress {
background-color: $red;
}
// sliders
scale {
margin: 0 4px;
trough {
min-height: 4px;
border-radius: 3px;
background-color: $bg_sel;
highlight {
min-height: 4px;
border-radius: 3px;
background-color: $blue;
}
slider {
min-width: 14px;
min-height: 14px;
border-radius: 50%;
background-color: $fg;
margin: -5px 0;
}
}
}
// buttons
.toggle-btn {
color: $gray;
padding: 2px 8px;
border-radius: 6px;
background-color: $bg_sel;
min-width: 30px;
&.active {
color: $bg_dim;
background-color: $green;
}
&:hover {
background-color: lighten($bg_sel, 10%);
}
}
// device list buttons
.device-btn {
color: $fg;
padding: 6px 10px;
border-radius: 6px;
background-color: transparent;
&.active {
background-color: $bg_sel;
color: $fg;
label {
font-weight: bold;
}
}
&:hover {
background-color: $bg;
}
}
// device rows (non-clickable list items)
.device-row {
padding: 4px 10px;
}
// close/disconnect button
.close-btn {
color: $gray;
padding: 2px 6px;
border-radius: 4px;
&:hover {
color: $red;
background-color: $bg;
}
}
// profile selector buttons
.profile-btn {
color: $gray;
padding: 6px 12px;
border-radius: 6px;
background-color: $bg;
&.active {
color: $bg_dim;
background-color: $cyan;
font-weight: bold;
}
&:hover {
background-color: $bg_sel;
}
}
// separator line
.separator {
min-height: 1px;
background-color: $bg_sel;
margin: 4px 0;
}
// status indicators
.indicator {
min-width: 14px;
&.online {
color: $green;
}
&.offline {
color: $gray;
}
}
// empty state
.empty {
color: $gray;
font-size: 12px;
padding: 8px 0;
}
// active player label
.playing {
color: $green;
}
// slider row layout
.slider-row {
padding: 4px 0;
}

421
eww/eww.yuck Normal file
View File

@@ -0,0 +1,421 @@
; sway shell popups
; =============================================================================
; variables
; =============================================================================
(defpoll sys :interval "5s"
:initial '{"cpu":0,"ram_percent":0,"ram_used":"0Gi","ram_total":"0Gi","temp":0,"disk_percent":0,"disk_used":"0","disk_total":"0","swap":"0/0","load":"0 0 0","uptime":""}'
"~/.config/eww/scripts/system.sh")
(defpoll bat :interval "30s"
:initial '{"capacity":0,"status":"Unknown","power":"0","time":"","cycles":0,"profile":"unknown"}'
"~/.config/eww/scripts/battery.sh")
(defpoll vol :interval "5s"
:initial '{"volume":0,"muted":false,"mic_volume":0,"mic_muted":false,"brightness":0,"sinks":[],"sources":[],"sink_count":0,"source_count":0}'
"~/.config/eww/scripts/volume.sh")
(defpoll bt :interval "15s"
:initial '{"powered":false,"count":0,"devices":[]}'
"~/.config/eww/scripts/bluetooth.sh")
(defpoll net :interval "10s"
:initial '{"type":"none","iface":"none","ip":"none","gateway":"none","ssid":"","signal":0,"conn_name":"","count":0,"networks":[],"unknown_count":0,"unknown":[]}'
"~/.config/eww/scripts/network.sh")
(defpoll vpn_data :interval "15s"
:initial '{"tailscale":{"running":false,"ip":"","hostname":"","login":"","exit_nodes":[],"exit_count":0,"peers":[],"peer_count":0},"wireguard":{"active":false,"iface":""}}'
"~/.config/eww/scripts/vpn.sh")
(defpoll kbd :interval "10s"
:initial '{"current":"unknown","layouts":[],"layout_count":0,"keyboards":[],"kb_count":0}'
"~/.config/eww/scripts/keyboard.sh")
(defpoll media :interval "5s"
:initial '{"count":0,"players":[]}'
"~/.config/eww/scripts/media.sh")
; =============================================================================
; media popup
; =============================================================================
(defwidget media-widget []
(box :class "popup" :orientation "v" :space-evenly false :spacing 8
(label :class "title" :text "media" :halign "start")
(box :visible {media.count == 0}
(label :class "empty" :text "no active players"))
(box :orientation "v" :space-evenly false :spacing 4
(for player in {media.players}
(box :class "device-row" :orientation "v" :space-evenly false :spacing 2
(box :orientation "h" :space-evenly false :spacing 8
(label :class "label ${player.status == 'Playing' ? 'playing' : ''}"
:text {player.display} :halign "start" :hexpand true)
(button :class "toggle-btn ${player.status == 'Playing' ? 'active' : ''}"
:onclick "~/.config/eww/scripts/media.sh play-pause '${player.name}'"
{player.status == "Playing" ? "pause" : "play"})
(button :class "close-btn"
:onclick "~/.config/eww/scripts/media.sh prev '${player.name}'"
"prev")
(button :class "close-btn"
:onclick "~/.config/eww/scripts/media.sh next '${player.name}'"
"next"))
(label :class "value" :text "${player.artist}${player.artist != '' ? ' - ' : ''}${player.title}"
:halign "start" :limit-width 45))))))
; =============================================================================
; system popup
; =============================================================================
(defwidget system-widget []
(box :class "popup" :orientation "v" :space-evenly false :spacing 8
(label :class "title" :text "system" :halign "start")
; cpu
(box :class "row" :orientation "v" :space-evenly false :spacing 2
(box :orientation "h"
(label :class "label" :text "cpu" :halign "start" :hexpand true)
(label :class "value" :text "${sys.cpu}%"))
(progress :class "bar" :value {sys.cpu}))
; ram
(box :class "row" :orientation "v" :space-evenly false :spacing 2
(box :orientation "h"
(label :class "label" :text "ram" :halign "start" :hexpand true)
(label :class "value" :text "${sys.ram_used}/${sys.ram_total}"))
(progress :class "bar" :value {sys.ram_percent}))
; temp
(box :class "row" :orientation "h"
(label :class "label" :text "temp" :halign "start" :hexpand true)
(label :class "value" :text "${sys.temp}°"))
; disk
(box :class "row" :orientation "v" :space-evenly false :spacing 2
(box :orientation "h"
(label :class "label" :text "disk" :halign "start" :hexpand true)
(label :class "value" :text "${sys.disk_used}/${sys.disk_total}"))
(progress :class "bar" :value {sys.disk_percent}))
; details
(box :class "details" :orientation "v" :space-evenly false :spacing 2
(label :class "detail" :text "swap ${sys.swap}" :halign "start")
(label :class "detail" :text "load ${sys.load}" :halign "start")
(label :class "detail" :text "up ${sys.uptime}" :halign "start"))))
; =============================================================================
; battery popup
; =============================================================================
(defwidget battery-widget []
(box :class "popup" :orientation "v" :space-evenly false :spacing 8
(label :class "title" :text "battery" :halign "start")
; capacity bar
(box :class "row" :orientation "v" :space-evenly false :spacing 2
(box :orientation "h"
(label :class "label" :text {bat.status} :halign "start" :hexpand true)
(label :class "value" :text "${bat.capacity}%"))
(progress :class "bar ${bat.capacity < 15 ? 'critical' : bat.capacity < 30 ? 'warning' : ''}"
:value {bat.capacity}))
; power + time
(box :class "row" :orientation "h"
(label :class "label" :text "power" :halign "start" :hexpand true)
(label :class "value" :text "${bat.power}W"))
(box :class "row" :orientation "h" :visible {bat.time != ""}
(label :class "label" :text "remaining" :halign "start" :hexpand true)
(label :class "value" :text {bat.time}))
(box :class "row" :orientation "h"
(label :class "label" :text "cycles" :halign "start" :hexpand true)
(label :class "value" :text {bat.cycles}))
; power profile selector
(box :class "separator")
(label :class "title" :text "profile" :halign "start")
(box :class "profiles" :orientation "h" :space-evenly true :spacing 4
(button :class "profile-btn ${bat.profile == 'power-saver' ? 'active' : ''}"
:onclick "~/.config/eww/scripts/battery.sh set-profile power-saver"
"saver")
(button :class "profile-btn ${bat.profile == 'balanced' ? 'active' : ''}"
:onclick "~/.config/eww/scripts/battery.sh set-profile balanced"
"balanced")
(button :class "profile-btn ${bat.profile == 'performance' ? 'active' : ''}"
:onclick "~/.config/eww/scripts/battery.sh set-profile performance"
"perform"))))
; =============================================================================
; volume popup
; =============================================================================
(defwidget volume-widget []
(box :class "popup" :orientation "v" :space-evenly false :spacing 8
; output section
(box :orientation "v" :space-evenly false :spacing 4
(label :class "title" :text "output" :halign "start")
(box :class "row slider-row" :orientation "h" :space-evenly false :spacing 8
(button :class "toggle-btn ${vol.muted ? 'active' : ''}"
:onclick "~/.config/eww/scripts/volume.sh toggle-mute"
{vol.muted ? "muted" : "vol"})
(scale :class "slider" :min 0 :max 100 :value {vol.volume}
:onchange "pamixer --set-volume {}" :hexpand true)
(label :class "value" :text "${vol.volume}%"))
(for sink in {vol.sinks}
(button :class "device-btn ${sink.active ? 'active' : ''}"
:onclick "~/.config/eww/scripts/volume.sh set-sink '${sink.sink_name}'"
(label :text {sink.name} :halign "start"))))
; input section
(box :class "separator")
(box :orientation "v" :space-evenly false :spacing 4
(label :class "title" :text "input" :halign "start")
(box :class "row slider-row" :orientation "h" :space-evenly false :spacing 8
(button :class "toggle-btn ${vol.mic_muted ? 'active' : ''}"
:onclick "~/.config/eww/scripts/volume.sh toggle-mic"
{vol.mic_muted ? "muted" : "mic"})
(scale :class "slider" :min 0 :max 100 :value {vol.mic_volume}
:onchange "pamixer --default-source --set-volume {}" :hexpand true)
(label :class "value" :text "${vol.mic_volume}%"))
(for source in {vol.sources}
(button :class "device-btn ${source.active ? 'active' : ''}"
:onclick "~/.config/eww/scripts/volume.sh set-source '${source.source_name}'"
(label :text {source.name} :halign "start"))))
; brightness section
(box :class "separator")
(label :class "title" :text "brightness" :halign "start")
(box :class "row slider-row" :orientation "h" :space-evenly false :spacing 8
(label :class "label" :text "bl")
(scale :class "slider" :min 0 :max 100 :value {vol.brightness}
:onchange "brightnessctl set {}%" :hexpand true)
(label :class "value" :text "${vol.brightness}%"))))
; =============================================================================
; bluetooth popup
; =============================================================================
(defwidget bluetooth-widget []
(box :class "popup" :orientation "v" :space-evenly false :spacing 8
(box :class "row" :orientation "h"
(label :class "title" :text "bluetooth" :halign "start" :hexpand true)
(button :class "toggle-btn ${bt.powered ? 'active' : ''}"
:onclick "~/.config/eww/scripts/bluetooth.sh toggle-power"
{bt.powered ? "on" : "off"}))
(box :visible {bt.powered} :orientation "v" :space-evenly false :spacing 4
(for device in {bt.devices}
(box :class "device-row" :orientation "h" :space-evenly false :spacing 8
(label :class "label" :text {device.name} :halign "start" :hexpand true)
(label :class "value dim" :visible {device.battery >= 0}
:text "${device.battery}%")
(button :class "close-btn"
:onclick "~/.config/eww/scripts/bluetooth.sh disconnect '${device.mac}'"
"x")))
(label :visible {bt.count == 0} :class "empty" :text "no devices connected"))))
; =============================================================================
; network popup
; =============================================================================
(defwidget network-widget []
(box :class "popup" :orientation "v" :space-evenly false :spacing 8
(label :class "title" :text "network" :halign "start")
; connection info
(box :class "row" :orientation "h"
(label :class "label" :text "interface" :halign "start" :hexpand true)
(label :class "value" :text {net.iface}))
(box :class "row" :orientation "h"
(label :class "label" :text "ip" :halign "start" :hexpand true)
(label :class "value" :text {net.ip}))
(box :class "row" :orientation "h"
(label :class "label" :text "gateway" :halign "start" :hexpand true)
(label :class "value" :text {net.gateway}))
(box :class "row" :orientation "h" :visible {net.type == "wifi"}
(label :class "label" :text "ssid" :halign "start" :hexpand true)
(label :class "value" :text "${net.ssid} ${net.signal}%"))
; saved wifi networks nearby
(box :visible {net.count > 0} :orientation "v" :space-evenly false :spacing 4
(box :class "separator")
(label :class "title" :text "saved" :halign "start")
(box :orientation "v" :space-evenly false :spacing 0
(for network in {net.networks}
(button :class "device-btn ${network.active ? 'active' : ''}"
:onclick {network.active ? "~/.config/eww/scripts/network.sh disconnect '${network.ssid}'" : "~/.config/eww/scripts/network.sh connect '${network.ssid}'"}
(box :orientation "h" :space-evenly false
(label :class "label" :text {network.ssid} :halign "start" :hexpand true)
(label :class "value dim" :text "${network.signal}%"))))))
; unknown wifi networks nearby
(box :visible {net.unknown_count > 0} :orientation "v" :space-evenly false :spacing 4
(box :class "separator")
(label :class "title" :text "nearby" :halign "start")
(box :orientation "v" :space-evenly false :spacing 0
(for network in {net.unknown}
(button :class "device-btn"
:onclick "~/.config/eww/scripts/network.sh connect-new '${network.ssid}'"
(box :orientation "h" :space-evenly false
(label :class "label" :text {network.ssid} :halign "start" :hexpand true)
(label :class "value dim" :text "${network.signal}%"))))))))
; =============================================================================
; vpn popup
; =============================================================================
(defwidget vpn-widget []
(box :class "popup" :orientation "v" :space-evenly false :spacing 8
; tailscale
(box :class "row" :orientation "h"
(label :class "title" :text "tailscale" :halign "start" :hexpand true)
(button :class "toggle-btn ${vpn_data.tailscale.running ? 'active' : ''}"
:onclick "~/.config/eww/scripts/vpn.sh ${vpn_data.tailscale.running ? 'ts-down' : 'ts-up'}"
{vpn_data.tailscale.running ? "on" : "off"}))
(box :visible {vpn_data.tailscale.running} :orientation "v" :space-evenly false :spacing 4
(box :class "row" :orientation "h"
(label :class "label" :text "ip" :halign "start" :hexpand true)
(label :class "value" :text {vpn_data.tailscale.ip}))
(box :class "row" :orientation "h"
(label :class "label" :text "host" :halign "start" :hexpand true)
(label :class "value" :text {vpn_data.tailscale.hostname}))
(box :class "row" :orientation "h"
(label :class "label" :text "network" :halign "start" :hexpand true)
(label :class "value" :text {vpn_data.tailscale.login}))
; exit nodes
(box :visible {vpn_data.tailscale.exit_count > 0} :orientation "v" :space-evenly false :spacing 4
(box :class "separator")
(label :class "subtitle" :text "exit node" :halign "start")
(box :orientation "v" :space-evenly false :spacing 0
(for node in {vpn_data.tailscale.exit_nodes}
(button :class "device-btn ${node.active ? 'active' : ''}"
:onclick "~/.config/eww/scripts/vpn.sh ts-exit '${node.active ? '' : node.ip}'"
(label :text {node.name} :halign "start")))))
; peers
(box :orientation "v" :space-evenly false :spacing 4
(box :class "separator")
(label :class "subtitle" :text "devices" :halign "start")
(for peer in {vpn_data.tailscale.peers}
(box :class "device-row" :orientation "h" :space-evenly false :spacing 8
(label :class "indicator ${peer.online ? 'online' : 'offline'}"
:text {peer.online ? "●" : "○"})
(label :class "label" :text {peer.name} :halign "start" :hexpand true)
(label :class "value dim" :text {peer.ip})))))
; wireguard
(box :class "separator")
(box :class "row" :orientation "h"
(label :class "title" :text "wireguard" :halign "start" :hexpand true)
(button :class "toggle-btn ${vpn_data.wireguard.active ? 'active' : ''}"
:onclick "~/.config/eww/scripts/vpn.sh ${vpn_data.wireguard.active ? 'wg-down' : 'wg-up'}"
{vpn_data.wireguard.active ? "on" : "off"}))))
; =============================================================================
; keyboard popup
; =============================================================================
(defwidget keyboard-widget []
(box :class "popup" :orientation "v" :space-evenly false :spacing 8
(label :class "title" :text "keyboard" :halign "start")
; layout selector
(label :class "subtitle" :text "layout" :halign "start")
(for layout in {kbd.layouts}
(button :class "device-btn ${layout == kbd.current ? 'active' : ''}"
:onclick "~/.config/eww/scripts/keyboard.sh switch"
(label :text {layout} :halign "start")))
; keyboard devices
(box :visible {kbd.kb_count > 1} :orientation "v" :space-evenly false :spacing 4
(box :class "separator")
(label :class "subtitle" :text "devices" :halign "start")
(for kb in {kbd.keyboards}
(box :class "device-row" :orientation "h" :space-evenly false :spacing 8
(label :class "label" :text {kb.name} :halign "start" :hexpand true)
(label :class "value dim" :text {kb.layout}))))))
; =============================================================================
; backdrop (click-away to close)
; =============================================================================
(defwidget backdrop-widget []
(eventbox :onclick "~/.config/eww/scripts/popup.sh close-all"
(box :class "backdrop" :hexpand true :vexpand true)))
(defwindow backdrop
:monitor 0
:geometry (geometry :x "0px" :y "0px" :width "100%" :height "100%" :anchor "top left")
:stacking "overlay"
:exclusive false
:focusable true
(backdrop-widget))
; =============================================================================
; windows
; =============================================================================
(defwindow system-popup
:monitor 0
:geometry (geometry :x "380px" :y "3px" :width "300px" :anchor "top right")
:stacking "overlay"
:exclusive false
:focusable false
(system-widget))
(defwindow battery-popup
:monitor 0
:geometry (geometry :x "260px" :y "3px" :width "300px" :anchor "top right")
:stacking "overlay"
:exclusive false
:focusable false
(battery-widget))
(defwindow volume-popup
:monitor 0
:geometry (geometry :x "140px" :y "3px" :width "320px" :anchor "top right")
:stacking "overlay"
:exclusive false
:focusable false
(volume-widget))
(defwindow bluetooth-popup
:monitor 0
:geometry (geometry :x "500px" :y "3px" :width "280px" :anchor "top right")
:stacking "overlay"
:exclusive false
:focusable false
(bluetooth-widget))
(defwindow network-popup
:monitor 0
:geometry (geometry :x "560px" :y "3px" :width "320px" :anchor "top right")
:stacking "overlay"
:exclusive false
:focusable false
(network-widget))
(defwindow vpn-popup
:monitor 0
:geometry (geometry :x "480px" :y "3px" :width "320px" :anchor "top right")
:stacking "overlay"
:exclusive false
:focusable false
(vpn-widget))
(defwindow keyboard-popup
:monitor 0
:geometry (geometry :x "50px" :y "3px" :width "280px" :anchor "top right")
:stacking "overlay"
:exclusive false
:focusable false
(keyboard-widget))
(defwindow media-popup
:monitor 0
:geometry (geometry :x "700px" :y "3px" :width "350px" :anchor "top right")
:stacking "overlay"
:exclusive false
:focusable false
(media-widget))

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

22
fuzzel/fuzzel.ini Normal file
View File

@@ -0,0 +1,22 @@
[main]
font=JetBrainsMono Nerd Font:size=14
prompt=" "
icons=no
terminal=ghostty -e
width=40
lines=12
layer=overlay
exit-on-keyboard-focus-loss=yes
[colors]
background=252423ff
text=d4be98ff
match=a9b665ff
selection=45403dff
selection-text=d4be98ff
selection-match=a9b665ff
border=45403dff
[border]
width=2
radius=8

View File

@@ -8,7 +8,7 @@ set $right l
# programs
set $term ghostty
set $menu wofi --show drun -iIG -w 2
set $menu fuzzel
set $browser google-chrome-stable
set $lockscreen swaylock

View File

@@ -1,6 +1,6 @@
# sway
bindsym $mod+Shift+c reload
bindsym $mod+Shift+e exec swaynag -t warning -m 'You pressed the exit shortcut. Do you really want to exit sway? This will end your Wayland session.' -B 'Yes, exit sway' 'swaymsg exit'
bindsym $mod+Shift+e exec ~/.config/sway/scripts/power-menu.sh
floating_modifier $mod normal
# programs
@@ -10,6 +10,16 @@ bindsym $mod+u exec $lockscreen
bindsym $mod+Shift+Ctrl+Alt+space exec 1password --quick-access
bindsym Print exec flameshot gui
# notifications
bindsym $mod+n exec swaync-client -t -sw
bindsym $mod+Shift+n exec swaync-client -C -sw
# clipboard
bindsym $mod+c exec cliphist list | fuzzel -d | cliphist decode | wl-copy
# eww popups
bindsym $mod+Escape exec ~/.config/eww/scripts/popup.sh close-all
# window
bindsym $mod+Shift+q kill
@@ -80,7 +90,7 @@ bindsym $mod+Shift+minus move scratchpad
bindsym $mod+minus scratchpad show
# xf86-volume
bindsym --locked XF86AudioMute exec pamixer -t && pamixer --get-volume > $wobs
bindsym --locked XF86AudioMute exec pamixer -t && ( pamixer --get-mute && echo 0 > $wobs || pamixer --get-volume > $wobs )
bindsym --locked XF86AudioLowerVolume exec pamixer -d 5 && pamixer --get-volume > $wobs
bindsym --locked XF86AudioRaiseVolume exec pamixer -i 5 && pamixer --get-volume > $wobs
bindsym --locked F16 exec pamixer --default-source -t

View File

@@ -1,7 +1,10 @@
# daemon
exec swayidle -w
exec eww daemon
exec swayidle -w
exec wlsunset -l 46.1 -L 14.5
exec pkill -x wob; rm -f $wobs && mkfifo $wobs && tail -f $wobs | wob
exec swaync
exec wl-paste --watch cliphist store
exec protonmail-bridge -n
# traditional "start when os starts" programs

13
sway/scripts/power-menu.sh Executable file
View File

@@ -0,0 +1,13 @@
#!/usr/bin/env bash
# power menu via fuzzel
choice=$(printf "lock\nlogout\nsuspend\nreboot\nshutdown" | fuzzel -d -p "power: ")
case "$choice" in
lock) swaylock ;;
logout) swaymsg exit ;;
suspend) systemctl suspend ;;
reboot) systemctl reboot ;;
shutdown) systemctl poweroff ;;
esac

View File

@@ -1,7 +1,7 @@
ignore-empty-password
show-failed-attempts
image=~/.assets/lockscreen.png
image=~/.assets/wallpaper.png
clock
datestr=%A, %d/%m/%y
@@ -12,23 +12,24 @@ indicator-idle-visible
indicator-y-position=300
indicator-x-position=1200
inside-color=000000
text-color=ffffff
ring-color=ffffff
# gruvbox-material-soft-dark
inside-color=#32302f
text-color=#d4be98
ring-color=#928374
inside-ver-color=000000
text-ver-color=000000
ring-ver-color=ffffff
inside-ver-color=#32302f
text-ver-color=#a9b665
ring-ver-color=#a9b665
inside-clear-color=000000
text-clear-color=ffffff
ring-clear-color=ffffff
inside-clear-color=#32302f
text-clear-color=#d8a657
ring-clear-color=#d8a657
inside-wrong-color=000000
text-wrong-color=ffffff
ring-wrong-color=ffffff
inside-wrong-color=#32302f
text-wrong-color=#ea6962
ring-wrong-color=#ea6962
key-hl-color=000000
bs-hl-color=000000
caps-lock-key-hl-color=000000
caps-lock-bs-hl-color=000000
key-hl-color=#7daea3
bs-hl-color=#ea6962
caps-lock-key-hl-color=#d8a657
caps-lock-bs-hl-color=#ea6962

50
swaync/config.json Normal file
View File

@@ -0,0 +1,50 @@
{
"positionX": "right",
"positionY": "top",
"layer": "overlay",
"control-center-layer": "overlay",
"cssPriority": "application",
"control-center-width": 400,
"control-center-height": 600,
"control-center-margin-top": 3,
"control-center-margin-right": 3,
"control-center-margin-bottom": 3,
"notification-window-width": 400,
"notification-icon-size": 48,
"notification-body-image-height": 100,
"notification-body-image-width": 200,
"timeout": 10,
"timeout-low": 5,
"timeout-critical": 0,
"transition-time": 200,
"notification-grouping": true,
"image-visibility": "when-available",
"relative-timestamps": true,
"keyboard-shortcuts": true,
"hide-on-clear": true,
"hide-on-action": true,
"fit-to-screen": true,
"widgets": [
"title",
"dnd",
"notifications"
],
"widget-config": {
"title": {
"text": "notifications",
"clear-all-button": true,
"button-text": "clear"
},
"dnd": {
"text": "do not disturb"
},
"notifications": {
"vexpand": true
}
}
}

163
swaync/style.css Normal file
View File

@@ -0,0 +1,163 @@
/* gruvbox-material-soft-dark */
@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;
}
/* floating notifications */
.floating-notifications {
background: transparent;
}
.notification {
background-color: @bg;
border: 2px solid @bg_sel;
border-radius: 8px;
margin: 4px;
padding: 8px;
}
.notification.critical {
border-color: @red;
}
.notification .summary {
color: @fg;
font-size: 14px;
}
.notification .body {
color: @gray;
font-size: 13px;
}
.notification .time {
color: @gray;
font-size: 12px;
}
.notification .image {
margin-right: 8px;
border-radius: 4px;
}
.close-button {
background-color: @bg_sel;
color: @fg;
border-radius: 50%;
min-width: 24px;
min-height: 24px;
padding: 0;
}
.close-button:hover {
background-color: @red;
color: @bg_dim;
}
/* action buttons */
.notification .text-button {
background-color: @bg_sel;
color: @fg;
border-radius: 4px;
padding: 4px 8px;
margin: 2px;
}
.notification .text-button:hover {
background-color: @blue;
color: @bg_dim;
}
/* control center panel */
.control-center {
background-color: @bg_dim;
border: 2px solid @bg_sel;
border-radius: 8px;
padding: 8px;
}
.control-center .notification-row {
background-color: transparent;
margin: 2px 0;
}
.control-center .notification-row .notification {
background-color: @bg;
border: 1px solid @bg_sel;
}
.control-center .notification-row:hover .notification {
border-color: @gray;
}
/* title widget */
.widget-title {
color: @fg;
padding: 4px 8px;
}
.widget-title button {
background-color: @bg_sel;
color: @fg;
border-radius: 4px;
padding: 4px 12px;
}
.widget-title button:hover {
background-color: @red;
color: @bg_dim;
}
/* dnd toggle */
.widget-dnd {
color: @fg;
padding: 4px 8px;
}
.widget-dnd > switch {
background-color: @bg_sel;
border-radius: 12px;
}
.widget-dnd > switch:checked {
background-color: @yellow;
}
.widget-dnd > switch slider {
background-color: @fg;
border-radius: 50%;
min-width: 20px;
min-height: 20px;
}
/* empty state */
.widget-notifications > label {
color: @gray;
padding: 16px;
}
/* notification group */
.notification-group {
background-color: @bg;
border-radius: 8px;
margin: 4px;
}
.notification-group .notification-group-headers {
padding: 4px 8px;
color: @gray;
}

179
waybar/config.jsonc Normal file
View File

@@ -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": "<big><tt>{calendar}</tt></big>"
}
}
]

12
waybar/scripts/mic-status.sh Executable file
View File

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

View File

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

21
waybar/scripts/ssh-session.sh Executable file
View File

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

40
waybar/scripts/vpn-status.sh Executable file
View File

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

232
waybar/style.css Normal file
View File

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