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