;; Flyout panels opened from the Waybar buttons (scripts/toggle.sh): the control ;; center and the notification list. `cc-closer` is an invisible click-catcher (click ;; outside = close), shared by both panels. All windows take a `monitor` argument (a ;; number), which scripts/toggle.sh fills in with the monitor you clicked on. ;; --------------------------------------------------------------------------- ;; State ;; --------------------------------------------------------------------------- (defvar confirm "none") ;; none | logout | reboot | poweroff (deflisten audio :initial '{"vol":0,"muted":false,"mic":0,"mic_muted":false}' "python3 $HOME/.config/waybar/eww/scripts/audio.py") (defpoll net :interval "3s" :initial '{"available":false,"on":false,"connected":false,"icon":"","label":"Unavailable"}' "bash $HOME/.config/waybar/eww/scripts/network.sh") (defpoll mouse :interval "30s" :initial '{"text":"","percentage":0,"class":"","tooltip":"","label":"Not connected","connected":false}' "bash $HOME/.config/waybar/scripts/mouse-battery.sh") (defpoll bt :interval "2s" :initial '{"available":false,"on":false,"device":"","label":"Unavailable"}' "bash $HOME/.config/waybar/eww/scripts/bluetooth.sh") (defpoll disk :interval "30s" :initial "0" "df --output=pcent / | tail -1 | tr -dc '0-9'") ;; "26.11 · nixpkgs from 15 Oct", from nixos-version (26.11.20261015.fe13242). (defpoll nixos :interval "10m" :initial "" "v=$(nixos-version | cut -d' ' -f1); date -d \"$(cut -d. -f3 <<<\"$v\")\" \"+$(cut -d. -f1-2 <<<\"$v\") · nixpkgs from %-d %b\"") ;; Last notifications from dunst's history. Only polled while the panel is open; ;; scripts/toggle.sh fills it once right before opening. (defvar notifs_open false) ;; Clear animation (scripts/clear-notifications.sh): entries first slide out one after ;; another (notifs_clearing), then the emptied list folds up (notifs_folded). (defvar notifs_clearing false) (defvar notifs_folded false) (defpoll notifs :interval "2s" :run-while notifs_open :initial "[]" "python3 $HOME/.config/waybar/eww/scripts/notifications.py") ;; --------------------------------------------------------------------------- ;; Windows ;; --------------------------------------------------------------------------- (defwindow cc-closer [monitor] :monitor monitor :geometry (geometry :x "0px" :y "0px" :width "100%" :height "100%" :anchor "top left") :stacking "overlay" :namespace "eww-cc-closer" (eventbox :class "closer" :timeout "2s" :onclick "bash $HOME/.config/waybar/eww/scripts/toggle.sh close")) (defwindow control-center [monitor] :monitor monitor :geometry (geometry :x "15px" :y "8px" :width "360px" :anchor "top right") :stacking "overlay" :namespace "eww-control-center" (control-center)) ;; Same spot and namespace as the control center, so Hyprland's blur rule applies. (defwindow notifications [monitor] :monitor monitor :geometry (geometry :x "15px" :y "8px" :width "360px" :anchor "top right") :stacking "overlay" :namespace "eww-control-center" (notifications)) ;; --------------------------------------------------------------------------- ;; Layout ;; --------------------------------------------------------------------------- (defwidget control-center [] (box :class "cc" :orientation "v" :space-evenly false :spacing 10 (box :orientation "h" :space-evenly true :spacing 10 (split-tile :icon {net.icon} :title "Network" :sub {net.label} :active {net.available && net.connected} :ontoggle "bash $HOME/.config/waybar/eww/scripts/net-toggle.sh ${net.on ? 'off' : 'on'}" :onopen "${EWW_CMD} close control-center cc-closer; setsid -f nm-connection-editor") (split-tile :icon {bt.on ? "󰂯" : "󰂲"} :title "Bluetooth" :sub {bt.label} :active {bt.available && bt.on} :ontoggle "bash $HOME/.config/waybar/eww/scripts/bt-toggle.sh ${bt.on ? 'off' : 'on'}" :onopen "${EWW_CMD} close control-center cc-closer; setsid -f blueman-manager")) (sound) (stats) (power) (update))) ;; --------------------------------------------------------------------------- ;; Building blocks ;; --------------------------------------------------------------------------- ;; A tile in two halves: the round icon switches it on and off, the rest opens its ;; settings. The halves sit side by side rather than one inside the other, because ;; a click on an inner eventbox would reach the outer one too. (defwidget split-tile [icon title sub active ontoggle onopen] (box :class "tile split" :orientation "h" :space-evenly false (eventbox :class "toggle-wrap" :cursor "pointer" :timeout "2s" :onclick ontoggle (box :class "toggle" (box :class "icon-circle ${active ? 'on' : ''}" :space-evenly true (label :text icon)))) (eventbox :class "open-wrap" :cursor "pointer" :timeout "2s" :hexpand true :onclick onopen (box :class "open" :orientation "h" :space-evenly false (box :orientation "v" :valign "center" :halign "start" :hexpand true :space-evenly false (label :class "tile-title" :halign "start" :limit-width 12 :text title) (label :class "tile-sub" :halign "start" :limit-width 15 :text sub)) (label :class "chevron" :valign "center" :text "󰅂"))))) (defwidget slider-row [icon muted value onmute onchange] (box :class "slider-row" :orientation "h" :space-evenly false :spacing 10 (eventbox :cursor "pointer" :timeout "1s" :onclick onmute (label :class "slider-icon ${muted ? 'muted' : ''}" :text icon)) (scale :class "slider" :hexpand true :min 0 :max 100 :round-digits 0 :timeout "1s" :value value :onchange onchange) (label :class "slider-val" :text "${value}%"))) ;; `text` replaces the percentage in the middle; `hot` (red) defaults to above 85%. (defwidget ring [value name ?text ?hot] (box :class "ring-wrap" :orientation "v" :space-evenly false :spacing 6 :hexpand true (overlay :halign "center" (circular-progress :class "ring ${(hot ?: value > 85) ? 'hot' : ''}" :value value :thickness 6 :start-at 75 :clockwise true :width 60 :height 60) (label :class "ring-value" :text {text ?: "${round(value, 0)}%"})) (label :class "ring-name" :text name))) (defwidget power-btn [icon label onclick] ;; :height is a hard GTK size request (unlike CSS padding, which the box/eventbox ;; layout here wasn't actually growing from -- the row just stayed shrink-wrapped ;; to the icon+label no matter how much padding was added). This is what actually ;; makes the hover rectangle taller. (eventbox :class "power-btn" :cursor "pointer" :timeout "3s" :onclick onclick :height 62 (box :orientation "v" :space-evenly false :spacing 4 :valign "center" :halign "center" (label :class "power-icon" :text icon) (label :class "power-label" :text label)))) ;; --------------------------------------------------------------------------- ;; Cards ;; --------------------------------------------------------------------------- (defwidget sound [] (box :class "card" :orientation "v" :space-evenly false :spacing 10 (label :class "card-title" :halign "start" :text "Sound") (slider-row :icon {audio.muted ? "󰝟" : "󰕾"} :muted {audio.muted} :value {audio.vol} :onmute "pactl set-sink-mute @DEFAULT_SINK@ toggle" :onchange "pactl set-sink-volume @DEFAULT_SINK@ {}%") (slider-row :icon {audio.mic_muted ? "󰍭" : "󰍬"} :muted {audio.mic_muted} :value {audio.mic} :onmute "pactl set-source-mute @DEFAULT_SOURCE@ toggle" :onchange "pactl set-source-volume @DEFAULT_SOURCE@ {}%"))) (defwidget stats [] (box :class "card" :orientation "h" :space-evenly true (ring :value {EWW_CPU.avg} :name "CPU") (ring :value {EWW_RAM.used_mem_perc} :name "RAM") (ring :value disk :name "Disk") ;; The Superlight's battery; red when low rather than when full. (ring :value {mouse.connected ? mouse.percentage : 0} :text {mouse.connected ? "${mouse.percentage}%" : "–"} :hot {mouse.class == "warning" || mouse.class == "critical"} :name {mouse.class == "charging" ? "Charging" : "Mouse"}))) (defwidget power [] (box :class "card" :orientation "v" :space-evenly false :spacing 6 (box :orientation "h" :space-evenly true (power-btn :icon "󰌾" :label "Lock" :onclick "${EWW_CMD} close control-center cc-closer; pidof hyprlock || setsid -f hyprlock") (power-btn :icon "󰒲" :label "Sleep" :onclick "${EWW_CMD} close control-center cc-closer; systemctl suspend") (power-btn :icon "󰍃" :label "Log out" :onclick "${EWW_CMD} update confirm=logout") (power-btn :icon "󰜉" :label "Restart" :onclick "${EWW_CMD} update confirm=reboot") (power-btn :icon "󰐥" :label "Shut down" :onclick "${EWW_CMD} update confirm=poweroff")) (revealer :transition "slidedown" :duration "150ms" :reveal {confirm != "none"} (box :class "confirm" :orientation "h" :space-evenly false :spacing 8 (label :hexpand true :halign "start" :text {confirm == "reboot" ? "Restart now?" : (confirm == "poweroff" ? "Shut down now?" : "Log out now?")}) ;; Padding/background live on the inner box: GTK ignores CSS padding on an ;; eventbox, so styling the eventbox itself left the pills shrink-wrapped. (eventbox :class "btn-wrap" :cursor "pointer" :onclick "${EWW_CMD} update confirm=none" (box :class "btn" (label :text "Cancel"))) (eventbox :class "btn-wrap" :cursor "pointer" :timeout "3s" :onclick "${EWW_CMD} update confirm=none; ${EWW_CMD} close control-center cc-closer; ${confirm == 'reboot' ? 'systemctl reboot' : (confirm == 'poweroff' ? 'systemctl poweroff' : 'hyprctl dispatch exit')}" (box :class "btn danger" (label :text "Confirm"))))))) ;; Updates and reboots in a kitty window (tools/nixos-update), asking before it ;; installs anything. (defwidget update [] (eventbox :class "tile-wrap" :cursor "pointer" :timeout "3s" :onclick "${EWW_CMD} close control-center cc-closer; setsid -f kitty --title 'NixOS update' nixos-update" (box :class "tile" :orientation "h" :space-evenly false :spacing 10 (box :class "icon-circle" :space-evenly true (label :class "nix" :text "")) (box :orientation "v" :valign "center" :halign "start" :hexpand true :space-evenly false (label :class "tile-title" :halign "start" :text "Update NixOS") (label :class "tile-sub" :halign "start" :limit-width 34 :text nixos)) (label :class "chevron" :valign "center" :text "󰅂")))) ;; --------------------------------------------------------------------------- ;; Notifications panel ;; --------------------------------------------------------------------------- (defwidget notifications [] ;; One panel, no inner card: the header sits straight on the panel and every ;; notification is its own card. Spacing comes from CSS margins rather than ;; :spacing, because the two revealers stay in the layout even when folded shut. (box :class "cc" :orientation "v" :space-evenly false ;; Fixed height, so the header doesn't shrink when the Clear button hides. (box :class "notif-header" :orientation "h" :space-evenly false :height 28 (label :class "card-title" :hexpand true :halign "start" :valign "center" :text "Notifications") (eventbox :class "btn-wrap" :cursor "pointer" :timeout "2s" :valign "center" :visible {arraylength(notifs) > 0 && !notifs_clearing} :onclick "bash $HOME/.config/waybar/eww/scripts/clear-notifications.sh &" (box :class "btn" (label :text "Clear")))) ;; Slides in while the list folds up, so the panel goes straight to its empty size. (revealer :transition "slidedown" :duration "220ms" :reveal {arraylength(notifs) == 0 || notifs_folded} (label :class "notif-empty" :halign "start" :text "No notifications")) (revealer :transition "slideup" :duration "220ms" :reveal {!notifs_folded} (box :orientation "v" :space-evenly false (for n in notifs ;; Click: jump to the app that sent it (scripts/open-notification.py). Only the ;; numeric id is passed, never the notification's own text. (eventbox :class "notif-wrap" :cursor "pointer" :timeout "3s" :onclick "python3 $HOME/.config/waybar/eww/scripts/open-notification.py ${n.id} &" ;; `d` delays each entry's slide-out a little more than the one above it (box :class "notif d${n.i} ${notifs_clearing ? 'out' : ''}" :orientation "v" :space-evenly false :spacing 2 (box :orientation "h" :space-evenly false (label :class "notif-meta" :hexpand true :halign "start" :limit-width 30 :text {n.app}) (label :class "notif-meta" :text {n.time})) (label :class "notif-summary" :halign "start" :xalign 0 :wrap true :text {n.summary}) (label :class "notif-body" :halign "start" :xalign 0 :wrap true :visible {n.body != ""} :text {n.body}))))))))