This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

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}