dot
vizie's NixOS desktop: Hyprland, CachyOS kernel and gaming tweaks
| 1 | ;; Flyout panels opened from the Waybar buttons (scripts/toggle.sh): the control |
| 2 | ;; center and the notification list. `cc-closer` is an invisible click-catcher (click |
| 3 | ;; outside = close), shared by both panels. All windows take a `monitor` argument (a |
| 4 | ;; number), which scripts/toggle.sh fills in with the monitor you clicked on. |
| 5 | |
| 6 | ;; --------------------------------------------------------------------------- |
| 7 | ;; State |
| 8 | ;; --------------------------------------------------------------------------- |
| 9 | |
| 10 | (defvar confirm "none") ;; none | logout | reboot | poweroff |
| 11 | |
| 12 | (deflisten audio |
| 13 | :initial '{"vol":0,"muted":false,"mic":0,"mic_muted":false}' |
| 14 | "python3 $HOME/.config/waybar/eww/scripts/audio.py") |
| 15 | |
| 16 | (defpoll net |
| 17 | :interval "3s" |
| 18 | :initial '{"available":false,"on":false,"connected":false,"icon":"","label":"Unavailable"}' |
| 19 | "bash $HOME/.config/waybar/eww/scripts/network.sh") |
| 20 | |
| 21 | (defpoll mouse |
| 22 | :interval "30s" |
| 23 | :initial '{"text":"","percentage":0,"class":"","tooltip":"","label":"Not connected","connected":false}' |
| 24 | "bash $HOME/.config/waybar/scripts/mouse-battery.sh") |
| 25 | |
| 26 | (defpoll bt |
| 27 | :interval "2s" |
| 28 | :initial '{"available":false,"on":false,"device":"","label":"Unavailable"}' |
| 29 | "bash $HOME/.config/waybar/eww/scripts/bluetooth.sh") |
| 30 | |
| 31 | (defpoll disk |
| 32 | :interval "30s" |
| 33 | :initial "0" |
| 34 | "df --output=pcent / | tail -1 | tr -dc '0-9'") |
| 35 | |
| 36 | ;; "26.11 · nixpkgs from 15 Oct", from nixos-version (26.11.20261015.fe13242). |
| 37 | (defpoll nixos |
| 38 | :interval "10m" |
| 39 | :initial "" |
| 40 | "v=$(nixos-version | cut -d' ' -f1); date -d \"$(cut -d. -f3 <<<\"$v\")\" \"+$(cut -d. -f1-2 <<<\"$v\") · nixpkgs from %-d %b\"") |
| 41 | |
| 42 | ;; Last notifications from dunst's history. Only polled while the panel is open; |
| 43 | ;; scripts/toggle.sh fills it once right before opening. |
| 44 | (defvar notifs_open false) |
| 45 | ;; Clear animation (scripts/clear-notifications.sh): entries first slide out one after |
| 46 | ;; another (notifs_clearing), then the emptied list folds up (notifs_folded). |
| 47 | (defvar notifs_clearing false) |
| 48 | (defvar notifs_folded false) |
| 49 | (defpoll notifs |
| 50 | :interval "2s" |
| 51 | :run-while notifs_open |
| 52 | :initial "[]" |
| 53 | "python3 $HOME/.config/waybar/eww/scripts/notifications.py") |
| 54 | |
| 55 | ;; --------------------------------------------------------------------------- |
| 56 | ;; Windows |
| 57 | ;; --------------------------------------------------------------------------- |
| 58 | |
| 59 | (defwindow cc-closer [monitor] |
| 60 | :monitor monitor |
| 61 | :geometry (geometry :x "0px" :y "0px" :width "100%" :height "100%" :anchor "top left") |
| 62 | :stacking "overlay" |
| 63 | :namespace "eww-cc-closer" |
| 64 | (eventbox :class "closer" |
| 65 | :timeout "2s" |
| 66 | :onclick "bash $HOME/.config/waybar/eww/scripts/toggle.sh close")) |
| 67 | |
| 68 | (defwindow control-center [monitor] |
| 69 | :monitor monitor |
| 70 | :geometry (geometry :x "15px" :y "8px" :width "360px" :anchor "top right") |
| 71 | :stacking "overlay" |
| 72 | :namespace "eww-control-center" |
| 73 | (control-center)) |
| 74 | |
| 75 | ;; Same spot and namespace as the control center, so Hyprland's blur rule applies. |
| 76 | (defwindow notifications [monitor] |
| 77 | :monitor monitor |
| 78 | :geometry (geometry :x "15px" :y "8px" :width "360px" :anchor "top right") |
| 79 | :stacking "overlay" |
| 80 | :namespace "eww-control-center" |
| 81 | (notifications)) |
| 82 | |
| 83 | ;; --------------------------------------------------------------------------- |
| 84 | ;; Layout |
| 85 | ;; --------------------------------------------------------------------------- |
| 86 | |
| 87 | (defwidget control-center [] |
| 88 | (box :class "cc" :orientation "v" :space-evenly false :spacing 10 |
| 89 | (box :orientation "h" :space-evenly true :spacing 10 |
| 90 | (split-tile :icon {net.icon} |
| 91 | :title "Network" |
| 92 | :sub {net.label} |
| 93 | :active {net.available && net.connected} |
| 94 | :ontoggle "bash $HOME/.config/waybar/eww/scripts/net-toggle.sh ${net.on ? 'off' : 'on'}" |
| 95 | :onopen "${EWW_CMD} close control-center cc-closer; setsid -f nm-connection-editor") |
| 96 | (split-tile :icon {bt.on ? "" : ""} |
| 97 | :title "Bluetooth" |
| 98 | :sub {bt.label} |
| 99 | :active {bt.available && bt.on} |
| 100 | :ontoggle "bash $HOME/.config/waybar/eww/scripts/bt-toggle.sh ${bt.on ? 'off' : 'on'}" |
| 101 | :onopen "${EWW_CMD} close control-center cc-closer; setsid -f blueman-manager")) |
| 102 | (sound) |
| 103 | (stats) |
| 104 | (power) |
| 105 | (update))) |
| 106 | |
| 107 | ;; --------------------------------------------------------------------------- |
| 108 | ;; Building blocks |
| 109 | ;; --------------------------------------------------------------------------- |
| 110 | |
| 111 | ;; A tile in two halves: the round icon switches it on and off, the rest opens its |
| 112 | ;; settings. The halves sit side by side rather than one inside the other, because |
| 113 | ;; a click on an inner eventbox would reach the outer one too. |
| 114 | (defwidget split-tile [icon title sub active ontoggle onopen] |
| 115 | (box :class "tile split" :orientation "h" :space-evenly false |
| 116 | (eventbox :class "toggle-wrap" :cursor "pointer" :timeout "2s" :onclick ontoggle |
| 117 | (box :class "toggle" |
| 118 | (box :class "icon-circle ${active ? 'on' : ''}" :space-evenly true |
| 119 | (label :text icon)))) |
| 120 | (eventbox :class "open-wrap" :cursor "pointer" :timeout "2s" :hexpand true :onclick onopen |
| 121 | (box :class "open" :orientation "h" :space-evenly false |
| 122 | (box :orientation "v" :valign "center" :halign "start" :hexpand true :space-evenly false |
| 123 | (label :class "tile-title" :halign "start" :limit-width 12 :text title) |
| 124 | (label :class "tile-sub" :halign "start" :limit-width 15 :text sub)) |
| 125 | (label :class "chevron" :valign "center" :text ""))))) |
| 126 | |
| 127 | (defwidget slider-row [icon muted value onmute onchange] |
| 128 | (box :class "slider-row" :orientation "h" :space-evenly false :spacing 10 |
| 129 | (eventbox :cursor "pointer" :timeout "1s" :onclick onmute |
| 130 | (label :class "slider-icon ${muted ? 'muted' : ''}" :text icon)) |
| 131 | (scale :class "slider" :hexpand true :min 0 :max 100 :round-digits 0 |
| 132 | :timeout "1s" :value value :onchange onchange) |
| 133 | (label :class "slider-val" :text "${value}%"))) |
| 134 | |
| 135 | ;; `text` replaces the percentage in the middle; `hot` (red) defaults to above 85%. |
| 136 | (defwidget ring [value name ?text ?hot] |
| 137 | (box :class "ring-wrap" :orientation "v" :space-evenly false :spacing 6 :hexpand true |
| 138 | (overlay :halign "center" |
| 139 | (circular-progress :class "ring ${(hot ?: value > 85) ? 'hot' : ''}" |
| 140 | :value value :thickness 6 :start-at 75 :clockwise true |
| 141 | :width 60 :height 60) |
| 142 | (label :class "ring-value" :text {text ?: "${round(value, 0)}%"})) |
| 143 | (label :class "ring-name" :text name))) |
| 144 | |
| 145 | (defwidget power-btn [icon label onclick] |
| 146 | ;; :height is a hard GTK size request (unlike CSS padding, which the box/eventbox |
| 147 | ;; layout here wasn't actually growing from -- the row just stayed shrink-wrapped |
| 148 | ;; to the icon+label no matter how much padding was added). This is what actually |
| 149 | ;; makes the hover rectangle taller. |
| 150 | (eventbox :class "power-btn" :cursor "pointer" :timeout "3s" :onclick onclick :height 62 |
| 151 | (box :orientation "v" :space-evenly false :spacing 4 :valign "center" :halign "center" |
| 152 | (label :class "power-icon" :text icon) |
| 153 | (label :class "power-label" :text label)))) |
| 154 | |
| 155 | ;; --------------------------------------------------------------------------- |
| 156 | ;; Cards |
| 157 | ;; --------------------------------------------------------------------------- |
| 158 | |
| 159 | (defwidget sound [] |
| 160 | (box :class "card" :orientation "v" :space-evenly false :spacing 10 |
| 161 | (label :class "card-title" :halign "start" :text "Sound") |
| 162 | (slider-row :icon {audio.muted ? "" : ""} |
| 163 | :muted {audio.muted} |
| 164 | :value {audio.vol} |
| 165 | :onmute "pactl set-sink-mute @DEFAULT_SINK@ toggle" |
| 166 | :onchange "pactl set-sink-volume @DEFAULT_SINK@ {}%") |
| 167 | (slider-row :icon {audio.mic_muted ? "" : ""} |
| 168 | :muted {audio.mic_muted} |
| 169 | :value {audio.mic} |
| 170 | :onmute "pactl set-source-mute @DEFAULT_SOURCE@ toggle" |
| 171 | :onchange "pactl set-source-volume @DEFAULT_SOURCE@ {}%"))) |
| 172 | |
| 173 | (defwidget stats [] |
| 174 | (box :class "card" :orientation "h" :space-evenly true |
| 175 | (ring :value {EWW_CPU.avg} :name "CPU") |
| 176 | (ring :value {EWW_RAM.used_mem_perc} :name "RAM") |
| 177 | (ring :value disk :name "Disk") |
| 178 | ;; The Superlight's battery; red when low rather than when full. |
| 179 | (ring :value {mouse.connected ? mouse.percentage : 0} |
| 180 | :text {mouse.connected ? "${mouse.percentage}%" : "–"} |
| 181 | :hot {mouse.class == "warning" || mouse.class == "critical"} |
| 182 | :name {mouse.class == "charging" ? "Charging" : "Mouse"}))) |
| 183 | |
| 184 | (defwidget power [] |
| 185 | (box :class "card" :orientation "v" :space-evenly false :spacing 6 |
| 186 | (box :orientation "h" :space-evenly true |
| 187 | (power-btn :icon "" :label "Lock" |
| 188 | :onclick "${EWW_CMD} close control-center cc-closer; pidof hyprlock || setsid -f hyprlock") |
| 189 | (power-btn :icon "" :label "Sleep" |
| 190 | :onclick "${EWW_CMD} close control-center cc-closer; systemctl suspend") |
| 191 | (power-btn :icon "" :label "Log out" |
| 192 | :onclick "${EWW_CMD} update confirm=logout") |
| 193 | (power-btn :icon "" :label "Restart" |
| 194 | :onclick "${EWW_CMD} update confirm=reboot") |
| 195 | (power-btn :icon "" :label "Shut down" |
| 196 | :onclick "${EWW_CMD} update confirm=poweroff")) |
| 197 | (revealer :transition "slidedown" :duration "150ms" :reveal {confirm != "none"} |
| 198 | (box :class "confirm" :orientation "h" :space-evenly false :spacing 8 |
| 199 | (label :hexpand true :halign "start" |
| 200 | :text {confirm == "reboot" ? "Restart now?" : (confirm == "poweroff" ? "Shut down now?" : "Log out now?")}) |
| 201 | ;; Padding/background live on the inner box: GTK ignores CSS padding on an |
| 202 | ;; eventbox, so styling the eventbox itself left the pills shrink-wrapped. |
| 203 | (eventbox :class "btn-wrap" :cursor "pointer" :onclick "${EWW_CMD} update confirm=none" |
| 204 | (box :class "btn" |
| 205 | (label :text "Cancel"))) |
| 206 | (eventbox :class "btn-wrap" :cursor "pointer" :timeout "3s" |
| 207 | :onclick "${EWW_CMD} update confirm=none; ${EWW_CMD} close control-center cc-closer; ${confirm == 'reboot' ? 'systemctl reboot' : (confirm == 'poweroff' ? 'systemctl poweroff' : 'hyprctl dispatch exit')}" |
| 208 | (box :class "btn danger" |
| 209 | (label :text "Confirm"))))))) |
| 210 | |
| 211 | ;; Updates and reboots in a kitty window (tools/nixos-update), asking before it |
| 212 | ;; installs anything. |
| 213 | (defwidget update [] |
| 214 | (eventbox :class "tile-wrap" :cursor "pointer" :timeout "3s" |
| 215 | :onclick "${EWW_CMD} close control-center cc-closer; setsid -f kitty --title 'NixOS update' nixos-update" |
| 216 | (box :class "tile" :orientation "h" :space-evenly false :spacing 10 |
| 217 | (box :class "icon-circle" :space-evenly true |
| 218 | (label :class "nix" :text "")) |
| 219 | (box :orientation "v" :valign "center" :halign "start" :hexpand true :space-evenly false |
| 220 | (label :class "tile-title" :halign "start" :text "Update NixOS") |
| 221 | (label :class "tile-sub" :halign "start" :limit-width 34 :text nixos)) |
| 222 | (label :class "chevron" :valign "center" :text "")))) |
| 223 | |
| 224 | ;; --------------------------------------------------------------------------- |
| 225 | ;; Notifications panel |
| 226 | ;; --------------------------------------------------------------------------- |
| 227 | |
| 228 | (defwidget notifications [] |
| 229 | ;; One panel, no inner card: the header sits straight on the panel and every |
| 230 | ;; notification is its own card. Spacing comes from CSS margins rather than |
| 231 | ;; :spacing, because the two revealers stay in the layout even when folded shut. |
| 232 | (box :class "cc" :orientation "v" :space-evenly false |
| 233 | ;; Fixed height, so the header doesn't shrink when the Clear button hides. |
| 234 | (box :class "notif-header" :orientation "h" :space-evenly false :height 28 |
| 235 | (label :class "card-title" :hexpand true :halign "start" :valign "center" :text "Notifications") |
| 236 | (eventbox :class "btn-wrap" :cursor "pointer" :timeout "2s" :valign "center" |
| 237 | :visible {arraylength(notifs) > 0 && !notifs_clearing} |
| 238 | :onclick "bash $HOME/.config/waybar/eww/scripts/clear-notifications.sh &" |
| 239 | (box :class "btn" |
| 240 | (label :text "Clear")))) |
| 241 | ;; Slides in while the list folds up, so the panel goes straight to its empty size. |
| 242 | (revealer :transition "slidedown" :duration "220ms" |
| 243 | :reveal {arraylength(notifs) == 0 || notifs_folded} |
| 244 | (label :class "notif-empty" :halign "start" :text "No notifications")) |
| 245 | (revealer :transition "slideup" :duration "220ms" :reveal {!notifs_folded} |
| 246 | (box :orientation "v" :space-evenly false |
| 247 | (for n in notifs |
| 248 | ;; Click: jump to the app that sent it (scripts/open-notification.py). Only the |
| 249 | ;; numeric id is passed, never the notification's own text. |
| 250 | (eventbox :class "notif-wrap" :cursor "pointer" :timeout "3s" |
| 251 | :onclick "python3 $HOME/.config/waybar/eww/scripts/open-notification.py ${n.id} &" |
| 252 | ;; `d<n>` delays each entry's slide-out a little more than the one above it |
| 253 | (box :class "notif d${n.i} ${notifs_clearing ? 'out' : ''}" |
| 254 | :orientation "v" :space-evenly false :spacing 2 |
| 255 | (box :orientation "h" :space-evenly false |
| 256 | (label :class "notif-meta" :hexpand true :halign "start" :limit-width 30 :text {n.app}) |
| 257 | (label :class "notif-meta" :text {n.time})) |
| 258 | (label :class "notif-summary" :halign "start" :xalign 0 :wrap true :text {n.summary}) |
| 259 | (label :class "notif-body" :halign "start" :xalign 0 :wrap true |
| 260 | :visible {n.body != ""} :text {n.body})))))))) |