This repository has no description
16 kB
506 lines
1{
2 pkgs,
3 lib,
4 ...
5}:
6let
7
8 # eww configuration
9 ewwYuck = pkgs.writeText "eww.yuck" ''
10 ; Magic variables (built-in, no polling needed)
11 ; EWW_TIME provides current time
12 ; EWW_CPU provides {cores: [...], avg: X}
13 ; EWW_RAM provides {used_mem_perc: X, available_mem: Y, total_mem: Z, ...}
14
15 ; Custom variables with polling
16 (defpoll cpu-freq :interval "2s" :initial 0 "${lib.getExe cpuFreqScript}")
17 (defpoll cpu-load :interval "2s" :initial "0.00" "cut -d' ' -f1 /proc/loadavg")
18 (defpoll temperature :interval "2s" :initial '{"temp": 0, "fan": 0}' "${lib.getExe tempScript}")
19 (defpoll battery :interval "10s" :initial '{"capacity": "100", "icon": "", "watts": "0", "time_remaining": "--", "status": "Full"}' "${lib.getExe batteryScript}")
20 (defpoll volume :interval "1s" :initial '{"level": "0", "icon": ""}' "${lib.getExe volumeScript}")
21 (defpoll network :interval "5s" :initial " ..." "${lib.getExe networkScript}")
22 (defpoll brightness :interval "1s" :initial 100 "${lib.getExe brightnessScript}")
23 (defpoll idle-inhibited :interval "0.5s" :initial "false" "${lib.getExe idleStatusScript}")
24 (deflisten workspaces :initial '[]' "${lib.getExe workspacesScript}")
25
26 ; Widget definitions
27 (defwidget bar []
28 (centerbox :orientation "h"
29 (left)
30 (center)
31 (right)))
32
33 (defwidget left []
34 (box :class "left" :orientation "h" :space-evenly false :halign "start"
35 (workspaces-widget)))
36
37 (defwidget center []
38 (box :class "center" :orientation "h" :space-evenly false :halign "center"
39 (label :text {formattime(EWW_TIME, "%H:%M")})))
40
41 (defwidget right []
42 (box :class "right" :orientation "h" :space-evenly false :halign "end" :spacing 8
43 (network-widget)
44 (volume-widget)
45 (graph-widget :value {EWW_CPU.avg} :icon "" :class "cpu" :unit "%" :tooltip "Load: ''${cpu-load} | ''${cpu-freq} MHz")
46 (graph-widget :value {EWW_RAM.used_mem_perc} :icon "" :class "memory" :unit "%")
47 (graph-widget :value {temperature.temp} :icon "" :class "temp" :unit "°" :tooltip "Fan: ''${temperature.fan} RPM")
48 (brightness-widget)
49 (battery-widget)
50 (idle-inhibitor)
51 (label :class "date" :text {formattime(EWW_TIME, "%Y-%m-%d")})
52 (systray :class "systray" :icon-size 18 :spacing 4)))
53
54 (defwidget workspaces-widget []
55 (box :class "workspaces" :orientation "h" :space-evenly false :spacing 0
56 (for ws in workspaces
57 (button :class {ws.urgent ? "workspace urgent" : (ws.focused ? "workspace focused" : "workspace")}
58 :onclick "swaymsg workspace ''${ws.name}"
59 {ws.name}))))
60
61 (defwidget graph-widget [value icon class unit ?tooltip]
62 (box :class "graph-module ''${class}" :orientation "h" :space-evenly false :spacing 4
63 :tooltip {tooltip ?: ""}
64 (graph :class "graph"
65 :value value
66 :thickness 3
67 :time-range "60s"
68 :min 0
69 :max 100
70 :dynamic true
71 :line-style "round"
72 :width 75)
73 (label :text "''${round(value, 0)}''${unit} ''${icon}")))
74
75 (defwidget battery-widget []
76 (box :class "battery" :orientation "h" :space-evenly false
77 :tooltip "''${battery.watts}W | ''${battery.time_remaining} ''${battery.status == 'Charging' ? 'to full' : 'remaining'}"
78 (label :text "''${battery.capacity}% ''${battery.icon}")))
79
80 (defwidget volume-widget []
81 (box :class "volume" :orientation "h" :space-evenly false
82 (label :text "''${volume.level}% ''${volume.icon}")))
83
84 (defwidget network-widget []
85 (box :class "network" :orientation "h" :space-evenly false
86 (label :text "''${network}")))
87
88 (defwidget brightness-widget []
89 (button :class "brightness" :onclick "/run/current-system/sw/bin/switch-theme toggle"
90 (label :text "''${brightness}% ")))
91
92 (defwidget idle-inhibitor []
93 (button :class {idle-inhibited == "true" ? "idle-inhibitor active" : "idle-inhibitor"}
94 :onclick "${lib.getExe idleToggleScript}"
95 {idle-inhibited == "true" ? "" : ""}))
96
97 ; Window definition - screen name passed as argument
98 (defwindow bar [screen]
99 :monitor screen
100 :geometry (geometry :x "0%"
101 :y "0%"
102 :width "100%"
103 :height "36px"
104 :anchor "top center")
105 :stacking "fg"
106 :exclusive true
107 :focusable false
108 (bar))
109 '';
110
111 ewwScss = pkgs.writeText "eww.scss" ''
112 * {
113 all: unset;
114 font-family: "Iosevka Nerd Font", monospace;
115 font-size: 14px;
116 }
117
118 window {
119 background-color: #1a1b26;
120 color: #c0caf5;
121 }
122
123 .left, .center, .right {
124 margin: 4px 8px;
125 }
126
127 .workspaces {
128 button {
129 padding: 0 4px;
130 margin: 0 1px;
131 border-radius: 2px;
132 min-width: 24px;
133 background-color: #24283b;
134 &.focused {
135 background-color: #7aa2f7;
136 color: #1a1b26;
137 }
138 &.urgent {
139 background-color: #f7768e;
140 color: #1a1b26;
141 }
142 &:hover {
143 background-color: #414868;
144 }
145 }
146 }
147
148 .graph-module {
149 padding: 0 8px;
150 margin: 0 2px;
151 border-radius: 4px;
152 &.cpu {
153 background-color: #9ece6a;
154 color: #1a1b26;
155 }
156 &.memory {
157 background-color: #7dcfff;
158 color: #1a1b26;
159 }
160 &.temp {
161 background-color: #ff9e64;
162 color: #1a1b26;
163 }
164 }
165
166 .graph {
167 min-height: 20px;
168 }
169
170 .battery, .volume, .network, .date, .brightness {
171 padding: 0 8px;
172 margin: 0 2px;
173 border-radius: 4px;
174 background-color: #24283b;
175 }
176
177 .idle-inhibitor {
178 padding: 0 8px;
179 margin: 0 2px;
180 border-radius: 4px;
181 background-color: #24283b;
182 &.active {
183 background-color: #bb9af7;
184 color: #1a1b26;
185 }
186 &:hover {
187 background-color: #414868;
188 }
189 }
190
191 .systray {
192 padding: 0 8px;
193 margin: 0 2px;
194 }
195
196 tooltip {
197 background-color: #1a1b26;
198 color: #c0caf5;
199 border: 1px solid #414868;
200 border-radius: 4px;
201 padding: 4px 8px;
202 }
203 '';
204
205 cpuFreqScript = pkgs.writeShellApplication {
206 name = "eww-cpu-freq";
207 runtimeInputs = [ ];
208 text = ''
209 # Get average CPU frequency in MHz
210 freq=0
211 count=0
212 for f in /sys/devices/system/cpu/cpu*/cpufreq/scaling_cur_freq; do
213 if [ -f "$f" ]; then
214 val=$(cat "$f")
215 freq=$((freq + val))
216 count=$((count + 1))
217 fi
218 done
219 if [ "$count" -gt 0 ]; then
220 freq=$((freq / count / 1000))
221 fi
222 echo "$freq"
223 '';
224 };
225
226 tempScript = pkgs.writeShellApplication {
227 name = "eww-temp";
228 runtimeInputs = [ pkgs.jq ];
229 text = ''
230 temp_raw=$(cat /sys/class/thermal/thermal_zone0/temp 2>/dev/null || echo 0)
231 temp=$((temp_raw / 1000))
232
233 # Find gpdfan hwmon for fan speed
234 fan=0
235 for hwmon in /sys/class/hwmon/hwmon*; do
236 if [ "$(cat "$hwmon/name" 2>/dev/null)" = "gpdfan" ]; then
237 fan=$(cat "$hwmon/fan1_input" 2>/dev/null || echo 0)
238 break
239 fi
240 done
241
242 jq -n --argjson temp "$temp" --argjson fan "$fan" '{temp: $temp, fan: $fan}'
243 '';
244 };
245
246 batteryScript = pkgs.writeShellApplication {
247 name = "eww-battery";
248 runtimeInputs = [
249 pkgs.jq
250 pkgs.bc
251 ];
252 text = ''
253 # Find battery device dynamically
254 bat_path=""
255 for p in /sys/class/power_supply/BAT* /sys/class/power_supply/BATT*; do
256 if [ -f "$p/capacity" ]; then
257 bat_path="$p"
258 break
259 fi
260 done
261
262 if [ -z "$bat_path" ]; then
263 echo '{"capacity": "N/A", "icon": "", "watts": "0", "time_remaining": "N/A"}'
264 exit 0
265 fi
266
267 capacity=$(cat "$bat_path/capacity" 2>/dev/null || echo 100)
268 status=$(cat "$bat_path/status" 2>/dev/null || echo "Full")
269 power_now=$(cat "$bat_path/power_now" 2>/dev/null || echo 0)
270 energy_now=$(cat "$bat_path/energy_now" 2>/dev/null || echo 0)
271 energy_full=$(cat "$bat_path/energy_full" 2>/dev/null || echo 0)
272
273 # Convert microwatts to watts
274 watts=$(echo "scale=1; $power_now / 1000000" | bc)
275
276 # Calculate time remaining
277 if [ "$power_now" -gt 0 ]; then
278 if [ "$status" = "Charging" ]; then
279 # Time to full
280 remaining_energy=$((energy_full - energy_now))
281 hours=$(echo "scale=2; $remaining_energy / $power_now" | bc)
282 else
283 # Time to empty
284 hours=$(echo "scale=2; $energy_now / $power_now" | bc)
285 fi
286 # Convert to hours:minutes
287 h=$(echo "$hours" | cut -d. -f1)
288 m=$(echo "scale=0; ($hours - $h) * 60 / 1" | bc)
289 time_remaining="''${h}h ''${m}m"
290 else
291 time_remaining="--"
292 fi
293
294 if [ "$status" = "Charging" ]; then
295 icon=""
296 elif [ "$status" = "Full" ]; then
297 icon=""
298 elif [ "$capacity" -gt 80 ]; then
299 icon=""
300 elif [ "$capacity" -gt 60 ]; then
301 icon=""
302 elif [ "$capacity" -gt 40 ]; then
303 icon=""
304 elif [ "$capacity" -gt 20 ]; then
305 icon=""
306 else
307 icon=""
308 fi
309 jq -n --arg cap "$capacity" --arg icon "$icon" --arg watts "$watts" --arg time "$time_remaining" --arg status "$status" \
310 '{capacity: $cap, icon: $icon, watts: $watts, time_remaining: $time, status: $status}'
311 '';
312 };
313
314 volumeScript = pkgs.writeShellApplication {
315 name = "eww-volume";
316 runtimeInputs = [
317 pkgs.pulseaudio
318 pkgs.jq
319 pkgs.gnugrep
320 ];
321 text = ''
322 vol=$(pactl get-sink-volume @DEFAULT_SINK@ | grep -oP '\d+%' | head -1 | tr -d '%')
323 muted=$(pactl get-sink-mute @DEFAULT_SINK@ | grep -oP '(yes|no)')
324 if [ "$muted" = "yes" ]; then
325 icon=""
326 elif [ "$vol" -gt 50 ]; then
327 icon=""
328 elif [ "$vol" -gt 0 ]; then
329 icon=""
330 else
331 icon=""
332 fi
333 jq -n --arg level "$vol" --arg icon "$icon" '{level: $level, icon: $icon}'
334 '';
335 };
336
337 networkScript = pkgs.writeShellApplication {
338 name = "eww-network";
339 runtimeInputs = [ pkgs.networkmanager ];
340 text = ''
341 connection=$(nmcli -t -f NAME,TYPE,DEVICE connection show --active 2>/dev/null | head -1)
342 if [ -z "$connection" ]; then
343 echo " Disconnected"
344 else
345 name=$(echo "$connection" | cut -d: -f1)
346 type=$(echo "$connection" | cut -d: -f2)
347 if [ "$type" = "802-11-wireless" ]; then
348 echo " $name"
349 else
350 echo " $name"
351 fi
352 fi
353 '';
354 };
355
356 workspacesScript = pkgs.writeShellApplication {
357 name = "sway-workspaces";
358 runtimeInputs = [
359 pkgs.sway
360 pkgs.jq
361 ];
362 text = ''
363 # Output initial state
364 swaymsg -t get_workspaces | jq -c '[.[] | {name: .name, focused: .focused, urgent: .urgent}]'
365
366 # Subscribe to workspace events and output updated state
367 swaymsg -t subscribe -m '["workspace"]' | while read -r _; do
368 swaymsg -t get_workspaces | jq -c '[.[] | {name: .name, focused: .focused, urgent: .urgent}]'
369 done
370 '';
371 };
372
373 brightnessScript = pkgs.writeShellApplication {
374 name = "eww-brightness";
375 runtimeInputs = [ pkgs.brightnessctl ];
376 text = ''
377 brightnessctl -m | cut -d, -f4 | tr -d '%'
378 '';
379 };
380
381 idleToggleScript = pkgs.writeShellApplication {
382 name = "eww-idle-toggle";
383 runtimeInputs = [ pkgs.systemd ];
384 text = ''
385 if systemctl --user is-active --quiet sway-idle-inhibit.service; then
386 systemctl --user stop sway-idle-inhibit.service
387 else
388 systemctl --user start sway-idle-inhibit.service
389 fi
390 '';
391 };
392
393 idleStatusScript = pkgs.writeShellApplication {
394 name = "eww-idle-status";
395 runtimeInputs = [ pkgs.systemd ];
396 text = ''
397 if systemctl --user is-active --quiet sway-idle-inhibit.service; then
398 echo "true"
399 else
400 echo "false"
401 fi
402 '';
403 };
404
405 ewwConfigDir = pkgs.runCommand "eww-config" { } ''
406 mkdir -p $out
407 cp ${ewwYuck} $out/eww.yuck
408 cp ${ewwScss} $out/eww.scss
409 '';
410
411in
412{
413 # eww daemon
414 systemd.user.services.eww = {
415 description = "ElKowars Wacky Widgets daemon";
416 partOf = [ "sway-session.target" ];
417 wantedBy = [ "sway-session.target" ];
418 path = [
419 pkgs.bash
420 pkgs.sway
421 ];
422 environment.EWW_CONFIG = "${ewwConfigDir}";
423 serviceConfig = {
424 Type = "simple";
425 ExecStart = "${lib.getExe pkgs.eww} daemon --config ${ewwConfigDir} --no-daemonize";
426 ExecStop = "${lib.getExe pkgs.eww} --config ${ewwConfigDir} kill";
427 Restart = "on-failure";
428 RestartSec = 1;
429 };
430 };
431
432 # Idle inhibit service - toggled by eww button
433 systemd.user.services.sway-idle-inhibit = {
434 description = "Inhibit idle/sleep";
435 serviceConfig = {
436 Type = "simple";
437 ExecStart = "${pkgs.systemd}/bin/systemd-inhibit --what=idle:sleep --who=eww --why='User requested' --mode=block sleep infinity";
438 };
439 };
440
441 # eww bar per output - template unit using output name (e.g. eww-bar@eDP-1.service)
442 systemd.user.services."eww-bar@" = {
443 description = "Eww bar on output %i";
444 partOf = [ "sway-session.target" ];
445 after = [ "eww.service" ];
446 requires = [ "eww.service" ];
447 serviceConfig = {
448 Type = "oneshot";
449 RemainAfterExit = true;
450 ExecStart = "${pkgs.eww}/bin/eww --config ${ewwConfigDir} open bar --id bar-%i --arg screen=%i";
451 ExecStop = "${pkgs.eww}/bin/eww --config ${ewwConfigDir} close bar-%i";
452 };
453 };
454
455 # Watches for output changes and starts/stops eww-bar@ instances
456 systemd.user.services.eww-output-watcher = {
457 description = "Watch for output changes and manage eww bars";
458 partOf = [ "sway-session.target" ];
459 wantedBy = [ "sway-session.target" ];
460 after = [ "eww.service" ];
461 requires = [ "eww.service" ];
462 serviceConfig = {
463 Type = "simple";
464 ExecStart = lib.getExe (
465 pkgs.writeShellApplication {
466 name = "eww-output-watcher";
467 runtimeInputs = [
468 pkgs.sway
469 pkgs.jq
470 pkgs.systemd
471 pkgs.gawk
472 pkgs.gnused
473 pkgs.gnugrep
474 ];
475 text = ''
476 update_bars() {
477 # Get current outputs
478 outputs=$(swaymsg -t get_outputs | jq -r '.[].name')
479
480 # Start bars for connected outputs
481 for output in $outputs; do
482 systemctl --user start "eww-bar@$output.service" || true
483 done
484
485 # Stop bars for disconnected outputs
486 running=$(systemctl --user list-units --type=service --state=running --plain --no-legend 'eww-bar@*' | awk '{print $1}' | sed 's/eww-bar@\(.*\)\.service/\1/')
487 for output in $running; do
488 if ! echo "$outputs" | grep -qx "$output"; then
489 systemctl --user stop "eww-bar@$output.service" || true
490 fi
491 done
492 }
493
494 update_bars
495 swaymsg -t subscribe -m '["output"]' | while read -r _; do
496 sleep 0.5
497 update_bars
498 done
499 '';
500 }
501 );
502 Restart = "on-failure";
503 RestartSec = 1;
504 };
505 };
506}