From d20745840e43bfabef2a60190071d78016ddb658 Mon Sep 17 00:00:00 2001 From: Don Williams Date: Sun, 5 Oct 2025 20:50:47 -0400 Subject: Updated Weather.py to correct version On branch development Your branch is up to date with 'origin/development'. Changes to be committed: modified: config/hypr/UserScripts/Weather.py --- config/hypr/UserScripts/Weather.py | 627 ++++++++++++++++++++++++++++++------- 1 file changed, 512 insertions(+), 115 deletions(-) (limited to 'config/hypr/UserScripts') diff --git a/config/hypr/UserScripts/Weather.py b/config/hypr/UserScripts/Weather.py index a8b70417..ca1d5281 100755 --- a/config/hypr/UserScripts/Weather.py +++ b/config/hypr/UserScripts/Weather.py @@ -1,15 +1,69 @@ #!/usr/bin/env python3 # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ # -# original code https://gist.github.com/Surendrajat/ff3876fd2166dd86fb71180f4e9342d7 -# weather using python +# Rewritten to use Open-Meteo APIs (worldwide, no API key) for robust weather data. +# Outputs Waybar-compatible JSON and a simple text cache. -import requests import json import os -from pyquery import PyQuery # install using `pip install pyquery` +import sys +import time +import html +from typing import Any, Dict, List, Optional, Tuple + +import requests + +# =============== Configuration =============== +# You can configure behavior via environment variables OR the constants below. +# Examples (zsh): +# # One-off run +# # WEATHER_UNITS can be "metric" or "imperial" +# WEATHER_UNITS=imperial WEATHER_PLACE="Concord, NH" python3 /home/dwilliams/Projects/Weather.py +# +# # Persist in current shell session +# export WEATHER_UNITS=imperial +# export WEATHER_LAT=43.2229 +# export WEATHER_LON=-71.332 +# export WEATHER_PLACE="Concord, NH" +# export WEATHER_TOOLTIP_MARKUP=1 # 1 to enable Pango markup, 0 to disable +# export WEATHER_LOC_ICON="๐Ÿ“" # or "*" for ASCII-only +# +CACHE_DIR = os.path.expanduser("~/.cache") +API_CACHE_PATH = os.path.join(CACHE_DIR, "open_meteo_cache.json") +SIMPLE_TEXT_CACHE_PATH = os.path.join(CACHE_DIR, ".weather_cache") +CACHE_TTL_SECONDS = int(os.getenv("WEATHER_CACHE_TTL", "600")) # default 10 minutes + +# Units: metric or imperial (default metric) +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 +ENV_PLACE = os.getenv("WEATHER_PLACE") +# Manual place name set inside this file. If set (non-empty), this takes top priority. +# Example: MANUAL_PLACE = "Concord, NH, US" +MANUAL_PLACE: Optional[str] = None + +# Location icon in tooltip (default to a standard emoji to avoid missing glyphs) +LOC_ICON = os.getenv("WEATHER_LOC_ICON", "๐Ÿ“") +# Enable/disable Pango markup in tooltip (1/0, true/false) +TOOLTIP_MARKUP = os.getenv("WEATHER_TOOLTIP_MARKUP", "1").lower() not in ("0", "false", "no") +# Optional debug logging to stderr (set WEATHER_DEBUG=1 to enable) +DEBUG = os.getenv("WEATHER_DEBUG", "0").lower() not in ("0", "false", "no") + +# HTTP settings +UA = ( + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/128.0 Safari/537.36" +) +TIMEOUT = 8 -# weather icons -weather_icons = { +SESSION = requests.Session() +SESSION.headers.update({"User-Agent": UA}) + +# =============== Icon and status mapping =============== +# Reuse prior icon set for continuity +WEATHER_ICONS = { "sunnyDay": "๓ฐ–™", "clearNight": "๓ฐ–”", "cloudyFoggyDay": "๎Œƒ", @@ -22,126 +76,469 @@ weather_icons = { "default": "๎Œ‚", } +WMO_STATUS = { + 0: "Clear sky", + 1: "Mainly clear", + 2: "Partly cloudy", + 3: "Overcast", + 45: "Fog", + 48: "Depositing rime fog", + 51: "Light drizzle", + 53: "Moderate drizzle", + 55: "Dense drizzle", + 56: "Freezing drizzle", + 57: "Freezing drizzle", + 61: "Light rain", + 63: "Moderate rain", + 65: "Heavy rain", + 66: "Freezing rain", + 67: "Freezing rain", + 71: "Slight snow", + 73: "Moderate snow", + 75: "Heavy snow", + 77: "Snow grains", + 80: "Rain showers", + 81: "Rain showers", + 82: "Violent rain showers", + 85: "Snow showers", + 86: "Heavy snow showers", + 95: "Thunderstorm", + 96: "Thunderstorm w/ hail", + 99: "Thunderstorm w/ hail", +} -# Get current location based on IP address -def get_location(): - response = requests.get("https://ipinfo.io") - data = response.json() - loc = data["loc"].split(",") - return float(loc[0]), float(loc[1]) +def wmo_to_icon(code: int, is_day: int) -> str: + day = bool(is_day) + if code == 0: + return WEATHER_ICONS["sunnyDay" if day else "clearNight"] + if code in (1, 2, 3, 45, 48): + return WEATHER_ICONS["cloudyFoggyDay" if day else "cloudyFoggyNight"] + if code in (51, 53, 55, 61, 63, 65, 80, 81, 82): + return WEATHER_ICONS["rainyDay" if day else "rainyNight"] + if code in (56, 57, 66, 67, 71, 73, 75, 77, 85, 86): + return WEATHER_ICONS["snowyIcyDay" if day else "snowyIcyNight"] + if code in (95, 96, 99): + return WEATHER_ICONS["severe"] + return WEATHER_ICONS["default"] -# Get latitude and longitude -latitude, longitude = get_location() -# Open-Meteo API endpoint -url = f"https://weather.com/en-PH/weather/today/l/{latitude},{longitude}" +def wmo_to_status(code: int) -> str: + return WMO_STATUS.get(code, "Unknown") -# manual location_id -# NOTE: if you want to add manually, make sure you disable def get_location above -# to get your own location_id, go to https://weather.com & search your location. -# once you choose your location, you can see the location_id in the URL(64 chars long hex string) -# like this: https://weather.com/en-PH/weather/today/l/bca47d1099e762a012b9a139c36f30a0b1e647f69c0c4ac28b537e7ae9c1c200 -# location_id = "bca47d1099e762a012b9a139c36f30a0b1e647f69c0c4ac28b537e7ae9c1c200" # TODO -# NOTE to change to deg F, change the URL to your preffered location after weather.com -# Default is English-Philippines with Busan, South Korea as location_id -# get html page -# url = "https://weather.com/en-PH/weather/today/l/" + location_id +# =============== Utilities =============== -html_data = PyQuery(url=url) +def esc(s: Optional[str]) -> str: + return html.escape(s, quote=False) if s else "" -# current temperature -temp = html_data("span[data-testid='TemperatureValue']").eq(0).text() +def log_debug(msg: str) -> None: + if DEBUG: + print(msg, file=sys.stderr) -# current status phrase -status = html_data("div[data-testid='wxPhrase']").text() -status = f"{status[:16]}.." if len(status) > 17 else status +def ensure_cache_dir() -> None: + try: + os.makedirs(CACHE_DIR, exist_ok=True) + except Exception as e: + print(f"Error creating cache dir: {e}", file=sys.stderr) -# status code -# Fix provided by mio-dokuhaki -status_code = html_data("#regionHeader").attr("class").split(" ")[1].split("-")[0] -# status icon -icon = ( - weather_icons[status_code] - if status_code in weather_icons - else weather_icons["default"] -) +def read_api_cache() -> Optional[Dict[str, Any]]: + try: + if not os.path.exists(API_CACHE_PATH): + return None + with open(API_CACHE_PATH, "r", encoding="utf-8") as f: + data = json.load(f) + if (time.time() - data.get("timestamp", 0)) <= CACHE_TTL_SECONDS: + return data + return None + except Exception as e: + print(f"Error reading cache: {e}", file=sys.stderr) + return None -# temperature feels like -temp_feel = html_data( - "div[data-testid='FeelsLikeSection'] > span > span[data-testid='TemperatureValue']" -).text() -temp_feel_text = f"Feels like {temp_feel}c" - -# min-max temperature -temp_min = ( - html_data("div[data-testid='wxData'] > span[data-testid='TemperatureValue']") - .eq(1) - .text() -) -temp_max = ( - html_data("div[data-testid='wxData'] > span[data-testid='TemperatureValue']") - .eq(0) - .text() -) -temp_min_max = f"๏‹‹ {temp_min}\t\t๏‹‡ {temp_max}" - -# wind speed -wind_speed = str(html_data("span[data-testid='Wind'] > span").text()) -wind_text = f"๎‰พ {wind_speed}" - -# humidity -humidity = html_data("span[data-testid='PercentageValue']").text() -humidity_text = f"๎ณ {humidity}" - -# visibility -visibility = html_data("span[data-testid='VisibilityValue']").text() -visibility_text = f"๏ฎ {visibility}" - -# air quality index -air_quality_index = html_data("text[data-testid='DonutChartValue']").text() - -# hourly rain prediction -prediction = html_data("section[aria-label='Hourly Forecast']")( - "div[data-testid='SegmentPrecipPercentage'] > span" -).text() -prediction = prediction.replace("Chance of Rain", "") -prediction = f"\n\n๎Œ˜ (hourly) {prediction}" if len(prediction) > 0 else prediction - -# tooltip text -tooltip_text = str.format( - "\t\t{}\t\t\n{}\n{}\n{}\n\n{}\n{}\n{}{}", - f'{temp}', - f" {icon}", - f"{status}", - f"{temp_feel_text}", - f"{temp_min_max}", - f"{wind_text}\t{humidity_text}", - f"{visibility_text}\tAQI {air_quality_index}", - f" {prediction}", -) -# print waybar module data -out_data = { - "text": f"{icon} {temp}", - "alt": status, - "tooltip": tooltip_text, - "class": status_code, -} -print(json.dumps(out_data)) - -simple_weather = ( - f"{icon} {status}\n" - + f"๏‹‰ {temp} ({temp_feel_text})\n" - + f"{wind_text} \n" - + f"{humidity_text} \n" - + f"{visibility_text} AQI{air_quality_index}\n" -) +def write_api_cache(payload: Dict[str, Any]) -> None: + try: + ensure_cache_dir() + payload["timestamp"] = time.time() + with open(API_CACHE_PATH, "w", encoding="utf-8") as f: + json.dump(payload, f) + except Exception as e: + print(f"Error writing API cache: {e}", file=sys.stderr) + + +def write_simple_text_cache(text: str) -> None: + try: + ensure_cache_dir() + with open(SIMPLE_TEXT_CACHE_PATH, "w", encoding="utf-8") as f: + f.write(text) + except Exception as e: + print(f"Error writing simple cache: {e}", file=sys.stderr) + + +def get_coords() -> Tuple[float, float]: + # 1) Explicit env + if ENV_LAT and ENV_LON: + try: + return float(ENV_LAT), float(ENV_LON) + except ValueError: + print("Invalid WEATHER_LAT/WEATHER_LON; falling back to IP geolocation", file=sys.stderr) + + # 2) Try cached coordinates from last successful forecast + try: + cached = read_api_cache() + if cached and isinstance(cached, dict): + fc = cached.get("forecast") or {} + lat = fc.get("latitude") + lon = fc.get("longitude") + if isinstance(lat, (int, float)) and isinstance(lon, (int, float)): + return float(lat), float(lon) + except Exception as e: + print(f"Reading cached coords failed: {e}", file=sys.stderr) + + # 3) IP-based geolocation with multiple providers (prefer ipwho.is, ipapi.co; ipinfo.io as fallback) + # ipwho.is + try: + resp = SESSION.get("https://ipwho.is/", timeout=TIMEOUT) + resp.raise_for_status() + data = resp.json() + if data.get("success"): + lat = data.get("latitude") + lon = data.get("longitude") + if isinstance(lat, (int, float)) and isinstance(lon, (int, float)): + return float(lat), float(lon) + except Exception as e: + print(f"ipwho.is failed: {e}", file=sys.stderr) + + # ipapi.co + try: + resp = SESSION.get("https://ipapi.co/json", timeout=TIMEOUT) + resp.raise_for_status() + data = resp.json() + lat = data.get("latitude") + lon = data.get("longitude") + if isinstance(lat, (int, float)) and isinstance(lon, (int, float)): + return float(lat), float(lon) + except Exception as e: + print(f"ipapi.co failed: {e}", file=sys.stderr) + + # ipinfo.io (fallback) + try: + resp = SESSION.get("https://ipinfo.io/json", timeout=TIMEOUT) + resp.raise_for_status() + data = resp.json() + loc = data.get("loc") + if loc and "," in loc: + lat_s, lon_s = loc.split(",", 1) + return float(lat_s), float(lon_s) + except Exception as e: + print(f"ipinfo.io failed: {e}", file=sys.stderr) + + # 4) Last resort + print("IP geolocation failed: no providers succeeded", file=sys.stderr) + return 0.0, 0.0 + + +def units_params(units: str) -> Dict[str, str]: + if units == "imperial": + return { + "temperature_unit": "fahrenheit", + "wind_speed_unit": "mph", + "precipitation_unit": "inch", + } + # default metric + return { + "temperature_unit": "celsius", + "wind_speed_unit": "kmh", + "precipitation_unit": "mm", + } + + +def format_visibility(meters: Optional[float]) -> str: + if meters is None: + return "" + try: + if UNITS == "imperial": + miles = meters / 1609.344 + return f"{miles:.1f} mi" + else: + km = meters / 1000.0 + return f"{km:.1f} km" + except Exception: + return "" + + +# =============== API Fetching =============== + +def fetch_open_meteo(lat: float, lon: float) -> Dict[str, Any]: + base = "https://api.open-meteo.com/v1/forecast" + params = { + "latitude": lat, + "longitude": lon, + "current": "temperature_2m,apparent_temperature,relative_humidity_2m,wind_speed_10m,wind_direction_10m,weather_code,visibility,precipitation,pressure_msl,is_day", + "hourly": "precipitation_probability", + "daily": "temperature_2m_max,temperature_2m_min", + "timezone": "auto", + } + params.update(units_params(UNITS)) + resp = SESSION.get(base, params=params, timeout=TIMEOUT) + resp.raise_for_status() + return resp.json() + + +def fetch_aqi(lat: float, lon: float) -> Optional[Dict[str, Any]]: + try: + base = "https://air-quality-api.open-meteo.com/v1/air-quality" + params = { + "latitude": lat, + "longitude": lon, + "current": "european_aqi", + "timezone": "auto", + } + resp = SESSION.get(base, params=params, timeout=TIMEOUT) + resp.raise_for_status() + return resp.json() + except Exception as e: + print(f"AQI fetch failed: {e}", file=sys.stderr) + return None + + +def fetch_place(lat: float, lon: float) -> Optional[str]: + """Reverse geocode lat/lon to an approximate place. Tries Nominatim first, then Open-Meteo.""" + lang = os.getenv("WEATHER_LANG", "en") + + # 1) Nominatim (OpenStreetMap) + try: + base = "https://nominatim.openstreetmap.org/reverse" + params = { + "lat": lat, + "lon": lon, + "format": "jsonv2", + "accept-language": lang, + } + headers = {"User-Agent": UA + " Weather.py/1.0"} + resp = SESSION.get(base, params=params, headers=headers, timeout=TIMEOUT) + resp.raise_for_status() + data = resp.json() + address = data.get("address", {}) + name = data.get("name") or address.get("city") or address.get("town") or address.get("village") or address.get("hamlet") + admin1 = address.get("state") + country = address.get("country") + parts = [part for part in [name, admin1, country] if part] + if parts: + return ", ".join(parts) + except Exception as e: + log_debug(f"Reverse geocoding (Nominatim) failed: {e}") + + # 2) Open-Meteo reverse (fallback) + try: + base = "https://geocoding-api.open-meteo.com/v1/reverse" + params = { + "latitude": lat, + "longitude": lon, + "language": lang, + "format": "json", + } + resp = SESSION.get(base, params=params, timeout=TIMEOUT) + resp.raise_for_status() + data = resp.json() + results = data.get("results") or [] + if results: + p = results[0] + name = p.get("name") + admin1 = p.get("admin1") + country = p.get("country") + parts = [part for part in [name, admin1, country] if part] + if parts: + return ", ".join(parts) + except Exception as e: + log_debug(f"Reverse geocoding (Open-Meteo) failed: {e}") + + return None + + +# =============== Build Output =============== + +def safe_get(dct: Dict[str, Any], *keys, default=None): + cur: Any = dct + for k in keys: + if isinstance(cur, dict): + if k not in cur: + return default + cur = cur[k] + elif isinstance(cur, list): + try: + cur = cur[k] # type: ignore[index] + except Exception: + return default + else: + return default + return cur + + +def build_hourly_precip(forecast: Dict[str, Any]) -> str: + try: + times: List[str] = safe_get(forecast, "hourly", "time", default=[]) or [] + probs: List[Optional[float]] = safe_get( + forecast, "hourly", "precipitation_probability", default=[] + ) or [] + cur_time: Optional[str] = safe_get(forecast, "current", "time") + idx = times.index(cur_time) if cur_time in times else 0 + window = probs[idx : idx + 6] + if not window: + return "" + parts = [f"{int(p)}%" if p is not None else "-" for p in window] + return "๎Œ˜ (next 6h) " + " ".join(parts) + except Exception: + return "" + + +def build_output(lat: float, lon: float, forecast: Dict[str, Any], aqi: Optional[Dict[str, Any]], place: Optional[str] = None) -> Tuple[Dict[str, Any], str]: + cur = forecast.get("current", {}) + cur_units = forecast.get("current_units", {}) + daily = forecast.get("daily", {}) + daily_units = forecast.get("daily_units", {}) + + temp_val = cur.get("temperature_2m") + temp_unit = cur_units.get("temperature_2m", "") + temp_str = f"{int(round(temp_val))}{temp_unit}" if isinstance(temp_val, (int, float)) else "N/A" + + feels_val = cur.get("apparent_temperature") + feels_unit = cur_units.get("apparent_temperature", "") + feels_str = f"Feels like {int(round(feels_val))}{feels_unit}" if isinstance(feels_val, (int, float)) else "" + + is_day = int(cur.get("is_day", 1) or 1) + code = int(cur.get("weather_code", -1) or -1) + icon = wmo_to_icon(code, is_day) + status = wmo_to_status(code) + + # min/max today (index 0) + tmin_val = safe_get(daily, "temperature_2m_min", 0) + tmax_val = safe_get(daily, "temperature_2m_max", 0) + dtemp_unit = daily_units.get("temperature_2m_min", temp_unit) + tmin_str = f"{int(round(tmin_val))}{dtemp_unit}" if isinstance(tmin_val, (int, float)) else "" + tmax_str = f"{int(round(tmax_val))}{dtemp_unit}" if isinstance(tmax_val, (int, float)) else "" + min_max = f"๏‹‹ {tmin_str}\t\t๏‹‡ {tmax_str}" if tmin_str and tmax_str else "" + + wind_val = cur.get("wind_speed_10m") + wind_unit = cur_units.get("wind_speed_10m", "") + wind_text = f"๎‰พ {int(round(wind_val))}{wind_unit}" if isinstance(wind_val, (int, float)) else "" + + hum_val = cur.get("relative_humidity_2m") + humidity_text = f"๎ณ {int(hum_val)}%" if isinstance(hum_val, (int, float)) else "" + + vis_val = cur.get("visibility") + visibility_text = f"๏ฎ {format_visibility(vis_val)}" if isinstance(vis_val, (int, float)) else "" + + aqi_val = safe_get(aqi or {}, "current", "european_aqi") + aqi_text = f"AQI {int(aqi_val)}" if isinstance(aqi_val, (int, float)) else "AQI N/A" + + hourly_precip = build_hourly_precip(forecast) + prediction = f"\n\n{hourly_precip}" if hourly_precip else "" + + # Build place string (priority: MANUAL_PLACE > ENV_PLACE > reverse geocode > lat,lon) + place_str = (MANUAL_PLACE or ENV_PLACE or place or f"{lat:.3f}, {lon:.3f}") + location_text = f"{LOC_ICON} {place_str}" + + # Build tooltip (markup or plain) + if TOOLTIP_MARKUP: + # Escape dynamic text to avoid breaking Pango markup + tooltip_text = str.format( + "\t\t{}\t\t\n{}\n{}\n{}\n{}\n\n{}\n{}\n{}{}", + f'{esc(temp_str)}', + f" {icon}", + f"{esc(status)}", + esc(location_text), + f"{esc(feels_str)}" if feels_str else "", + f"{esc(min_max)}" if min_max else "", + f"{esc(wind_text)}\t{esc(humidity_text)}", + f"{esc(visibility_text)}\t{esc(aqi_text)}", + f" {esc(prediction)}" if prediction else "", + ) + else: + lines = [ + f"{icon} {temp_str}", + status, + location_text, + ] + if feels_str: + lines.append(feels_str) + if min_max: + lines.append(min_max) + lines.append(f"{wind_text} {humidity_text}".strip()) + lines.append(f"{visibility_text} {aqi_text}".strip()) + if prediction: + lines.append(hourly_precip) + tooltip_text = "\n".join([ln for ln in lines if ln]) + + out_data = { + "text": f"{icon} {temp_str}", + "alt": status, + "tooltip": tooltip_text, + "class": f"wmo-{code} {'day' if is_day else 'night'}", + } + + simple_weather = ( + f"{icon} {status}\n" + + f"๏‹‰ {temp_str} ({feels_str})\n" + + (f"{wind_text} \n" if wind_text else "") + + (f"{humidity_text} \n" if humidity_text else "") + + f"{visibility_text} {aqi_text}\n" + ) + + return out_data, simple_weather + + +def main() -> None: + lat, lon = get_coords() + + # Try cache first + cached = read_api_cache() + if cached and isinstance(cached, dict): + forecast = cached.get("forecast") + aqi = cached.get("aqi") + cached_place = cached.get("place") if isinstance(cached.get("place"), str) else None + place_effective = MANUAL_PLACE or ENV_PLACE or cached_place + try: + out, simple = build_output(lat, lon, forecast, aqi, place_effective) + print(json.dumps(out, ensure_ascii=False)) + write_simple_text_cache(simple) + return + except Exception as e: + print(f"Cached data build failed, refetching: {e}", file=sys.stderr) + + # Fetch fresh + try: + forecast = fetch_open_meteo(lat, lon) + aqi = fetch_aqi(lat, lon) + # Use manual/env place if provided; otherwise reverse geocode + place_effective = MANUAL_PLACE or ENV_PLACE or fetch_place(lat, lon) + write_api_cache({"forecast": forecast, "aqi": aqi, "place": place_effective}) + out, simple = build_output(lat, lon, forecast, aqi, place_effective) + print(json.dumps(out, ensure_ascii=False)) + write_simple_text_cache(simple) + except Exception as e: + print(f"Open-Meteo fetch failed: {e}", file=sys.stderr) + # Last resort: try stale cache without TTL + try: + if os.path.exists(API_CACHE_PATH): + with open(API_CACHE_PATH, "r", encoding="utf-8") as f: + stale = json.load(f) + out, simple = build_output(lat, lon, stale.get("forecast", {}), stale.get("aqi"), stale.get("place") if isinstance(stale.get("place"), str) else None) + print(json.dumps(out, ensure_ascii=False)) + write_simple_text_cache(simple) + return + except Exception as e2: + print(f"Failed to use stale cache: {e2}", file=sys.stderr) + # Fallback minimal output + fallback = { + "text": f"{WEATHER_ICONS['default']} N/A", + "alt": "Unavailable", + "tooltip": "Weather unavailable", + "class": "unavailable", + } + print(json.dumps(fallback, ensure_ascii=False)) + -try: - with open(os.path.expanduser("~/.cache/.weather_cache"), "w") as file: - file.write(simple_weather) -except Exception as e: - print(f"Error writing to cache: {e}") +if __name__ == "__main__": + main() -- cgit v1.2.3 From 7a147c0da9fb515cdb751014b737a33701063a74 Mon Sep 17 00:00:00 2001 From: prabinpanta0 Date: Sun, 26 Oct 2025 19:35:33 +0545 Subject: config(hypr): refactor Weather.py โ€” pathlib, typing, safer parsing & modular flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Convert to dataclasses (Location, WeatherData) and add richer typing/casts - Replace os.path with pathlib for cache paths and file I/O - Add robust numeric coercion helpers (coerce_int/float/number) and unit-safe parsing - Introduce ensure_dict/ensure_list and improved safe_get for resilient JSON traversal - Split geolocation into env/cache/ip providers and modular reverse-geocoding helpers - Modularize cache/fetch logic (try_cached, fetch_fresh, try_stale) and unify output builder - Safer handling of API cache timestamp/TTL and stale-cache fallback - Add simple tests for coercion functions --- config/hypr/UserScripts/Weather.py | 682 +++++++++++++++++++++++++++---------- 1 file changed, 502 insertions(+), 180 deletions(-) (limited to 'config/hypr/UserScripts') diff --git a/config/hypr/UserScripts/Weather.py b/config/hypr/UserScripts/Weather.py index ca1d5281..d76fb8db 100755 --- a/config/hypr/UserScripts/Weather.py +++ b/config/hypr/UserScripts/Weather.py @@ -3,15 +3,42 @@ # Rewritten to use Open-Meteo APIs (worldwide, no API key) for robust weather data. # Outputs Waybar-compatible JSON and a simple text cache. +from __future__ import annotations + import json import os import sys import time import html -from typing import Any, Dict, List, Optional, Tuple - +from pathlib import Path +from typing import Any, Dict, List, Optional, Tuple, TypeVar, Union, cast +from typing import NamedTuple import requests +from dataclasses import dataclass + +@dataclass +class Location: + lat: float + lon: float + place: Optional[str] = None + + +@dataclass +class WeatherData: + temp_str: str + feels_str: str + icon: str + status: str + min_max: str + wind_text: str + humidity_text: str + visibility_text: str + aqi_text: str + hourly_precip: str + is_day: int + code: int + # =============== Configuration =============== # You can configure behavior via environment variables OR the constants below. # Examples (zsh): @@ -27,9 +54,9 @@ import requests # export WEATHER_TOOLTIP_MARKUP=1 # 1 to enable Pango markup, 0 to disable # export WEATHER_LOC_ICON="๐Ÿ“" # or "*" for ASCII-only # -CACHE_DIR = os.path.expanduser("~/.cache") -API_CACHE_PATH = os.path.join(CACHE_DIR, "open_meteo_cache.json") -SIMPLE_TEXT_CACHE_PATH = os.path.join(CACHE_DIR, ".weather_cache") +CACHE_DIR: Path = Path.home() / ".cache" +API_CACHE_PATH: Path = CACHE_DIR / "open_meteo_cache.json" +SIMPLE_TEXT_CACHE_PATH: Path = CACHE_DIR / ".weather_cache" CACHE_TTL_SECONDS = int(os.getenv("WEATHER_CACHE_TTL", "600")) # default 10 minutes # Units: metric or imperial (default metric) @@ -138,19 +165,68 @@ def log_debug(msg: str) -> None: def ensure_cache_dir() -> None: try: - os.makedirs(CACHE_DIR, exist_ok=True) + # CACHE_DIR is a Path + CACHE_DIR.mkdir(parents=True, exist_ok=True) except Exception as e: print(f"Error creating cache dir: {e}", file=sys.stderr) +def _coerce_numeric(value: Any, to_int: bool) -> Optional[Union[int, float]]: + if to_int: + if isinstance(value, int): + return value + if isinstance(value, (float, str)): + try: + return int(float(value)) + except (ValueError, TypeError): + return None + return None + else: + if isinstance(value, float): + return value + if isinstance(value, int): + return float(value) + if isinstance(value, str): + try: + return float(value) + except (ValueError, TypeError): + return None + return None + + +def coerce_int(value: Any) -> Optional[int]: + return cast(Optional[int], _coerce_numeric(value, True)) + + +def coerce_float(value: Any) -> Optional[float]: + return cast(Optional[float], _coerce_numeric(value, False)) + + +def coerce_number(value: Any) -> Union[int, float, None]: + if isinstance(value, (int, float)): + return value + if isinstance(value, str): + try: + # Try float first, then int if no decimal + f = float(value) + return f if '.' in value or 'e' in value.lower() else int(f) + except (ValueError, TypeError): + return None + return None + + def read_api_cache() -> Optional[Dict[str, Any]]: try: - if not os.path.exists(API_CACHE_PATH): + if not API_CACHE_PATH.exists(): return None - with open(API_CACHE_PATH, "r", encoding="utf-8") as f: + with API_CACHE_PATH.open("r", encoding="utf-8") as f: data = json.load(f) - if (time.time() - data.get("timestamp", 0)) <= CACHE_TTL_SECONDS: - return data + # Use ensure_dict for safety + data_dict = ensure_dict(data) + timestamp_val = data_dict.get("timestamp", 0) + timestamp = coerce_float(timestamp_val) or 0 + if (time.time() - timestamp) <= CACHE_TTL_SECONDS: + return data_dict return None except Exception as e: print(f"Error reading cache: {e}", file=sys.stderr) @@ -161,7 +237,7 @@ def write_api_cache(payload: Dict[str, Any]) -> None: try: ensure_cache_dir() payload["timestamp"] = time.time() - with open(API_CACHE_PATH, "w", encoding="utf-8") as f: + with API_CACHE_PATH.open("w", encoding="utf-8") as f: json.dump(payload, f) except Exception as e: print(f"Error writing API cache: {e}", file=sys.stderr) @@ -170,34 +246,42 @@ def write_api_cache(payload: Dict[str, Any]) -> None: def write_simple_text_cache(text: str) -> None: try: ensure_cache_dir() - with open(SIMPLE_TEXT_CACHE_PATH, "w", encoding="utf-8") as f: + with SIMPLE_TEXT_CACHE_PATH.open("w", encoding="utf-8") as f: f.write(text) except Exception as e: print(f"Error writing simple cache: {e}", file=sys.stderr) -def get_coords() -> Tuple[float, float]: - # 1) Explicit env +def get_coords_from_env() -> Optional[Tuple[float, float]]: if ENV_LAT and ENV_LON: try: return float(ENV_LAT), float(ENV_LON) except ValueError: print("Invalid WEATHER_LAT/WEATHER_LON; falling back to IP geolocation", file=sys.stderr) + return None + - # 2) Try cached coordinates from last successful forecast +def get_coords_from_cache() -> Optional[Tuple[float, float]]: try: cached = read_api_cache() - if cached and isinstance(cached, dict): - fc = cached.get("forecast") or {} - lat = fc.get("latitude") - lon = fc.get("longitude") - if isinstance(lat, (int, float)) and isinstance(lon, (int, float)): - return float(lat), float(lon) + if cached: + fc = ensure_dict(cached.get("forecast")) + lat_raw = safe_get(fc, "latitude") + lon_raw = safe_get(fc, "longitude") + lat = coerce_float(lat_raw) + lon = coerce_float(lon_raw) + if lat is None: + log_debug(f"Unexpected type for cached latitude: {type(lat_raw)}") + if lon is None: + log_debug(f"Unexpected type for cached longitude: {type(lon_raw)}") + if lat is not None and lon is not None: + return lat, lon except Exception as e: print(f"Reading cached coords failed: {e}", file=sys.stderr) + return None + - # 3) IP-based geolocation with multiple providers (prefer ipwho.is, ipapi.co; ipinfo.io as fallback) - # ipwho.is +def get_coords_from_ipwho() -> Optional[Tuple[float, float]]: try: resp = SESSION.get("https://ipwho.is/", timeout=TIMEOUT) resp.raise_for_status() @@ -209,8 +293,10 @@ def get_coords() -> Tuple[float, float]: return float(lat), float(lon) except Exception as e: print(f"ipwho.is failed: {e}", file=sys.stderr) + return None + - # ipapi.co +def get_coords_from_ipapi() -> Optional[Tuple[float, float]]: try: resp = SESSION.get("https://ipapi.co/json", timeout=TIMEOUT) resp.raise_for_status() @@ -221,8 +307,10 @@ def get_coords() -> Tuple[float, float]: return float(lat), float(lon) except Exception as e: print(f"ipapi.co failed: {e}", file=sys.stderr) + return None - # ipinfo.io (fallback) + +def get_coords_from_ipinfo() -> Optional[Tuple[float, float]]: try: resp = SESSION.get("https://ipinfo.io/json", timeout=TIMEOUT) resp.raise_for_status() @@ -233,6 +321,24 @@ def get_coords() -> Tuple[float, float]: return float(lat_s), float(lon_s) except Exception as e: print(f"ipinfo.io failed: {e}", file=sys.stderr) + return None + + +def get_coords() -> Tuple[float, float]: + # 1) Explicit env + coords = get_coords_from_env() + if coords: + return coords + + # 2) Try cached coordinates + coords = get_coords_from_cache() + if coords: + return coords + + # 3) 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 print("IP geolocation failed: no providers succeeded", file=sys.stderr) @@ -272,7 +378,7 @@ def format_visibility(meters: Optional[float]) -> str: def fetch_open_meteo(lat: float, lon: float) -> Dict[str, Any]: base = "https://api.open-meteo.com/v1/forecast" - params = { + params: Dict[str, Union[str, float]] = { "latitude": lat, "longitude": lon, "current": "temperature_2m,apparent_temperature,relative_humidity_2m,wind_speed_10m,wind_direction_10m,weather_code,visibility,precipitation,pressure_msl,is_day", @@ -289,7 +395,7 @@ def fetch_open_meteo(lat: float, lon: float) -> Dict[str, Any]: def fetch_aqi(lat: float, lon: float) -> Optional[Dict[str, Any]]: try: base = "https://air-quality-api.open-meteo.com/v1/air-quality" - params = { + params: Dict[str, Union[str, float]] = { "latitude": lat, "longitude": lon, "current": "european_aqi", @@ -303,37 +409,51 @@ def fetch_aqi(lat: float, lon: float) -> Optional[Dict[str, Any]]: return None -def fetch_place(lat: float, lon: float) -> Optional[str]: - """Reverse geocode lat/lon to an approximate place. Tries Nominatim first, then Open-Meteo.""" - lang = os.getenv("WEATHER_LANG", "en") - - # 1) Nominatim (OpenStreetMap) +def extract_place_parts_nominatim(data_dict: JSONDict) -> List[str]: + address = ensure_dict(data_dict.get("address")) + candidates = [data_dict.get("name"), address.get("city"), address.get("town"), address.get("village"), address.get("hamlet")] + name = cast(Optional[str], next((c for c in candidates if c is not None), None)) + admin1 = cast(Optional[str], address.get("state")) + country = cast(Optional[str], address.get("country")) + parts: List[str] = [] + if name is not None: + parts.append(name) + if admin1 is not None: + parts.append(admin1) + if country is not None: + parts.append(country) + return parts + + +def extract_place_parts_open_meteo(p: JSONDict) -> List[str]: + name = cast(Optional[str], p.get("name")) + admin1 = cast(Optional[str], p.get("admin1")) + country = cast(Optional[str], p.get("country")) + parts: List[str] = [] + for part in [name, admin1, country]: + if part is not None: + parts.append(part) + return parts + + +def reverse_geocode(base: str, params: Dict[str, Union[str, float]], headers: Optional[Dict[str, str]] = None) -> Optional[str]: try: - base = "https://nominatim.openstreetmap.org/reverse" - params = { - "lat": lat, - "lon": lon, - "format": "jsonv2", - "accept-language": lang, - } - headers = {"User-Agent": UA + " Weather.py/1.0"} resp = SESSION.get(base, params=params, headers=headers, timeout=TIMEOUT) resp.raise_for_status() data = resp.json() - address = data.get("address", {}) - name = data.get("name") or address.get("city") or address.get("town") or address.get("village") or address.get("hamlet") - admin1 = address.get("state") - country = address.get("country") - parts = [part for part in [name, admin1, country] if part] + data_dict = ensure_dict(data) + parts = extract_place_parts_nominatim(data_dict) if parts: return ", ".join(parts) except Exception as e: - log_debug(f"Reverse geocoding (Nominatim) failed: {e}") + log_debug(f"Reverse geocoding failed: {e}") + return None - # 2) Open-Meteo reverse (fallback) + +def reverse_geocode_open_meteo(lat: float, lon: float, lang: str) -> Optional[str]: try: base = "https://geocoding-api.open-meteo.com/v1/reverse" - params = { + params: Dict[str, Union[str, float]] = { "latitude": lat, "longitude": lon, "language": lang, @@ -342,48 +462,107 @@ def fetch_place(lat: float, lon: float) -> Optional[str]: resp = SESSION.get(base, params=params, timeout=TIMEOUT) resp.raise_for_status() data = resp.json() - results = data.get("results") or [] + data_dict = ensure_dict(data) + results = ensure_list(data_dict.get("results")) if results: - p = results[0] - name = p.get("name") - admin1 = p.get("admin1") - country = p.get("country") - parts = [part for part in [name, admin1, country] if part] + p = ensure_dict(results[0]) + parts = extract_place_parts_open_meteo(p) if parts: return ", ".join(parts) except Exception as e: log_debug(f"Reverse geocoding (Open-Meteo) failed: {e}") - return None +def fetch_place(lat: float, lon: float) -> Optional[str]: + """Reverse geocode lat/lon to an approximate place. Tries Nominatim first, then Open-Meteo.""" + lang = os.getenv("WEATHER_LANG", "en") + + # 1) Nominatim (OpenStreetMap) + base = "https://nominatim.openstreetmap.org/reverse" + params: Dict[str, Union[str, float]] = { + "lat": lat, + "lon": lon, + "format": "jsonv2", + "accept-language": lang, + } + headers = {"User-Agent": UA + " Weather.py/1.0"} + place = reverse_geocode(base, params, headers) + if place: + return place + + # 2) Open-Meteo reverse (fallback) + return reverse_geocode_open_meteo(lat, lon, lang) + + # =============== Build Output =============== -def safe_get(dct: Dict[str, Any], *keys, default=None): - cur: Any = dct - for k in keys: +_T = TypeVar("_T") + +JSONValue = Union[str, int, float, bool, None, "JSONDict", "JSONList"] +JSONDict = Dict[str, JSONValue] +JSONList = List[JSONValue] + + +def ensure_dict(value: Any) -> JSONDict: + """Return a JSON-like dict when the incoming value looks like one.""" + if isinstance(value, dict): + return cast(JSONDict, value) + return cast(JSONDict, {}) + + +def ensure_list(value: Any) -> JSONList: + """Return a JSON-like list when the incoming value looks like one.""" + if isinstance(value, list): + return cast(JSONList, value) + return cast(JSONList, []) + + +def safe_get( + obj: JSONValue | None, + *keys: Union[str, int], + default: _T | None = None, +) -> _T | JSONValue | None: + """Safely traverse nested dict/list structures. + + Keys may be strings (for mapping lookups) or ints (for list indices). + Returns ``default`` if any lookup fails. + """ + + cur: JSONValue | None = obj + for key in keys: if isinstance(cur, dict): - if k not in cur: + if not isinstance(key, str) or key not in cur: return default - cur = cur[k] + cur = cur[key] elif isinstance(cur, list): - try: - cur = cur[k] # type: ignore[index] - except Exception: + if not isinstance(key, int) or key < 0 or key >= len(cur): return default + cur = cur[key] else: return default - return cur + return cast(_T | JSONValue | None, cur) -def build_hourly_precip(forecast: Dict[str, Any]) -> str: +def get_precipitation_probabilities(forecast: JSONDict) -> List[Optional[float]]: + probs_raw = safe_get(forecast, "hourly", "precipitation_probability") + probs_raw_list = ensure_list(probs_raw) + return [coerce_float(p) if p is not None else None for p in probs_raw_list] + + +def find_current_index(times: List[str], cur_time: Optional[str]) -> int: + if cur_time is not None and cur_time in times: + return times.index(cur_time) + return 0 + + +def build_hourly_precip(forecast: JSONDict) -> str: try: - times: List[str] = safe_get(forecast, "hourly", "time", default=[]) or [] - probs: List[Optional[float]] = safe_get( - forecast, "hourly", "precipitation_probability", default=[] - ) or [] - cur_time: Optional[str] = safe_get(forecast, "current", "time") - idx = times.index(cur_time) if cur_time in times else 0 + times_raw = safe_get(forecast, "hourly", "time") + times: List[str] = cast(List[str], ensure_list(times_raw)) + probs = get_precipitation_probabilities(forecast) + cur_time: Optional[str] = cast(Optional[str], safe_get(forecast, "current", "time")) + idx = find_current_index(times, cur_time) window = probs[idx : idx + 6] if not window: return "" @@ -393,152 +572,295 @@ def build_hourly_precip(forecast: Dict[str, Any]) -> str: return "" -def build_output(lat: float, lon: float, forecast: Dict[str, Any], aqi: Optional[Dict[str, Any]], place: Optional[str] = None) -> Tuple[Dict[str, Any], str]: - cur = forecast.get("current", {}) - cur_units = forecast.get("current_units", {}) - daily = forecast.get("daily", {}) - daily_units = forecast.get("daily_units", {}) +def build_weather_strings(cur: JSONDict, cur_units: JSONDict, daily: JSONDict, daily_units: JSONDict, temp_unit: str) -> Tuple[str, str, int, int, str, str, str]: + temp_val = coerce_float(cur.get("temperature_2m")) + temp_unit_str = cast(str, cur_units.get("temperature_2m", "")) + temp_str = f"{int(round(temp_val))}{temp_unit_str}" if temp_val is not None else "N/A" - temp_val = cur.get("temperature_2m") - temp_unit = cur_units.get("temperature_2m", "") - temp_str = f"{int(round(temp_val))}{temp_unit}" if isinstance(temp_val, (int, float)) else "N/A" + feels_val = coerce_float(cur.get("apparent_temperature")) + feels_unit = cast(str, cur_units.get("apparent_temperature", "")) + feels_str = f"Feels like {int(round(feels_val))}{feels_unit}" if feels_val is not None else "" - feels_val = cur.get("apparent_temperature") - feels_unit = cur_units.get("apparent_temperature", "") - feels_str = f"Feels like {int(round(feels_val))}{feels_unit}" if isinstance(feels_val, (int, float)) else "" - - is_day = int(cur.get("is_day", 1) or 1) - code = int(cur.get("weather_code", -1) or -1) + is_day_val = cur.get("is_day") + is_day = coerce_int(is_day_val) or 1 + weather_code_val = cur.get("weather_code") + code = coerce_int(weather_code_val) or -1 icon = wmo_to_icon(code, is_day) status = wmo_to_status(code) - # min/max today (index 0) - tmin_val = safe_get(daily, "temperature_2m_min", 0) - tmax_val = safe_get(daily, "temperature_2m_max", 0) - dtemp_unit = daily_units.get("temperature_2m_min", temp_unit) - tmin_str = f"{int(round(tmin_val))}{dtemp_unit}" if isinstance(tmin_val, (int, float)) else "" - tmax_str = f"{int(round(tmax_val))}{dtemp_unit}" if isinstance(tmax_val, (int, float)) else "" + tmin_val = coerce_float(safe_get(daily, "temperature_2m_min", 0)) + tmax_val = coerce_float(safe_get(daily, "temperature_2m_max", 0)) + dtemp_unit = cast(str, daily_units.get("temperature_2m_min", temp_unit)) + tmin_str = f"{int(round(tmin_val))}{dtemp_unit}" if tmin_val is not None else "" + tmax_str = f"{int(round(tmax_val))}{dtemp_unit}" if tmax_val is not None else "" min_max = f"๏‹‹ {tmin_str}\t\t๏‹‡ {tmax_str}" if tmin_str and tmax_str else "" - wind_val = cur.get("wind_speed_10m") - wind_unit = cur_units.get("wind_speed_10m", "") - wind_text = f"๎‰พ {int(round(wind_val))}{wind_unit}" if isinstance(wind_val, (int, float)) else "" + return temp_str, feels_str, is_day, code, icon, status, min_max + + +def build_weather_details(cur: JSONDict, cur_units: JSONDict) -> Tuple[str, str, str]: + wind_val_raw = cur.get("wind_speed_10m") + wind_val = coerce_float(wind_val_raw) + wind_unit = cast(str, cur_units.get("wind_speed_10m", "")) + if wind_val is None: + log_debug(f"Unexpected type for wind_speed_10m: {type(wind_val_raw)}") + wind_text = f" {int(round(wind_val))}{wind_unit}" if wind_val is not None else "" + + hum_val_raw = cur.get("relative_humidity_2m") + hum_val = coerce_float(hum_val_raw) + if hum_val is None: + log_debug(f"Unexpected type for relative_humidity_2m: {type(hum_val_raw)}") + humidity_text = f" {int(hum_val)}%" if hum_val is not None else "" + + vis_val_raw = cur.get("visibility") + vis_val = coerce_float(vis_val_raw) + if vis_val is None: + log_debug(f"Unexpected type for visibility: {type(vis_val_raw)}") + visibility_text = f" {format_visibility(vis_val)}" if vis_val is not None else "" + + return wind_text, humidity_text, visibility_text + + +def build_aqi_info(aqi: Optional[Dict[str, Any]]) -> str: + aqi_dict = ensure_dict(aqi) + aqi_val_raw = safe_get(aqi_dict, "current", "european_aqi") + aqi_val = coerce_float(aqi_val_raw) + if aqi_val is None: + log_debug(f"Unexpected type for european_aqi: {type(aqi_val_raw)}") + return f"AQI {int(aqi_val)}" if aqi_val is not None else "AQI N/A" + + +def build_place_str(lat: float, lon: float, place: Optional[str]) -> str: + return MANUAL_PLACE or ENV_PLACE or place or f"{lat:.3f}, {lon:.3f}" + + + + +class TooltipParams(NamedTuple): + temp_str: str + icon: str + status: str + location_text: str + feels_str: str + min_max: str + wind_text: str + humidity_text: str + visibility_text: str + aqi_text: str + hourly_precip: str + + +def build_tooltip_markup(params: TooltipParams) -> str: + return str.format( + "\t\t{}\t\t\n{}\n{}\n{}\n{}\n\n{}\n{}\n{}{}", + f'{esc(params.temp_str)}', + f" {params.icon}", + f"{esc(params.status)}", + esc(params.location_text), + f"{esc(params.feels_str)}" if params.feels_str else "", + f"{esc(params.min_max)}" if params.min_max else "", + f"{esc(params.wind_text)}\t{esc(params.humidity_text)}", + f"{esc(params.visibility_text)}\t{esc(params.aqi_text)}", + f" {esc(params.hourly_precip)}" if params.hourly_precip else "", + ) - hum_val = cur.get("relative_humidity_2m") - humidity_text = f"๎ณ {int(hum_val)}%" if isinstance(hum_val, (int, float)) else "" - vis_val = cur.get("visibility") - visibility_text = f"๏ฎ {format_visibility(vis_val)}" if isinstance(vis_val, (int, float)) else "" +def build_tooltip_plain(params: TooltipParams) -> str: + lines = [ + f"{params.icon} {params.temp_str}", + params.status, + params.location_text, + ] + if params.feels_str: + lines.append(params.feels_str) + if params.min_max: + lines.append(params.min_max) + combined_wind = f"{params.wind_text} {params.humidity_text}".strip() + if combined_wind: + lines.append(combined_wind) + combined_visibility = f"{params.visibility_text} {params.aqi_text}".strip() + if combined_visibility: + lines.append(combined_visibility) + if params.hourly_precip: + lines.append(params.hourly_precip) + return "\n".join([ln for ln in lines if ln]) + + +def build_tooltip_text(params: TooltipParams) -> str: + if TOOLTIP_MARKUP: + return build_tooltip_markup(params) + else: + return build_tooltip_plain(params) + + +def gather_weather_data(forecast: Optional[Dict[str, Any]], aqi: Optional[Dict[str, Any]]) -> WeatherData: + forecast_dict = ensure_dict(forecast) + cur = ensure_dict(forecast_dict.get("current")) + cur_units = ensure_dict(forecast_dict.get("current_units")) + daily = ensure_dict(forecast_dict.get("daily")) + daily_units = ensure_dict(forecast_dict.get("daily_units")) + + temp_str, feels_str, is_day, code, icon, status, min_max = build_weather_strings(cur, cur_units, daily, daily_units, cast(str, cur_units.get("temperature_2m", ""))) + wind_text, humidity_text, visibility_text = build_weather_details(cur, cur_units) + aqi_text = build_aqi_info(aqi) + hourly_precip = build_hourly_precip(forecast_dict) + + return WeatherData( + temp_str=temp_str, + feels_str=feels_str, + icon=icon, + status=status, + min_max=min_max, + wind_text=wind_text, + humidity_text=humidity_text, + visibility_text=visibility_text, + aqi_text=aqi_text, + hourly_precip=hourly_precip, + is_day=is_day, + code=code, + ) - aqi_val = safe_get(aqi or {}, "current", "european_aqi") - aqi_text = f"AQI {int(aqi_val)}" if isinstance(aqi_val, (int, float)) else "AQI N/A" - hourly_precip = build_hourly_precip(forecast) - prediction = f"\n\n{hourly_precip}" if hourly_precip else "" +def build_output(loc: Location, forecast: Optional[Dict[str, Any]], aqi: Optional[Dict[str, Any]]) -> Tuple[Dict[str, str], str]: + data = gather_weather_data(forecast, aqi) - # Build place string (priority: MANUAL_PLACE > ENV_PLACE > reverse geocode > lat,lon) - place_str = (MANUAL_PLACE or ENV_PLACE or place or f"{lat:.3f}, {lon:.3f}") + place_str = build_place_str(loc.lat, loc.lon, loc.place) location_text = f"{LOC_ICON} {place_str}" - # Build tooltip (markup or plain) - if TOOLTIP_MARKUP: - # Escape dynamic text to avoid breaking Pango markup - tooltip_text = str.format( - "\t\t{}\t\t\n{}\n{}\n{}\n{}\n\n{}\n{}\n{}{}", - f'{esc(temp_str)}', - f" {icon}", - f"{esc(status)}", - esc(location_text), - f"{esc(feels_str)}" if feels_str else "", - f"{esc(min_max)}" if min_max else "", - f"{esc(wind_text)}\t{esc(humidity_text)}", - f"{esc(visibility_text)}\t{esc(aqi_text)}", - f" {esc(prediction)}" if prediction else "", + tooltip_text = build_tooltip_text( + TooltipParams( + data.temp_str, data.icon, data.status, location_text, data.feels_str, data.min_max, + data.wind_text, data.humidity_text, data.visibility_text, data.aqi_text, data.hourly_precip ) - else: - lines = [ - f"{icon} {temp_str}", - status, - location_text, - ] - if feels_str: - lines.append(feels_str) - if min_max: - lines.append(min_max) - lines.append(f"{wind_text} {humidity_text}".strip()) - lines.append(f"{visibility_text} {aqi_text}".strip()) - if prediction: - lines.append(hourly_precip) - tooltip_text = "\n".join([ln for ln in lines if ln]) - - out_data = { - "text": f"{icon} {temp_str}", - "alt": status, + ) + + out_data: Dict[str, str] = { + "text": f"{data.icon} {data.temp_str}", + "alt": data.status, "tooltip": tooltip_text, - "class": f"wmo-{code} {'day' if is_day else 'night'}", + "class": f"wmo-{data.code} {'day' if data.is_day else 'night'}", } simple_weather = ( - f"{icon} {status}\n" - + f"๏‹‰ {temp_str} ({feels_str})\n" - + (f"{wind_text} \n" if wind_text else "") - + (f"{humidity_text} \n" if humidity_text else "") - + f"{visibility_text} {aqi_text}\n" + f"{data.icon} {data.status}\n" + + f"๏‹‰ {data.temp_str} ({data.feels_str})\n" + + (f"{data.wind_text} \n" if data.wind_text else "") + + (f"{data.humidity_text} \n" if data.humidity_text else "") + + f"{data.visibility_text} {data.aqi_text}\n" ) return out_data, simple_weather -def main() -> None: - lat, lon = get_coords() - - # Try cache first +def try_cached_weather(lat: float, lon: float) -> Optional[Tuple[Dict[str, str], str]]: cached = read_api_cache() - if cached and isinstance(cached, dict): - forecast = cached.get("forecast") - aqi = cached.get("aqi") - cached_place = cached.get("place") if isinstance(cached.get("place"), str) else None + if cached: + forecast = cast(Optional[Dict[str, Any]], cached.get("forecast")) + 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 place_effective = MANUAL_PLACE or ENV_PLACE or cached_place try: - out, simple = build_output(lat, lon, forecast, aqi, place_effective) - print(json.dumps(out, ensure_ascii=False)) - write_simple_text_cache(simple) - return + return build_output(Location(lat, lon, place_effective), forecast, aqi) except Exception as e: print(f"Cached data build failed, refetching: {e}", file=sys.stderr) + return None - # Fetch fresh + +def fetch_fresh_weather(lat: float, lon: float) -> Optional[Tuple[Dict[str, str], str]]: try: forecast = fetch_open_meteo(lat, lon) aqi = fetch_aqi(lat, lon) - # Use manual/env place if provided; otherwise reverse geocode place_effective = MANUAL_PLACE or ENV_PLACE or fetch_place(lat, lon) write_api_cache({"forecast": forecast, "aqi": aqi, "place": place_effective}) - out, simple = build_output(lat, lon, forecast, aqi, place_effective) - print(json.dumps(out, ensure_ascii=False)) - write_simple_text_cache(simple) + return build_output(Location(lat, lon, place_effective), forecast, aqi) except Exception as e: print(f"Open-Meteo fetch failed: {e}", file=sys.stderr) - # Last resort: try stale cache without TTL - try: - if os.path.exists(API_CACHE_PATH): - with open(API_CACHE_PATH, "r", encoding="utf-8") as f: - stale = json.load(f) - out, simple = build_output(lat, lon, stale.get("forecast", {}), stale.get("aqi"), stale.get("place") if isinstance(stale.get("place"), str) else None) - print(json.dumps(out, ensure_ascii=False)) - write_simple_text_cache(simple) - return - except Exception as e2: - print(f"Failed to use stale cache: {e2}", file=sys.stderr) - # Fallback minimal output - fallback = { - "text": f"{WEATHER_ICONS['default']} N/A", - "alt": "Unavailable", - "tooltip": "Weather unavailable", - "class": "unavailable", - } - print(json.dumps(fallback, ensure_ascii=False)) + return None + + +def try_stale_weather(lat: float, lon: float) -> Optional[Tuple[Dict[str, str], str]]: + try: + if API_CACHE_PATH.exists(): + with API_CACHE_PATH.open("r", encoding="utf-8") as f: + stale = json.load(f) + stale_dict = ensure_dict(stale) + place_val = stale_dict.get("place") + place = place_val if isinstance(place_val, str) else None + forecast = cast(Optional[Dict[str, Any]], stale_dict.get("forecast")) + aqi = cast(Optional[Dict[str, Any]], stale_dict.get("aqi")) + return build_output(Location(lat, lon, place), forecast, aqi) + except Exception as e2: + print(f"Failed to use stale cache: {e2}", file=sys.stderr) + return None + + +def main() -> None: + lat, lon = get_coords() + + # Try cache first + result = try_cached_weather(lat, lon) + if result: + out, simple = result + print(json.dumps(out, ensure_ascii=False)) + write_simple_text_cache(simple) + return + + # Fetch fresh + result = fetch_fresh_weather(lat, lon) + if result: + out, simple = result + print(json.dumps(out, ensure_ascii=False)) + write_simple_text_cache(simple) + return + + # Last resort: try stale cache + result = try_stale_weather(lat, lon) + if result: + out, simple = result + print(json.dumps(out, ensure_ascii=False)) + write_simple_text_cache(simple) + return + + # Fallback minimal output + fallback = { + "text": f"{WEATHER_ICONS['default']} N/A", + "alt": "Unavailable", + "tooltip": "Weather unavailable", + "class": "unavailable", + } + print(json.dumps(fallback, ensure_ascii=False)) + + +def test_coerce_functions(): + """Manual testing for coerce functions.""" + # Test coerce_int + assert coerce_int(5) == 5 + assert coerce_int(5.5) == 5 + assert coerce_int("5") == 5 + assert coerce_int("5.7") == 5 + assert coerce_int("abc") is None + assert coerce_int(None) is None + + # Test coerce_float + assert coerce_float(5.5) == 5.5 + assert coerce_float(5) == 5.0 + assert coerce_float("5.5") == 5.5 + assert coerce_float("abc") is None + assert coerce_float(None) is None + + # Test coerce_number + assert coerce_number(5) == 5 + assert coerce_number(5.5) == 5.5 + assert coerce_number("5") == 5 + assert coerce_number("5.5") == 5.5 + assert coerce_number("abc") is None + + print("All coerce function tests passed.", file=sys.stderr) if __name__ == "__main__": - main() + if len(sys.argv) > 1 and sys.argv[1] == "--test": + test_coerce_functions() + else: + main() -- cgit v1.2.3 From 30839413484f72343d66035bbd77af700059d0a3 Mon Sep 17 00:00:00 2001 From: prabinpanta0 Date: Sun, 26 Oct 2025 19:42:36 +0545 Subject: config(hypr): improve numeric coercion and add ensure_* warnings - Parse numeric strings more robustly in coerce_number: convert to float then return int when the float has no fractional part (handles scientific notation and avoids brittle '.'/'e' checks). - Add diagnostic warnings to ensure_dict and ensure_list that print the unexpected type and a truncated repr to stderr to help detect API shape mismatches. --- config/hypr/UserScripts/Weather.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) (limited to 'config/hypr/UserScripts') diff --git a/config/hypr/UserScripts/Weather.py b/config/hypr/UserScripts/Weather.py index d76fb8db..a566d5ec 100755 --- a/config/hypr/UserScripts/Weather.py +++ b/config/hypr/UserScripts/Weather.py @@ -207,9 +207,9 @@ def coerce_number(value: Any) -> Union[int, float, None]: return value if isinstance(value, str): try: - # Try float first, then int if no decimal + # Parse to float, then return int if it has no fractional part f = float(value) - return f if '.' in value or 'e' in value.lower() else int(f) + return int(f) if f.is_integer() else f except (ValueError, TypeError): return None return None @@ -508,6 +508,11 @@ def ensure_dict(value: Any) -> JSONDict: """Return a JSON-like dict when the incoming value looks like one.""" if isinstance(value, dict): return cast(JSONDict, value) + # Warn about unexpected type to catch API shape mismatches + val_repr = repr(value) if value is not None else "None" + if len(val_repr) > 100: + val_repr = val_repr[:100] + "..." + print(f"Warning: ensure_dict received {type(value).__name__} instead of dict: {val_repr}", file=sys.stderr) return cast(JSONDict, {}) @@ -515,6 +520,11 @@ def ensure_list(value: Any) -> JSONList: """Return a JSON-like list when the incoming value looks like one.""" if isinstance(value, list): return cast(JSONList, value) + # Warn about unexpected type to catch API shape mismatches + val_repr = repr(value) if value is not None else "None" + if len(val_repr) > 100: + val_repr = val_repr[:100] + "..." + print(f"Warning: ensure_list received {type(value).__name__} instead of list: {val_repr}", file=sys.stderr) return cast(JSONList, []) -- cgit v1.2.3 From f876fc42308b949e791e6f685fa9c6f32605667e Mon Sep 17 00:00:00 2001 From: prabinpanta0 Date: Sun, 26 Oct 2025 19:53:08 +0545 Subject: config(hypr): relax out_data typing in Weather.py to Dict[str, Any] Change out_data annotation from Dict[str, str] to Dict[str, Any] so the output can include non-string values (tooltip, class, etc.) and avoid type mismatches. --- config/hypr/UserScripts/Weather.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'config/hypr/UserScripts') diff --git a/config/hypr/UserScripts/Weather.py b/config/hypr/UserScripts/Weather.py index a566d5ec..3c5d58f9 100755 --- a/config/hypr/UserScripts/Weather.py +++ b/config/hypr/UserScripts/Weather.py @@ -744,7 +744,7 @@ def build_output(loc: Location, forecast: Optional[Dict[str, Any]], aqi: Optiona ) ) - out_data: Dict[str, str] = { + out_data: Dict[str, Any] = { "text": f"{data.icon} {data.temp_str}", "alt": data.status, "tooltip": tooltip_text, -- cgit v1.2.3 From 95b85dd6c59620d660ff320a07bdf7b3c42bdc46 Mon Sep 17 00:00:00 2001 From: prabinpanta0 Date: Mon, 27 Oct 2025 10:32:36 +0545 Subject: config(hypr): disable tooltip markup by default, refine place handling & simple output; replace wttr wrapper - Set TOOLTIP_MARKUP default to off (ENV-driven still supported). - Use truthy checks in place extraction helpers and simplify extraction logic. - Change place formatting to show resolved place with coordinates or coordinates-only. - Prepend place to simple text cache and add icons for wind, humidity and visibility. - Replace legacy wttr-based Weather.sh with a direct Python Weather.py invocation. --- config/hypr/UserScripts/Weather.py | 28 ++++++++----- config/hypr/UserScripts/Weather.sh | 86 +------------------------------------- 2 files changed, 19 insertions(+), 95 deletions(-) (limited to 'config/hypr/UserScripts') diff --git a/config/hypr/UserScripts/Weather.py b/config/hypr/UserScripts/Weather.py index 3c5d58f9..3b58d8ff 100755 --- a/config/hypr/UserScripts/Weather.py +++ b/config/hypr/UserScripts/Weather.py @@ -74,7 +74,7 @@ MANUAL_PLACE: Optional[str] = None # Location icon in tooltip (default to a standard emoji to avoid missing glyphs) LOC_ICON = os.getenv("WEATHER_LOC_ICON", "๐Ÿ“") # Enable/disable Pango markup in tooltip (1/0, true/false) -TOOLTIP_MARKUP = os.getenv("WEATHER_TOOLTIP_MARKUP", "1").lower() not in ("0", "false", "no") +TOOLTIP_MARKUP = os.getenv("WEATHER_TOOLTIP_MARKUP", "0").lower() not in ("0", "false", "no") # Optional debug logging to stderr (set WEATHER_DEBUG=1 to enable) DEBUG = os.getenv("WEATHER_DEBUG", "0").lower() not in ("0", "false", "no") @@ -416,11 +416,11 @@ def extract_place_parts_nominatim(data_dict: JSONDict) -> List[str]: admin1 = cast(Optional[str], address.get("state")) country = cast(Optional[str], address.get("country")) parts: List[str] = [] - if name is not None: + if name: parts.append(name) - if admin1 is not None: + if admin1: parts.append(admin1) - if country is not None: + if country: parts.append(country) return parts @@ -430,9 +430,12 @@ def extract_place_parts_open_meteo(p: JSONDict) -> List[str]: admin1 = cast(Optional[str], p.get("admin1")) country = cast(Optional[str], p.get("country")) parts: List[str] = [] - for part in [name, admin1, country]: - if part is not None: - parts.append(part) + if name: + parts.append(name) + if admin1: + parts.append(admin1) + if country: + parts.append(country) return parts @@ -641,7 +644,9 @@ def build_aqi_info(aqi: Optional[Dict[str, Any]]) -> str: def build_place_str(lat: float, lon: float, place: Optional[str]) -> str: - return MANUAL_PLACE or ENV_PLACE or place or f"{lat:.3f}, {lon:.3f}" + if place: + return f"{place} ({lat:.3f}, {lon:.3f})" + return f"{lat:.3f}, {lon:.3f}" @@ -752,11 +757,12 @@ def build_output(loc: Location, forecast: Optional[Dict[str, Any]], aqi: Optiona } simple_weather = ( + f"{place_str}\n" f"{data.icon} {data.status}\n" + f"๏‹‰ {data.temp_str} ({data.feels_str})\n" - + (f"{data.wind_text} \n" if data.wind_text else "") - + (f"{data.humidity_text} \n" if data.humidity_text else "") - + f"{data.visibility_text} {data.aqi_text}\n" + + (f"๎‰พ {data.wind_text} \n" if data.wind_text else "") + + (f"๎ณ {data.humidity_text} \n" if data.humidity_text else "") + + f"๏ฎ {data.visibility_text} {data.aqi_text}\n" ) return out_data, simple_weather diff --git a/config/hypr/UserScripts/Weather.sh b/config/hypr/UserScripts/Weather.sh index 9bdaff4a..744878a9 100755 --- a/config/hypr/UserScripts/Weather.sh +++ b/config/hypr/UserScripts/Weather.sh @@ -1,87 +1,5 @@ #!/bin/bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ ## -# weather info from wttr. https://github.com/chubin/wttr.in -# Remember to add city +# weather info using Python script with Open-Meteo APIs -city= -cachedir="$HOME/.cache/rbn" -cachefile=${0##*/}-$1 - -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' - -cacheage=$(($(date +%s) - $(stat -c '%Y' "$cachedir/$cachefile"))) -if [ $cacheage -gt 1740 ] || [ ! -s $cachedir/$cachefile ]; then - data=($(curl -s https://en.wttr.in/"$city"$1\?0qnT 2>&1)) - echo ${data[0]} | cut -f1 -d, > $cachedir/$cachefile - echo ${data[1]} | sed -E 's/^.{15}//' >> $cachedir/$cachefile - echo ${data[2]} | sed -E 's/^.{15}//' >> $cachedir/$cachefile -fi - -weather=($(cat $cachedir/$cachefile)) - -# 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 -case $(echo ${weather[1]##*,} | tr '[:upper:]' '[:lower:]') 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") - 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="๏ช" - echo -e "{\"text\":\""$condition"\", \"alt\":\""${weather[0]}"\", \"tooltip\":\""${weather[0]}: $temperature ${weather[1]}"\"}" - ;; -esac - -#echo $temp $condition - -echo -e "{\"text\":\""$temperature $condition"\", \"alt\":\""${weather[0]}"\", \"tooltip\":\""${weather[0]}: $temperature ${weather[1]}"\"}" - -cached_weather="๏‹‰ $temperature \n$condition ${weather[1]}" - -echo -e $cached_weather > "$HOME/.cache/.weather_cache" \ No newline at end of file +python3 "$(dirname "$0")/Weather.py" \ No newline at end of file -- cgit v1.2.3 From 7275b5f73ff6c1164cba645a5f41ac088a10e7cc Mon Sep 17 00:00:00 2001 From: prabinpanta0 Date: Mon, 27 Oct 2025 10:35:13 +0545 Subject: config(hypr): run Weather.sh at startup; lower weather cache TTL to 5 minutes --- config/hypr/UserConfigs/Startup_Apps.conf | 3 +++ config/hypr/UserScripts/Weather.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) (limited to 'config/hypr/UserScripts') diff --git a/config/hypr/UserConfigs/Startup_Apps.conf b/config/hypr/UserConfigs/Startup_Apps.conf index 738eb2da..976208e9 100644 --- a/config/hypr/UserConfigs/Startup_Apps.conf +++ b/config/hypr/UserConfigs/Startup_Apps.conf @@ -47,6 +47,9 @@ exec-once = $UserScripts/RainbowBorders.sh # Starting hypridle to start hyprlock exec-once = hypridle +# Weather script to populate cache on startup +exec-once = $UserScripts/Weather.sh + # Here are list of features available but disabled by default # exec-once = swww-daemon --format xrgb && swww img $HOME/Pictures/wallpapers/mecha-nostalgia.png # persistent wallpaper diff --git a/config/hypr/UserScripts/Weather.py b/config/hypr/UserScripts/Weather.py index 3b58d8ff..47c88d1f 100755 --- a/config/hypr/UserScripts/Weather.py +++ b/config/hypr/UserScripts/Weather.py @@ -57,7 +57,7 @@ class WeatherData: CACHE_DIR: Path = Path.home() / ".cache" API_CACHE_PATH: Path = CACHE_DIR / "open_meteo_cache.json" SIMPLE_TEXT_CACHE_PATH: Path = CACHE_DIR / ".weather_cache" -CACHE_TTL_SECONDS = int(os.getenv("WEATHER_CACHE_TTL", "600")) # default 10 minutes +CACHE_TTL_SECONDS = int(os.getenv("WEATHER_CACHE_TTL", "300")) # default 5 minutes # Units: metric or imperial (default metric) UNITS = os.getenv("WEATHER_UNITS", "metric").strip().lower() # metric|imperial -- cgit v1.2.3 From a37147a7c3f236028f1f44ebdc6fbb915cf2d5f0 Mon Sep 17 00:00:00 2001 From: prabinpanta0 Date: Mon, 27 Oct 2025 10:44:54 +0545 Subject: config(hypr): add python3 availability check, forward args and propagate exit status in Weather.sh --- config/hypr/UserScripts/Weather.sh | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) (limited to 'config/hypr/UserScripts') diff --git a/config/hypr/UserScripts/Weather.sh b/config/hypr/UserScripts/Weather.sh index 744878a9..0540e51d 100755 --- a/config/hypr/UserScripts/Weather.sh +++ b/config/hypr/UserScripts/Weather.sh @@ -2,4 +2,14 @@ # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ ## # weather info using Python script with Open-Meteo APIs -python3 "$(dirname "$0")/Weather.py" \ No newline at end of file +if ! command -v python3 >/dev/null 2>&1; then + echo "python3 not found in PATH" >&2 + exit 127 +fi + +python3 "$(dirname "$0")/Weather.py" "$@" +exit_code=$? +if [ "$exit_code" -ne 0 ]; then + echo "Failed to run Weather.py" >&2 +fi +exit "$exit_code" \ No newline at end of file -- cgit v1.2.3 From d299040a6826be545e18a892994226a58ad0c10a Mon Sep 17 00:00:00 2001 From: prabinpanta0 Date: Mon, 27 Oct 2025 10:57:46 +0545 Subject: config(hypr): preserve empty place strings and use explicit None checks Use `is not None` when extracting open-meteo place parts so empty strings aren't discarded. Replace truthy `or` chains for selecting the effective place with explicit None-aware conditionals so MANUAL_PLACE/ENV_PLACE empty values are honored instead of being treated as false. --- config/hypr/UserScripts/Weather.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) (limited to 'config/hypr/UserScripts') diff --git a/config/hypr/UserScripts/Weather.py b/config/hypr/UserScripts/Weather.py index 47c88d1f..6189c647 100755 --- a/config/hypr/UserScripts/Weather.py +++ b/config/hypr/UserScripts/Weather.py @@ -430,11 +430,11 @@ def extract_place_parts_open_meteo(p: JSONDict) -> List[str]: admin1 = cast(Optional[str], p.get("admin1")) country = cast(Optional[str], p.get("country")) parts: List[str] = [] - if name: + if name is not None: parts.append(name) - if admin1: + if admin1 is not None: parts.append(admin1) - if country: + if country is not None: parts.append(country) return parts @@ -775,7 +775,7 @@ 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 - place_effective = MANUAL_PLACE or ENV_PLACE or cached_place + place_effective = MANUAL_PLACE if MANUAL_PLACE is not None else (ENV_PLACE if ENV_PLACE is not None else cached_place) try: return build_output(Location(lat, lon, place_effective), forecast, aqi) except Exception as e: @@ -787,7 +787,7 @@ def fetch_fresh_weather(lat: float, lon: float) -> Optional[Tuple[Dict[str, str] try: forecast = fetch_open_meteo(lat, lon) aqi = fetch_aqi(lat, lon) - place_effective = MANUAL_PLACE or ENV_PLACE or fetch_place(lat, lon) + place_effective = MANUAL_PLACE if MANUAL_PLACE is not None else (ENV_PLACE if ENV_PLACE is not None else fetch_place(lat, lon)) write_api_cache({"forecast": forecast, "aqi": aqi, "place": place_effective}) return build_output(Location(lat, lon, place_effective), forecast, aqi) except Exception as e: @@ -803,9 +803,10 @@ def try_stale_weather(lat: float, lon: float) -> Optional[Tuple[Dict[str, str], stale_dict = ensure_dict(stale) place_val = stale_dict.get("place") place = place_val if isinstance(place_val, str) else None + place_effective = MANUAL_PLACE if MANUAL_PLACE is not None else (ENV_PLACE if ENV_PLACE is not None else place) forecast = cast(Optional[Dict[str, Any]], stale_dict.get("forecast")) aqi = cast(Optional[Dict[str, Any]], stale_dict.get("aqi")) - return build_output(Location(lat, lon, place), forecast, aqi) + return build_output(Location(lat, lon, place_effective), forecast, aqi) except Exception as e2: print(f"Failed to use stale cache: {e2}", file=sys.stderr) return None -- cgit v1.2.3 From f930ca046894ab12bb5907ffc9b4e5cbd3a5a2e4 Mon Sep 17 00:00:00 2001 From: prabinpanta0 Date: Mon, 27 Oct 2025 11:05:44 +0545 Subject: config(hypr): skip empty place parts and simplify place selection using or-chaining Use truthy checks when building Open-Meteo place parts (ignore empty strings) and replace verbose None-check ternaries with `or` chains when resolving the effective place (prefer MANUAL_PLACE, then ENV_PLACE, then cached/fetched place). --- config/hypr/UserScripts/Weather.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'config/hypr/UserScripts') diff --git a/config/hypr/UserScripts/Weather.py b/config/hypr/UserScripts/Weather.py index 6189c647..7e350c30 100755 --- a/config/hypr/UserScripts/Weather.py +++ b/config/hypr/UserScripts/Weather.py @@ -430,11 +430,11 @@ def extract_place_parts_open_meteo(p: JSONDict) -> List[str]: admin1 = cast(Optional[str], p.get("admin1")) country = cast(Optional[str], p.get("country")) parts: List[str] = [] - if name is not None: + if name: parts.append(name) - if admin1 is not None: + if admin1: parts.append(admin1) - if country is not None: + if country: parts.append(country) return parts @@ -775,7 +775,7 @@ 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 - place_effective = MANUAL_PLACE if MANUAL_PLACE is not None else (ENV_PLACE if ENV_PLACE is not None else cached_place) + place_effective = MANUAL_PLACE or ENV_PLACE or cached_place try: return build_output(Location(lat, lon, place_effective), forecast, aqi) except Exception as e: @@ -787,7 +787,7 @@ def fetch_fresh_weather(lat: float, lon: float) -> Optional[Tuple[Dict[str, str] try: forecast = fetch_open_meteo(lat, lon) aqi = fetch_aqi(lat, lon) - place_effective = MANUAL_PLACE if MANUAL_PLACE is not None else (ENV_PLACE if ENV_PLACE is not None else fetch_place(lat, lon)) + place_effective = MANUAL_PLACE or ENV_PLACE or fetch_place(lat, lon) write_api_cache({"forecast": forecast, "aqi": aqi, "place": place_effective}) return build_output(Location(lat, lon, place_effective), forecast, aqi) except Exception as e: @@ -803,7 +803,7 @@ def try_stale_weather(lat: float, lon: float) -> Optional[Tuple[Dict[str, str], stale_dict = ensure_dict(stale) place_val = stale_dict.get("place") place = place_val if isinstance(place_val, str) else None - place_effective = MANUAL_PLACE if MANUAL_PLACE is not None else (ENV_PLACE if ENV_PLACE is not None else place) + place_effective = MANUAL_PLACE or ENV_PLACE or place forecast = cast(Optional[Dict[str, Any]], stale_dict.get("forecast")) aqi = cast(Optional[Dict[str, Any]], stale_dict.get("aqi")) return build_output(Location(lat, lon, place_effective), forecast, aqi) -- cgit v1.2.3 From c47eadb340dcdea51587537f4237f347653cb675 Mon Sep 17 00:00:00 2001 From: prabinpanta0 Date: Mon, 27 Oct 2025 11:16:58 +0545 Subject: config(hypr): preserve empty place strings & prefer MANUAL/ENV place; use explicit None checks - Use explicit "is not None" checks when building place parts so empty strings are kept instead of being treated as falsy. - Build place string from MANUAL_PLACE or ENV_PLACE before reverse-geocoded place, preserving explicit overrides and empty place values. --- config/hypr/UserScripts/Weather.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) (limited to 'config/hypr/UserScripts') diff --git a/config/hypr/UserScripts/Weather.py b/config/hypr/UserScripts/Weather.py index 7e350c30..2a3e0ad2 100755 --- a/config/hypr/UserScripts/Weather.py +++ b/config/hypr/UserScripts/Weather.py @@ -416,11 +416,11 @@ def extract_place_parts_nominatim(data_dict: JSONDict) -> List[str]: admin1 = cast(Optional[str], address.get("state")) country = cast(Optional[str], address.get("country")) parts: List[str] = [] - if name: + if name is not None: parts.append(name) - if admin1: + if admin1 is not None: parts.append(admin1) - if country: + if country is not None: parts.append(country) return parts @@ -430,11 +430,11 @@ def extract_place_parts_open_meteo(p: JSONDict) -> List[str]: admin1 = cast(Optional[str], p.get("admin1")) country = cast(Optional[str], p.get("country")) parts: List[str] = [] - if name: + if name is not None: parts.append(name) - if admin1: + if admin1 is not None: parts.append(admin1) - if country: + if country is not None: parts.append(country) return parts @@ -644,8 +644,9 @@ def build_aqi_info(aqi: Optional[Dict[str, Any]]) -> str: def build_place_str(lat: float, lon: float, place: Optional[str]) -> str: - if place: - return f"{place} ({lat:.3f}, {lon:.3f})" + effective_place = MANUAL_PLACE or ENV_PLACE or place + if effective_place: + return f"{effective_place} ({lat:.3f}, {lon:.3f})" return f"{lat:.3f}, {lon:.3f}" -- cgit v1.2.3 From a8b355b490d6f0d4e5e5e219426ddffd26ca19f1 Mon Sep 17 00:00:00 2001 From: Prabin Panta Date: Mon, 27 Oct 2025 11:46:38 +0545 Subject: Update config/hypr/UserScripts/Weather.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- config/hypr/UserScripts/Weather.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'config/hypr/UserScripts') diff --git a/config/hypr/UserScripts/Weather.py b/config/hypr/UserScripts/Weather.py index 2a3e0ad2..ee861ebb 100755 --- a/config/hypr/UserScripts/Weather.py +++ b/config/hypr/UserScripts/Weather.py @@ -74,7 +74,7 @@ MANUAL_PLACE: Optional[str] = None # Location icon in tooltip (default to a standard emoji to avoid missing glyphs) LOC_ICON = os.getenv("WEATHER_LOC_ICON", "๐Ÿ“") # Enable/disable Pango markup in tooltip (1/0, true/false) -TOOLTIP_MARKUP = os.getenv("WEATHER_TOOLTIP_MARKUP", "0").lower() not in ("0", "false", "no") +TOOLTIP_MARKUP = os.getenv("WEATHER_TOOLTIP_MARKUP", "0").lower() in ("1", "true", "yes") # Optional debug logging to stderr (set WEATHER_DEBUG=1 to enable) DEBUG = os.getenv("WEATHER_DEBUG", "0").lower() not in ("0", "false", "no") -- cgit v1.2.3 From 94e1d96814cdaf41da1f4129f6a01447bd771af6 Mon Sep 17 00:00:00 2001 From: prabinpanta0 Date: Mon, 27 Oct 2025 11:52:04 +0545 Subject: config(hypr): preserve fetched/cached place and stop preemptive MANUAL/ENV override Use the exact place value returned from fetch_place or stored in the API cache when writing/reading and when building output. Remove the early coalescing with MANUAL_PLACE/ENV_PLACE so cached/fetched place strings (including empty/None) are preserved and final selection is handled centrally by build_place_str. --- config/hypr/UserScripts/Weather.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) (limited to 'config/hypr/UserScripts') diff --git a/config/hypr/UserScripts/Weather.py b/config/hypr/UserScripts/Weather.py index ee861ebb..c7638599 100755 --- a/config/hypr/UserScripts/Weather.py +++ b/config/hypr/UserScripts/Weather.py @@ -776,9 +776,8 @@ 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 - place_effective = MANUAL_PLACE or ENV_PLACE or cached_place try: - return build_output(Location(lat, lon, place_effective), forecast, aqi) + return build_output(Location(lat, lon, cached_place), forecast, aqi) except Exception as e: print(f"Cached data build failed, refetching: {e}", file=sys.stderr) return None @@ -788,9 +787,9 @@ def fetch_fresh_weather(lat: float, lon: float) -> Optional[Tuple[Dict[str, str] try: forecast = fetch_open_meteo(lat, lon) aqi = fetch_aqi(lat, lon) - place_effective = MANUAL_PLACE or ENV_PLACE or fetch_place(lat, lon) - write_api_cache({"forecast": forecast, "aqi": aqi, "place": place_effective}) - return build_output(Location(lat, lon, place_effective), forecast, aqi) + place = fetch_place(lat, lon) + write_api_cache({"forecast": forecast, "aqi": aqi, "place": place}) + return build_output(Location(lat, lon, place), forecast, aqi) except Exception as e: print(f"Open-Meteo fetch failed: {e}", file=sys.stderr) return None @@ -804,10 +803,9 @@ def try_stale_weather(lat: float, lon: float) -> Optional[Tuple[Dict[str, str], stale_dict = ensure_dict(stale) place_val = stale_dict.get("place") place = place_val if isinstance(place_val, str) else None - place_effective = MANUAL_PLACE or ENV_PLACE or place forecast = cast(Optional[Dict[str, Any]], stale_dict.get("forecast")) aqi = cast(Optional[Dict[str, Any]], stale_dict.get("aqi")) - return build_output(Location(lat, lon, place_effective), forecast, aqi) + return build_output(Location(lat, lon, place), forecast, aqi) except Exception as e2: print(f"Failed to use stale cache: {e2}", file=sys.stderr) return None -- cgit v1.2.3 From 2d4f945c6ffb850e21d3f2d24dc25087a207068d Mon Sep 17 00:00:00 2001 From: prabinpanta0 Date: Mon, 27 Oct 2025 12:22:12 +0545 Subject: config(hypr): preserve valid zero values for is_day and weather_code by using explicit None checks --- config/hypr/UserScripts/Weather.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'config/hypr/UserScripts') diff --git a/config/hypr/UserScripts/Weather.py b/config/hypr/UserScripts/Weather.py index c7638599..4c64221f 100755 --- a/config/hypr/UserScripts/Weather.py +++ b/config/hypr/UserScripts/Weather.py @@ -595,9 +595,11 @@ def build_weather_strings(cur: JSONDict, cur_units: JSONDict, daily: JSONDict, d feels_str = f"Feels like {int(round(feels_val))}{feels_unit}" if feels_val is not None else "" is_day_val = cur.get("is_day") - is_day = coerce_int(is_day_val) or 1 + is_day_int = coerce_int(is_day_val) + is_day = is_day_int if is_day_int is not None else 1 weather_code_val = cur.get("weather_code") - code = coerce_int(weather_code_val) or -1 + code_int = coerce_int(weather_code_val) + code = code_int if code_int is not None else -1 icon = wmo_to_icon(code, is_day) status = wmo_to_status(code) -- cgit v1.2.3 From 1ecc4dd6201aba5c0b0575c7b2d89bf9ff95d6af Mon Sep 17 00:00:00 2001 From: prabinpanta0 Date: Tue, 28 Oct 2025 16:36:20 +0545 Subject: feat(weather): prefer structured wttr.in format, harden cache and icons - Use wttr.in structured format first (%l, %C, %t) for stable 3-line parsing - Fallback to individual field requests when combined output omits temperature - Keep ASCII fallback as last resort; best-effort extraction of loc/cond/temp - Read/write cache via mapfile to avoid word splitting; validate temp line - Normalize condition string; extend mapping and add substring icon heuristics - Remove accidental double JSON print in default-case; ensure single output --- config/hypr/UserScripts/Weather.sh | 185 +++++++++++++++++++++++++++++++++++-- 1 file changed, 176 insertions(+), 9 deletions(-) (limited to 'config/hypr/UserScripts') diff --git a/config/hypr/UserScripts/Weather.sh b/config/hypr/UserScripts/Weather.sh index 0540e51d..deba2d41 100755 --- a/config/hypr/UserScripts/Weather.sh +++ b/config/hypr/UserScripts/Weather.sh @@ -1,15 +1,182 @@ #!/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 + + +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' + +cacheage=$(($(date +%s) - $(stat -c '%Y' "$cachedir/$cachefile"))) +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/${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/${city}?format=%25l&lang=en" 2>/dev/null || true) + cond_only=$(curl -fsS "https://wttr.in/${city}?format=%25C&lang=en" 2>/dev/null || true) + temp_only=$(curl -fsS "https://wttr.in/${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/${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/${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/${city}?format=%25l&lang=en" 2>/dev/null || true) + cond_only=$(curl -fsS "https://wttr.in/${city}?format=%25C&lang=en" 2>/dev/null || true) + temp_only=$(curl -fsS "https://wttr.in/${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 + +# 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 -python3 "$(dirname "$0")/Weather.py" "$@" -exit_code=$? -if [ "$exit_code" -ne 0 ]; then - echo "Failed to run Weather.py" >&2 +#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 -exit "$exit_code" \ No newline at end of file + +cond_disp=$(echo "${weather[1]}" | sed -E 's/^\s+//; s/\s+$//') +echo -e "{\"text\":\""$temperature" "$condition"\", \"alt\":\""${weather[0]}"\", \"tooltip\":\""${weather[0]}: $temperature $cond_disp"\"}" + +cached_weather="๏‹‰ $temperature \n$condition ${weather[1]}" + +echo -e $cached_weather > "$HOME/.cache/.weather_cache" \ No newline at end of file -- cgit v1.2.3 From c93aba52fb00e23b6902581c0837525334cbe837 Mon Sep 17 00:00:00 2001 From: prabinpanta0 Date: Tue, 28 Oct 2025 16:36:50 +0545 Subject: feat(weather): forward geocode MANUAL_PLACE/WEATHER_PLACE to lat/lon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Openโ€‘Meteo geocoding for place names; use when env coords arenโ€™t set - Adjust get_coords precedence: env coords > manual/env place > cache > IP - Guard cache reuse by verifying cached forecast lat/lon matches requested - Preserve tooltip place display; no changes to JSON schema/output fields --- config/hypr/UserScripts/Weather.py | 57 +++++++++++++++++++++++++++++++++----- 1 file changed, 50 insertions(+), 7 deletions(-) (limited to 'config/hypr/UserScripts') 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: -- cgit v1.2.3 From 2b750637da39e4418132c70c2b53c4070a813a84 Mon Sep 17 00:00:00 2001 From: prabinpanta0 Date: Tue, 28 Oct 2025 16:41:43 +0545 Subject: feat(weatherWrap): add Pythonโ€‘first entrypoint with Bash fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Prefer Weather.py (Openโ€‘Meteo) for reliable data and caching - Fallback to Weather.sh if Python is missing or Weather.py exits nonโ€‘zero - Pass through all CLI args; invoke Bash explicitly for consistent execution - Helps external callers (e.g., lock hooks) refresh ~/.cache/.weather_cache robustly --- config/hypr/UserConfigs/Startup_Apps.conf | 2 +- config/hypr/UserScripts/weatherWrap.sh | 33 +++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100755 config/hypr/UserScripts/weatherWrap.sh (limited to 'config/hypr/UserScripts') diff --git a/config/hypr/UserConfigs/Startup_Apps.conf b/config/hypr/UserConfigs/Startup_Apps.conf index 976208e9..c40d0b0a 100644 --- a/config/hypr/UserConfigs/Startup_Apps.conf +++ b/config/hypr/UserConfigs/Startup_Apps.conf @@ -48,7 +48,7 @@ exec-once = $UserScripts/RainbowBorders.sh exec-once = hypridle # Weather script to populate cache on startup -exec-once = $UserScripts/Weather.sh +exec-once = $UserScripts/weatherWrap.sh # Here are list of features available but disabled by default 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 -- cgit v1.2.3 From 2c32d1cc6ca9397db2db94bb50ad5238fa75d0c9 Mon Sep 17 00:00:00 2001 From: prabinpanta0 Date: Tue, 28 Oct 2025 17:13:29 +0545 Subject: feat(weather): URL-encode city, harden file handling, and produce safe JSON/cache output - URL-encode city (python3 / jq / minimal sed fallback) and use encoded_city for all wttr.in calls - Quote cachedir/cachefile usages and introduce a file variable for clarity - Use portable stat (GNU/BSD fallback) and compute cache age robustly - Replace brittle echo JSON with json_escape + printf to safely escape output - Ensure .weather_cache is written as a two-line file with a proper trailing newline --- config/hypr/UserScripts/Weather.sh | 61 +++++++++++++++++++++++++++----------- 1 file changed, 44 insertions(+), 17 deletions(-) (limited to 'config/hypr/UserScripts') diff --git a/config/hypr/UserScripts/Weather.sh b/config/hypr/UserScripts/Weather.sh index deba2d41..b69662e6 100755 --- a/config/hypr/UserScripts/Weather.sh +++ b/config/hypr/UserScripts/Weather.sh @@ -12,6 +12,18 @@ if [ -z "$city" ]; then 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 + + cachedir="$HOME/.cache/rbn" # Include city and arg in cache key so changing city invalidates old cache cache_key="${city}_${1}" @@ -19,12 +31,12 @@ cache_key="${city}_${1}" safe_key=$(printf '%s' "$cache_key" | tr -c '[:alnum:]_-' '_') cachefile=${0##*/}-$safe_key -if [ ! -d $cachedir ]; then - mkdir -p $cachedir +if [ ! -d "$cachedir" ]; then + mkdir -p "$cachedir" fi -if [ ! -f $cachedir/$cachefile ]; then - touch $cachedir/$cachefile +if [ ! -f "$cachedir/$cachefile" ]; then + touch "$cachedir/$cachefile" fi # Save current IFS @@ -32,26 +44,32 @@ SAVEIFS=$IFS # Change IFS to new line. IFS=$'\n' -cacheage=$(($(date +%s) - $(stat -c '%Y' "$cachedir/$cachefile"))) +file="$cachedir/$cachefile" +# Portable file mtime retrieval (GNU/BSD): +# - GNU: stat -c %Y +# - BSD/macOS: stat -f %m +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/${city}?format=%25l%0A%25C%0A%25t&lang=en" 2>/dev/null || true) + 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/${city}?format=%25l&lang=en" 2>/dev/null || true) - cond_only=$(curl -fsS "https://wttr.in/${city}?format=%25C&lang=en" 2>/dev/null || true) - temp_only=$(curl -fsS "https://wttr.in/${city}?format=%25t" 2>/dev/null || true) + 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/${city}?1" + 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/^.*: *//') @@ -75,16 +93,16 @@ 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/${city}?format=%25l%0A%25C%0A%25t&lang=en" 2>/dev/null || true) + 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/${city}?format=%25l&lang=en" 2>/dev/null || true) - cond_only=$(curl -fsS "https://wttr.in/${city}?format=%25C&lang=en" 2>/dev/null || true) - temp_only=$(curl -fsS "https://wttr.in/${city}?format=%25t" 2>/dev/null || true) + 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" @@ -175,8 +193,17 @@ if [ -z "$temperature" ]; then fi cond_disp=$(echo "${weather[1]}" | sed -E 's/^\s+//; s/\s+$//') -echo -e "{\"text\":\""$temperature" "$condition"\", \"alt\":\""${weather[0]}"\", \"tooltip\":\""${weather[0]}: $temperature $cond_disp"\"}" -cached_weather="๏‹‰ $temperature \n$condition ${weather[1]}" +# 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" -echo -e $cached_weather > "$HOME/.cache/.weather_cache" \ No newline at end of file +# 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 -- cgit v1.2.3 From 0914a7105188ae2f606f6056d4584f85b951599d Mon Sep 17 00:00:00 2001 From: prabinpanta0 Date: Tue, 28 Oct 2025 18:08:17 +0545 Subject: feat(weather): prioritize MANUAL_PLACE for geocoding and skip reverse lookup when set - Treat MANUAL_PLACE as highest priority: forward-geocode it first and return coords if found - Make ENV_PLACE a separate forward-geocode step (after explicit env coords) - When fetching fresh weather, use MANUAL_PLACE directly as the place string to avoid an unnecessary reverse geocode call - Update comment numbering to reflect the new priority order --- config/hypr/UserScripts/Weather.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) (limited to 'config/hypr/UserScripts') diff --git a/config/hypr/UserScripts/Weather.py b/config/hypr/UserScripts/Weather.py index 1a7380cf..a9a826e1 100755 --- a/config/hypr/UserScripts/Weather.py +++ b/config/hypr/UserScripts/Weather.py @@ -354,29 +354,36 @@ def get_coords_from_place_name(name: str) -> Optional[Tuple[float, float]]: def get_coords() -> Tuple[float, float]: - # 1) Explicit env coordinates + # 1) Forward geocode from MANUAL_PLACE first (highest priority) + if MANUAL_PLACE: + place_name = MANUAL_PLACE.strip() + coords = get_coords_from_place_name(place_name) + if coords: + return coords + + # 2) Explicit env coordinates coords = get_coords_from_env() if coords: return coords - # 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: + # 3) Forward geocode from ENV_PLACE + if ENV_PLACE: + place_name = ENV_PLACE.strip() coords = get_coords_from_place_name(place_name) if coords: return coords - # 3) Try cached coordinates + # 4) Try cached coordinates coords = get_coords_from_cache() if coords: return coords - # 4) IP-based geolocation + # 5) IP-based geolocation coords = get_coords_from_ipwho() or get_coords_from_ipapi() or get_coords_from_ipinfo() if coords: return coords - # 5) Last resort + # 6) Last resort print("IP geolocation failed: no providers succeeded", file=sys.stderr) return 0.0, 0.0 @@ -832,7 +839,8 @@ def fetch_fresh_weather(lat: float, lon: float) -> Optional[Tuple[Dict[str, str] try: forecast = fetch_open_meteo(lat, lon) aqi = fetch_aqi(lat, lon) - place = fetch_place(lat, lon) + # If MANUAL_PLACE is set, don't reverse geocode - use the manual place instead + place = MANUAL_PLACE if MANUAL_PLACE else fetch_place(lat, lon) write_api_cache({"forecast": forecast, "aqi": aqi, "place": place}) return build_output(Location(lat, lon, place), forecast, aqi) except Exception as e: -- cgit v1.2.3 From 2c1ce157c97ae690e1dfb8087a3bfa808ac57c18 Mon Sep 17 00:00:00 2001 From: brockar Date: Tue, 28 Oct 2025 18:08:12 -0300 Subject: weatherWrap.sh -> WeatherWrap.sh --- config/hypr/UserConfigs/Startup_Apps.conf | 2 +- config/hypr/UserScripts/WeatherWrap.sh | 33 +++++++++++++++++++++++++++++++ config/hypr/UserScripts/weatherWrap.sh | 33 ------------------------------- config/hypr/scripts/LockScreen.sh | 7 ++++--- 4 files changed, 38 insertions(+), 37 deletions(-) create mode 100755 config/hypr/UserScripts/WeatherWrap.sh delete mode 100755 config/hypr/UserScripts/weatherWrap.sh (limited to 'config/hypr/UserScripts') diff --git a/config/hypr/UserConfigs/Startup_Apps.conf b/config/hypr/UserConfigs/Startup_Apps.conf index c40d0b0a..2f5c7ae7 100644 --- a/config/hypr/UserConfigs/Startup_Apps.conf +++ b/config/hypr/UserConfigs/Startup_Apps.conf @@ -48,7 +48,7 @@ exec-once = $UserScripts/RainbowBorders.sh exec-once = hypridle # Weather script to populate cache on startup -exec-once = $UserScripts/weatherWrap.sh +exec-once = $UserScripts/WeatherWrap.sh # Here are list of features available but disabled by default 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 diff --git a/config/hypr/UserScripts/weatherWrap.sh b/config/hypr/UserScripts/weatherWrap.sh deleted file mode 100755 index 4c9a16dc..00000000 --- a/config/hypr/UserScripts/weatherWrap.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/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 diff --git a/config/hypr/scripts/LockScreen.sh b/config/hypr/scripts/LockScreen.sh index 17239406..e61490cd 100755 --- a/config/hypr/scripts/LockScreen.sh +++ b/config/hypr/scripts/LockScreen.sh @@ -2,9 +2,10 @@ # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ ## # For Hyprlock -#pidof hyprlock || hyprlock -q +#pidof hyprlock || hyprlock -q # Ensure weather cache is up-to-date before locking (Waybar/lockscreen readers) -bash "$HOME/.secret/Hyprland-Dots/config/hypr/UserScripts/weatherWrap.sh" >/dev/null 2>&1 +bash "$HOME/.config/hypr/UserScripts/WeatherWrap.sh" >/dev/null 2>&1 + +loginctl lock-session -loginctl lock-session \ No newline at end of file -- cgit v1.2.3 From 758b466c5f2030188d46511b20ad39c6d8b46a0d Mon Sep 17 00:00:00 2001 From: Don Williams Date: Tue, 28 Oct 2025 17:30:59 -0400 Subject: Changed /usr/bin/bash to /usr/bin/env bash On branch development Your branch is up to date with 'origin/development'. Changes to be committed: modified: Distro-Hyprland.sh modified: config/hypr/UserScripts/RainbowBorders.sh modified: config/hypr/UserScripts/RofiBeats.sh modified: config/hypr/UserScripts/RofiCalc.sh modified: config/hypr/UserScripts/Tak0-Autodispatch.sh modified: config/hypr/UserScripts/WallpaperAutoChange.sh modified: config/hypr/UserScripts/WallpaperEffects.sh modified: config/hypr/UserScripts/WallpaperRandom.sh modified: config/hypr/UserScripts/WallpaperSelect.sh modified: config/hypr/UserScripts/Weather.sh modified: config/hypr/UserScripts/WeatherWrap.sh modified: config/hypr/UserScripts/ZshChangeTheme.sh new file: config/hypr/configs/Startup_Apps.conf new file: config/hypr/configs/WindowRules.conf modified: config/hypr/hyprland.conf modified: config/hypr/initial-boot.sh modified: config/hypr/scripts/AirplaneMode.sh modified: config/hypr/scripts/Animations.sh modified: config/hypr/scripts/Battery.sh modified: config/hypr/scripts/Brightness.sh modified: config/hypr/scripts/BrightnessKbd.sh modified: config/hypr/scripts/ChangeBlur.sh modified: config/hypr/scripts/ChangeLayout.sh modified: config/hypr/scripts/ClipManager.sh new file: config/hypr/scripts/ComposeHyprConfigs.sh modified: config/hypr/scripts/DarkLight.sh modified: config/hypr/scripts/Distro_update.sh modified: config/hypr/scripts/Dropterminal.sh modified: config/hypr/scripts/GameMode.sh modified: config/hypr/scripts/Hypridle.sh modified: config/hypr/scripts/KeyBinds.sh modified: config/hypr/scripts/KeyHints.sh modified: config/hypr/scripts/KillActiveProcess.sh modified: config/hypr/scripts/Kitty_themes.sh modified: config/hypr/scripts/KooLsDotsUpdate.sh modified: config/hypr/scripts/Kool_Quick_Settings.sh modified: config/hypr/scripts/LockScreen.sh modified: config/hypr/scripts/MediaCtrl.sh modified: config/hypr/scripts/MonitorProfiles.sh modified: config/hypr/scripts/Polkit-NixOS.sh modified: config/hypr/scripts/Polkit.sh modified: config/hypr/scripts/PortalHyprland.sh modified: config/hypr/scripts/Refresh.sh modified: config/hypr/scripts/RefreshNoWaybar.sh modified: config/hypr/scripts/RofiEmoji.sh modified: config/hypr/scripts/RofiSearch.sh modified: config/hypr/scripts/RofiThemeSelector-modified.sh modified: config/hypr/scripts/RofiThemeSelector.sh modified: config/hypr/scripts/ScreenShot.sh modified: config/hypr/scripts/Sounds.sh modified: config/hypr/scripts/SwitchKeyboardLayout.sh modified: config/hypr/scripts/Tak0-Autodispatch.sh modified: config/hypr/scripts/TouchPad.sh modified: config/hypr/scripts/Volume.sh modified: config/hypr/scripts/WallustSwww.sh modified: config/hypr/scripts/WaybarLayout.sh modified: config/hypr/scripts/WaybarScripts.sh modified: config/hypr/scripts/WaybarStyles.sh modified: config/hypr/scripts/Wlogout.sh modified: config/hypr/scripts/sddm_wallpaper.sh modified: copy.sh modified: release.sh modified: upgrade.sh --- Distro-Hyprland.sh | 2 +- config/hypr/UserScripts/RainbowBorders.sh | 2 +- config/hypr/UserScripts/RofiBeats.sh | 2 +- config/hypr/UserScripts/RofiCalc.sh | 2 +- config/hypr/UserScripts/Tak0-Autodispatch.sh | 2 +- config/hypr/UserScripts/WallpaperAutoChange.sh | 2 +- config/hypr/UserScripts/WallpaperEffects.sh | 2 +- config/hypr/UserScripts/WallpaperRandom.sh | 2 +- config/hypr/UserScripts/WallpaperSelect.sh | 2 +- config/hypr/UserScripts/Weather.sh | 2 +- config/hypr/UserScripts/WeatherWrap.sh | 2 +- config/hypr/UserScripts/ZshChangeTheme.sh | 2 +- config/hypr/configs/Startup_Apps.conf | 58 ++++++ config/hypr/configs/WindowRules.conf | 235 ++++++++++++++++++++++ config/hypr/hyprland.conf | 4 +- config/hypr/initial-boot.sh | 2 +- config/hypr/scripts/AirplaneMode.sh | 2 +- config/hypr/scripts/Animations.sh | 2 +- config/hypr/scripts/Battery.sh | 2 +- config/hypr/scripts/Brightness.sh | 2 +- config/hypr/scripts/BrightnessKbd.sh | 2 +- config/hypr/scripts/ChangeBlur.sh | 2 +- config/hypr/scripts/ChangeLayout.sh | 2 +- config/hypr/scripts/ClipManager.sh | 2 +- config/hypr/scripts/ComposeHyprConfigs.sh | 109 ++++++++++ config/hypr/scripts/DarkLight.sh | 2 +- config/hypr/scripts/Distro_update.sh | 2 +- config/hypr/scripts/Dropterminal.sh | 2 +- config/hypr/scripts/GameMode.sh | 2 +- config/hypr/scripts/Hypridle.sh | 2 +- config/hypr/scripts/KeyBinds.sh | 2 +- config/hypr/scripts/KeyHints.sh | 2 +- config/hypr/scripts/KillActiveProcess.sh | 2 +- config/hypr/scripts/Kitty_themes.sh | 2 +- config/hypr/scripts/KooLsDotsUpdate.sh | 2 +- config/hypr/scripts/Kool_Quick_Settings.sh | 2 +- config/hypr/scripts/LockScreen.sh | 2 +- config/hypr/scripts/MediaCtrl.sh | 2 +- config/hypr/scripts/MonitorProfiles.sh | 2 +- config/hypr/scripts/Polkit-NixOS.sh | 2 +- config/hypr/scripts/Polkit.sh | 2 +- config/hypr/scripts/PortalHyprland.sh | 2 +- config/hypr/scripts/Refresh.sh | 2 +- config/hypr/scripts/RefreshNoWaybar.sh | 2 +- config/hypr/scripts/RofiEmoji.sh | 2 +- config/hypr/scripts/RofiSearch.sh | 2 +- config/hypr/scripts/RofiThemeSelector-modified.sh | 2 +- config/hypr/scripts/RofiThemeSelector.sh | 2 +- config/hypr/scripts/ScreenShot.sh | 2 +- config/hypr/scripts/Sounds.sh | 2 +- config/hypr/scripts/SwitchKeyboardLayout.sh | 2 +- config/hypr/scripts/Tak0-Autodispatch.sh | 2 +- config/hypr/scripts/TouchPad.sh | 2 +- config/hypr/scripts/Volume.sh | 2 +- config/hypr/scripts/WallustSwww.sh | 2 +- config/hypr/scripts/WaybarLayout.sh | 2 +- config/hypr/scripts/WaybarScripts.sh | 2 +- config/hypr/scripts/WaybarStyles.sh | 2 +- config/hypr/scripts/Wlogout.sh | 2 +- config/hypr/scripts/sddm_wallpaper.sh | 2 +- copy.sh | 84 +++++++- release.sh | 2 +- upgrade.sh | 9 +- 63 files changed, 544 insertions(+), 69 deletions(-) create mode 100644 config/hypr/configs/Startup_Apps.conf create mode 100644 config/hypr/configs/WindowRules.conf create mode 100644 config/hypr/scripts/ComposeHyprConfigs.sh (limited to 'config/hypr/UserScripts') diff --git a/Distro-Hyprland.sh b/Distro-Hyprland.sh index ff88325f..99a1fc34 100755 --- a/Distro-Hyprland.sh +++ b/Distro-Hyprland.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # https://github.com/JaKooLit # Script design to clone the Distro-Hyprland install scripts diff --git a/config/hypr/UserScripts/RainbowBorders.sh b/config/hypr/UserScripts/RainbowBorders.sh index cc1419fb..0a7fd721 100755 --- a/config/hypr/UserScripts/RainbowBorders.sh +++ b/config/hypr/UserScripts/RainbowBorders.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ ## # for rainbow borders animation diff --git a/config/hypr/UserScripts/RofiBeats.sh b/config/hypr/UserScripts/RofiBeats.sh index 1cddce09..ca566019 100755 --- a/config/hypr/UserScripts/RofiBeats.sh +++ b/config/hypr/UserScripts/RofiBeats.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ ## # For Rofi Beats to play online Music or Locally saved media files diff --git a/config/hypr/UserScripts/RofiCalc.sh b/config/hypr/UserScripts/RofiCalc.sh index 4b3b8b69..b72d5f3e 100755 --- a/config/hypr/UserScripts/RofiCalc.sh +++ b/config/hypr/UserScripts/RofiCalc.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ # /* Calculator (using qalculate) and rofi */ # /* Submitted by: https://github.com/JosephArmas */ diff --git a/config/hypr/UserScripts/Tak0-Autodispatch.sh b/config/hypr/UserScripts/Tak0-Autodispatch.sh index a1f72129..114a3e8e 100755 --- a/config/hypr/UserScripts/Tak0-Autodispatch.sh +++ b/config/hypr/UserScripts/Tak0-Autodispatch.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # USAGE / ะ†ะะกะขะ ะฃะšะฆะ†ะฏ: # 1) Run from terminal: # ./dispatch.sh diff --git a/config/hypr/UserScripts/WallpaperAutoChange.sh b/config/hypr/UserScripts/WallpaperAutoChange.sh index a6d2cedd..6d8e8735 100755 --- a/config/hypr/UserScripts/WallpaperAutoChange.sh +++ b/config/hypr/UserScripts/WallpaperAutoChange.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ ## # source https://wiki.archlinux.org/title/Hyprland#Using_a_script_to_change_wallpaper_every_X_minutes diff --git a/config/hypr/UserScripts/WallpaperEffects.sh b/config/hypr/UserScripts/WallpaperEffects.sh index ac8fc0e8..89577efa 100755 --- a/config/hypr/UserScripts/WallpaperEffects.sh +++ b/config/hypr/UserScripts/WallpaperEffects.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ # # Wallpaper Effects using ImageMagick (SUPER SHIFT W) diff --git a/config/hypr/UserScripts/WallpaperRandom.sh b/config/hypr/UserScripts/WallpaperRandom.sh index 79396508..654d4bd3 100755 --- a/config/hypr/UserScripts/WallpaperRandom.sh +++ b/config/hypr/UserScripts/WallpaperRandom.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ ## # Script for Random Wallpaper ( CTRL ALT W) diff --git a/config/hypr/UserScripts/WallpaperSelect.sh b/config/hypr/UserScripts/WallpaperSelect.sh index 466832ba..9e51125f 100755 --- a/config/hypr/UserScripts/WallpaperSelect.sh +++ b/config/hypr/UserScripts/WallpaperSelect.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ # This script for selecting wallpapers (SUPER W) diff --git a/config/hypr/UserScripts/Weather.sh b/config/hypr/UserScripts/Weather.sh index b69662e6..ac9abc13 100755 --- a/config/hypr/UserScripts/Weather.sh +++ b/config/hypr/UserScripts/Weather.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ ## # weather info from wttr. https://github.com/chubin/wttr.in # Remember to add city diff --git a/config/hypr/UserScripts/WeatherWrap.sh b/config/hypr/UserScripts/WeatherWrap.sh index 4c9a16dc..10c125dc 100755 --- a/config/hypr/UserScripts/WeatherWrap.sh +++ b/config/hypr/UserScripts/WeatherWrap.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ ## # Weather entrypoint: prefer Python (Openโ€‘Meteo), fallback to legacy Bash (wttr.in) diff --git a/config/hypr/UserScripts/ZshChangeTheme.sh b/config/hypr/UserScripts/ZshChangeTheme.sh index cffaf5cb..690f0f13 100755 --- a/config/hypr/UserScripts/ZshChangeTheme.sh +++ b/config/hypr/UserScripts/ZshChangeTheme.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ ## # Script for Oh my ZSH theme ( CTRL SHIFT O) diff --git a/config/hypr/configs/Startup_Apps.conf b/config/hypr/configs/Startup_Apps.conf new file mode 100644 index 00000000..d952e65b --- /dev/null +++ b/config/hypr/configs/Startup_Apps.conf @@ -0,0 +1,58 @@ +# /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ # +# Commands and Apps to be executed at launch (vendor defaults) + +$scriptsDir = $HOME/.config/hypr/scripts +$UserScripts = $HOME/.config/hypr/UserScripts + +$wallDIR=$HOME/Pictures/wallpapers +$lock = $scriptsDir/LockScreen.sh +$SwwwRandom = $UserScripts/WallpaperAutoChange.sh +$livewallpaper="" + +# wallpaper stuff +exec-once = swww-daemon --format xrgb +#exec-once = mpvpaper '*' -o "load-scripts=no no-audio --loop" $livewallpaper + +# wallpaper random +#exec-once = $SwwwRandom $wallDIR # random wallpaper switcher every 30 minutes + +# Startup +exec-once = dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP +exec-once = systemctl --user import-environment WAYLAND_DISPLAY XDG_CURRENT_DESKTOP + +# Initialize Drop Down terminal - See Bug#810 https://github.com/JaKooLit/Hyprland-Dots/issues/810#issuecomment-3351947644 +exec-once = $HOME/.config/hypr/scripts/Dropterminal.sh kitty & + + +# Polkit (Polkit Gnome / KDE) +exec-once = $scriptsDir/Polkit.sh + +# starup apps +exec-once = nm-applet --indicator +exec-once = nm-tray # For ubuntu +exec-once = swaync +#exec-once = ags +#exec-once = blueman-applet +#exec-once = rog-control-center +exec-once = waybar +exec-once = qs # quickshell AGS Desktop Overview alternative + +#clipboard manager +exec-once = wl-paste --type text --watch cliphist store +exec-once = wl-paste --type image --watch cliphist store + +# Rainbow borders +exec-once = $UserScripts/RainbowBorders.sh + +# Starting hypridle to start hyprlock +exec-once = hypridle + + +# Here are list of features available but disabled by default +# exec-once = swww-daemon --format xrgb && swww img $HOME/Pictures/wallpapers/mecha-nostalgia.png # persistent wallpaper + +#gnome polkit for nixos +#exec-once = $scriptsDir/Polkit-NixOS.sh + +# xdg-desktop-portal-hyprland (should be auto starting. However, you can force to start) +#exec-once = $scriptsDir/PortalHyprland.sh \ No newline at end of file diff --git a/config/hypr/configs/WindowRules.conf b/config/hypr/configs/WindowRules.conf new file mode 100644 index 00000000..f02c5d29 --- /dev/null +++ b/config/hypr/configs/WindowRules.conf @@ -0,0 +1,235 @@ +# /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ # +# Vendor defaults for window rules and layerrules +# See https://wiki.hyprland.org/Configuring/Window-Rules/ for more + +# NOTES: This is only for Hyprland > 0.48 + +# note for ja: This should NOT be implemented on Debian and Ubuntu + +# windowrule - tags - add apps under appropriate tag to use the same settings +# browser tags +windowrule = tag +browser, class:^([Ff]irefox|org.mozilla.firefox|[Ff]irefox-esr|[Ff]irefox-bin)$ +windowrule = tag +browser, class:^([Gg]oogle-chrome(-beta|-dev|-unstable)?)$ +windowrule = tag +browser, class:^(chrome-.+-Default)$ # Chrome PWAs +windowrule = tag +browser, class:^([Cc]hromium)$ +windowrule = tag +browser, class:^([Mm]icrosoft-edge(-stable|-beta|-dev|-unstable))$ +windowrule = tag +browser, class:^(Brave-browser(-beta|-dev|-unstable)?)$ +windowrule = tag +browser, class:^([Tt]horium-browser|[Cc]achy-browser)$ +windowrule = tag +browser, class:^(zen-alpha|zen)$ + +# notif tags +windowrule = tag +notif, class:^(swaync-control-center|swaync-notification-window|swaync-client|class)$ + +# KooL settings tag +windowrule = tag +KooL_Cheat, title:^(KooL Quick Cheat Sheet)$ +windowrule = tag +KooL_Settings, title:^(KooL Hyprland Settings)$ +windowrule = tag +KooL-Settings, class:^(nwg-displays|nwg-look)$ + +# terminal tags +windowrule = tag +terminal, class:^(Alacritty|kitty|kitty-dropterm)$ + +# email tags +windowrule = tag +email, class:^([Tt]hunderbird|org.gnome.Evolution)$ +windowrule = tag +email, class:^(eu.betterbird.Betterbird)$ + +# project tags +windowrule = tag +projects, class:^(codium|codium-url-handler|VSCodium)$ +windowrule = tag +projects, class:^(VSCode|code-url-handler)$ +windowrule = tag +projects, class:^(jetbrains-.+)$ # JetBrains IDEs + +# screenshare tags +windowrule = tag +screenshare, class:^(com.obsproject.Studio)$ + +# IM tags +windowrule = tag +im, class:^([Dd]iscord|[Ww]ebCord|[Vv]esktop)$ +windowrule = tag +im, class:^([Ff]erdium)$ +windowrule = tag +im, class:^([Ww]hatsapp-for-linux)$ +windowrule = tag +im, class:^(ZapZap|com.rtosta.zapzap)$ +windowrule = tag +im, class:^(org.telegram.desktop|io.github.tdesktop_x64.TDesktop)$ +windowrule = tag +im, class:^(teams-for-linux)$ +windowrule = tag +im, class:^(im.riot.Riot|Element)$ # Element Matrix client + +# game tags +windowrule = tag +games, class:^(gamescope)$ +windowrule = tag +games, class:^(steam_app_\d+)$ + +# gamestore tags +windowrule = tag +gamestore, class:^([Ss]team)$ +windowrule = tag +gamestore, title:^([Ll]utris)$ +windowrule = tag +gamestore, class:^(com.heroicgameslauncher.hgl)$ + +# file-manager tags +windowrule = tag +file-manager, class:^([Tt]hunar|org.gnome.Nautilus|[Pp]cmanfm-qt)$ +windowrule = tag +file-manager, class:^(app.drey.Warp)$ + +# wallpaper tags +windowrule = tag +wallpaper, class:^([Ww]aytrogen)$ + +# multimedia tags +windowrule = tag +multimedia, class:^([Aa]udacious)$ + +# multimedia-video tags +windowrule = tag +multimedia_video, class:^([Mm]pv|vlc)$ + +# settings tags +windowrule = tag +settings, title:^(ROG Control)$ +windowrule = tag +settings, class:^(wihotspot(-gui)?)$ # wifi hotspot +windowrule = tag +settings, class:^([Bb]aobab|org.gnome.[Bb]aobab)$ # Disk usage analyzer +windowrule = tag +settings, class:^(gnome-disks|wihotspot(-gui)?)$ +windowrule = tag +settings, title:(Kvantum Manager) +windowrule = tag +settings, class:^(file-roller|org.gnome.FileRoller)$ # archive manager +windowrule = tag +settings, class:^(nm-applet|nm-connection-editor|blueman-manager)$ +windowrule = tag +settings, class:^(pavucontrol|org.pulseaudio.pavucontrol|com.saivert.pwvucontrol)$ +windowrule = tag +settings, class:^(qt5ct|qt6ct|[Yy]ad)$ +windowrule = tag +settings, class:(xdg-desktop-portal-gtk) +windowrule = tag +settings, class:^(org.kde.polkit-kde-authentication-agent-1)$ +windowrule = tag +settings, class:^([Rr]ofi)$ + +# viewer tags +windowrule = tag +viewer, class:^(gnome-system-monitor|org.gnome.SystemMonitor|io.missioncenter.MissionCenter)$ # system monitor +windowrule = tag +viewer, class:^(evince)$ # document viewer +windowrule = tag +viewer, class:^(eog|org.gnome.Loupe)$ # image viewer + +# Some special override rules +windowrule = noblur, tag:multimedia_video* +windowrule = opacity 1.0, tag:multimedia_video* + +# POSITION +# windowrule = center,floating:1 # warning, it cause even the menu to float and center. +windowrule = center, tag:KooL_Cheat* +windowrule = center, class:([Tt]hunar), title:negative:(.*[Tt]hunar.*) +windowrule = center, title:^(ROG Control)$ +windowrule = center, tag:KooL-Settings* +windowrule = center, title:^(Keybindings)$ +windowrule = center, class:^(pavucontrol|org.pulseaudio.pavucontrol|com.saivert.pwvucontrol)$ +windowrule = center, class:^([Ww]hatsapp-for-linux|ZapZap|com.rtosta.zapzap)$ +windowrule = center, class:^([Ff]erdium)$ +windowrule = move 72% 7%,title:^(Picture-in-Picture)$ +#windowrule = move 72% 7%,title:^(Firefox)$ + +# windowrule to avoid idle for fullscreen apps +#windowrule = idleinhibit fullscreen, class:^(*)$ +#windowrule = idleinhibit fullscreen, title:^(*)$ +windowrule = idleinhibit fullscreen, fullscreen:1 + +# windowrule move to workspace +#windowrule = workspace 1, tag:email* +#windowrule = workspace 2, tag:browser* +#windowrule = workspace 3, class:^([Tt]hunar)$ +#windowrule = workspace 3, tag:projects* +#windowrule = workspace 5, tag:gamestore* +#windowrule = workspace 7, tag:im* +#windowrule = workspace 8, tag:games* + +# windowrule move to workspace (silent) +#windowrule = workspace 4 silent, tag:screenshare* +#windowrule = workspace 6 silent, class:^(virt-manager)$ +#windowrule = workspace 6 silent, class:^(.virt-manager-wrapped)$ +#windowrule = workspace 9 silent, tag:multimedia* +# +# FLOAT +windowrule = float, tag:KooL_Cheat* +windowrule = float, tag:wallpaper* +windowrule = float, tag:settings* +windowrule = float, tag:viewer* +windowrule = float, tag:KooL-Settings* +windowrule = float, class:([Zz]oom|onedriver|onedriver-launcher)$ +windowrule = float, class:(org.gnome.Calculator), title:(Calculator) +windowrule = float, class:^(mpv|com.github.rafostar.Clapper)$ +windowrule = float, class:^([Qq]alculate-gtk)$ +#windowrule = float, class:^([Ww]hatsapp-for-linux|ZapZap|com.rtosta.zapzap)$ +windowrule = float, class:^([Ff]erdium)$ +windowrule = float, title:^(Picture-in-Picture)$ +#windowrule = float, title:^(Firefox)$ + +# windowrule - ######### float popups and dialogue ####### +windowrule = float, title:^(Authentication Required)$ +windowrule = center, title:^(Authentication Required)$ +windowrule = float, class:(codium|codium-url-handler|VSCodium), title:negative:(.*codium.*|.*VSCodium.*) +windowrule = float, class:^(com.heroicgameslauncher.hgl)$, title:negative:(Heroic Games Launcher) +windowrule = float, class:^([Ss]team)$, title:negative:^([Ss]team)$ +windowrule = float, class:([Tt]hunar), title:negative:(.*[Tt]hunar.*) + +windowrule = float, title:^(Add Folder to Workspace)$ +windowrule = size 70% 60%, title:^(Add Folder to Workspace)$ +windowrule = center, title:^(Add Folder to Workspace)$ + +windowrule = float, title:^(Save As)$ +windowrule = size 70% 60%, title:^(Save As)$ +windowrule = center, title:^(Save As)$ + +windowrule = float, initialTitle:(Open Files) +windowrule = size 70% 60%, initialTitle:(Open Files) + +windowrule = float, title:^(SDDM Background)$ #KooL's Dots YAD for setting SDDM background +windowrule = center, title:^(SDDM Background)$ #KooL's Dots YAD for setting SDDM background +windowrule = size 16% 12%, title:^(SDDM Background)$ #KooL's Dots YAD for setting SDDM background +# END of float popups and dialogue ####### + +# OPACITY +windowrule = opacity 0.99 0.8, tag:browser* +windowrule = opacity 0.9 0.8, tag:projects* +windowrule = opacity 0.94 0.86, tag:im* +windowrule = opacity 0.94 0.86, tag:multimedia* +windowrule = opacity 0.9 0.8, tag:file-manager* +windowrule = opacity 0.9 0.7, tag:terminal* +windowrule = opacity 0.8 0.7, tag:settings* +windowrule = opacity 0.82 0.75, tag:viewer* +windowrule = opacity 0.9 0.7, tag:wallpaper* +windowrule = opacity 0.8 0.7, class:^(gedit|org.gnome.TextEditor|mousepad)$ +windowrule = opacity 0.9 0.8, class:^(deluge)$ +windowrule = opacity 0.9 0.8, class:^(seahorse)$ # gnome-keyring gui +windowrule = opacity 0.95 0.75, title:^(Picture-in-Picture)$ +windowrule = opacity 0.9,class:^(code)$ + +# SIZE +windowrule = size 65% 90%, tag:KooL_Cheat* +windowrule = size 70% 70%, tag:wallpaper* +windowrule = size 70% 70%, tag:settings* +windowrule = size 60% 70%, class:^([Ww]hatsapp-for-linux|ZapZap|com.rtosta.zapzap)$ +windowrule = size 60% 70%, class:^([Ff]erdium)$ + +#windowrule = size 25% 25%, title:^(Picture-in-Picture)$ +#windowrule = size 25% 25%, title:^(Firefox)$ + +# PINNING +windowrule = pin, title:^(Picture-in-Picture)$ +#windowrule = pin,title:^(Firefox)$ + +# windowrule - extras +windowrule = keepaspectratio, title:^(Picture-in-Picture)$ + +# BLUR & FULLSCREEN +windowrule = noblur, tag:games* +windowrule = fullscreen, tag:games* + + +#This not gonna take the focus to the window that appears when hovering over some of the parts of the IntelliJ Products +windowrule = noinitialfocus, class:^(jetbrains-*) +windowrule = noinitialfocus, title:^(wind.*)$ + +#This will gonna make the VS Code bluer like other apps +windowrule = opacity 0.8,class:^(code)$ + +#windowrule = bordercolor rgb(EE4B55) rgb(880808), fullscreen:1 +#windowrule = bordercolor rgb(282737) rgb(1E1D2D), floating:1 +#windowrule = opacity 0.8 0.8, pinned:1 + +# LAYER RULES +layerrule = blur, rofi +layerrule = ignorezero, rofi +layerrule = blur, notifications +layerrule = ignorezero, notifications +layerrule = blur, quickshell:overview +layerrule = ignorezero, quickshell:overview +layerrule = ignorealpha 0.5, quickshell:overview + +#layerrule = ignorealpha 0.5, tag:notif* + +#layerrule = ignorezero, class:^([Rr]ofi)$ +#layerrule = blur, class:^([Rr]ofi)$ +#layerrule = unset,class:^([Rr]ofi)$ +#layerrule = ignorezero, + +#layerrule = ignorezero, overview +#layerrule = blur, overview \ No newline at end of file diff --git a/config/hypr/hyprland.conf b/config/hypr/hyprland.conf index 71f243e7..cd9fed2d 100644 --- a/config/hypr/hyprland.conf +++ b/config/hypr/hyprland.conf @@ -15,7 +15,9 @@ source=$configs/Keybinds.conf # Pre-configured keybinds # ## This is where you want to start tinkering $UserConfigs = $HOME/.config/hypr/UserConfigs # User Configs directory path -source= $UserConfigs/Startup_Apps.conf # put your start-up packages on this file +# Generated merges (vendor + user overlay with dedupe/disable) +source= $HOME/.config/hypr/generated/Startup_Apps.conf +source= $HOME/.config/hypr/generated/WindowRules.conf source= $UserConfigs/ENVariables.conf # Environment variables to load diff --git a/config/hypr/initial-boot.sh b/config/hypr/initial-boot.sh index 5b49cb6d..1313f104 100755 --- a/config/hypr/initial-boot.sh +++ b/config/hypr/initial-boot.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ # # A bash script designed to run only once dotfiles installed diff --git a/config/hypr/scripts/AirplaneMode.sh b/config/hypr/scripts/AirplaneMode.sh index 4379935d..548b9d6b 100755 --- a/config/hypr/scripts/AirplaneMode.sh +++ b/config/hypr/scripts/AirplaneMode.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ ## # Airplane Mode. Turning on or off all wifi using rfkill. diff --git a/config/hypr/scripts/Animations.sh b/config/hypr/scripts/Animations.sh index 477e5cd3..4bbe050f 100755 --- a/config/hypr/scripts/Animations.sh +++ b/config/hypr/scripts/Animations.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ ## # For applying Animations from different users diff --git a/config/hypr/scripts/Battery.sh b/config/hypr/scripts/Battery.sh index d7830058..2baed6ca 100644 --- a/config/hypr/scripts/Battery.sh +++ b/config/hypr/scripts/Battery.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash for i in {0..3}; do if [ -f /sys/class/power_supply/BAT$i/capacity ]; then diff --git a/config/hypr/scripts/Brightness.sh b/config/hypr/scripts/Brightness.sh index 63fd02f3..ce443ef2 100755 --- a/config/hypr/scripts/Brightness.sh +++ b/config/hypr/scripts/Brightness.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ ## # Script for Monitor backlights (if supported) using brightnessctl diff --git a/config/hypr/scripts/BrightnessKbd.sh b/config/hypr/scripts/BrightnessKbd.sh index 24737b73..93e09d86 100755 --- a/config/hypr/scripts/BrightnessKbd.sh +++ b/config/hypr/scripts/BrightnessKbd.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ ## # Script for keyboard backlights (if supported) using brightnessctl diff --git a/config/hypr/scripts/ChangeBlur.sh b/config/hypr/scripts/ChangeBlur.sh index 895987a4..0060285b 100755 --- a/config/hypr/scripts/ChangeBlur.sh +++ b/config/hypr/scripts/ChangeBlur.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ ## # Script for changing blurs on the fly diff --git a/config/hypr/scripts/ChangeLayout.sh b/config/hypr/scripts/ChangeLayout.sh index b083fcdc..78428188 100755 --- a/config/hypr/scripts/ChangeLayout.sh +++ b/config/hypr/scripts/ChangeLayout.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ ## # for changing Hyprland Layouts (Master or Dwindle) on the fly diff --git a/config/hypr/scripts/ClipManager.sh b/config/hypr/scripts/ClipManager.sh index 9937b6f4..3ba5d91a 100755 --- a/config/hypr/scripts/ClipManager.sh +++ b/config/hypr/scripts/ClipManager.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ ## # Clipboard Manager. This script uses cliphist, rofi, and wl-copy. diff --git a/config/hypr/scripts/ComposeHyprConfigs.sh b/config/hypr/scripts/ComposeHyprConfigs.sh new file mode 100644 index 00000000..55bc3c5c --- /dev/null +++ b/config/hypr/scripts/ComposeHyprConfigs.sh @@ -0,0 +1,109 @@ +#!/usr/bin/env bash +# Compose merged Hyprland configs for Startup_Apps and WindowRules +set -euo pipefail + +BASE_DIR="$HOME/.config/hypr" +BASE_CFG_DIR="$BASE_DIR/configs" +USER_DIR="$BASE_DIR/UserConfigs" +GEN_DIR="$BASE_DIR/generated" + +mkdir -p "$GEN_DIR" + +log() { printf "[compose] %s\n" "$*"; } + +# Trim leading/trailing whitespace +trim() { sed -E 's/^\s+//;s/\s+$//'; } + +# Normalize spaces in a directive line +normalize() { awk '{$1=$1;print}'; } + +# Build merged Startup_Apps.conf +compose_startup_apps() { + local base_file="$BASE_CFG_DIR/Startup_Apps.conf" + local user_file="$USER_DIR/Startup_Apps.conf" + local disable_file="$USER_DIR/Startup_Apps.disable" + local out_file="$GEN_DIR/Startup_Apps.conf" + + : >"$out_file" + + # Header and variable lines come from base + if [[ -f "$base_file" ]]; then + # Copy all non exec-once lines (comments, blanks, variables, etc.) + grep -Ev '^\s*exec-once\s*=' "$base_file" || true >>"$out_file" + fi + + # Collect exec-once commands (the right side of '=') + declare -A cmds=() + + if [[ -f "$base_file" ]]; then + while IFS= read -r line; do + [[ "$line" =~ ^\s*exec-once\s*= ]] || continue + cmd="${line#*=}" + cmd="$(echo "$cmd" | trim)" + cmds["$cmd"]=1 + done <"$base_file" + fi + + if [[ -f "$user_file" ]]; then + while IFS= read -r line; do + [[ "$line" =~ ^\s*exec-once\s*= ]] || continue + cmd="${line#*=}" + cmd="$(echo "$cmd" | trim)" + cmds["$cmd"]=1 + done <"$user_file" + fi + + # Apply disables (exact match of command string) + if [[ -f "$disable_file" ]]; then + while IFS= read -r d; do + d="$(echo "$d" | trim)" + [[ -z "$d" || "$d" =~ ^# ]] && continue + unset 'cmds[$d]' + done <"$disable_file" + fi + + # Emit combined exec-once (stable sort) + for k in "${!cmds[@]}"; do echo "$k"; done | sort -u | while IFS= read -r cmd; do + [[ -z "$cmd" ]] && continue + printf "exec-once = %s\n" "$cmd" >>"$out_file" + done + + log "Wrote $out_file" +} + +# Build merged WindowRules.conf +compose_window_rules() { + local base_file="$BASE_CFG_DIR/WindowRules.conf" + local user_file="$USER_DIR/WindowRules.conf" + local disable_file="$USER_DIR/WindowRules.disable" + local out_file="$GEN_DIR/WindowRules.conf" + + : >"$out_file" + echo "# Generated merged WindowRules" >>"$out_file" + + declare -A rules=() + add_rules() { + local f="$1" + [[ -f "$f" ]] || return 0 + grep -E '^(windowrule|layerrule)\s*=' "$f" | trim | while IFS= read -r r; do + rules["$r"]=1 + done + } + + add_rules "$base_file" + add_rules "$user_file" + + if [[ -f "$disable_file" ]]; then + while IFS= read -r d; do + d="$(echo "$d" | trim)" + [[ -z "$d" || "$d" =~ ^# ]] && continue + unset 'rules[$d]' + done <"$disable_file" + fi + + for r in "${!rules[@]}"; do echo "$r"; done | sort -u >>"$out_file" + log "Wrote $out_file" +} + +compose_startup_apps +compose_window_rules \ No newline at end of file diff --git a/config/hypr/scripts/DarkLight.sh b/config/hypr/scripts/DarkLight.sh index 1bc1602f..e473efb2 100755 --- a/config/hypr/scripts/DarkLight.sh +++ b/config/hypr/scripts/DarkLight.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash ## /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ ## # For Dark and Light switching # Note: Scripts are looking for keywords Light or Dark except for wallpapers as the are in a separate directories diff --git a/config/hypr/scripts/Distro_update.sh b/config/hypr/scripts/Distro_update.sh index b0b1446b..2b3376e3 100755 --- a/config/hypr/scripts/Distro_update.sh +++ b/config/hypr/scripts/Distro_update.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ ## # Simple bash script to check and will try to update your system diff --git a/config/hypr/scripts/Dropterminal.sh b/config/hypr/scripts/Dropterminal.sh index 1c17fbb4..9b2eeecb 100755 --- a/config/hypr/scripts/Dropterminal.sh +++ b/config/hypr/scripts/Dropterminal.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ ## # # Made and brought to by Kiran George diff --git a/config/hypr/scripts/GameMode.sh b/config/hypr/scripts/GameMode.sh index 7a39da3d..ec1e541e 100755 --- a/config/hypr/scripts/GameMode.sh +++ b/config/hypr/scripts/GameMode.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ ## # Game Mode. Turning off all animations diff --git a/config/hypr/scripts/Hypridle.sh b/config/hypr/scripts/Hypridle.sh index 56176716..6acff434 100755 --- a/config/hypr/scripts/Hypridle.sh +++ b/config/hypr/scripts/Hypridle.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ ## # This is for custom version of waybar idle_inhibitor which activates / deactivates hypridle instead diff --git a/config/hypr/scripts/KeyBinds.sh b/config/hypr/scripts/KeyBinds.sh index 9c6b5ab7..3a19390f 100755 --- a/config/hypr/scripts/KeyBinds.sh +++ b/config/hypr/scripts/KeyBinds.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ ## # searchable enabled keybinds using rofi diff --git a/config/hypr/scripts/KeyHints.sh b/config/hypr/scripts/KeyHints.sh index 7917ae3a..8a478039 100755 --- a/config/hypr/scripts/KeyHints.sh +++ b/config/hypr/scripts/KeyHints.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ ## # GDK BACKEND. Change to either wayland or x11 if having issues diff --git a/config/hypr/scripts/KillActiveProcess.sh b/config/hypr/scripts/KillActiveProcess.sh index bee146d7..2bc108f2 100755 --- a/config/hypr/scripts/KillActiveProcess.sh +++ b/config/hypr/scripts/KillActiveProcess.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ ## # Copied from Discord post. Thanks to @Zorg diff --git a/config/hypr/scripts/Kitty_themes.sh b/config/hypr/scripts/Kitty_themes.sh index 48bfa99f..55da7e44 100755 --- a/config/hypr/scripts/Kitty_themes.sh +++ b/config/hypr/scripts/Kitty_themes.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ย  # # Kitty Themes Source https://github.com/dexpota/kitty-themes # diff --git a/config/hypr/scripts/KooLsDotsUpdate.sh b/config/hypr/scripts/KooLsDotsUpdate.sh index f4b8814a..51277ab1 100755 --- a/config/hypr/scripts/KooLsDotsUpdate.sh +++ b/config/hypr/scripts/KooLsDotsUpdate.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ ## # simple bash script to check if update is available by comparing local version and github version diff --git a/config/hypr/scripts/Kool_Quick_Settings.sh b/config/hypr/scripts/Kool_Quick_Settings.sh index e43749bf..79ddc163 100755 --- a/config/hypr/scripts/Kool_Quick_Settings.sh +++ b/config/hypr/scripts/Kool_Quick_Settings.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ ## # Rofi menu for KooL Hyprland Quick Settings (SUPER SHIFT E) diff --git a/config/hypr/scripts/LockScreen.sh b/config/hypr/scripts/LockScreen.sh index e61490cd..d58f5c21 100755 --- a/config/hypr/scripts/LockScreen.sh +++ b/config/hypr/scripts/LockScreen.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ ## # For Hyprlock diff --git a/config/hypr/scripts/MediaCtrl.sh b/config/hypr/scripts/MediaCtrl.sh index 000c3ade..9dc3571d 100755 --- a/config/hypr/scripts/MediaCtrl.sh +++ b/config/hypr/scripts/MediaCtrl.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ ## # Playerctl diff --git a/config/hypr/scripts/MonitorProfiles.sh b/config/hypr/scripts/MonitorProfiles.sh index 67316c09..1176a46a 100755 --- a/config/hypr/scripts/MonitorProfiles.sh +++ b/config/hypr/scripts/MonitorProfiles.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ ## # For applying Pre-configured Monitor Profiles diff --git a/config/hypr/scripts/Polkit-NixOS.sh b/config/hypr/scripts/Polkit-NixOS.sh index 51675eff..28642d19 100755 --- a/config/hypr/scripts/Polkit-NixOS.sh +++ b/config/hypr/scripts/Polkit-NixOS.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ ## # For NixOS starting of polkit-gnome. Dec 2023, the settings stated in NixOS wiki does not work so have to manual start it diff --git a/config/hypr/scripts/Polkit.sh b/config/hypr/scripts/Polkit.sh index dcea7653..1af8fd1b 100755 --- a/config/hypr/scripts/Polkit.sh +++ b/config/hypr/scripts/Polkit.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ ## # This script starts the first available Polkit agent from a list of possible locations diff --git a/config/hypr/scripts/PortalHyprland.sh b/config/hypr/scripts/PortalHyprland.sh index 9bdf4b8c..21cb7db4 100755 --- a/config/hypr/scripts/PortalHyprland.sh +++ b/config/hypr/scripts/PortalHyprland.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ ## # For manually starting xdg-desktop-portal-hyprland diff --git a/config/hypr/scripts/Refresh.sh b/config/hypr/scripts/Refresh.sh index 719c368d..2e772aa9 100755 --- a/config/hypr/scripts/Refresh.sh +++ b/config/hypr/scripts/Refresh.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ ## # Scripts for refreshing ags, waybar, rofi, swaync, wallust diff --git a/config/hypr/scripts/RefreshNoWaybar.sh b/config/hypr/scripts/RefreshNoWaybar.sh index 8454124e..54c760bd 100755 --- a/config/hypr/scripts/RefreshNoWaybar.sh +++ b/config/hypr/scripts/RefreshNoWaybar.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ ## # Modified version of Refresh.sh but waybar wont refresh diff --git a/config/hypr/scripts/RofiEmoji.sh b/config/hypr/scripts/RofiEmoji.sh index 4570831e..7e3ef0f3 100755 --- a/config/hypr/scripts/RofiEmoji.sh +++ b/config/hypr/scripts/RofiEmoji.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ ## # Variables diff --git a/config/hypr/scripts/RofiSearch.sh b/config/hypr/scripts/RofiSearch.sh index 4218bed3..8ef12c46 100755 --- a/config/hypr/scripts/RofiSearch.sh +++ b/config/hypr/scripts/RofiSearch.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ ## # For Searching via web browsers diff --git a/config/hypr/scripts/RofiThemeSelector-modified.sh b/config/hypr/scripts/RofiThemeSelector-modified.sh index 2cfc2d24..d6a353c0 100755 --- a/config/hypr/scripts/RofiThemeSelector-modified.sh +++ b/config/hypr/scripts/RofiThemeSelector-modified.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ ## # A modified version of Rofi-Theme-Selector, concentrating only on ~/.local and also, applying only 10 @themes in ~/.config/rofi/config.rasi # as opposed to continous adding of //@theme diff --git a/config/hypr/scripts/RofiThemeSelector.sh b/config/hypr/scripts/RofiThemeSelector.sh index 8b2fcb71..b7236e8f 100755 --- a/config/hypr/scripts/RofiThemeSelector.sh +++ b/config/hypr/scripts/RofiThemeSelector.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ย  # # Rofi Themes - Script to preview and apply themes by live-reloading the config. diff --git a/config/hypr/scripts/ScreenShot.sh b/config/hypr/scripts/ScreenShot.sh index 0a37c7e4..0ef70964 100755 --- a/config/hypr/scripts/ScreenShot.sh +++ b/config/hypr/scripts/ScreenShot.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ ## # Screenshots scripts diff --git a/config/hypr/scripts/Sounds.sh b/config/hypr/scripts/Sounds.sh index 8b2cc76e..b372d714 100755 --- a/config/hypr/scripts/Sounds.sh +++ b/config/hypr/scripts/Sounds.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ ## # This script is used to play system sounds. # Script is used by Volume.Sh and ScreenShots.sh diff --git a/config/hypr/scripts/SwitchKeyboardLayout.sh b/config/hypr/scripts/SwitchKeyboardLayout.sh index f505fa6c..18a9517e 100755 --- a/config/hypr/scripts/SwitchKeyboardLayout.sh +++ b/config/hypr/scripts/SwitchKeyboardLayout.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ ## # This is for changing kb_layouts. Set kb_layouts in $settings_file diff --git a/config/hypr/scripts/Tak0-Autodispatch.sh b/config/hypr/scripts/Tak0-Autodispatch.sh index a1f72129..114a3e8e 100755 --- a/config/hypr/scripts/Tak0-Autodispatch.sh +++ b/config/hypr/scripts/Tak0-Autodispatch.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # USAGE / ะ†ะะกะขะ ะฃะšะฆะ†ะฏ: # 1) Run from terminal: # ./dispatch.sh diff --git a/config/hypr/scripts/TouchPad.sh b/config/hypr/scripts/TouchPad.sh index 8509d79f..030c36de 100755 --- a/config/hypr/scripts/TouchPad.sh +++ b/config/hypr/scripts/TouchPad.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ ## # For disabling touchpad. # Edit the Touchpad_Device on ~/.config/hypr/UserConfigs/Laptops.conf according to your system diff --git a/config/hypr/scripts/Volume.sh b/config/hypr/scripts/Volume.sh index 8efdb55c..4c82f543 100755 --- a/config/hypr/scripts/Volume.sh +++ b/config/hypr/scripts/Volume.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ ## # Scripts for volume controls for audio and mic diff --git a/config/hypr/scripts/WallustSwww.sh b/config/hypr/scripts/WallustSwww.sh index 5e0148ee..657f41ab 100755 --- a/config/hypr/scripts/WallustSwww.sh +++ b/config/hypr/scripts/WallustSwww.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ ## # Wallust: derive colors from the current wallpaper and update templates # Usage: WallustSwww.sh [absolute_path_to_wallpaper] diff --git a/config/hypr/scripts/WaybarLayout.sh b/config/hypr/scripts/WaybarLayout.sh index f65c9c00..d3725a91 100755 --- a/config/hypr/scripts/WaybarLayout.sh +++ b/config/hypr/scripts/WaybarLayout.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ ## # Script for waybar layout or configs diff --git a/config/hypr/scripts/WaybarScripts.sh b/config/hypr/scripts/WaybarScripts.sh index 7b3aaba2..d2205c42 100755 --- a/config/hypr/scripts/WaybarScripts.sh +++ b/config/hypr/scripts/WaybarScripts.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ # # This file used on waybar modules sourcing defaults set in $HOME/.config/hypr/UserConfigs/01-UserDefaults.conf diff --git a/config/hypr/scripts/WaybarStyles.sh b/config/hypr/scripts/WaybarStyles.sh index 15767c2a..8ebfed92 100755 --- a/config/hypr/scripts/WaybarStyles.sh +++ b/config/hypr/scripts/WaybarStyles.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ ## # Script for waybar styles diff --git a/config/hypr/scripts/Wlogout.sh b/config/hypr/scripts/Wlogout.sh index f552b83d..8879858c 100755 --- a/config/hypr/scripts/Wlogout.sh +++ b/config/hypr/scripts/Wlogout.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ ## # wlogout (Power, Screen Lock, Suspend, etc) diff --git a/config/hypr/scripts/sddm_wallpaper.sh b/config/hypr/scripts/sddm_wallpaper.sh index 9487188c..9dca2f72 100644 --- a/config/hypr/scripts/sddm_wallpaper.sh +++ b/config/hypr/scripts/sddm_wallpaper.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ # SDDM Wallpaper and Wallust Colors Setter diff --git a/copy.sh b/copy.sh index d54402f3..496811d8 100755 --- a/copy.sh +++ b/copy.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ # clear @@ -111,8 +111,17 @@ fi # Proper Polkit for NixOS if hostnamectl | grep -q 'Operating System: NixOS'; then echo "${INFO} NixOS Distro Detected. Setting up proper env's and configs." 2>&1 | tee -a "$LOG" || true - sed -i -E '/^#?exec-once = \$scriptsDir\/Polkit-NixOS\.sh/s/^#//' config/hypr/UserConfigs/Startup_Apps.conf - sed -i '/^exec-once = \$scriptsDir\/Polkit\.sh$/ s/^#*/#/' config/hypr/UserConfigs/Startup_Apps.conf + # Ensure NixOS polkit is enabled via overlay and default polkit is disabled via disable list + OVERLAY_SA="config/hypr/UserConfigs/Startup_Apps.conf" + DISABLE_SA="config/hypr/UserConfigs/Startup_Apps.disable" + mkdir -p "$(dirname "$OVERLAY_SA")" + touch "$OVERLAY_SA" "$DISABLE_SA" + if ! grep -qx 'exec-once = $scriptsDir/Polkit-NixOS.sh' "$OVERLAY_SA"; then + echo 'exec-once = $scriptsDir/Polkit-NixOS.sh' >> "$OVERLAY_SA" + fi + if ! grep -qx '\$scriptsDir/Polkit.sh' "$DISABLE_SA"; then + echo '$scriptsDir/Polkit.sh' >> "$DISABLE_SA" + fi fi # activating hyprcursor on env by checking if the directory ~/.icons/Bibata-Modern-Ice/hyprcursors exists @@ -236,17 +245,23 @@ done # Check if asusctl is installed and add rog-control-center on Startup if command -v asusctl >/dev/null 2>&1; then - sed -i '/^\s*#exec-once = rog-control-center/s/^#//' config/hypr/UserConfigs/Startup_Apps.conf + OVERLAY_SA="config/hypr/UserConfigs/Startup_Apps.conf" + mkdir -p "$(dirname "$OVERLAY_SA")"; touch "$OVERLAY_SA" + grep -qx 'exec-once = rog-control-center' "$OVERLAY_SA" || echo 'exec-once = rog-control-center' >> "$OVERLAY_SA" fi # Check if blueman-applet is installed and add blueman-applet on Startup if command -v blueman-applet >/dev/null 2>&1; then - sed -i '/^\s*#exec-once = blueman-applet/s/^#//' config/hypr/UserConfigs/Startup_Apps.conf + OVERLAY_SA="config/hypr/UserConfigs/Startup_Apps.conf" + mkdir -p "$(dirname "$OVERLAY_SA")"; touch "$OVERLAY_SA" + grep -qx 'exec-once = blueman-applet' "$OVERLAY_SA" || echo 'exec-once = blueman-applet' >> "$OVERLAY_SA" fi # Check if ags is installed edit ags behaviour on configs if command -v ags >/dev/null 2>&1; then - sed -i '/^\s*#exec-once = ags/s/^#//' config/hypr/UserConfigs/Startup_Apps.conf + OVERLAY_SA="config/hypr/UserConfigs/Startup_Apps.conf" + mkdir -p "$(dirname "$OVERLAY_SA")"; touch "$OVERLAY_SA" + grep -qx 'exec-once = ags' "$OVERLAY_SA" || echo 'exec-once = ags' >> "$OVERLAY_SA" sed -i '/#ags -q && ags &/s/^#//' config/hypr/scripts/RefreshNoWaybar.sh sed -i '/#ags -q && ags &/s/^#//' config/hypr/scripts/Refresh.sh @@ -259,7 +274,9 @@ fi # Check if quickshell is installed; edit quickshell behaviour on configs if command -v qs >/dev/null 2>&1; then - sed -i '/^\s*#exec-once = qs/s/^#//' config/hypr/UserConfigs/Startup_Apps.conf + OVERLAY_SA="config/hypr/UserConfigs/Startup_Apps.conf" + mkdir -p "$(dirname "$OVERLAY_SA")"; touch "$OVERLAY_SA" + grep -qx 'exec-once = qs' "$OVERLAY_SA" || echo 'exec-once = qs' >> "$OVERLAY_SA" sed -i '/#pkill qs && qs &/s/^#//' config/hypr/scripts/RefreshNoWaybar.sh sed -i '/#pkill qs && qs &/s/^#//' config/hypr/scripts/Refresh.sh @@ -814,6 +831,37 @@ FILES_TO_RESTORE=( "WindowRules.conf" ) +# Helper to extract overlay (additions) and optional disables from a previous user file compared to vendor base +compose_overlay_from_backup() { + local type="$1" # startup|windowrules + local base_file="$2" + local old_user_file="$3" + local new_user_file="$4" + local disable_file="$5" + + mkdir -p "$(dirname "$new_user_file")" + : >"$new_user_file" + : >"$disable_file" + + if [ "$type" = "startup" ]; then + # additions: exec-once lines present in old user but not in base + grep -E '^\s*exec-once\s*=' "$old_user_file" | sed -E 's/^\s+//;s/\s+$//' | sort -u >"$old_user_file.tmp.exec" + grep -E '^\s*exec-once\s*=' "$base_file" | sed -E 's/^\s+//;s/\s+$//' | sort -u >"$base_file.tmp.exec" + comm -23 "$old_user_file.tmp.exec" "$base_file.tmp.exec" >"$new_user_file" + # treat commented exec-once in old user as disables + grep -E '^\s*#\s*exec-once\s*=' "$old_user_file" | sed -E 's/^\s*#\s*exec-once\s*=\s*//' | sed -E 's/^\s+//;s/\s+$//' | sort -u >"$disable_file" + rm -f "$old_user_file.tmp.exec" "$base_file.tmp.exec" + elif [ "$type" = "windowrules" ]; then + # additions + grep -E '^(windowrule|layerrule)\s*=' "$old_user_file" | sed -E 's/^\s+//;s/\s+$//' | sort -u >"$old_user_file.tmp.rules" + grep -E '^(windowrule|layerrule)\s*=' "$base_file" | sed -E 's/^\s+//;s/\s+$//' | sort -u >"$base_file.tmp.rules" + comm -23 "$old_user_file.tmp.rules" "$base_file.tmp.rules" >"$new_user_file" + # disables: lines commented in old user + grep -E '^\s*#\s*(windowrule|layerrule)\s*=' "$old_user_file" | sed -E 's/^\s*#\s*//' | sed -E 's/^\s+//;s/\s+$//' | sort -u >"$disable_file" + rm -f "$old_user_file.tmp.rules" "$base_file.tmp.rules" + fi +} + DIRPATH="$HOME/.config/$DIRH" BACKUP_DIR=$(get_backup_dirname) BACKUP_DIR_PATH="$DIRPATH-backup-$BACKUP_DIR/UserConfigs" @@ -830,14 +878,27 @@ if [ -d "$BACKUP_DIR_PATH" ]; then NOTES for RESTORING PREVIOUS CONFIGS โ–ˆโ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–„โ–ˆ - If you decide to restore your old configs, make sure to - handle the updates or changes manually !!! + We now auto-migrate Startup_Apps and WindowRules by extracting + your additions into overlay files and optional disable lists. + This keeps new defaults while preserving your custom changes. " echo -e "${MAGENTA}Kindly Visit and check KooL's Hyprland-Dots GitHub page for the history of commits.${RESET}" for FILE_NAME in "${FILES_TO_RESTORE[@]}"; do BACKUP_FILE="$BACKUP_DIR_PATH/$FILE_NAME" if [ -f "$BACKUP_FILE" ]; then + # Special handling for Startup_Apps.conf and WindowRules.conf + if [ "$FILE_NAME" = "Startup_Apps.conf" ]; then + compose_overlay_from_backup "startup" "$DIRPATH/configs/Startup_Apps.conf" "$BACKUP_FILE" "$DIRPATH/UserConfigs/Startup_Apps.conf" "$DIRPATH/UserConfigs/Startup_Apps.disable" + echo "${OK} - Migrated overlay for ${YELLOW}$FILE_NAME${RESET}" 2>&1 | tee -a "$LOG" + continue + fi + if [ "$FILE_NAME" = "WindowRules.conf" ]; then + compose_overlay_from_backup "windowrules" "$DIRPATH/configs/WindowRules.conf" "$BACKUP_FILE" "$DIRPATH/UserConfigs/WindowRules.conf" "$DIRPATH/UserConfigs/WindowRules.disable" + echo "${OK} - Migrated overlay for ${YELLOW}$FILE_NAME${RESET}" 2>&1 | tee -a "$LOG" + continue + fi + printf "\n${INFO} Found ${YELLOW}$FILE_NAME${RESET} in hypr backup...\n" echo -n "${CAT} Do you want to restore ${YELLOW}$FILE_NAME${RESET} from backup? (y/N): " read file_restore @@ -855,6 +916,11 @@ if [ -d "$BACKUP_DIR_PATH" ]; then done fi +# Compose merged configs (Startup_Apps and WindowRules) +if [ -x "$DIRPATH/scripts/ComposeHyprConfigs.sh" ]; then + "$DIRPATH/scripts/ComposeHyprConfigs.sh" 2>&1 | tee -a "$LOG" || true +fi + printf "\n%.0s" {1..1} # Restoring previous UserScripts diff --git a/release.sh b/release.sh index 78063ee1..e29eaa79 100755 --- a/release.sh +++ b/release.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ # # For downloading dots from releases diff --git a/upgrade.sh b/upgrade.sh index a47bd48b..9aee3c2c 100755 --- a/upgrade.sh +++ b/upgrade.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ # # for Semi-Manual upgrading your system. # NOTE: requires rsync @@ -196,7 +196,12 @@ if version_gt "$latest_version" "$stored_version"; then chmod +x "$HOME/.config/hypr/scripts/"* 2>&1 | tee -a "$LOG" chmod +x "$HOME/.config/hypr/UserScripts/"* 2>&1 | tee -a "$LOG" # Set executable for initial-boot.sh - chmod +x "$HOME/.config/hypr/initial-boot.sh" 2>&1 | tee -a "$LOG" + chmod +x "$HOME/.config/hypr/initial-boot.sh" 2>&1 | tee -a "$LOG" + + # Compose merged configs (Startup_Apps and WindowRules) + if [ -x "$HOME/.config/hypr/scripts/ComposeHyprConfigs.sh" ]; then + "$HOME/.config/hypr/scripts/ComposeHyprConfigs.sh" 2>&1 | tee -a "$LOG" || true + fi else echo "$MAGENTA Upgrade declined. No files or directories changed" 2>&1 | tee -a "$LOG" fi -- cgit v1.2.3 From c9c54016a8675448eba7accdea9978f8d780725c Mon Sep 17 00:00:00 2001 From: mio-dokuhaki Date: Wed, 5 Nov 2025 02:24:25 +0900 Subject: feat(rofi): merge RofiBeats dynamic music system into development --- config/hypr/UserScripts/RofiBeats.sh | 205 ++++++++++++++++------------------- config/rofi/online_music.list | 17 +++ 2 files changed, 111 insertions(+), 111 deletions(-) create mode 100644 config/rofi/online_music.list (limited to 'config/hypr/UserScripts') diff --git a/config/hypr/UserScripts/RofiBeats.sh b/config/hypr/UserScripts/RofiBeats.sh index ca566019..adb5aa2c 100755 --- a/config/hypr/UserScripts/RofiBeats.sh +++ b/config/hypr/UserScripts/RofiBeats.sh @@ -1,35 +1,39 @@ -#!/usr/bin/env bash +#!/bin/bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ ## -# For Rofi Beats to play online Music or Locally saved media files +# RofiBeats - unified, dynamic UI (add, remove, manage, play) -# Variables mDIR="$HOME/Music/" iDIR="$HOME/.config/swaync/icons" rofi_theme="$HOME/.config/rofi/config-rofi-Beats.rasi" -rofi_theme_1="$HOME/.config/rofi/config-rofi-Beats-menu.rasi" +rofi_theme_menu="$HOME/.config/rofi/config-rofi-Beats-menu.rasi" +music_list="$HOME/.config/rofi/online_music.list" -# Online Stations. Edit as required -declare -A online_music=( - ["FM - Easy Rock 96.3 ๐Ÿ“ป๐ŸŽถ"]="https://radio-stations-philippines.com/easy-rock" - ["FM - Easy Rock - Baguio 91.9 ๐Ÿ“ป๐ŸŽถ"]="https://radio-stations-philippines.com/easy-rock-baguio" - ["FM - Love Radio 90.7 ๐Ÿ“ป๐ŸŽถ"]="https://radio-stations-philippines.com/love" - ["FM - WRock - CEBU 96.3 ๐Ÿ“ป๐ŸŽถ"]="https://onlineradio.ph/126-96-3-wrock.html" - ["FM - Fresh Philippines ๐Ÿ“ป๐ŸŽถ"]="https://onlineradio.ph/553-fresh-fm.html" - ["Radio - Lofi Girl ๐ŸŽง๐ŸŽถ"]="https://play.streamafrica.net/lofiradio" - ["Radio - Chillhop ๐ŸŽง๐ŸŽถ"]="http://stream.zeno.fm/fyn8eh3h5f8uv" - ["Radio - Ibiza Global ๐ŸŽง๐ŸŽถ"]="https://filtermusic.net/ibiza-global" - ["Radio - Metal Music ๐ŸŽง๐ŸŽถ"]="https://tunein.com/radio/mETaLmuSicRaDio-s119867/" - ["YT - Wish 107.5 YT Pinoy HipHop ๐Ÿ“ป๐ŸŽถ"]="https://youtube.com/playlist?list=PLkrzfEDjeYJnmgMYwCKid4XIFqUKBVWEs&si=vahW_noh4UDJ5d37" - ["YT - Youtube Top 100 Songs Global ๐Ÿ“น๐ŸŽถ"]="https://youtube.com/playlist?list=PL4fGSI1pDJn6puJdseH2Rt9sMvt9E2M4i&si=5jsyfqcoUXBCSLeu" - ["YT - Wish 107.5 YT Wishclusives ๐Ÿ“น๐ŸŽถ"]="https://youtube.com/playlist?list=PLkrzfEDjeYJn5B22H9HOWP3Kxxs-DkPSM&si=d_Ld2OKhGvpH48WO" - ["YT - Relaxing Piano Music ๐ŸŽน๐ŸŽถ"]="https://youtu.be/6H7hXzjFoVU?si=nZTPREC9lnK1JJUG" - ["YT - Youtube Remix ๐Ÿ“น๐ŸŽถ"]="https://youtube.com/playlist?list=PLeqTkIUlrZXlSNn3tcXAa-zbo95j0iN-0" - ["YT - Korean Drama OST ๐Ÿ“น๐ŸŽถ"]="https://youtube.com/playlist?list=PLUge_o9AIFp4HuA-A3e3ZqENh63LuRRlQ" - ["YT - lofi hip hop radio beats ๐Ÿ“น๐ŸŽถ"]="https://www.youtube.com/live/jfKfPfyJRdk?si=PnJIA9ErQIAw6-qd" - ["YT - Relaxing Piano Jazz Music ๐ŸŽน๐ŸŽถ"]="https://youtu.be/85UEqRat6E4?si=jXQL1Yp2VP_G6NSn" -) +mkdir -p "$(dirname "$music_list")" +[[ -f "$music_list" ]] || touch "$music_list" -# Populate local_music array with files from music directory and subdirectories +# Send notification +notification() { + notify-send -u normal -i "$iDIR/music.png" "$@" +} + +# Check if mpv is currently playing +music_playing() { pgrep -x "mpv" >/dev/null; } + +# Stop all mpv processes except mpvpaper +stop_music() { + mpv_pids=$(pgrep -x mpv) + if [ -n "$mpv_pids" ]; then + mpvpaper_pid=$(ps aux | grep -- 'unique-wallpaper-process' | grep -v 'grep' | awk '{print $2}') + for pid in $mpv_pids; do + if ! echo "$mpvpaper_pid" | grep -q "$pid"; then + kill -9 $pid || true + fi + done + notification "Music stopped" + fi +} + +# Populate local music file list populate_local_music() { local_music=() filenames=() @@ -39,115 +43,94 @@ populate_local_music() { done < <(find -L "$mDIR" -type f \( -iname "*.mp3" -o -iname "*.flac" -o -iname "*.wav" -o -iname "*.ogg" -o -iname "*.mp4" \)) } -# Function for displaying notifications -notification() { - notify-send -u normal -i "$iDIR/music.png" "Now Playing:" "$@" -} - -# Main function for playing local music +# Play selected local music file play_local_music() { populate_local_music - - # Prompt the user to select a song - choice=$(printf "%s\n" "${filenames[@]}" | rofi -i -dmenu -config $rofi_theme) - - if [ -z "$choice" ]; then - exit 1 - fi - - # Find the corresponding file path based on user's choice and set that to play the song then continue on the list - for (( i=0; i<"${#filenames[@]}"; ++i )); do + choice=$(printf "%s\n" "${filenames[@]}" | rofi -i -dmenu -config "$rofi_theme" \ + -theme-str 'entry { placeholder: "๐ŸŽต Choose Local Music"; }') + [[ -z "$choice" ]] && exit 1 + for ((i = 0; i < "${#filenames[@]}"; ++i)); do if [ "${filenames[$i]}" = "$choice" ]; then - - if music_playing; then - stop_music - fi - notification "$choice" - mpv --playlist-start="$i" --loop-playlist --vid=no "${local_music[@]}" - + music_playing && stop_music + notification "Now Playing:" "$choice" + mpv --no-video --playlist-start="$i" --loop-playlist "${local_music[@]}" break fi done } -# Main function for shuffling local music +# Shuffle and play all local music shuffle_local_music() { - if music_playing; then - stop_music - fi + music_playing && stop_music notification "Shuffle Play local music" - - # Play music in $mDIR on shuffle - mpv --shuffle --loop-playlist --vid=no "$mDIR" + mpv --no-video --shuffle --loop-playlist "$mDIR" } -# Main function for playing online music +# Play selected online music play_online_music() { - choice=$(for online in "${!online_music[@]}"; do - echo "$online" - done | sort | rofi -i -dmenu -config "$rofi_theme") - - if [ -z "$choice" ]; then - exit 1 - fi - - link="${online_music[$choice]}" - - if music_playing; then - stop_music + if [ ! -s "$music_list" ]; then + notify-send -u low -i "$iDIR/music.png" "No online music found" "Add some with Manage Music" + exit 0 fi - notification "$choice" - - # Play the selected online music using mpv - mpv --shuffle --vid=no "$link" -} - -# Function to check if music is already playing -music_playing() { - pgrep -x "mpv" > /dev/null + choice=$(awk -F'|' '{print $1}' "$music_list" | sort | rofi -i -dmenu -config "$rofi_theme" \ + -theme-str 'entry { placeholder: "๐ŸŒ Choose Online Station"; }') + [[ -z "$choice" ]] && exit 1 + link=$(awk -F'|' -v name="$choice" '$1 == name {print $2; exit}' "$music_list") + [[ -z "$link" ]] && { + notify-send -u low -i "$iDIR/music.png" "URL not found for" "$choice" + exit 1 + } + music_playing && stop_music + notification "Now Playing:" "$choice" + mpv --no-video --shuffle "$link" } -# Function to stop music and kill mpv processes -stop_music() { - mpv_pids=$(pgrep -x mpv) - - if [ -n "$mpv_pids" ]; then - # Get the PID of the mpv process used by mpvpaper (using the unique argument added) - mpvpaper_pid=$(ps aux | grep -- 'unique-wallpaper-process' | grep -v 'grep' | awk '{print $2}') - - for pid in $mpv_pids; do - if ! echo "$mpvpaper_pid" | grep -q "$pid"; then - kill -9 $pid || true - fi - done - notify-send -u low -i "$iDIR/music.png" "Music stopped" || true - fi +# Manage online music list (add, remove, view) +manage_music() { + sub_choice=$(printf "Add Music\nRemove Music\nView List" | rofi -dmenu \ + -config "$rofi_theme_menu" \ + -theme-str 'entry { placeholder: "๐Ÿ› ๏ธ Manage Music List"; }') + + case "$sub_choice" in + "Add Music") + name=$(rofi -dmenu -lines 0 -config "$rofi_theme_menu" \ + -theme-str 'entry { placeholder: "๐ŸŽผ Enter Music Title"; }') + [[ -z "$name" ]] && return + url=$(rofi -dmenu -lines 0 -config "$rofi_theme_menu" \ + -theme-str 'entry { placeholder: "๐Ÿ”— Enter Music URL"; }') + [[ -z "$url" ]] && return + echo "$name|$url" >>"$music_list" + notification "Added" "$name" + ;; + "Remove Music") + entry=$(awk -F'|' '{print $1}' "$music_list" | rofi -dmenu -config "$rofi_theme_menu" \ + -theme-str 'entry { placeholder: "๐Ÿ—‘๏ธ Select Music to Remove"; }') + [[ -z "$entry" ]] && return + grep -vF "$entry" "$music_list" >"$music_list.tmp" && mv "$music_list.tmp" "$music_list" + notification "Removed" "$entry" + ;; + "View List") + # Show only titles, not URLs + awk -F'|' '{print $1}' "$music_list" | rofi -dmenu -config "$rofi_theme_menu" \ + -theme-str 'entry { placeholder: "๐Ÿ“œ Online Music List"; }' >/dev/null + ;; + esac } +# Main menu user_choice=$(printf "%s\n" \ "Play from Online Stations" \ "Play from Music directory" \ "Shuffle Play from Music directory" \ "Stop RofiBeats" \ - | rofi -dmenu -config $rofi_theme_1) - -echo "User choice: $user_choice" + "Manage Music List" | + rofi -dmenu -config "$rofi_theme_menu" \ + -theme-str 'entry { placeholder: "๐ŸŽง RofiBeats Menu"; }') case "$user_choice" in - "Play from Online Stations") - play_online_music - ;; - "Play from Music directory") - play_local_music - ;; - "Shuffle Play from Music directory") - shuffle_local_music - ;; - "Stop RofiBeats") - if music_playing; then - stop_music - fi - ;; - *) - ;; +"Play from Online Stations") play_online_music ;; +"Play from Music directory") play_local_music ;; +"Shuffle Play from Music directory") shuffle_local_music ;; +"Stop RofiBeats") music_playing && stop_music ;; +"Manage Music List") manage_music ;; esac diff --git a/config/rofi/online_music.list b/config/rofi/online_music.list new file mode 100644 index 00000000..bb21b9d4 --- /dev/null +++ b/config/rofi/online_music.list @@ -0,0 +1,17 @@ +FM - Easy Rock 96.3 ๐Ÿ“ป๐ŸŽถ|https://radio-stations-philippines.com/easy-rock +FM - Easy Rock - Baguio 91.9 ๐Ÿ“ป๐ŸŽถ|https://radio-stations-philippines.com/easy-rock-baguio +FM - Love Radio 90.7 ๐Ÿ“ป๐ŸŽถ|https://radio-stations-philippines.com/love +FM - WRock - CEBU 96.3 ๐Ÿ“ป๐ŸŽถ|https://onlineradio.ph/126-96-3-wrock.html +FM - Fresh Philippines ๐Ÿ“ป๐ŸŽถ|https://onlineradio.ph/553-fresh-fm.html +Radio - Lofi Girl ๐ŸŽง๐ŸŽถ|https://play.streamafrica.net/lofiradio +Radio - Chillhop ๐ŸŽง๐ŸŽถ|http://stream.zeno.fm/fyn8eh3h5f8uv +Radio - Ibiza Global ๐ŸŽง๐ŸŽถ|https://filtermusic.net/ibiza-global +Radio - Metal Music ๐ŸŽง๐ŸŽถ|https://tunein.com/radio/mETaLmuSicRaDio-s119867/ +YT - Wish 107.5 YT Pinoy HipHop ๐Ÿ“ป๐ŸŽถ|https://youtube.com/playlist?list=PLkrzfEDjeYJnmgMYwCKid4XIFqUKBVWEs&si=vahW_noh4UDJ5d37 +YT - Youtube Top 100 Songs Global ๐Ÿ“น๐ŸŽถ|https://youtube.com/playlist?list=PL4fGSI1pDJn6puJdseH2Rt9sMvt9E2M4i&si=5jsyfqcoUXBCSLeu +YT - Wish 107.5 YT Wishclusives ๐Ÿ“น๐ŸŽถ|https://youtube.com/playlist?list=PLkrzfEDjeYJn5B22H9HOWP3Kxxs-DkPSM&si=d_Ld2OKhGvpH48WO +YT - Relaxing Piano Music ๐ŸŽน๐ŸŽถ|https://youtu.be/6H7hXzjFoVU?si=nZTPREC9lnK1JJUG +YT - Youtube Remix ๐Ÿ“น๐ŸŽถ|https://youtube.com/playlist?list=PLeqTkIUlrZXlSNn3tcXAa-zbo95j0iN-0 +YT - Korean Drama OST ๐Ÿ“น๐ŸŽถ|https://youtube.com/playlist?list=PLUge_o9AIFp4HuA-A3e3ZqENh63LuRRlQ +YT - lofi hip hop radio beats ๐Ÿ“น๐ŸŽถ|https://www.youtube.com/live/jfKfPfyJRdk?si=PnJIA9ErQIAw6-qd +YT - Relaxing Piano Jazz Music ๐ŸŽน๐ŸŽถ|https://youtu.be/85UEqRat6E4?si=jXQL1Yp2VP_G6NSn -- cgit v1.2.3 From 073fc0fb287777e39e6b9089a5db7a7cf010f24f Mon Sep 17 00:00:00 2001 From: mio-dokuhaki Date: Wed, 5 Nov 2025 02:43:32 +0900 Subject: fix(rofi): update RofiBeats shebang to /usr/bin/env bash --- config/hypr/UserScripts/RofiBeats.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'config/hypr/UserScripts') diff --git a/config/hypr/UserScripts/RofiBeats.sh b/config/hypr/UserScripts/RofiBeats.sh index adb5aa2c..a002a518 100755 --- a/config/hypr/UserScripts/RofiBeats.sh +++ b/config/hypr/UserScripts/RofiBeats.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # /* ---- ๐Ÿ’ซ https://github.com/JaKooLit ๐Ÿ’ซ ---- */ ## # RofiBeats - unified, dynamic UI (add, remove, manage, play) -- cgit v1.2.3 From 0cda1d47c8ff9344ea62bc3741911b3da9367f86 Mon Sep 17 00:00:00 2001 From: brockar Date: Sun, 9 Nov 2025 10:05:45 -0300 Subject: fix: Weather.py one-off run --- config/hypr/UserScripts/Weather.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'config/hypr/UserScripts') diff --git a/config/hypr/UserScripts/Weather.py b/config/hypr/UserScripts/Weather.py index a9a826e1..a6483777 100755 --- a/config/hypr/UserScripts/Weather.py +++ b/config/hypr/UserScripts/Weather.py @@ -44,7 +44,7 @@ class WeatherData: # Examples (zsh): # # One-off run # # WEATHER_UNITS can be "metric" or "imperial" -# WEATHER_UNITS=imperial WEATHER_PLACE="Concord, NH" python3 /home/dwilliams/Projects/Weather.py +# WEATHER_UNITS=imperial WEATHER_PLACE="Concord, NH" python3 ~/.config/hypr/UserScripts/Weather.py # # # Persist in current shell session # export WEATHER_UNITS=imperial -- cgit v1.2.3