This repository has no description
0

Configure Feed

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

sway: migrate from waybar to eww

+463 -131
+463 -131
2configs/desktops/sway/default.nix
··· 8 8 9 9 term = "/run/current-system/sw/bin/alacritty"; 10 10 11 - waybarConfig = pkgs.writeText "waybar-config.json" ( 12 - builtins.toJSON { 13 - height = 30; 14 - spacing = 4; 15 - modules-left = [ 16 - "sway/workspaces" 17 - "sway/mode" 18 - "sway/scratchpad" 19 - ]; 20 - modules-center = [ "sway/window" ]; 21 - modules-right = [ 22 - "idle_inhibitor" 23 - "pulseaudio" 24 - "network" 25 - "cpu" 26 - "memory" 27 - "temperature" 28 - "backlight" 29 - "battery" 30 - "clock" 31 - "tray" 32 - ]; 11 + # eww configuration 12 + ewwYuck = pkgs.writeText "eww.yuck" '' 13 + ; Variables with polling - set initial values to avoid empty string errors 14 + (defpoll time :interval "1s" :initial "00:00" "date '+%H:%M'") 15 + (defpoll date :interval "60s" :initial "0000-00-00" "date '+%Y-%m-%d'") 16 + (defpoll cpu :interval "2s" :initial 0 "${lib.getExe cpuScript}") 17 + (defpoll memory :interval "2s" :initial 0 "${lib.getExe memScript}") 18 + (defpoll temperature :interval "2s" :initial 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 + (deflisten workspaces :initial '[]' "${lib.getExe workspacesScript}") 33 23 34 - "sway/mode".format = "<span style=\"italic\">{}</span>"; 24 + ; Widget definitions 25 + (defwidget bar [] 26 + (centerbox :orientation "h" 27 + (left) 28 + (center) 29 + (right))) 35 30 36 - "sway/scratchpad" = { 37 - format = "{icon} {count}"; 38 - show-empty = false; 39 - format-icons = [ 40 - "" 41 - "󰏃" 42 - ]; 43 - tooltip = true; 44 - tooltip-format = "{app}: {title}"; 45 - }; 31 + (defwidget left [] 32 + (box :class "left" :orientation "h" :space-evenly false :halign "start" 33 + (workspaces-widget))) 46 34 47 - idle_inhibitor = { 48 - format = "{icon}"; 49 - format-icons = { 50 - activated = "󰅶"; 51 - deactivated = "󰾪"; 52 - }; 53 - }; 35 + (defwidget center [] 36 + (box :class "center" :orientation "h" :space-evenly false :halign "center" 37 + (label :text "''${time}"))) 54 38 55 - tray.spacing = 10; 39 + (defwidget right [] 40 + (box :class "right" :orientation "h" :space-evenly false :halign "end" :spacing 8 41 + (network-widget) 42 + (volume-widget) 43 + (graph-widget :value cpu :icon "󰍛" :class "cpu" :unit "%") 44 + (graph-widget :value memory :icon "󰘚" :class "memory" :unit "%") 45 + (graph-widget :value temperature :icon "󰔏" :class "temp" :unit "°") 46 + (battery-widget) 47 + (label :class "date" :text "''${date}") 48 + (systray :class "systray" :icon-size 18 :spacing 4))) 56 49 57 - clock = { 58 - tooltip-format = "<big>{:%Y %B}</big>\n<tt><small>{calendar}</small></tt>"; 59 - format-alt = "{:%Y-%m-%d}"; 60 - }; 50 + (defwidget workspaces-widget [] 51 + (box :class "workspaces" :orientation "h" :space-evenly false :spacing 0 52 + (for ws in workspaces 53 + (button :class {ws.urgent ? "workspace urgent" : (ws.focused ? "workspace focused" : "workspace")} 54 + :onclick "swaymsg workspace ''${ws.name}" 55 + {ws.name})))) 61 56 62 - cpu = { 63 - format = "{usage}% 󰍛"; 64 - tooltip = false; 65 - }; 57 + (defwidget graph-widget [value icon class unit] 58 + (box :class "graph-module ''${class}" :orientation "h" :space-evenly false :spacing 4 59 + (graph :class "graph" 60 + :value value 61 + :thickness 3 62 + :time-range "60s" 63 + :min 0 64 + :max 100 65 + :dynamic true 66 + :line-style "round" 67 + :width 75) 68 + (label :text "''${value}''${unit} ''${icon}"))) 66 69 67 - memory.format = "{}% 󰘚"; 70 + (defwidget battery-widget [] 71 + (box :class "battery" :orientation "h" :space-evenly false 72 + :tooltip "''${battery.watts}W | ''${battery.time_remaining} ''${battery.status == 'Charging' ? 'to full' : 'remaining'}" 73 + (label :text "''${battery.capacity}% ''${battery.icon}"))) 68 74 69 - temperature = { 70 - critical-threshold = 80; 71 - format = "{temperatureC}°C {icon}"; 72 - format-icons = [ 73 - "󰔏" 74 - "󰔏" 75 - "󰔏" 76 - ]; 77 - }; 75 + (defwidget volume-widget [] 76 + (box :class "volume" :orientation "h" :space-evenly false 77 + (label :text "''${volume.level}% ''${volume.icon}"))) 78 78 79 - backlight = { 80 - format = "{percent}% {icon}"; 81 - format-icons = [ 82 - "󰃞" 83 - "󰃟" 84 - "󰃠" 85 - ]; 86 - }; 79 + (defwidget network-widget [] 80 + (box :class "network" :orientation "h" :space-evenly false 81 + (label :text "''${network}"))) 87 82 88 - battery = { 89 - states = { 90 - warning = 30; 91 - critical = 15; 92 - }; 93 - format = "{capacity}% {icon}"; 94 - format-full = "{capacity}% {icon}"; 95 - format-charging = "{capacity}% 󰂄"; 96 - format-plugged = "{capacity}% 󰚥"; 97 - format-alt = "{time} {icon}"; 98 - format-icons = [ 99 - "󰂎" 100 - "󰁺" 101 - "󰁻" 102 - "󰁼" 103 - "󰁽" 104 - "󰁾" 105 - "󰁿" 106 - "󰂀" 107 - "󰂁" 108 - "󰂂" 109 - "󰁹" 110 - ]; 111 - }; 83 + ; Window definition - screen name passed as argument 84 + (defwindow bar [screen] 85 + :monitor screen 86 + :geometry (geometry :x "0%" 87 + :y "0%" 88 + :width "100%" 89 + :height "36px" 90 + :anchor "top center") 91 + :stacking "fg" 92 + :exclusive true 93 + :focusable false 94 + (bar)) 95 + ''; 112 96 113 - network = { 114 - format-wifi = "{essid} ({signalStrength}%) 󰖩"; 115 - format-ethernet = "{ipaddr}/{cidr} 󰈀"; 116 - tooltip-format = "{ifname} via {gwaddr} 󰛳"; 117 - format-linked = "{ifname} (No IP) 󰈀"; 118 - format-disconnected = "Disconnected 󰖪"; 119 - format-alt = "{ifname}: {ipaddr}/{cidr}"; 120 - }; 121 - 122 - pulseaudio = { 123 - format = "{volume}% {icon} {format_source}"; 124 - format-bluetooth = "{volume}% {icon}󰂯 {format_source}"; 125 - format-bluetooth-muted = "󰖁 {icon}󰂯 {format_source}"; 126 - format-muted = "󰖁 {format_source}"; 127 - format-source = "{volume}% 󰍬"; 128 - format-source-muted = "󰍭"; 129 - format-icons = { 130 - headphone = "󰋋"; 131 - hands-free = "󰋎"; 132 - headset = "󰋎"; 133 - phone = "󰏲"; 134 - portable = "󰏲"; 135 - car = "󰄋"; 136 - default = [ 137 - "󰕿" 138 - "󰖀" 139 - "󰕾" 140 - ]; 141 - }; 142 - on-click = "pavucontrol"; 143 - }; 144 - } 145 - ); 146 - 147 - waybarStyle = pkgs.writeText "waybar-style.css" '' 148 - @import "${pkgs.waybar}/etc/xdg/waybar/style.css"; 97 + ewwScss = pkgs.writeText "eww.scss" '' 149 98 * { 99 + all: unset; 150 100 font-family: "Iosevka Nerd Font", monospace; 101 + font-size: 14px; 151 102 } 103 + 104 + window { 105 + background-color: #1a1b26; 106 + color: #c0caf5; 107 + } 108 + 109 + .left, .center, .right { 110 + margin: 4px 8px; 111 + } 112 + 113 + .workspaces { 114 + button { 115 + padding: 0 4px; 116 + margin: 0 1px; 117 + border-radius: 2px; 118 + min-width: 24px; 119 + background-color: #24283b; 120 + &.focused { 121 + background-color: #7aa2f7; 122 + color: #1a1b26; 123 + } 124 + &.urgent { 125 + background-color: #f7768e; 126 + color: #1a1b26; 127 + } 128 + &:hover { 129 + background-color: #414868; 130 + } 131 + } 132 + } 133 + 134 + .graph-module { 135 + padding: 0 8px; 136 + margin: 0 2px; 137 + border-radius: 4px; 138 + &.cpu { 139 + background-color: #9ece6a; 140 + color: #1a1b26; 141 + } 142 + &.memory { 143 + background-color: #7dcfff; 144 + color: #1a1b26; 145 + } 146 + &.temp { 147 + background-color: #ff9e64; 148 + color: #1a1b26; 149 + } 150 + } 151 + 152 + .graph { 153 + min-height: 20px; 154 + } 155 + 156 + .battery, .volume, .network, .date { 157 + padding: 0 8px; 158 + margin: 0 2px; 159 + border-radius: 4px; 160 + background-color: #24283b; 161 + } 162 + 163 + .systray { 164 + padding: 0 8px; 165 + margin: 0 2px; 166 + } 167 + 168 + tooltip { 169 + background-color: #1a1b26; 170 + color: #c0caf5; 171 + border: 1px solid #414868; 172 + border-radius: 4px; 173 + padding: 4px 8px; 174 + } 175 + ''; 176 + 177 + cpuScript = pkgs.writeShellApplication { 178 + name = "eww-cpu"; 179 + text = '' 180 + STATE_FILE="/tmp/eww-cpu-state" 181 + read -r _cpu user nice system idle _rest < /proc/stat 182 + total=$((user + nice + system + idle)) 183 + 184 + if [ -f "$STATE_FILE" ]; then 185 + read -r prev_total prev_idle < "$STATE_FILE" 186 + diff_total=$((total - prev_total)) 187 + diff_idle=$((idle - prev_idle)) 188 + if [ "$diff_total" -gt 0 ]; then 189 + echo $((100 * (diff_total - diff_idle) / diff_total)) 190 + else 191 + echo 0 192 + fi 193 + else 194 + echo 0 195 + fi 196 + echo "$total $idle" > "$STATE_FILE" 197 + ''; 198 + }; 199 + 200 + memScript = pkgs.writeShellApplication { 201 + name = "eww-mem"; 202 + runtimeInputs = [ pkgs.gawk ]; 203 + text = '' 204 + awk '/MemTotal/ {total=$2} /MemAvailable/ {available=$2} END {printf "%d", (total-available)*100/total}' /proc/meminfo 205 + ''; 206 + }; 207 + 208 + tempScript = pkgs.writeShellApplication { 209 + name = "eww-temp"; 210 + text = '' 211 + temp_raw=$(cat /sys/class/thermal/thermal_zone0/temp) 212 + echo $((temp_raw / 1000)) 213 + ''; 214 + }; 215 + 216 + batteryScript = pkgs.writeShellApplication { 217 + name = "eww-battery"; 218 + runtimeInputs = [ 219 + pkgs.jq 220 + pkgs.bc 221 + ]; 222 + text = '' 223 + # Find battery device dynamically 224 + bat_path="" 225 + for p in /sys/class/power_supply/BAT* /sys/class/power_supply/BATT*; do 226 + if [ -f "$p/capacity" ]; then 227 + bat_path="$p" 228 + break 229 + fi 230 + done 231 + 232 + if [ -z "$bat_path" ]; then 233 + echo '{"capacity": "N/A", "icon": "󰂃", "watts": "0", "time_remaining": "N/A"}' 234 + exit 0 235 + fi 236 + 237 + capacity=$(cat "$bat_path/capacity" 2>/dev/null || echo 100) 238 + status=$(cat "$bat_path/status" 2>/dev/null || echo "Full") 239 + power_now=$(cat "$bat_path/power_now" 2>/dev/null || echo 0) 240 + energy_now=$(cat "$bat_path/energy_now" 2>/dev/null || echo 0) 241 + energy_full=$(cat "$bat_path/energy_full" 2>/dev/null || echo 0) 242 + 243 + # Convert microwatts to watts 244 + watts=$(echo "scale=1; $power_now / 1000000" | bc) 245 + 246 + # Calculate time remaining 247 + if [ "$power_now" -gt 0 ]; then 248 + if [ "$status" = "Charging" ]; then 249 + # Time to full 250 + remaining_energy=$((energy_full - energy_now)) 251 + hours=$(echo "scale=2; $remaining_energy / $power_now" | bc) 252 + else 253 + # Time to empty 254 + hours=$(echo "scale=2; $energy_now / $power_now" | bc) 255 + fi 256 + # Convert to hours:minutes 257 + h=$(echo "$hours" | cut -d. -f1) 258 + m=$(echo "scale=0; ($hours - $h) * 60 / 1" | bc) 259 + time_remaining="''${h}h ''${m}m" 260 + else 261 + time_remaining="--" 262 + fi 263 + 264 + if [ "$status" = "Charging" ]; then 265 + icon="󰂄" 266 + elif [ "$status" = "Full" ]; then 267 + icon="󰁹" 268 + elif [ "$capacity" -gt 80 ]; then 269 + icon="󰂂" 270 + elif [ "$capacity" -gt 60 ]; then 271 + icon="󰂀" 272 + elif [ "$capacity" -gt 40 ]; then 273 + icon="󰁾" 274 + elif [ "$capacity" -gt 20 ]; then 275 + icon="󰁼" 276 + else 277 + icon="󰁺" 278 + fi 279 + jq -n --arg cap "$capacity" --arg icon "$icon" --arg watts "$watts" --arg time "$time_remaining" --arg status "$status" \ 280 + '{capacity: $cap, icon: $icon, watts: $watts, time_remaining: $time, status: $status}' 281 + ''; 282 + }; 283 + 284 + volumeScript = pkgs.writeShellApplication { 285 + name = "eww-volume"; 286 + runtimeInputs = [ 287 + pkgs.pulseaudio 288 + pkgs.jq 289 + pkgs.gnugrep 290 + ]; 291 + text = '' 292 + vol=$(pactl get-sink-volume @DEFAULT_SINK@ | grep -oP '\d+%' | head -1 | tr -d '%') 293 + muted=$(pactl get-sink-mute @DEFAULT_SINK@ | grep -oP '(yes|no)') 294 + if [ "$muted" = "yes" ]; then 295 + icon="󰖁" 296 + elif [ "$vol" -gt 50 ]; then 297 + icon="󰕾" 298 + elif [ "$vol" -gt 0 ]; then 299 + icon="󰖀" 300 + else 301 + icon="󰕿" 302 + fi 303 + jq -n --arg level "$vol" --arg icon "$icon" '{level: $level, icon: $icon}' 304 + ''; 305 + }; 306 + 307 + networkScript = pkgs.writeShellApplication { 308 + name = "eww-network"; 309 + runtimeInputs = [ pkgs.networkmanager ]; 310 + text = '' 311 + connection=$(nmcli -t -f NAME,TYPE,DEVICE connection show --active 2>/dev/null | head -1) 312 + if [ -z "$connection" ]; then 313 + echo "󰖪 Disconnected" 314 + else 315 + name=$(echo "$connection" | cut -d: -f1) 316 + type=$(echo "$connection" | cut -d: -f2) 317 + if [ "$type" = "802-11-wireless" ]; then 318 + echo "󰖩 $name" 319 + else 320 + echo "󰈀 $name" 321 + fi 322 + fi 323 + ''; 324 + }; 325 + 326 + workspacesScript = pkgs.writeShellApplication { 327 + name = "sway-workspaces"; 328 + runtimeInputs = [ 329 + pkgs.sway 330 + pkgs.jq 331 + ]; 332 + text = '' 333 + # Output initial state 334 + swaymsg -t get_workspaces | jq -c '[.[] | {name: .name, focused: .focused, urgent: .urgent}]' 335 + 336 + # Subscribe to workspace events and output updated state 337 + swaymsg -t subscribe -m '["workspace"]' | while read -r _; do 338 + swaymsg -t get_workspaces | jq -c '[.[] | {name: .name, focused: .focused, urgent: .urgent}]' 339 + done 340 + ''; 341 + }; 342 + 343 + ewwConfigDir = pkgs.runCommand "eww-config" { } '' 344 + mkdir -p $out 345 + cp ${ewwYuck} $out/eww.yuck 346 + cp ${ewwScss} $out/eww.scss 152 347 ''; 153 348 154 349 in ··· 164 359 ''; 165 360 }; 166 361 167 - systemd.user.services.waybar = { 168 - description = "Highly customizable Wayland bar for Sway"; 169 - documentation = [ "man:waybar(5)" ]; 362 + # Enable realtime scheduling for sway 363 + security.pam.loginLimits = [ 364 + { 365 + domain = "@users"; 366 + item = "rtprio"; 367 + type = "-"; 368 + value = 1; 369 + } 370 + ]; 371 + 372 + # eww daemon 373 + systemd.user.services.eww = { 374 + description = "ElKowars Wacky Widgets daemon"; 170 375 partOf = [ "sway-session.target" ]; 171 376 wantedBy = [ "sway-session.target" ]; 377 + path = [ 378 + pkgs.bash 379 + pkgs.sway 380 + ]; 172 381 serviceConfig = { 173 382 Type = "simple"; 174 - ExecStart = "${pkgs.waybar}/bin/waybar -c ${waybarConfig} -s ${waybarStyle}"; 383 + ExecStart = "${pkgs.eww}/bin/eww daemon --config ${ewwConfigDir} --no-daemonize"; 384 + ExecStop = "${pkgs.eww}/bin/eww --config ${ewwConfigDir} kill"; 385 + Restart = "on-failure"; 386 + RestartSec = 1; 387 + }; 388 + }; 389 + 390 + # eww bar per output - template unit using output name (e.g. eww-bar@eDP-1.service) 391 + systemd.user.services."eww-bar@" = { 392 + description = "Eww bar on output %i"; 393 + partOf = [ "sway-session.target" ]; 394 + after = [ "eww.service" ]; 395 + requires = [ "eww.service" ]; 396 + serviceConfig = { 397 + Type = "oneshot"; 398 + RemainAfterExit = true; 399 + ExecStart = "${pkgs.eww}/bin/eww --config ${ewwConfigDir} open bar --id bar-%i --arg screen=%i"; 400 + ExecStop = "${pkgs.eww}/bin/eww --config ${ewwConfigDir} close bar-%i"; 401 + }; 402 + }; 403 + 404 + # Watches for output changes and starts/stops eww-bar@ instances 405 + systemd.user.services.eww-output-watcher = { 406 + description = "Watch for output changes and manage eww bars"; 407 + partOf = [ "sway-session.target" ]; 408 + wantedBy = [ "sway-session.target" ]; 409 + after = [ "eww.service" ]; 410 + requires = [ "eww.service" ]; 411 + serviceConfig = { 412 + Type = "simple"; 413 + ExecStart = lib.getExe ( 414 + pkgs.writeShellApplication { 415 + name = "eww-output-watcher"; 416 + runtimeInputs = [ 417 + pkgs.sway 418 + pkgs.jq 419 + pkgs.systemd 420 + pkgs.gawk 421 + pkgs.gnused 422 + pkgs.gnugrep 423 + ]; 424 + text = '' 425 + update_bars() { 426 + # Get current outputs 427 + outputs=$(swaymsg -t get_outputs | jq -r '.[].name') 428 + 429 + # Start bars for connected outputs 430 + for output in $outputs; do 431 + systemctl --user start "eww-bar@$output.service" || true 432 + done 433 + 434 + # Stop bars for disconnected outputs 435 + running=$(systemctl --user list-units --type=service --state=running --plain --no-legend 'eww-bar@*' | awk '{print $1}' | sed 's/eww-bar@\(.*\)\.service/\1/') 436 + for output in $running; do 437 + if ! echo "$outputs" | grep -qx "$output"; then 438 + systemctl --user stop "eww-bar@$output.service" || true 439 + fi 440 + done 441 + } 442 + 443 + update_bars 444 + swaymsg -t subscribe -m '["output"]' | while read -r _; do 445 + sleep 0.5 446 + update_bars 447 + done 448 + ''; 449 + } 450 + ); 175 451 Restart = "on-failure"; 176 452 RestartSec = 1; 177 453 }; ··· 401 677 402 678 bindsym $mod+Tab focus next 403 679 bindsym $mod+Escape workspace back_and_forth 680 + 681 + # Focus primary output 682 + bindsym $mod+F1 exec ${ 683 + lib.getExe ( 684 + pkgs.writeShellApplication { 685 + name = "focus-primary-output"; 686 + runtimeInputs = [ 687 + pkgs.sway 688 + pkgs.jq 689 + ]; 690 + text = '' 691 + PRIMARY=$(swaymsg -r -t get_outputs | jq -r 'sort_by(.x, .y) | .[0].name') 692 + swaymsg focus output "$PRIMARY" 693 + ''; 694 + } 695 + ) 696 + } 697 + 698 + # Cycle through secondary outputs 699 + bindsym $mod+F2 exec ${ 700 + lib.getExe ( 701 + pkgs.writeShellApplication { 702 + name = "cycle-secondary-outputs"; 703 + runtimeInputs = [ 704 + pkgs.sway 705 + pkgs.jq 706 + pkgs.gnugrep 707 + pkgs.coreutils 708 + ]; 709 + text = '' 710 + OUTPUTS=$(swaymsg -r -t get_outputs | jq -r 'sort_by(.x, .y)') 711 + PRIMARY=$(echo "$OUTPUTS" | jq -r '.[0].name') 712 + CURRENT=$(echo "$OUTPUTS" | jq -r '.[] | select(.focused == true).name') 713 + SECONDARIES=$(echo "$OUTPUTS" | jq -r '.[1:] | .[].name') 714 + 715 + if [ -z "$SECONDARIES" ]; then 716 + exit 0 717 + fi 718 + 719 + if [ "$CURRENT" = "$PRIMARY" ]; then 720 + # Focus first secondary 721 + NEXT=$(echo "$SECONDARIES" | head -n1) 722 + else 723 + # Find next secondary in the list 724 + NEXT=$(echo "$SECONDARIES" | grep -A1 "^$CURRENT$" | tail -n1) 725 + # If we're at the end, wrap to first secondary 726 + if [ "$NEXT" = "$CURRENT" ]; then 727 + NEXT=$(echo "$SECONDARIES" | head -n1) 728 + fi 729 + fi 730 + 731 + swaymsg focus output "$NEXT" 732 + ''; 733 + } 734 + ) 735 + } 404 736 405 737 # screenlock 406 738 bindsym $mod+F11 exec ${lib.getExe' pkgs.systemd "systemctl"} --user start lock.target