diff --git a/modules/home/wayland/apps/eww/bar/css/_clock.scss b/modules/home/wayland/apps/eww/bar/css/_clock.scss index 459a169..87aeba9 100644 --- a/modules/home/wayland/apps/eww/bar/css/_clock.scss +++ b/modules/home/wayland/apps/eww/bar/css/_clock.scss @@ -42,4 +42,39 @@ calendar { .datetime { padding: $gaps-window; +} + +// Clock window accents +.weather-accent { background-color: $base0A; } +.vol-accent { background-color: $base0D; } + +// Weather section +.weather-main { margin-bottom: 8pt; } +.weather-icon { font-size: 2.2em; margin-right: 12pt; @include color-accent; } +.weather-temp { font-size: 1.3em; font-weight: bold; @include color-base; } +.weather-desc { font-size: 0.78em; @include color-body; } +.weather-stats { margin-top: 4pt; } + +// Volume section +.ctrl-row { margin-bottom: 4pt; } +.ctrl-icon { font-size: 1.1em; min-width: 22pt; @include color-body; } +.ctrl-muted { @include color-inactive; } +.ctrl-value { font-size: 0.72em; min-width: 28pt; @include color-active; } + +scale.ctrl-slider { + padding: 2pt 4pt; + trough { + @include border-radius; + @include background-base2; + min-height: 5px; + highlight { background-color: $base0C; @include border-radius; } + } + slider { + background-color: $base07; + min-height: 11px; + min-width: 11px; + @include border-radius; + margin: -3px; + border: none; + } } \ No newline at end of file diff --git a/modules/home/wayland/apps/eww/bar/eww.yuck b/modules/home/wayland/apps/eww/bar/eww.yuck index a83e688..f95d4e2 100644 --- a/modules/home/wayland/apps/eww/bar/eww.yuck +++ b/modules/home/wayland/apps/eww/bar/eww.yuck @@ -9,6 +9,7 @@ (include "windows/sys.yuck") (include "windows/net.yuck") +(include "windows/clock.yuck") (include "windows/popup.yuck") (include "windows/radio.yuck") (include "windows/powermenu.yuck") diff --git a/modules/home/wayland/apps/eww/bar/modules/clock.yuck b/modules/home/wayland/apps/eww/bar/modules/clock.yuck index 3f1e17c..d473cdb 100644 --- a/modules/home/wayland/apps/eww/bar/modules/clock.yuck +++ b/modules/home/wayland/apps/eww/bar/modules/clock.yuck @@ -5,7 +5,7 @@ (eventbox :onhover "${EWW_CMD} update date_rev=true" :onhoverlost "${EWW_CMD} update date_rev=false" - :onclick "(sleep 0.1 && scripts/panel-toggle calendar)" + :onclick "(sleep 0.1 && scripts/panel-toggle clock)" :onrightclick "(sleep 0.1 && eww-open-on-current-screen powermenu --toggle)" (box :class "datetime" diff --git a/modules/home/wayland/apps/eww/bar/scripts/panel-toggle b/modules/home/wayland/apps/eww/bar/scripts/panel-toggle index 0a14244..f4bbcad 100755 --- a/modules/home/wayland/apps/eww/bar/scripts/panel-toggle +++ b/modules/home/wayland/apps/eww/bar/scripts/panel-toggle @@ -1,7 +1,4 @@ #!/usr/bin/env bash -# Toggle the shared popup window between panels (sys, net, calendar). -# Same panel clicked again → close. Different panel → close and reopen -# so the window always appears fully rendered with no transparent flash. PANEL="$1" CURRENT=$(eww state 2>/dev/null | grep '^active-panel:' | sed 's/^active-panel: //' | tr -d '"') @@ -9,7 +6,6 @@ if [ "$CURRENT" = "$PANEL" ]; then eww close popup eww update active-panel="" else - eww close popup 2>/dev/null eww update active-panel="$PANEL" SCREEN=$(hyprctl monitors -j 2>/dev/null | jq -r '.[] | select(.focused == true) | .name' | head -n1) [ -n "$SCREEN" ] && eww open popup --screen "$SCREEN" || eww open popup diff --git a/modules/home/wayland/apps/eww/bar/scripts/volume b/modules/home/wayland/apps/eww/bar/scripts/volume new file mode 100755 index 0000000..307ac45 --- /dev/null +++ b/modules/home/wayland/apps/eww/bar/scripts/volume @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +volicons=("󰕿" "󰖀" "󰕾") + +vol() { wpctl get-volume @DEFAULT_AUDIO_"$1"@ | awk '{print int($2*100)}'; } +ismuted() { wpctl get-volume @DEFAULT_AUDIO_"$1"@ | rg -qi muted; echo -n $?; } +setvol() { wpctl set-volume @DEFAULT_AUDIO_"$1"@ "$(awk -v n="$2" 'BEGIN{print n/100}')"; } +setmute() { wpctl set-mute @DEFAULT_AUDIO_"$1"@ toggle; } + +gen_output() { + percent=$(vol "SINK") + lvl=$(awk -v n="$percent" 'BEGIN{print int(n/34)}') + sink_muted=$(ismuted "SINK") + source_muted=$(ismuted "SOURCE") + + [ "$sink_muted" = 0 ] && icon="󰝟" || icon="${volicons[$lvl]}" + [ "$source_muted" = 0 ] && mic_icon="󰍭" || mic_icon="󰍬" + + printf '{"icon":"%s","percent":%s,"sink_muted":%s,"mic_icon":"%s","microphone":%s,"source_muted":%s}\n' \ + "$icon" "$percent" "$([ "$sink_muted" = 0 ] && echo true || echo false)" \ + "$mic_icon" "$(vol SOURCE)" "$([ "$source_muted" = 0 ] && echo true || echo false)" +} + +case "$1" in + mute) setmute "$2" ;; + setvol) setvol "$2" "$3" ;; + *) + gen_output + pw-cli -m 2>/dev/null | rg --line-buffered "PipeWire:Interface:Client" | while read -r _; do + gen_output + done + ;; +esac diff --git a/modules/home/wayland/apps/eww/bar/scripts/weather b/modules/home/wayland/apps/eww/bar/scripts/weather new file mode 100755 index 0000000..9139dca --- /dev/null +++ b/modules/home/wayland/apps/eww/bar/scripts/weather @@ -0,0 +1,100 @@ +#!/usr/bin/env bash + +FALLBACK='{"temp":0,"feelslike":0,"humidity":0,"wind":0,"desc":"Unavailable","icon":"󰖐","city":""}' +CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/eww" +LOC_CACHE="$CACHE_DIR/weather-location" +UA="eww-bar/1.0 cedric.hoelzl@gmail.com" + +mkdir -p "$CACHE_DIR" + +get_location() { + # Cache location for 1 hour; IP rarely changes + if [ -f "$LOC_CACHE" ] && [ -n "$(find "$LOC_CACHE" -mmin -60 2>/dev/null)" ]; then + cat "$LOC_CACHE" + return 0 + fi + data=$(curl -sf --max-time 5 "http://ip-api.com/json?fields=lat,lon,city") || return 1 + echo "$data" | tee "$LOC_CACHE" +} + +icon_for() { + case "$1" in + *thunder*) echo "󰖔" ;; + *snow*|*sleet*) echo "󰖘" ;; + heavyrain*|*heavyrainshowers*) echo "󰖖" ;; + *rain*|*shower*) echo "󰖗" ;; + fog*) echo "󰖑" ;; + cloudy*) echo "󰖐" ;; + partlycloudy*) echo "󰖕" ;; + fair*|clearsky*) echo "󰖙" ;; + *) echo "󰖐" ;; + esac +} + +desc_for() { + case "$(echo "$1" | sed 's/_day//;s/_night//;s/_polartwilight//')" in + clearsky) echo "Clear sky" ;; + fair) echo "Fair" ;; + partlycloudy) echo "Partly cloudy" ;; + cloudy) echo "Cloudy" ;; + fog) echo "Foggy" ;; + lightrain) echo "Light rain" ;; + rain) echo "Rain" ;; + heavyrain) echo "Heavy rain" ;; + lightrainshowers) echo "Light showers" ;; + rainshowers) echo "Rain showers" ;; + heavyrainshowers) echo "Heavy showers" ;; + lightrainandthunder) echo "Light rain & thunder" ;; + rainandthunder) echo "Rain & thunder" ;; + heavyrainandthunder) echo "Heavy rain & thunder" ;; + *showersandthunder) echo "Showers & thunder" ;; + lightsleet|lightsleetshowers) echo "Light sleet" ;; + sleet|sleetshowers) echo "Sleet" ;; + heavysleet|heavysleetshowers) echo "Heavy sleet" ;; + *sleetandthunder) echo "Sleet & thunder" ;; + lightsnow|lightsnowshowers) echo "Light snow" ;; + snow|snowshowers) echo "Snow" ;; + heavysnow|heavysnowshowers) echo "Heavy snow" ;; + *snowandthunder) echo "Snow & thunder" ;; + *) echo "$1" ;; + esac +} + +fetch() { + loc=$(get_location) || { echo "$FALLBACK"; return; } + lat=$(echo "$loc" | jq -r '.lat') + lon=$(echo "$loc" | jq -r '.lon') + city=$(echo "$loc" | jq -r '.city') + + data=$(curl -sf --max-time 8 \ + -H "User-Agent: $UA" \ + -H "Accept: application/json" \ + "https://api.met.no/weatherapi/locationforecast/2.0/compact?lat=$lat&lon=$lon") \ + || { echo "$FALLBACK"; return; } + + d='.properties.timeseries[0].data' + temp=$( echo "$data" | jq -r "${d}.instant.details.air_temperature | round") + humidity=$(echo "$data" | jq -r "${d}.instant.details.relative_humidity | round") + wind_ms=$( echo "$data" | jq -r "${d}.instant.details.wind_speed") + wind=$( echo "$wind_ms" | awk '{printf "%d", $1 * 3.6}') + code=$( echo "$data" | jq -r \ + "(${d}.next_1_hours.summary.symbol_code) // (${d}.next_6_hours.summary.symbol_code) // \"cloudy\"") + + # Simplified apparent temperature: wind chill below 10°C, else = temp + feelslike=$(echo "$temp $wind" | awk '{ + t=$1; v=$2 + if (v > 4.8 && t < 10) + printf "%d", 13.12 + 0.6215*t - 11.37*(v^0.16) + 0.3965*t*(v^0.16) + else + printf "%d", t + }') + + icon=$(icon_for "$code") + desc=$(desc_for "$code") + + printf '{"temp":%s,"feelslike":%s,"humidity":%s,"wind":%s,"desc":"%s","icon":"%s","city":"%s"}\n' \ + "$temp" "$feelslike" "$humidity" "$wind" "$desc" "$icon" "$city" +} + +fetch +while true; do sleep 600; fetch; done diff --git a/modules/home/wayland/apps/eww/bar/windows/clock.yuck b/modules/home/wayland/apps/eww/bar/windows/clock.yuck new file mode 100644 index 0000000..361cb43 --- /dev/null +++ b/modules/home/wayland/apps/eww/bar/windows/clock.yuck @@ -0,0 +1,72 @@ + +(deflisten weather + :initial '{"temp":0,"feelslike":0,"humidity":0,"wind":0,"desc":"","icon":"󰖐","city":""}' + "scripts/weather") + +(deflisten volume + :initial '{"icon":"󰕾","percent":50,"sink_muted":false,"mic_icon":"󰍬","microphone":50,"source_muted":false}' + "scripts/volume") + +; --- Weather --- + +(defwidget weather-section [] + (box :orientation "v" :space-evenly false :class "sys-section" + (section-header + :title {weather.city != "" ? "Weather · ${weather.city}" : "Weather"} + :accent "weather-accent") + (box :orientation "h" :space-evenly false :valign "center" :class "weather-main" + (label :class "weather-icon" :text {weather.icon}) + (box :orientation "v" :space-evenly false + (label :class "weather-temp" :text "${weather.temp}°C") + (label :class "weather-desc" :text {weather.desc}))) + (box :orientation "h" :space-evenly true :class "weather-stats" + (box :orientation "v" :space-evenly false :halign "center" + (label :class "gpu-stat-value" :text "${weather.feelslike}°C") + (label :class "gpu-stat-label" :text "feels like")) + (box :orientation "v" :space-evenly false :halign "center" + (label :class "gpu-stat-value" :text "${weather.humidity}%") + (label :class "gpu-stat-label" :text "humidity")) + (box :orientation "v" :space-evenly false :halign "center" + (label :class "gpu-stat-value" :text "${weather.wind} km/h") + (label :class "gpu-stat-label" :text "wind"))))) + +; --- Volume --- + +(defwidget vol-row [icon value onchange onclick muted] + (box :orientation "h" :space-evenly false :valign "center" :class "ctrl-row" + (button + :class "ctrl-icon ${muted ? 'ctrl-muted' : ''}" + :onclick onclick + (label :text icon)) + (scale + :min 0 :max 100 :value value + :hexpand true :class "ctrl-slider" + :onchange onchange) + (label :class "ctrl-value" :halign "end" :text "${value}%"))) + +(defwidget volume-section [] + (box :orientation "v" :space-evenly false :class "sys-section" + (section-header :title "Volume" :accent "vol-accent") + (vol-row + :icon {volume.icon} + :value {volume.percent} + :muted {volume.sink_muted} + :onchange "scripts/volume setvol SINK {}" + :onclick "scripts/volume mute SINK") + (vol-row + :icon {volume.mic_icon} + :value {volume.microphone} + :muted {volume.source_muted} + :onchange "scripts/volume setvol SOURCE {}" + :onclick "scripts/volume mute SOURCE"))) + +; --- Root --- + +(defwidget clock-win [] + (box :class "sys-win" :orientation "v" :space-evenly false + (weather-section) + (box :class "section-sep") + (calendar) + (box :class "section-sep") + (volume-section))) + diff --git a/modules/home/wayland/apps/eww/bar/windows/popup.yuck b/modules/home/wayland/apps/eww/bar/windows/popup.yuck index 9837f73..32f6e42 100644 --- a/modules/home/wayland/apps/eww/bar/windows/popup.yuck +++ b/modules/home/wayland/apps/eww/bar/windows/popup.yuck @@ -1,12 +1,12 @@ (defwidget popup-win [] (box :space-evenly false :orientation "v" - (box :visible {active-panel == "sys"} + (revealer :reveal {active-panel == "sys"} :transition "slidedown" :duration 120 (sys-win)) - (box :visible {active-panel == "net"} + (revealer :reveal {active-panel == "net"} :transition "slidedown" :duration 120 (net-win)) - (box :visible {active-panel == "calendar"} :class "sys-win" - (calendar)))) + (revealer :reveal {active-panel == "clock"} :transition "slidedown" :duration 120 + (clock-win)))) (defwindow popup :monitor 0