Weather&Audio

This commit is contained in:
soraefir
2026-06-13 12:19:46 +02:00
parent 58af37ea8c
commit 43a074f355
8 changed files with 246 additions and 9 deletions

View File

@@ -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;
}
}

View File

@@ -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")

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)))

View File

@@ -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