aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--config/hypr/UserConfigs/UserKeybinds.conf5
-rw-r--r--config/hypr/UserConfigs/UserSettings.conf4
-rwxr-xr-xconfig/hypr/UserScripts/WallpaperAutoChange.sh5
-rwxr-xr-xconfig/hypr/UserScripts/WallpaperSelect.sh4
-rwxr-xr-xconfig/hypr/UserScripts/Weather.py1075
-rwxr-xr-xconfig/hypr/initial-boot.sh2
-rwxr-xr-xconfig/hypr/scripts/Dropterminal.sh141
-rwxr-xr-xconfig/hypr/scripts/RefreshNoWaybar.sh5
-rwxr-xr-xconfig/hypr/scripts/WallustSwww.sh75
-rwxr-xr-xconfig/hypr/scripts/WaybarCava.sh51
-rw-r--r--config/hypr/v2.3.175
-rwxr-xr-xcopy.sh2
12 files changed, 1247 insertions, 127 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
diff --git a/copy.sh b/copy.sh
index 50090c0f..f6f736dc 100755
--- a/copy.sh
+++ b/copy.sh
@@ -3,7 +3,7 @@
clear
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"
waybar_config="$HOME/.config/waybar/configs/[TOP] Default"
waybar_config_laptop="$HOME/.config/waybar/configs/[TOP] Default Laptop"
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage