diff --git a/homeModules/waybar.nix b/homeModules/waybar.nix index 2b11d9c..b77da79 100644 --- a/homeModules/waybar.nix +++ b/homeModules/waybar.nix @@ -139,7 +139,7 @@ "custom/weather" = { exec = "${pkgs.nushell}/bin/nu ${../res/custom_waybar_modules/weather.nu}"; format = "{}"; - interval = 600; + interval = 900; on-click = "xdg-open https://duckduckgo.com/?q=weather"; return-type = "json"; }; diff --git a/res/custom_waybar_modules/weather.nu b/res/custom_waybar_modules/weather.nu index d3ffeb1..1d546ea 100644 --- a/res/custom_waybar_modules/weather.nu +++ b/res/custom_waybar_modules/weather.nu @@ -1,12 +1,67 @@ #!/usr/bin/env nu -# const WMO_CODE = { -# 0: "Sunny", -# 1: "PartlyCloudy", -# 2: "Cloudy", -# 3: "VeryCloudy", - -# } +def search_loc [name: string] { + let params = { + name: $name, + count: 1, + language: "en", + format: "json" + } | url build-query; + let endpoint = "https://geocoding-api.open-meteo.com/v1/search"; + + let resp = http get $"($endpoint)?($params)" | get -o results; + + if (($resp == null) or ($resp | is-empty)) { + error make $"No geocode data for city: ($name)" + } else { + $resp | first | select latitude longitude + } +} + +def query_weather [loc: record] { + let loc_params = $loc | url build-query; + let current_fields = [ + temperature_2m + apparent_temperature + is_day + precipitation + relative_humidity_2m + weather_code + ] | str join ","; + let hourly_fields = [ + temperature_2m + precipitation + precipitation_probability + weather_code + is_day + ] | str join ","; + let daily_fields = [ + sunrise + sunset + temperature_2m_max + temperature_2m_min + weather_code + precipitation_probability_max + ] | str join ","; + + let opt_params = { + forecast_days: 7, + forecast_hours: 12, + wind_speed_unit: "mph", + temperature_unit: "fahrenheit", + precipitation_unit: "inch", + timezone: "auto", + current: $current_fields, + hourly: $hourly_fields, + daily: $daily_fields, + } | url build-query; + + let params = $"($loc_params)&($opt_params)"; + + let endpoint = "https://api.open-meteo.com/v1/forecast"; + + http get $"($endpoint)?($params)" +} def match_wmo_code [code: int] { match $code { @@ -30,59 +85,11 @@ def match_wmo_code [code: int] { } } -const WWO_CODE = { - "113": "Sunny", - "116": "PartlyCloudy", - "119": "Cloudy", - "122": "VeryCloudy", - "143": "Fog", - "176": "LightShowers", - "179": "LightSleetShowers", - "182": "LightSleet", - "185": "LightSleet", - "200": "ThunderyShowers", - "227": "LightSnow", - "230": "HeavySnow", - "248": "Fog", - "260": "Fog", - "263": "LightShowers", - "266": "LightRain", - "281": "LightSleet", - "284": "LightSleet", - "293": "LightRain", - "296": "LightRain", - "299": "HeavyShowers", - "302": "HeavyRain", - "305": "HeavyShowers", - "308": "HeavyRain", - "311": "LightSleet", - "314": "LightSleet", - "317": "LightSleet", - "320": "LightSnow", - "323": "LightSnowShowers", - "326": "LightSnowShowers", - "329": "HeavySnow", - "332": "HeavySnow", - "335": "HeavySnowShowers", - "338": "HeavySnow", - "350": "LightSleet", - "353": "LightShowers", - "356": "HeavyShowers", - "359": "HeavyRain", - "362": "LightSleetShowers", - "365": "LightSleetShowers", - "368": "LightSnowShowers", - "371": "HeavySnowShowers", - "374": "LightSleetShowers", - "377": "LightSleet", - "386": "ThunderyShowers", - "389": "ThunderyHeavyRain", - "392": "ThunderySnowShowers", - "395": "HeavySnowShowers", -} +const NIGHT_MAP = {"󰖙": "󰖔", "󰖕": "󰼱"}; const WEATHER_ICONS = { "Unknown": "󰨹", + "Error": "󰧠", "Cloudy": "󰖐", "Fog": "󰖑", "HeavyRain": "󰖖", @@ -100,86 +107,165 @@ const WEATHER_ICONS = { "ThunderyHeavyRain": "󰙾", "ThunderyShowers": "󰙾", "ThunderySnowShowers": "󰙾", - "VeryCloudy": "󰖐󰖐", + "VeryCloudy": "", +}; + +const WEATHER_DESCS = { + "Unknown": "mysterious", + "Cloudy": "cloudy", + "Fog": "foggy", + "HeavyRain": "heavily raining", + "HeavyShowers": "heavily showering", + "HeavySnow": "heavily snowing", + "HeavySnowShowers": "heavily raining and snowing", + "LightRain": "lightly raining", + "LightShowers": "lightly showering", + "LightSleet": "lightly sleeting", + "LightSleetShowers": "lightly sleeting and raining", + "LightSnow": "lightly snowing", + "LightSnowShowers": "lightly snowing and showering", + "PartlyCloudy": "partly cloudy", + "Sunny": "sunny", + "ThunderyHeavyRain": "heavily thunderstorming", + "ThunderyShowers": "thunderstorming", + "ThunderySnowShowers": "thunder and snow-storming", + "VeryCloudy": "very cloudy", +}; + +def get_icon [cond: string, is_day: int] { + let icon = $WEATHER_ICONS | get -o $cond | default $WEATHER_ICONS.Unknown; + + if $is_day == 0 { + $NIGHT_MAP | get -o $icon | default $icon + } else { + $icon + } } -const NIGHT_MAP = {"󰖙": "󰖔", "󰖕": "󰼱"} +def get_desc [cond: string] { + $WEATHER_DESCS | get -o $cond | default $WEATHER_DESCS.Unknown +} -def is_night [astronomy: record] { - let now_utc = date now +def mk_condition [r: record, is_day: int] { + let cond = match_wmo_code $r.weather_code; - let sunrise = $astronomy.sunrise | into datetime - let sunset = $astronomy.sunset | into datetime + $"(get_icon $cond $is_day) (get_desc $cond)" +} - $now_utc > $sunset or $now_utc < $sunrise +def evil_transpose [a: record] { + 0..($a | values | get 0 | length | $in - 1) | each {|i| $a | columns | each {|c| [$c, ($a | get $c | get $i)]} | into record} } -def get_icon [condition: string, is_night: bool] { - let icon = $WEATHER_ICONS | get -o $condition | default $WEATHER_ICONS.Unknown; +def mk_text [weather: record] { + let icon = get_icon (match_wmo_code $weather.current.weather_code) $weather.current.is_day; + let temp = $weather.current.temperature_2m | math round; + let unit = $weather.current_units.temperature_2m; - if $is_night { - $NIGHT_MAP | get -o $icon | default $icon - } else { - $icon - } + $"($icon) ($temp)($unit)" } -def error-exit [msg: string] { - let out = { - text: "󰧠", - tooltip: $"Failed to fetch weather:\n($msg)", - class: ["Unknown"] - }; - print ($out | to json -r) - exit +def mk_current_text [weather: record] { + let c = $weather.current; + let u = $weather.current_units; + let cond = match_wmo_code $c.weather_code; + let today = (evil_transpose $weather.daily) | first; + + [ + $"󱃂 ($c.temperature_2m | math round)($u.temperature_2m) \(Feels like ($c.apparent_temperature | math round)($u.apparent_temperature)\)" + $"(get_icon $cond $c.is_day) (get_desc $cond | str title-case)" + $" ($c.precipitation) ($u.precipitation)(if $c.precipitation == 1 { '' } else { 'es' })" + $" ($c.relative_humidity_2m)($u.relative_humidity_2m)" + $"󰖜 ($today.sunrise | into datetime | format date '%I:%M %p')" + $"󰖛 ($today.sunset | into datetime | format date '%I:%M %p')" + ] | str join "\n" } -def main [] { +def mk_hour_line [h: record, u: record] { + let nice_time = $h.time | into datetime | format date "%_I %p"; + let cond = match_wmo_code $h.weather_code; + $"(get_icon $cond $h.is_day) ($nice_time) | ($h.temperature_2m | math round)($u.temperature_2m), ($h.precipitation_probability)($u.precipitation_probability)" +} - let raw = try { - http get https://wttr.in/?format=j1 - } catch { |err| - error-exit $err.msg; - } +def mk_hourly_text [weather: record] { + let hours = evil_transpose $weather.hourly + let u = $weather.hourly_units; + + $hours | skip 1 | each {mk_hour_line $in $u} | str join "\n" +} - if ((($raw | describe) == "string") and ($raw | str starts-with "Unknown location;")) { - error-exit $raw; +def mk_daily_line [d: record, u: record] { + let cond = match_wmo_code $d.weather_code; + $"(get_icon $cond 1) ($d.time | into datetime | format date "%a"). | ($d.temperature_2m_min | math round)($u.temperature_2m_min) / ($d.temperature_2m_max | math round)($u.temperature_2m_max), ($d.precipitation_probability_max)($u.precipitation_probability_max)" +} + +def mk_daily_text [weather: record] { + let days = evil_transpose $weather.daily; + let u = $weather.daily_units; + + $days | each {mk_daily_line $in $u} | str join "\n" +} + +def mk_tooltip [weather: record, city: string] { + [ + $"Current Weather in ($city)" + (mk_current_text $weather) + " " + "Hourly Forecast" + (mk_hourly_text $weather) + " " + "Daily Forecast" + (mk_daily_text $weather) + ] | str join "\n" +} + +def mk_waybar_module [city: string] { + let cache_loc_name = $"nu-waybar-loc-($city | hash sha256).json"; + let cache_loc_path = $env | get -o "TMPDIR" | default "/tmp" | path join $cache_loc_name; + + let loc = if ($cache_loc_path | path exists) { + try { + let cached_loc = open $cache_loc_path; + if ($cached_loc | describe) != ({latitude: 0.0, longitude: 0.0} | describe) { + error make "Invalid cache" + } + $cached_loc + } catch { + let new_loc = search_loc $city + $new_loc | save -f $cache_loc_path + $new_loc + } + } else { + let new_loc = search_loc $city; + $new_loc | save $cache_loc_path + $new_loc + }; + + let w = query_weather $loc; + + { + text: (mk_text $w), + tooltip: (mk_tooltip $w $city), + class: [(match_wmo_code $w.current.weather_code), (if $w.current.is_day == 1 { "day" } else { "night" })], } +} - let current_condition = $raw.current_condition.0 - let weather = $raw.weather.0 - let astronomy = $weather.astronomy.0 - let area = $raw.nearest_area.0 - - let condition = $WWO_CODE | get -o $current_condition.weatherCode | default "Unknown" - - let night = is_night $astronomy - - let icon = get_icon $condition $night - - let text = $"($icon) ($current_condition.temp_F)°F" - let tooltip = [ - $condition, - $"Temperature: ($current_condition.temp_F) °F", - $"Feels like ($current_condition.FeelsLikeF) °F", - $"High of ($weather.maxtempF) °F / Low of ($weather.mintempF) °F", - $"($current_condition.humidity)% Humidity", - $"($current_condition.pressure) in. Pressure", - $"($current_condition.uvIndex) UV Index", - $"($current_condition.precipInches) in. Precipitation", - $"($current_condition.visibilityMiles) mi. Visibility", - $"($current_condition.windspeedMiles) mph Wind, Going ($current_condition.winddir16Point)", - $"Sunrise: ($astronomy.sunrise) / Sunset: ($astronomy.sunset)", - $"Location: ($area.areaName.0.value), ($area.region.0.value), ($area.country.0.value)", - "Fetched from https://wttr.in" - ] | str join "\n" - - let out = { - text: $text, - tooltip: $tooltip, - class: [$condition, (if $night { "night" } else { "day" })] +def mk_error_module [msg: string] { + { + text: $"($WEATHER_ICONS.Error) Err", + tooltip: $"Failed to fetch weather\n\n($msg)", + class: ["Error"], } +} - print ($out | to json -r) +def main [] { + let city = http get "https://ipapi.co/json" | get city; + + let out = try { + mk_waybar_module $city + } catch {|err| + mk_error_module $err.rendered + }; + $out | to json -r } + diff --git a/res/waybar.css b/res/waybar.css index 916a8c3..147620d 100644 --- a/res/waybar.css +++ b/res/waybar.css @@ -113,6 +113,10 @@ tooltip label { margin-bottom: 0; } +#custom-weather.Error { + border-color: @red; +} + #custom-weather.VeryCloudy, #custom-weather.Cloudy, #custom-weather.Fog {