dot

vizie's NixOS desktop: Hyprland, CachyOS kernel and gaming tweaks

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