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