diff options
Diffstat (limited to 'config/hypr/UserScripts')
| -rwxr-xr-x | config/hypr/UserScripts/Weather.py | 57 | ||||
| -rwxr-xr-x | config/hypr/UserScripts/Weather.sh | 212 | ||||
| -rwxr-xr-x | config/hypr/UserScripts/weatherWrap.sh | 33 |
3 files changed, 286 insertions, 16 deletions
diff --git a/config/hypr/UserScripts/Weather.py b/config/hypr/UserScripts/Weather.py index 4c64221f..1a7380cf 100755 --- a/config/hypr/UserScripts/Weather.py +++ b/config/hypr/UserScripts/Weather.py @@ -65,11 +65,12 @@ UNITS = os.getenv("WEATHER_UNITS", "metric").strip().lower() # metric|imperial # Optional manual coordinates ENV_LAT = os.getenv("WEATHER_LAT") ENV_LON = os.getenv("WEATHER_LON") -# Optional manual place override for tooltip +# Optional manual place override for tooltip (and optional forward geocoding) ENV_PLACE = os.getenv("WEATHER_PLACE") -# Manual place name set inside this file. If set (non-empty), this takes top priority. +# Manual place name set inside this file. If set (non-empty), this takes top priority for display +# and, if coordinates are not provided, will be used to geocode latitude/longitude. # Example: MANUAL_PLACE = "Concord, NH, US" -MANUAL_PLACE: Optional[str] = None +MANUAL_PLACE: Optional[str] = "" #Set your city HERE # Location icon in tooltip (default to a standard emoji to avoid missing glyphs) LOC_ICON = os.getenv("WEATHER_LOC_ICON", "๐") @@ -324,23 +325,58 @@ def get_coords_from_ipinfo() -> Optional[Tuple[float, float]]: return None +def get_coords_from_place_name(name: str) -> Optional[Tuple[float, float]]: + """Forward geocode a place name to coordinates using Open-Meteo Geocoding API. + + Returns (lat, lon) if found, else None. + """ + try: + base = "https://geocoding-api.open-meteo.com/v1/search" + params: Dict[str, Union[str, float]] = { + "name": name, + "count": 1, + "language": os.getenv("WEATHER_LANG", "en"), + "format": "json", + } + resp = SESSION.get(base, params=params, timeout=TIMEOUT) + resp.raise_for_status() + data = ensure_dict(resp.json()) + results = ensure_list(data.get("results")) + if results: + p = ensure_dict(results[0]) + lat = coerce_float(p.get("latitude")) + lon = coerce_float(p.get("longitude")) + if lat is not None and lon is not None: + return float(lat), float(lon) + except Exception as e: + print(f"Place geocoding failed: {e}", file=sys.stderr) + return None + + def get_coords() -> Tuple[float, float]: - # 1) Explicit env + # 1) Explicit env coordinates coords = get_coords_from_env() if coords: return coords - # 2) Try cached coordinates + # 2) Forward geocode from a specified place name (manual takes precedence over env) + place_name = (MANUAL_PLACE or "").strip() or (ENV_PLACE or "").strip() + if place_name: + coords = get_coords_from_place_name(place_name) + if coords: + return coords + + # 3) Try cached coordinates coords = get_coords_from_cache() if coords: return coords - # 3) IP-based geolocation + # 4) IP-based geolocation coords = get_coords_from_ipwho() or get_coords_from_ipapi() or get_coords_from_ipinfo() if coords: return coords - # 4) Last resort + # 5) Last resort print("IP geolocation failed: no providers succeeded", file=sys.stderr) return 0.0, 0.0 @@ -778,6 +814,13 @@ def try_cached_weather(lat: float, lon: float) -> Optional[Tuple[Dict[str, str], aqi = cast(Optional[Dict[str, Any]], cached.get("aqi")) place_val = cached.get("place") cached_place = place_val if isinstance(place_val, str) else None + # Ensure the cached forecast corresponds to the requested lat/lon + fc = ensure_dict(cached.get("forecast")) + c_lat = coerce_float(safe_get(fc, "latitude")) + c_lon = coerce_float(safe_get(fc, "longitude")) + if c_lat is not None and c_lon is not None: + if abs(c_lat - lat) > 0.1 or abs(c_lon - lon) > 0.1: + return None # force fresh fetch for new location try: return build_output(Location(lat, lon, cached_place), forecast, aqi) except Exception as e: diff --git a/config/hypr/UserScripts/Weather.sh b/config/hypr/UserScripts/Weather.sh index 0540e51d..b69662e6 100755 --- a/config/hypr/UserScripts/Weather.sh +++ b/config/hypr/UserScripts/Weather.sh @@ -1,15 +1,209 @@ #!/bin/bash # /* ---- ๐ซ https://github.com/JaKooLit ๐ซ ---- */ ## -# weather info using Python script with Open-Meteo APIs +# weather info from wttr. https://github.com/chubin/wttr.in +# Remember to add city -if ! command -v python3 >/dev/null 2>&1; then - echo "python3 not found in PATH" >&2 - exit 127 +city="" + + +# if city is blank, use https://ipapi.co/json to get location from IP +if [ -z "$city" ]; then + city=$(curl -fsS https://ipapi.co/json | grep city | cut -f4 -d'"') +fi + + +# URL-encode city for safe use in URLs +encoded_city="$city" +if command -v python3 >/dev/null 2>&1; then + encoded_city=$(python3 -c 'import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))' "$city") +elif command -v jq >/dev/null 2>&1; then + encoded_city=$(printf '%s' "$city" | jq -sRr @uri) +else + # Minimal fallback: encode a few common special characters + encoded_city=$(printf '%s' "$city" | sed -e 's/ /%20/g' -e 's/&/%26/g' -e 's/?/%3F/g' -e 's/#/%23/g') fi -python3 "$(dirname "$0")/Weather.py" "$@" -exit_code=$? -if [ "$exit_code" -ne 0 ]; then - echo "Failed to run Weather.py" >&2 + +cachedir="$HOME/.cache/rbn" +# Include city and arg in cache key so changing city invalidates old cache +cache_key="${city}_${1}" +# Sanitize cache key to avoid problematic characters in filename +safe_key=$(printf '%s' "$cache_key" | tr -c '[:alnum:]_-' '_') +cachefile=${0##*/}-$safe_key + +if [ ! -d "$cachedir" ]; then + mkdir -p "$cachedir" +fi + +if [ ! -f "$cachedir/$cachefile" ]; then + touch "$cachedir/$cachefile" +fi + +# Save current IFS +SAVEIFS=$IFS +# Change IFS to new line. +IFS=$'\n' + +file="$cachedir/$cachefile" +# Portable file mtime retrieval (GNU/BSD): +# - GNU: stat -c %Y <file> +# - BSD/macOS: stat -f %m <file> +mtime=$(stat -c %Y "$file" 2>/dev/null || stat -f %m "$file" 2>/dev/null || echo 0) +now=$(date +%s) +cacheage=$(( now - mtime )) +if [ $cacheage -gt 1740 ] || [ ! -s "$cachedir/$cachefile" ]; then + # Prefer structured format for reliable parsing (3 lines: location, condition, temperature) + mapfile -t sdata < <(curl -fsS "https://wttr.in/${encoded_city}?format=%25l%0A%25C%0A%25t&lang=en" 2>/dev/null || true) + if [ ${#sdata[@]} -ge 3 ]; then + printf "%s\n" "${sdata[0]}" > "$cachedir/$cachefile" + printf "%s\n" "${sdata[1]}" >> "$cachedir/$cachefile" + printf "%s\n" "${sdata[2]}" >> "$cachedir/$cachefile" + else + # Try fetching each field separately if combined format is flaky + loc=$(curl -fsS "https://wttr.in/${encoded_city}?format=%25l&lang=en" 2>/dev/null || true) + cond_only=$(curl -fsS "https://wttr.in/${encoded_city}?format=%25C&lang=en" 2>/dev/null || true) + temp_only=$(curl -fsS "https://wttr.in/${encoded_city}?format=%25t" 2>/dev/null || true) + if [ -n "$loc" ] && [ -n "$cond_only" ] && [ -n "$temp_only" ]; then + printf "%s\n" "$loc" > "$cachedir/$cachefile" + printf "%s\n" "$cond_only" >> "$cachedir/$cachefile" + printf "%s\n" "$temp_only" >> "$cachedir/$cachefile" + else + # Fallback: try ASCII output and extract best-effort fields + url="https://en.wttr.in/${encoded_city}?1" + mapfile -t data < <(curl -fsS "$url" 2>/dev/null || true) + if [ ${#data[@]} -ge 3 ] && ! echo "${data[0]}" | grep -qi 'not found\|unknown location'; then + loc=$(echo "${data[0]}" | sed -E 's/^.*: *//') + # Attempt to pull condition and temperature hints from nearby lines + cond=$(echo "${data[2]}" | sed -E 's/^.{0,15}//; s/^\s+//') + temp=$(printf "%s\n" "${data[@]}" | grep -Eo '\+?-?[0-9]+(\([^)]+\))? ?ยฐ?[CF]' | head -n1) + # Only write if we have at least location and something else meaningful + if [ -n "$loc" ] && { [ -n "$cond" ] || [ -n "$temp" ]; }; then + printf "%s\n" "$loc" > "$cachedir/$cachefile" + printf "%s\n" "${cond:-Unknown}" >> "$cachedir/$cachefile" + printf "%s\n" "${temp:-N/A}" >> "$cachedir/$cachefile" + fi + fi + fi + fi +fi + +# Read cache robustly (line-wise) +mapfile -t weather < "$cachedir/$cachefile" + +# If cache is still empty or invalid, emit a single error JSON and exit to avoid double-prints +if [ ${#weather[@]} -lt 3 ] || ! echo "${weather[2]}" | grep -qE '[-+0-9].*ยฐ'; then + # Last-chance: try live structured fetch and populate cache and runtime weather + mapfile -t sdata < <(curl -fsS "https://wttr.in/${encoded_city}?format=%25l%0A%25C%0A%25t&lang=en" 2>/dev/null || true) + if [ ${#sdata[@]} -ge 3 ]; then + weather=("${sdata[@]}") + printf "%s\n" "${sdata[0]}" > "$cachedir/$cachefile" + printf "%s\n" "${sdata[1]}" >> "$cachedir/$cachefile" + printf "%s\n" "${sdata[2]}" >> "$cachedir/$cachefile" + else + loc=$(curl -fsS "https://wttr.in/${encoded_city}?format=%25l&lang=en" 2>/dev/null || true) + cond_only=$(curl -fsS "https://wttr.in/${encoded_city}?format=%25C&lang=en" 2>/dev/null || true) + temp_only=$(curl -fsS "https://wttr.in/${encoded_city}?format=%25t" 2>/dev/null || true) + if [ -n "$loc" ] && [ -n "$cond_only" ] && [ -n "$temp_only" ]; then + weather=("$loc" "$cond_only" "$temp_only") + printf "%s\n" "$loc" > "$cachedir/$cachefile" + printf "%s\n" "$cond_only" >> "$cachedir/$cachefile" + printf "%s\n" "$temp_only" >> "$cachedir/$cachefile" + else + echo -e "{\"text\":\"\uf06a\", \"alt\":\"\", \"tooltip\":\": \"}" + exit 1 + fi + fi fi -exit "$exit_code"
\ No newline at end of file + +# Restore IFSClear +IFS=$SAVEIFS + +temperature=$(echo "${weather[2]}" | sed -E 's/([[:digit:]]+)\.\./\1 to /g') + +#echo ${weather[1]##*,} + +# https://fontawesome.com/icons?s=solid&c=weather +# Normalize condition string for matching +cond_key=$(echo "${weather[1]##*,}" | tr '[:upper:]' '[:lower:]' | sed -E 's/^\s+//; s/\s+$//') +case "$cond_key" in +"clear" | "sunny") + condition="๎" + ;; +"partly cloudy") + condition="๓ฐ" + ;; +"cloudy") + condition="๎" + ;; +"overcast") + condition="๎" + ;; +"fog" | "freezing fog") + condition="๎" + ;; +"patchy rain possible" | "patchy light drizzle" | "light drizzle" | "patchy light rain" | "light rain" | "light rain shower" | "mist" | "rain" | "patchy rain nearby") + condition="๓ฐผณ" + ;; +"moderate rain at times" | "moderate rain" | "heavy rain at times" | "heavy rain" | "moderate or heavy rain shower" | "torrential rain shower" | "rain shower") + condition="๎น" + ;; +"patchy snow possible" | "patchy sleet possible" | "patchy freezing drizzle possible" | "freezing drizzle" | "heavy freezing drizzle" | "light freezing rain" | "moderate or heavy freezing rain" | "light sleet" | "ice pellets" | "light sleet showers" | "moderate or heavy sleet showers") + condition="๓ฐผด" + ;; +"blowing snow" | "moderate or heavy sleet" | "patchy light snow" | "light snow" | "light snow showers") + condition="๓ฐฟ" + ;; +"blizzard" | "patchy moderate snow" | "moderate snow" | "patchy heavy snow" | "heavy snow" | "moderate or heavy snow with thunder" | "moderate or heavy snow showers") + condition="๎" + ;; +"thundery outbreaks possible" | "patchy light rain with thunder" | "moderate or heavy rain with thunder" | "patchy light snow with thunder") + condition="๎" + ;; +*) + condition="๏ช" + ;; +esac + +# If still unknown, try substring heuristics to pick a reasonable icon +if [ "$condition" = "๏ช" ]; then + if echo "$cond_key" | grep -q "rain\|drizzle\|shower"; then + condition="๓ฐผณ" + elif echo "$cond_key" | grep -q "heavy rain\|torrential"; then + condition="๎น" + elif echo "$cond_key" | grep -q "snow"; then + condition="๓ฐฟ" + elif echo "$cond_key" | grep -q "sleet\|freezing\|ice"; then + condition="๓ฐผด" + elif echo "$cond_key" | grep -q "thunder"; then + condition="๎" + elif echo "$cond_key" | grep -q "overcast"; then + condition="๎" + elif echo "$cond_key" | grep -q "cloud"; then + condition="๎" + elif echo "$cond_key" | grep -q "sunny\|clear"; then + condition="๎" + fi +fi + +#echo $temp $condition + +# Ensure temperature has a value; if empty, keep whatever is in weather[2] or N/A +if [ -z "$temperature" ]; then + temperature="${weather[2]:-N/A}" +fi + +cond_disp=$(echo "${weather[1]}" | sed -E 's/^\s+//; s/\s+$//') + +# Escape strings for safe JSON embedding (escape backslashes and double quotes) +json_escape() { + printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/\"/\\\"/g' +} + +text_json=$(json_escape "$temperature $condition") +alt_json=$(json_escape "${weather[0]}") +tooltip_json=$(json_escape "${weather[0]}: $temperature $cond_disp") + +printf '{"text":"%s", "alt":"%s", "tooltip":"%s"}\n' "$text_json" "$alt_json" "$tooltip_json" + +# Write a two-line cache with an actual newline between lines +printf '๏ %s \n%s %s\n' "$temperature" "$condition" "${weather[1]}" > "$HOME/.cache/.weather_cache"
\ No newline at end of file diff --git a/config/hypr/UserScripts/weatherWrap.sh b/config/hypr/UserScripts/weatherWrap.sh new file mode 100755 index 00000000..4c9a16dc --- /dev/null +++ b/config/hypr/UserScripts/weatherWrap.sh @@ -0,0 +1,33 @@ +#!/bin/bash +# /* ---- ๐ซ https://github.com/JaKooLit ๐ซ ---- */ ## +# Weather entrypoint: prefer Python (OpenโMeteo), fallback to legacy Bash (wttr.in) + +SCRIPT_DIR="$(dirname "$0")" +PY_SCRIPT="$SCRIPT_DIR/Weather.py" +BASH_FALLBACK="$SCRIPT_DIR/Weather.sh" + +run_fallback() { + if [ -f "$BASH_FALLBACK" ]; then + # Invoke via bash to avoid requiring +x and ensure consistent shell + bash "$BASH_FALLBACK" "$@" + return $? + else + echo "Weather fallback not found: $BASH_FALLBACK" >&2 + return 127 + fi +} + +if command -v python3 >/dev/null 2>&1; then + python3 "$PY_SCRIPT" "$@" + exit_code=$? + if [ "$exit_code" -eq 0 ]; then + exit 0 + fi + echo "Weather.py failed with code $exit_code โ falling back to Weather.sh" >&2 + run_fallback "$@" + exit $? +else + echo "python3 not found in PATH โ falling back to Weather.sh" >&2 + run_fallback "$@" + exit $? +fi
\ No newline at end of file |
