diff options
| author | Donald Williams <129223418+dwilliam62@users.noreply.github.com> | 2025-10-03 23:28:49 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-10-03 23:28:49 -0400 |
| commit | cd6730a58e7c5fb9fe2cb278152a5423bddea498 (patch) | |
| tree | 7e0fa9fc2b65a93544315e059eedc64a8ee7de26 /config/hypr | |
| parent | 3909e6850e669aee87404542bc6eb86f8a633ceb (diff) | |
| parent | cc437da9dd067a9a001c0a68cd6f1be57b333a78 (diff) | |
Merge pull request #822 from dwilliam62/development
Weather.py: switch to Open-Meteo; add caching, reverse geocoding
Diffstat (limited to 'config/hypr')
| -rw-r--r-- | config/hypr/UserConfigs/UserKeybinds.conf | 5 | ||||
| -rw-r--r-- | config/hypr/UserConfigs/UserSettings.conf | 4 | ||||
| -rwxr-xr-x | config/hypr/UserScripts/WallpaperAutoChange.sh | 5 | ||||
| -rwxr-xr-x | config/hypr/UserScripts/WallpaperSelect.sh | 4 | ||||
| -rwxr-xr-x | config/hypr/UserScripts/Weather.py | 1075 | ||||
| -rwxr-xr-x | config/hypr/initial-boot.sh | 2 | ||||
| -rwxr-xr-x | config/hypr/scripts/Dropterminal.sh | 141 | ||||
| -rwxr-xr-x | config/hypr/scripts/RefreshNoWaybar.sh | 5 | ||||
| -rwxr-xr-x | config/hypr/scripts/WallustSwww.sh | 75 | ||||
| -rwxr-xr-x | config/hypr/scripts/WaybarCava.sh | 51 | ||||
| -rw-r--r-- | config/hypr/v2.3.17 | 5 |
11 files changed, 1246 insertions, 126 deletions
diff --git a/config/hypr/UserConfigs/UserKeybinds.conf b/config/hypr/UserConfigs/UserKeybinds.conf index dd18b971..2f0e808f 100644 --- a/config/hypr/UserConfigs/UserKeybinds.conf +++ b/config/hypr/UserConfigs/UserKeybinds.conf @@ -68,6 +68,11 @@ bindln = ALT_L, SHIFT_L, exec, $scriptsDir/SwitchKeyboardLayout.sh # Change keyb bindln = SHIFT_L, ALT_L, exec, $scriptsDir/Tak0-Per-Window-Switch.sh # Change keyboard layout locally for each window bind = $mainMod ALT, C, exec, $UserScripts/RofiCalc.sh # calculator (qalculate) +# Move current workspaces to monitors (left right up or down) +bind = $mainMod CTRL, F9, movecurrentworkspacetomonitor, l #move current workspace to LEFT monitor +bind = $mainMod CTRL, F10, movecurrentworkspacetomonitor, r #move current workspace to RIGHT monitor +bind = $mainMod CTRL, F11, movecurrentworkspacetomonitor, u #move current workspace to UP monitor +bind = $mainMod CTRL, F12, movecurrentworkspacetomonitor, d #move current workspace to DOWN monitor # For passthrough keyboard into a VM # bind = $mainMod ALT, P, submap, passthru diff --git a/config/hypr/UserConfigs/UserSettings.conf b/config/hypr/UserConfigs/UserSettings.conf index 2ba23acc..325d24f7 100644 --- a/config/hypr/UserConfigs/UserSettings.conf +++ b/config/hypr/UserConfigs/UserSettings.conf @@ -64,8 +64,7 @@ input { } gestures { - workspace_swipe = true - workspace_swipe_fingers = 3 + gesture = 3, horizontal, workspace workspace_swipe_distance = 500 workspace_swipe_invert = true workspace_swipe_min_speed_to_force = 30 @@ -88,6 +87,7 @@ misc { middle_click_paste = false enable_anr_dialog = true # Application not Responding (ANR) anr_missed_pings = 15 # ANR Threshold default 1 is too low + allow_session_lock_restore = true # Prevent lockscreen crash when resume from suspend } #opengl { diff --git a/config/hypr/UserScripts/WallpaperAutoChange.sh b/config/hypr/UserScripts/WallpaperAutoChange.sh index f54620bb..a6d2cedd 100755 --- a/config/hypr/UserScripts/WallpaperAutoChange.sh +++ b/config/hypr/UserScripts/WallpaperAutoChange.sh @@ -31,7 +31,10 @@ while true; do done \ | sort -n | cut -d':' -f2- \ | while read -r img; do - swww img -o $focused_monitor "$img" + swww img -o $focused_monitor "$img" + # Regenerate colors from the exact image path to avoid cache races + $HOME/.config/hypr/scripts/WallustSwww.sh "$img" + # Refresh UI components that depend on wallust output $wallust_refresh sleep $INTERVAL diff --git a/config/hypr/UserScripts/WallpaperSelect.sh b/config/hypr/UserScripts/WallpaperSelect.sh index a6e6c4d4..a08b53ce 100755 --- a/config/hypr/UserScripts/WallpaperSelect.sh +++ b/config/hypr/UserScripts/WallpaperSelect.sh @@ -168,8 +168,8 @@ apply_image_wallpaper() { swww img -o "$focused_monitor" "$image_path" $SWWW_PARAMS - # Run additional scripts - "$SCRIPTSDIR/WallustSwww.sh" + # Run additional scripts (pass the image path to avoid cache race conditions) + "$SCRIPTSDIR/WallustSwww.sh" "$image_path" sleep 2 "$SCRIPTSDIR/Refresh.sh" sleep 1 diff --git a/config/hypr/UserScripts/Weather.py b/config/hypr/UserScripts/Weather.py index efa96813..a71fe8ca 100755 --- a/config/hypr/UserScripts/Weather.py +++ b/config/hypr/UserScripts/Weather.py @@ -1,15 +1,70 @@ #!/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 +from datetime import datetime + +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 ~/.config/hypr/UserScripts/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,39 +77,519 @@ 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", +} + + +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"] + + +def wmo_to_status(code: int) -> str: + return WMO_STATUS.get(code, "Unknown") + + +# =============== Utilities =============== + +def esc(s: Optional[str]) -> str: + return html.escape(s, quote=False) if s else "" + +def log_debug(msg: str) -> None: + if DEBUG: + print(msg, file=sys.stderr) + +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) + + +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 + + +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,weather_code", + "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) + + unavailable = False + if code == -1: + try: + times: List[str] = safe_get(forecast, "hourly", "time", default=[]) or [] + codes: List[Optional[int]] = safe_get(forecast, "hourly", "weather_code", default=[]) or [] + cur_time: Optional[str] = safe_get(forecast, "current", "time") + + idx = 0 + if cur_time and times: + try: + ct = datetime.fromisoformat(cur_time) + diffs = [] + for t in times: + try: + diffs.append(abs((datetime.fromisoformat(t) - ct).total_seconds())) + except Exception: + diffs.append(float("inf")) + idx = min(range(len(diffs)), key=lambda i: diffs[i]) if diffs else 0 + except Exception: + idx = times.index(cur_time) if cur_time in times else 0 + + hcode = None + if isinstance(codes, list) and codes: + if idx < len(codes) and isinstance(codes[idx], (int, float)): + hcode = int(codes[idx]) + else: + for c in codes: + if isinstance(c, (int, float)): + hcode = int(c) + break + if isinstance(hcode, int): + code = hcode + log_debug(f"Fallback hourly weather_code used: code={code} idx={idx} cur_time={cur_time}") + except Exception as e: + log_debug(f"Hourly code fallback failed: {e}") + + if not isinstance(code, int) or code < 0: + unavailable = True + log_debug("Weather code invalid; setting status to 'Condition Unavailable'") + + if unavailable: + icon = WEATHER_ICONS["default"] + status = "Condition Unavailable" + code_for_class = "unavailable" + else: + icon = wmo_to_icon(code, is_day) + status = wmo_to_status(code) + code_for_class = f"wmo-{code} {'day' if is_day else 'night'}" + + # 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 "" -# 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]) + 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 "" -# Get latitude and longitude -latitude, longitude = get_location() + # 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}" -# Open-Meteo API endpoint -url = f"https://weather.com/en-PH/weather/today/l/{latitude},{longitude}" + # 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'<span size="xx-large">{esc(temp_str)}</span>', + f"<big> {icon}</big>", + f"<b>{esc(status)}</b>", + esc(location_text), + f"<small>{esc(feels_str)}</small>" if feels_str else "", + f"<b>{esc(min_max)}</b>" if min_max else "", + f"{esc(wind_text)}\t{esc(humidity_text)}", + f"{esc(visibility_text)}\t{esc(aqi_text)}", + f"<i> {esc(prediction)}</i>" 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]) -# 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 + out_data = { + "text": f"{icon} {temp_str}", + "alt": status, + "tooltip": tooltip_text, + "class": code_for_class, + } -# 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 + 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" + ) -html_data = PyQuery(url=url) + return out_data, simple_weather -# current temperature -temp = html_data("span[data-testid='TemperatureValue']").eq(0).text() -# current status phrase +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)) + + +if __name__ == "__main__": + main() status = html_data("div[data-testid='wxPhrase']").text() status = f"{status[:16]}.." if len(status) > 17 else status @@ -88,7 +623,7 @@ temp_max = ( temp_min_max = f"๏ {temp_min}\t\t๏ {temp_max}" # wind speed -wind_speed = str(html_data("span[data-testid='Wind'] > span").text()) +wind_speed = html_data("span[data-testid='Wind']").text().split("\n")[1] wind_text = f"๎พ {wind_speed}" # humidity @@ -128,19 +663,471 @@ out_data = { "alt": status, "tooltip": tooltip_text, "class": status_code, +======= +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", +>>>>>>> 2a5a7c5 (Weather.py: switch to Open-Meteo; add caching, reverse geocoding, robust geolocation, and config options) } -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" -) -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}") +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"] + + +def wmo_to_status(code: int) -> str: + return WMO_STATUS.get(code, "Unknown") + + +# =============== Utilities =============== + +def esc(s: Optional[str]) -> str: + return html.escape(s, quote=False) if s else "" + +def log_debug(msg: str) -> None: + if DEBUG: + print(msg, file=sys.stderr) + +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) + + +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 + + +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'<span size="xx-large">{esc(temp_str)}</span>', + f"<big> {icon}</big>", + f"<b>{esc(status)}</b>", + esc(location_text), + f"<small>{esc(feels_str)}</small>" if feels_str else "", + f"<b>{esc(min_max)}</b>" if min_max else "", + f"{esc(wind_text)}\t{esc(humidity_text)}", + f"{esc(visibility_text)}\t{esc(aqi_text)}", + f"<i> {esc(prediction)}</i>" 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)) + + +if __name__ == "__main__": + main() diff --git a/config/hypr/initial-boot.sh b/config/hypr/initial-boot.sh index 7f92ce7d..5b49cb6d 100755 --- a/config/hypr/initial-boot.sh +++ b/config/hypr/initial-boot.sh @@ -10,7 +10,7 @@ # Variables scriptsDir=$HOME/.config/hypr/scripts wallpaper=$HOME/.config/hypr/wallpaper_effects/.wallpaper_current -waybar_style="$HOME/.config/waybar/style/[Extra] Modern-Combined - Transparent.css" +waybar_style="$HOME/.config/waybar/style/[Extra] Neon Circuit.css" kvantum_theme="catppuccin-mocha-blue" color_scheme="prefer-dark" gtk_theme="Flat-Remix-GTK-Blue-Dark" diff --git a/config/hypr/scripts/Dropterminal.sh b/config/hypr/scripts/Dropterminal.sh index f1bfe0a5..4833545c 100755 --- a/config/hypr/scripts/Dropterminal.sh +++ b/config/hypr/scripts/Dropterminal.sh @@ -1,5 +1,8 @@ #!/bin/bash # /* ---- ๐ซ https://github.com/JaKooLit ๐ซ ---- */ ## +# +# Made and brought to by Kiran George +# /* -- โจ https://github.com/SherLock707 โจ -- */ ## # Dropdown Terminal # Usage: ./Dropdown.sh [-d] <terminal_command> # Example: ./Dropdown.sh foot @@ -14,8 +17,7 @@ ADDR_FILE="/tmp/dropdown_terminal_addr" # Dropdown size and position configuration (percentages) WIDTH_PERCENT=50 # Width as percentage of screen width HEIGHT_PERCENT=50 # Height as percentage of screen height -X_PERCENT=25 # X position as percentage from left (25% centers a 50% width window) -Y_PERCENT=5 # Y position as percentage from top +Y_PERCENT=5 # Y position as percentage from top (X is auto-centered) # Animation settings ANIMATION_DURATION=100 # milliseconds @@ -49,8 +51,8 @@ if [ -z "$TERMINAL_CMD" ]; then echo "Edit the script to modify size and position:" echo " WIDTH_PERCENT - Width as percentage of screen (default: 50)" echo " HEIGHT_PERCENT - Height as percentage of screen (default: 50)" - echo " X_PERCENT - X position from left as percentage (default: 25)" echo " Y_PERCENT - Y position from top as percentage (default: 5)" + echo " Note: X position is automatically centered" exit 1 fi @@ -117,26 +119,82 @@ animate_slide_up() { debug_echo "Slide up animation completed" } -# Function to get monitor info for centering +# Function to get monitor info including scale and name of focused monitor get_monitor_info() { - hyprctl monitors -j | jq -r '.[] | select(.focused == true) | "\(.x) \(.y) \(.width) \(.height)"' + local monitor_data=$(hyprctl monitors -j | jq -r '.[] | select(.focused == true) | "\(.x) \(.y) \(.width) \(.height) \(.scale) \(.name)"') + if [ -z "$monitor_data" ] || [[ "$monitor_data" =~ ^null ]]; then + debug_echo "Error: Could not get focused monitor information" + return 1 + fi + echo "$monitor_data" } -# Function to calculate dropdown position +# Function to calculate dropdown position with proper scaling and centering calculate_dropdown_position() { local monitor_info=$(get_monitor_info) + + if [ $? -ne 0 ] || [ -z "$monitor_info" ]; then + debug_echo "Error: Failed to get monitor info, using fallback values" + echo "100 100 800 600 fallback-monitor" + return 1 + fi + local mon_x=$(echo $monitor_info | cut -d' ' -f1) local mon_y=$(echo $monitor_info | cut -d' ' -f2) local mon_width=$(echo $monitor_info | cut -d' ' -f3) local mon_height=$(echo $monitor_info | cut -d' ' -f4) + local mon_scale=$(echo $monitor_info | cut -d' ' -f5) + local mon_name=$(echo $monitor_info | cut -d' ' -f6) + + debug_echo "Monitor info: x=$mon_x, y=$mon_y, width=$mon_width, height=$mon_height, scale=$mon_scale" + + # Validate scale value and provide fallback + if [ -z "$mon_scale" ] || [ "$mon_scale" = "null" ] || [ "$mon_scale" = "0" ]; then + debug_echo "Invalid scale value, using 1.0 as fallback" + mon_scale="1.0" + fi + + # Calculate logical dimensions by dividing physical dimensions by scale + local logical_width logical_height + if command -v bc >/dev/null 2>&1; then + # Use bc for precise floating point calculation + logical_width=$(echo "scale=0; $mon_width / $mon_scale" | bc | cut -d'.' -f1) + logical_height=$(echo "scale=0; $mon_height / $mon_scale" | bc | cut -d'.' -f1) + else + # Fallback to integer math (multiply by 100 for precision, then divide) + local scale_int=$(echo "$mon_scale" | sed 's/\.//' | sed 's/^0*//') + if [ -z "$scale_int" ]; then scale_int=100; fi + + logical_width=$(((mon_width * 100) / scale_int)) + logical_height=$(((mon_height * 100) / scale_int)) + fi + + # Ensure we have valid integer values + if ! [[ "$logical_width" =~ ^-?[0-9]+$ ]]; then logical_width=$mon_width; fi + if ! [[ "$logical_height" =~ ^-?[0-9]+$ ]]; then logical_height=$mon_height; fi + + debug_echo "Physical resolution: ${mon_width}x${mon_height}" + debug_echo "Logical resolution: ${logical_width}x${logical_height} (physical รท scale)" + + # Calculate window dimensions based on LOGICAL space percentages + local width=$((logical_width * WIDTH_PERCENT / 100)) + local height=$((logical_height * HEIGHT_PERCENT / 100)) - # Calculate position and size based on percentages - local width=$((mon_width * WIDTH_PERCENT / 100)) - local height=$((mon_height * HEIGHT_PERCENT / 100)) - local x=$((mon_x + (mon_width * X_PERCENT / 100))) - local y=$((mon_y + (mon_height * Y_PERCENT / 100))) + # Calculate Y position from top based on percentage of LOGICAL height + local y_offset=$((logical_height * Y_PERCENT / 100)) - echo "$x $y $width $height" + # Calculate centered X position in LOGICAL space + local x_offset=$(((logical_width - width) / 2)) + + # Apply monitor offset to get final positions in logical coordinates + local final_x=$((mon_x + x_offset)) + local final_y=$((mon_y + y_offset)) + + debug_echo "Window size: ${width}x${height} (logical pixels)" + debug_echo "Final position: x=$final_x, y=$final_y (logical coordinates)" + debug_echo "Hyprland will scale these to physical coordinates automatically" + + echo "$final_x $final_y $width $height $mon_name" } # Get the current workspace @@ -145,7 +203,14 @@ CURRENT_WS=$(hyprctl activeworkspace -j | jq -r '.id') # Function to get stored terminal address get_terminal_address() { if [ -f "$ADDR_FILE" ] && [ -s "$ADDR_FILE" ]; then - cat "$ADDR_FILE" + cut -d' ' -f1 "$ADDR_FILE" + fi +} + +# Function to get stored monitor name +get_terminal_monitor() { + if [ -f "$ADDR_FILE" ] && [ -s "$ADDR_FILE" ]; then + cut -d' ' -f2- "$ADDR_FILE" fi } @@ -174,17 +239,22 @@ spawn_terminal() { debug_echo "Creating new dropdown terminal with command: $TERMINAL_CMD" # Calculate dropdown position for later use - pos_info=$(calculate_dropdown_position) - target_x=$(echo $pos_info | cut -d' ' -f1) - target_y=$(echo $pos_info | cut -d' ' -f2) - width=$(echo $pos_info | cut -d' ' -f3) - height=$(echo $pos_info | cut -d' ' -f4) + local pos_info=$(calculate_dropdown_position) + if [ $? -ne 0 ]; then + debug_echo "Warning: Using fallback positioning" + fi - debug_echo "Target position: ${target_x}x${target_y}, size: ${width}x${height}" + local target_x=$(echo $pos_info | cut -d' ' -f1) + local target_y=$(echo $pos_info | cut -d' ' -f2) + local width=$(echo $pos_info | cut -d' ' -f3) + local height=$(echo $pos_info | cut -d' ' -f4) + local monitor_name=$(echo $pos_info | cut -d' ' -f5) + + debug_echo "Target position: ${target_x},${target_y}, size: ${width}x${height}" # Get window count before spawning - windows_before=$(hyprctl clients -j) - count_before=$(echo "$windows_before" | jq 'length') + local windows_before=$(hyprctl clients -j) + local count_before=$(echo "$windows_before" | jq 'length') # Launch terminal directly in special workspace to avoid visible spawn hyprctl dispatch exec "[float; size $width $height; workspace special:scratchpad silent] $TERMINAL_CMD" @@ -193,10 +263,10 @@ spawn_terminal() { sleep 0.1 # Get windows after spawning - windows_after=$(hyprctl clients -j) - count_after=$(echo "$windows_after" | jq 'length') + local windows_after=$(hyprctl clients -j) + local count_after=$(echo "$windows_after" | jq 'length') - new_addr="" + local new_addr="" if [ "$count_after" -gt "$count_before" ]; then # Find the new window by comparing before/after lists @@ -212,9 +282,9 @@ spawn_terminal() { fi if [ -n "$new_addr" ] && [ "$new_addr" != "null" ]; then - # Store the address - echo "$new_addr" > "$ADDR_FILE" - debug_echo "Terminal created with address: $new_addr in special workspace" + # Store the address and monitor name + echo "$new_addr $monitor_name" > "$ADDR_FILE" + debug_echo "Terminal created with address: $new_addr in special workspace on monitor $monitor_name" # Small delay to ensure it's properly in special workspace sleep 0.2 @@ -236,6 +306,23 @@ spawn_terminal() { if terminal_exists; then TERMINAL_ADDR=$(get_terminal_address) debug_echo "Found existing terminal: $TERMINAL_ADDR" + focused_monitor=$(get_monitor_info | awk '{print $6}') + dropdown_monitor=$(get_terminal_monitor) + if [ "$focused_monitor" != "$dropdown_monitor" ]; then + debug_echo "Monitor focus changed: moving dropdown to $focused_monitor" + # Calculate new position for focused monitor + pos_info=$(calculate_dropdown_position) + target_x=$(echo $pos_info | cut -d' ' -f1) + target_y=$(echo $pos_info | cut -d' ' -f2) + width=$(echo $pos_info | cut -d' ' -f3) + height=$(echo $pos_info | cut -d' ' -f4) + monitor_name=$(echo $pos_info | cut -d' ' -f5) + # Move and resize window + hyprctl dispatch movewindowpixel "exact $target_x $target_y,address:$TERMINAL_ADDR" + hyprctl dispatch resizewindowpixel "exact $width $height,address:$TERMINAL_ADDR" + # Update ADDR_FILE + echo "$TERMINAL_ADDR $monitor_name" > "$ADDR_FILE" + fi if terminal_in_special; then debug_echo "Bringing terminal from scratchpad with slide down animation" diff --git a/config/hypr/scripts/RefreshNoWaybar.sh b/config/hypr/scripts/RefreshNoWaybar.sh index f950db51..8454124e 100755 --- a/config/hypr/scripts/RefreshNoWaybar.sh +++ b/config/hypr/scripts/RefreshNoWaybar.sh @@ -31,8 +31,9 @@ done # quit quickshell & relaunch quickshell #pkill qs && qs & -# Wallust refresh -${SCRIPTSDIR}/WallustSwww.sh & +# Wallust refresh (synchronous to ensure colors are ready) +${SCRIPTSDIR}/WallustSwww.sh +sleep 0.2 # reload swaync swaync-client --reload-config diff --git a/config/hypr/scripts/WallustSwww.sh b/config/hypr/scripts/WallustSwww.sh index 62dde375..5a0bc491 100755 --- a/config/hypr/scripts/WallustSwww.sh +++ b/config/hypr/scripts/WallustSwww.sh @@ -1,39 +1,58 @@ #!/bin/bash # /* ---- ๐ซ https://github.com/JaKooLit ๐ซ ---- */ ## -# Wallust Colors for current wallpaper +# Wallust: derive colors from the current wallpaper and update templates +# Usage: WallustSwww.sh [absolute_path_to_wallpaper] -# Define the path to the swww cache directory +set -euo pipefail + +# Inputs and paths +passed_path="${1:-}" cache_dir="$HOME/.cache/swww/" +rofi_link="$HOME/.config/rofi/.current_wallpaper" +wallpaper_current="$HOME/.config/hypr/wallpaper_effects/.wallpaper_current" -# Get a list of monitor outputs -monitor_outputs=($(ls "$cache_dir")) +# Helper: get focused monitor name (prefer JSON) +get_focused_monitor() { + if command -v jq >/dev/null 2>&1; then + hyprctl monitors -j | jq -r '.[] | select(.focused) | .name' + else + hyprctl monitors | awk '/^Monitor/{name=$2} /focused: yes/{print name}' + fi +} -# Initialize a flag to determine if the ln command was executed -ln_success=false +# Determine wallpaper_path +wallpaper_path="" +if [[ -n "$passed_path" && -f "$passed_path" ]]; then + wallpaper_path="$passed_path" +else + # Try to read from swww cache for the focused monitor, with a short retry loop + current_monitor="$(get_focused_monitor)" + cache_file="$cache_dir$current_monitor" -# Get current focused monitor -current_monitor=$(hyprctl monitors | awk '/^Monitor/{name=$2} /focused: yes/{print name}') -echo $current_monitor -# Construct the full path to the cache file -cache_file="$cache_dir$current_monitor" -echo $cache_file -# Check if the cache file exists for the current monitor output -if [ -f "$cache_file" ]; then - # Get the wallpaper path from the cache file - wallpaper_path=$(grep -v 'Lanczos3' "$cache_file" | head -n 1) - echo $wallpaper_path - # symlink the wallpaper to the location Rofi can access - if ln -sf "$wallpaper_path" "$HOME/.config/rofi/.current_wallpaper"; then - ln_success=true # Set the flag to true upon successful execution + # Wait briefly for swww to write its cache after an image change + for i in {1..10}; do + if [[ -f "$cache_file" ]]; then + break fi - # copy the wallpaper for wallpaper effects - cp -r "$wallpaper_path" "$HOME/.config/hypr/wallpaper_effects/.wallpaper_current" + sleep 0.1 + done + + if [[ -f "$cache_file" ]]; then + # The first non-filter line is the original wallpaper path + wallpaper_path="$(grep -v 'Lanczos3' "$cache_file" | head -n 1)" + fi fi -# Check the flag before executing further commands -if [ "$ln_success" = true ]; then - # execute wallust - echo 'about to execute wallust' - # execute wallust skipping tty and terminal changes - wallust run "$wallpaper_path" -s & +if [[ -z "${wallpaper_path:-}" || ! -f "$wallpaper_path" ]]; then + # Nothing to do; avoid failing loudly so callers can continue + exit 0 fi + +# Update helpers that depend on the path +ln -sf "$wallpaper_path" "$rofi_link" || true +mkdir -p "$(dirname "$wallpaper_current")" +cp -f "$wallpaper_path" "$wallpaper_current" || true + +# Run wallust (silent) to regenerate templates defined in ~/.config/wallust/wallust.toml +# -s is used in this repo to keep things quiet and avoid extra prompts +wallust run -s "$wallpaper_path" || true diff --git a/config/hypr/scripts/WaybarCava.sh b/config/hypr/scripts/WaybarCava.sh index d31a05b5..6809e60e 100755 --- a/config/hypr/scripts/WaybarCava.sh +++ b/config/hypr/scripts/WaybarCava.sh @@ -1,26 +1,42 @@ -#!/bin/bash -# /* ---- ๐ซ https://github.com/JaKooLit ๐ซ ---- */ ## -# Not my own work. This was added through Github PR. Credit to original author +#!/usr/bin/env bash +# WaybarCava.sh โ safer single-instance handling, cleanup, and robustness +# Original concept by JaKooLit; this variant focuses on lifecycle hardening. -#----- Optimized bars animation without much CPU usage increase -------- +set -euo pipefail + +# Ensure cava exists +if ! command -v cava >/dev/null 2>&1; then + echo "cava not found in PATH" >&2 + exit 1 +fi + +# 0..7 โ โโโโโ
โโโ bar="โโโโโ
โโโ" dict="s/;//g" - -# Calculate the length of the bar outside the loop bar_length=${#bar} - -# Create dictionary to replace char with bar for ((i = 0; i < bar_length; i++)); do - dict+=";s/$i/${bar:$i:1}/g" + dict+=";s/$i/${bar:$i:1}/g" done -# Create cava config -config_file="/tmp/bar_cava_config" +# Single-instance guard (only kill our previous instance if itโs still alive) +RUNTIME_DIR="${XDG_RUNTIME_DIR:-/tmp}" +pidfile="$RUNTIME_DIR/waybar-cava.pid" +if [[ -f "$pidfile" ]]; then + oldpid="$(cat "$pidfile" || true)" + if [[ -n "$oldpid" ]] && kill -0 "$oldpid" 2>/dev/null; then + kill "$oldpid" 2>/dev/null || true + sleep 0.1 || true + fi +fi +printf '%d' $$ >"$pidfile" + +# Unique temp config + cleanup on exit +config_file="$(mktemp "$RUNTIME_DIR/waybar-cava.XXXXXX.conf")" +cleanup() { rm -f "$config_file" "$pidfile"; } +trap cleanup EXIT INT TERM + cat >"$config_file" <<EOF [general] -# Older systems show significant CPU use with default framerate -# Setting maximum framerate to 30 -# You can increase the value if you wish framerate = 30 bars = 10 @@ -35,8 +51,5 @@ data_format = ascii ascii_max_range = 7 EOF -# Kill cava if it's already running -pkill -f "cava -p $config_file" - -# Read stdout from cava and perform substitution in a single sed command -cava -p "$config_file" | sed -u "$dict" +# Stream cava output and translate digits 0..7 to bar glyphs +exec cava -p "$config_file" | sed -u "$dict" diff --git a/config/hypr/v2.3.17 b/config/hypr/v2.3.17 new file mode 100644 index 00000000..31b3414d --- /dev/null +++ b/config/hypr/v2.3.17 @@ -0,0 +1,5 @@ +### https://github.com/JaKooLit ### +## https://github.com/JaKooLit/Hyprland-Dots +## This is to have a reference of which version would be + +## note that this will always be higher than the released versions
\ No newline at end of file |
