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