more tidying, fixed waybar, added comfyui to eva-03
This commit is contained in:
@@ -0,0 +1,719 @@
|
||||
#!/usr/bin/env python3
|
||||
# /* ---- 💫 https://github.com/JaKooLit 💫 ---- */ #
|
||||
# Rewritten to use Open-Meteo APIs (worldwide, no API key) for robust weather data.
|
||||
# Outputs Waybar-compatible JSON and a simple text cache.
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import html
|
||||
import subprocess
|
||||
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/waybar/scripts/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 imperial for ddubsos branch)
|
||||
UNITS = os.getenv("WEATHER_UNITS", "imperial").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
|
||||
|
||||
SESSION = requests.Session()
|
||||
SESSION.headers.update({"User-Agent": UA})
|
||||
|
||||
# =============== Icon and status mapping ===============
|
||||
# Icon style: 'emoji' (colorful), 'nerd' (mono glyphs), or 'gnome' (symbolic names, useful for other bars)
|
||||
ICON_STYLE = os.getenv("WEATHER_ICON_STYLE", "emoji").strip().lower() # emoji|nerd|gnome
|
||||
|
||||
# Nerd Font weather glyphs (mono)
|
||||
WEATHER_ICONS_NERD = {
|
||||
"sunnyDay": "",
|
||||
"clearNight": "",
|
||||
"cloudyFoggyDay": "",
|
||||
"cloudyFoggyNight": "",
|
||||
"rainyDay": "",
|
||||
"rainyNight": "",
|
||||
"snowyIcyDay": "",
|
||||
"snowyIcyNight": "",
|
||||
"severe": "",
|
||||
"default": "",
|
||||
}
|
||||
|
||||
# Colored emoji set (renders with system emoji font, gives the "3D" look)
|
||||
WEATHER_ICONS_EMOJI = {
|
||||
"sunnyDay": "☀️",
|
||||
"clearNight": "🌙",
|
||||
"cloudyFoggyDay": "⛅️",
|
||||
"cloudyFoggyNight": "☁️🌙",
|
||||
"rainyDay": "🌦️",
|
||||
"rainyNight": "🌧️",
|
||||
"snowyIcyDay": "🌨️",
|
||||
"snowyIcyNight": "❄️🌙",
|
||||
"severe": "⛈️",
|
||||
"default": "🌥️",
|
||||
}
|
||||
|
||||
# GNOME symbolic icon names (not used directly by Waybar, here for parity)
|
||||
WEATHER_ICONS_GNOME = {
|
||||
"sunnyDay": "Sun-symbolic",
|
||||
"clearNight": "Moon-symbolic",
|
||||
"cloudyFoggyDay": "CloudSun-symbolic",
|
||||
"cloudyFoggyNight": "CloudyMoon-symbolic",
|
||||
"rainyDay": "CloudRain-symbolic",
|
||||
"rainyNight": "CloudRain-symbolic",
|
||||
"snowyIcyDay": "CloudSnowfall-symbolic",
|
||||
"snowyIcyNight": "CloudSnowfall-symbolic",
|
||||
"severe": "CloudBolt-symbolic",
|
||||
"default": "Cloud-symbolic",
|
||||
}
|
||||
|
||||
# Optional: allow disabling automatic fallback via env
|
||||
DISABLE_EMOJI_FALLBACK = os.getenv("WEATHER_DISABLE_EMOJI_FALLBACK", "0").lower() in ("1", "true", "yes")
|
||||
|
||||
# Basic detection of color emoji font availability via fontconfig (fc-list)
|
||||
EMOJI_FONT_CANDIDATES = [
|
||||
"Noto Color Emoji",
|
||||
"Noto Emoji",
|
||||
"Apple Color Emoji",
|
||||
"Segoe UI Emoji",
|
||||
"Twitter Color Emoji",
|
||||
"Twemoji",
|
||||
"JoyPixels",
|
||||
]
|
||||
|
||||
def has_color_emoji_font() -> bool:
|
||||
try:
|
||||
# Quick check: list installed fonts; search for any candidate name
|
||||
proc = subprocess.run(
|
||||
["fc-list"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
text=True,
|
||||
timeout=1.5,
|
||||
check=False,
|
||||
)
|
||||
out = proc.stdout or ""
|
||||
out_lower = out.lower()
|
||||
for name in EMOJI_FONT_CANDIDATES:
|
||||
if name.lower() in out_lower:
|
||||
return True
|
||||
except Exception:
|
||||
# If detection fails, don't block emoji; assume not available to be safe on minimal setups
|
||||
return False
|
||||
return False
|
||||
|
||||
# Determine the active icon style with fallback from emoji -> nerd when no color emoji fonts are present
|
||||
if ICON_STYLE == "emoji" and not DISABLE_EMOJI_FALLBACK and not has_color_emoji_font():
|
||||
ACTIVE_ICON_STYLE = "nerd"
|
||||
else:
|
||||
ACTIVE_ICON_STYLE = ICON_STYLE
|
||||
|
||||
# Choose active set
|
||||
WEATHER_ICONS = (
|
||||
WEATHER_ICONS_EMOJI if ACTIVE_ICON_STYLE == "emoji" else WEATHER_ICONS_NERD if ACTIVE_ICON_STYLE == "nerd" else WEATHER_ICONS_GNOME
|
||||
)
|
||||
|
||||
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 temp_color(temp_val: Optional[float]) -> str:
|
||||
"""Choose a color for the temperature number based on value and unit set."""
|
||||
if not isinstance(temp_val, (int, float)):
|
||||
return "#cdd6f4" # default text
|
||||
# Work in Fahrenheit for thresholds if imperial, else Celsius
|
||||
t = float(temp_val)
|
||||
if UNITS == "metric":
|
||||
if t <= -5:
|
||||
return "#89dceb" # sky
|
||||
if t <= 5:
|
||||
return "#74c7ec" # sapphire
|
||||
if t <= 15:
|
||||
return "#a6e3a1" # green
|
||||
if t <= 25:
|
||||
return "#f9e2af" # yellow
|
||||
if t <= 32:
|
||||
return "#fab387" # peach
|
||||
return "#f38ba8" # red
|
||||
else:
|
||||
if t <= 25:
|
||||
return "#89dceb"
|
||||
if t <= 41:
|
||||
return "#74c7ec"
|
||||
if t <= 60:
|
||||
return "#a6e3a1"
|
||||
if t <= 77:
|
||||
return "#f9e2af"
|
||||
if t <= 90:
|
||||
return "#fab387"
|
||||
return "#f38ba8"
|
||||
|
||||
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"
|
||||
temp_col = temp_color(temp_val)
|
||||
|
||||
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
|
||||
|
||||
# Fallbacks: if current weather_code is missing/invalid
|
||||
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:
|
||||
# Find nearest index by absolute time difference
|
||||
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:
|
||||
# If parsing fails, try exact match fallback
|
||||
idx = times.index(cur_time) if cur_time in times else 0
|
||||
|
||||
# Prefer nearest code if valid, else first non-null code in window
|
||||
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}")
|
||||
|
||||
# Final fallback: no valid code at all
|
||||
if not isinstance(code, int) or code < 0:
|
||||
unavailable = True
|
||||
log_debug("Weather code invalid; setting status to 'Condition Unavailable'")
|
||||
|
||||
# Compute icon/status (override if 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 ""
|
||||
|
||||
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" foreground="{temp_col}">{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])
|
||||
|
||||
# Build main text with colorful icon and colored temp; prefer larger icon if emoji
|
||||
if ACTIVE_ICON_STYLE == "emoji":
|
||||
icon_text = f"<span size=\"x-large\">{icon}</span>"
|
||||
else:
|
||||
# Colorize mono icons subtly
|
||||
icon_text = f"<span foreground=\"{temp_col}\">{icon}</span>"
|
||||
|
||||
temp_text = f"<span foreground=\"{temp_col}\">{esc(temp_str)}</span>"
|
||||
|
||||
out_data = {
|
||||
"text": f"{icon_text} {temp_text}",
|
||||
"alt": status,
|
||||
"tooltip": tooltip_text,
|
||||
"class": code_for_class,
|
||||
}
|
||||
|
||||
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()
|
||||
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Original script by Eric Murphy
|
||||
# https://github.com/ericmurphyxyz/dotfiles/blob/master/.local/bin/battery-alert
|
||||
#
|
||||
# Modified by Jesse Mirabel (@sejjy)
|
||||
# https://github.com/sejjy/mechabar
|
||||
|
||||
# This script sends a notification when the battery is full, low, or critical.
|
||||
# icon theme used: tela-circle-icon-theme-dracula
|
||||
#
|
||||
# (see the bottom of the script for more information)
|
||||
|
||||
export DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/1000/bus"
|
||||
|
||||
# battery levels
|
||||
WARNING_LEVEL=20
|
||||
CRITICAL_LEVEL=10
|
||||
|
||||
# get the battery state and percentage using upower (waybar dependency)
|
||||
BAT_PATH=$(upower -e | grep BAT | head -n 1)
|
||||
BATTERY_STATE=$(upower -i "$BAT_PATH" | awk '/state:/ {print $2}')
|
||||
BATTERY_LEVEL=$(upower -i "$BAT_PATH" | awk '/percentage:/ {print $2}' | tr -d '%')
|
||||
|
||||
# prevent multiple notifications
|
||||
FILE_FULL=/tmp/battery-full
|
||||
FILE_WARNING=/tmp/battery-warning
|
||||
FILE_CRITICAL=/tmp/battery-critical
|
||||
|
||||
# remove the files if the battery is no longer in that state
|
||||
if [ "$BATTERY_STATE" == "discharging" ]; then
|
||||
rm -f $FILE_FULL
|
||||
elif [ "$BATTERY_STATE" == "charging" ]; then
|
||||
rm -f "$FILE_WARNING" "$FILE_CRITICAL"
|
||||
fi
|
||||
|
||||
# if the battery is full and is plugged in
|
||||
if [ "$BATTERY_LEVEL" -eq 100 ] && [ "$BATTERY_STATE" == "fully-charged" ] && [ ! -f $FILE_FULL ]; then
|
||||
notify-send -a "state" "Battery Charged (${BATTERY_LEVEL}%)" "You might want to unplug your PC." -i "battery-full" -r 9991
|
||||
touch $FILE_FULL
|
||||
|
||||
# if the battery is low and is discharging
|
||||
elif [ "$BATTERY_LEVEL" -le $WARNING_LEVEL ] && [ "$BATTERY_STATE" == "discharging" ] && [ ! -f $FILE_WARNING ]; then
|
||||
notify-send -a "state" "Battery Low (${BATTERY_LEVEL}%)" "You might want to plug in your PC." -u critical -i "battery-caution" -r 9991 -h string:fgcolor:\#fab387 -h string:frcolor:\#fab387
|
||||
touch $FILE_WARNING
|
||||
|
||||
# if the battery is critical and is discharging
|
||||
elif [ "$BATTERY_LEVEL" -le $CRITICAL_LEVEL ] && [ "$BATTERY_STATE" == "discharging" ] && [ ! -f $FILE_CRITICAL ]; then
|
||||
notify-send -a "state" "Battery Critical (${BATTERY_LEVEL}%)" "Plug in your PC now." -u critical -i "battery-empty" -r 9991
|
||||
touch $FILE_CRITICAL
|
||||
fi
|
||||
|
||||
# systemd service
|
||||
# Add the following to ~/.config/systemd/user/battery-level.service:
|
||||
|
||||
# [Unit]
|
||||
# Description=Battery Level Checker
|
||||
# After=graphical.target
|
||||
#
|
||||
# [Service]
|
||||
# ExecStart=%h/.config/waybar/scripts/battery-level.sh
|
||||
# Type=oneshot
|
||||
|
||||
# systemd timer
|
||||
# Add the following to ~/.config/systemd/user/battery-level.timer:
|
||||
|
||||
# [Unit]
|
||||
# Description=Run Battery Level Checker
|
||||
#
|
||||
# [Timer]
|
||||
# OnBootSec=1min
|
||||
# OnUnitActiveSec=1min
|
||||
# Unit=battery-level.service
|
||||
#
|
||||
# [Install]
|
||||
# WantedBy=timers.target
|
||||
|
||||
# enable the timer by running the following commands:
|
||||
# systemctl --user daemon-reload
|
||||
# systemctl --user enable --now battery-level.timer
|
||||
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Original script by Eric Murphy
|
||||
# https://github.com/ericmurphyxyz/dotfiles/blob/master/.local/bin/battery-alert
|
||||
#
|
||||
# Modified by Jesse Mirabel (@sejjy)
|
||||
# https://github.com/sejjy/mechabar
|
||||
|
||||
# This script sends a notification when the battery is charging or discharging.
|
||||
# icon theme used: tela-circle-icon-theme-dracula
|
||||
#
|
||||
# (see the bottom of the script for more information)
|
||||
|
||||
export DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/1000/bus"
|
||||
|
||||
# get the battery state from the udev rule
|
||||
BATTERY_STATE=$1
|
||||
|
||||
# get the battery percentage using upower (waybar dependency)
|
||||
BAT_PATH=$(upower -e | grep BAT | head -n 1)
|
||||
BATTERY_LEVEL=$(upower -i "$BAT_PATH" | awk '/percentage:/ {print $2}' | tr -d '%')
|
||||
|
||||
# set the battery charging state and icon
|
||||
case "$BATTERY_STATE" in
|
||||
"charging")
|
||||
BATTERY_CHARGING="Charging"
|
||||
BATTERY_ICON="090-charging"
|
||||
;;
|
||||
"discharging")
|
||||
BATTERY_CHARGING="Disharging"
|
||||
BATTERY_ICON="090"
|
||||
;;
|
||||
esac
|
||||
|
||||
# send the notification
|
||||
notify-send -a "state" "Battery ${BATTERY_CHARGING} (${BATTERY_LEVEL}%)" -u normal -i "battery-${BATTERY_ICON}" -r 9991
|
||||
|
||||
# udev rule
|
||||
# Add the following to /etc/udev/rules.d/60-power.rules:
|
||||
|
||||
# ACTION=="change", SUBSYSTEM=="power_supply", ATTR{type}=="Mains", ATTR{online}=="0", ENV{DISPLAY}=":0", RUN+="/usr/bin/su <username> -c '$HOME/.config/waybar/scripts/battery-state.sh discharging'"
|
||||
# ACTION=="change", SUBSYSTEM=="power_supply", ATTR{type}=="Mains", ATTR{online}=="1", ENV{DISPLAY}=":0", RUN+="/usr/bin/su <username> -c '$HOME/.config/waybar/scripts/battery-state.sh charging'"
|
||||
|
||||
# the number 60 in the udev rule can be changed to any number between 0 and 99.
|
||||
# the lower the number, the higher the priority.
|
||||
#
|
||||
# $USER does not work, so you have to replace "<username>" with your username.
|
||||
|
||||
# reload udev rules by running the following command:
|
||||
# sudo udevadm control --reload-rules
|
||||
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Print error message for invalid arguments
|
||||
print_error() {
|
||||
cat <<"EOF"
|
||||
Usage: ./brightnesscontrol.sh <action>
|
||||
Valid actions are:
|
||||
i -- <i>ncrease brightness [+2%]
|
||||
d -- <d>ecrease brightness [-2%]
|
||||
EOF
|
||||
}
|
||||
|
||||
# Send a notification with brightness info
|
||||
send_notification() {
|
||||
brightness=$(brightnessctl info | grep -oP "(?<=\()\d+(?=%)")
|
||||
notify-send -a "state" -r 91190 -i "gpm-brightness-lcd" -h int:value:"$brightness" "Brightness: ${brightness}%" -u low
|
||||
}
|
||||
|
||||
# Get the current brightness percentage and device name
|
||||
get_brightness() {
|
||||
brightness=$(brightnessctl -m | grep -o '[0-9]\+%' | head -c-2)
|
||||
device=$(brightnessctl -m | head -n 1 | awk -F',' '{print $1}' | sed 's/_/ /g; s/\<./\U&/g') # Get device name
|
||||
current_brightness=$(brightnessctl -m | head -n 1 | awk -F',' '{print $3}') # Get current brightness
|
||||
max_brightness=$(brightnessctl -m | head -n 1 | awk -F',' '{print $5}') # Get max brightness
|
||||
}
|
||||
get_brightness
|
||||
|
||||
# Handle options
|
||||
while getopts o: opt; do
|
||||
case "${opt}" in
|
||||
o)
|
||||
case $OPTARG in
|
||||
i) # Increase brightness
|
||||
if [[ $brightness -lt 10 ]]; then
|
||||
brightnessctl set +1%
|
||||
else
|
||||
brightnessctl set +2%
|
||||
fi
|
||||
send_notification
|
||||
;;
|
||||
d) # Decrease brightness
|
||||
if [[ $brightness -le 1 ]]; then
|
||||
brightnessctl set 1%
|
||||
elif [[ $brightness -le 10 ]]; then
|
||||
brightnessctl set 1%-
|
||||
else
|
||||
brightnessctl set 2%-
|
||||
fi
|
||||
send_notification
|
||||
;;
|
||||
*)
|
||||
print_error
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
*)
|
||||
print_error
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Determine the icon based on brightness level
|
||||
get_icon() {
|
||||
if ((brightness <= 5)); then
|
||||
icon=""
|
||||
elif ((brightness <= 15)); then
|
||||
icon=""
|
||||
elif ((brightness <= 30)); then
|
||||
icon=""
|
||||
elif ((brightness <= 45)); then
|
||||
icon=""
|
||||
elif ((brightness <= 55)); then
|
||||
icon=""
|
||||
elif ((brightness <= 65)); then
|
||||
icon=""
|
||||
elif ((brightness <= 80)); then
|
||||
icon=""
|
||||
elif ((brightness <= 95)); then
|
||||
icon=""
|
||||
else
|
||||
icon=""
|
||||
fi
|
||||
}
|
||||
|
||||
# Backlight module and tooltip
|
||||
get_icon
|
||||
module="${icon} ${brightness}%"
|
||||
|
||||
tooltip="Device Name: ${device}"
|
||||
tooltip+="\nBrightness: ${current_brightness} / ${max_brightness}"
|
||||
|
||||
echo "{\"text\": \"${module}\", \"tooltip\": \"${tooltip}\"}"
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
model=$(awk -F ': ' '/model name/{print $2}' /proc/cpuinfo | head -n 1 | sed 's/@.*//; s/ *\((R)\|(TM)\)//g; s/^[ \t]*//; s/[ \t]*$//')
|
||||
|
||||
# get CPU clock speeds
|
||||
get_cpu_frequency() {
|
||||
freqlist=$(awk '/cpu MHz/ {print $4}' /proc/cpuinfo)
|
||||
maxfreq=$(sed 's/...$//' /sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq)
|
||||
if [ -z "$freqlist" ] || [ -z "$maxfreq" ]; then
|
||||
echo "--"
|
||||
return
|
||||
fi
|
||||
average_freq=$(echo "$freqlist" | tr ' ' '\n' | awk "{sum+=\$1} END {printf \"%.0f/%s MHz\", sum/NR, $maxfreq}")
|
||||
echo "$average_freq"
|
||||
}
|
||||
|
||||
# get CPU temp
|
||||
get_cpu_temperature() {
|
||||
temp=$(sensors | awk '/Package id 0/ {print $4}' | awk -F '[+.]' '{print $2}')
|
||||
if [[ -z "$temp" ]]; then
|
||||
temp=$(sensors | awk '/Tctl/ {print $2}' | tr -d '+°C')
|
||||
fi
|
||||
if [[ -z "$temp" ]]; then
|
||||
temp="--"
|
||||
temp_f="--"
|
||||
else
|
||||
temp=${temp%.*}
|
||||
temp_f=$(awk "BEGIN {printf \"%.1f\", ($temp * 9 / 5) + 32}")
|
||||
fi
|
||||
# Celsius and Fahrenheit
|
||||
echo "${temp:---} ${temp_f:---}"
|
||||
}
|
||||
|
||||
get_temperature_icon() {
|
||||
temp_value=$1
|
||||
if [ "$temp_value" = "--" ]; then
|
||||
icon="" # none
|
||||
elif [ "$temp_value" -ge 80 ]; then
|
||||
icon="" # high
|
||||
elif [ "$temp_value" -ge 70 ]; then
|
||||
icon="" # medium
|
||||
elif [ "$temp_value" -ge 60 ]; then
|
||||
icon="" # normal
|
||||
else
|
||||
icon="" # low
|
||||
fi
|
||||
echo "$icon"
|
||||
}
|
||||
|
||||
cpu_frequency=$(get_cpu_frequency)
|
||||
read -r temp_info < <(get_cpu_temperature)
|
||||
temp=$(echo "$temp_info" | awk '{print $1}')
|
||||
temp_f=$(echo "$temp_info" | awk '{print $2}')
|
||||
thermo_icon=$(get_temperature_icon "$temp")
|
||||
|
||||
# high temp warning
|
||||
if [ "$temp" == "--" ] || [ "$temp" -ge 80 ]; then
|
||||
text_output="<span color='#f38ba8'>${thermo_icon} ${temp}°C</span>"
|
||||
else
|
||||
text_output="${thermo_icon} ${temp}°C"
|
||||
fi
|
||||
|
||||
tooltip=":: ${model}\n"
|
||||
tooltip+="Clock Speed: ${cpu_frequency}\nTemperature: ${temp_f}°F"
|
||||
|
||||
# module and tooltip
|
||||
echo "{\"text\": \"$text_output\", \"tooltip\": \"$tooltip\"}"
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Use a dedicated narrow dark theme for the power menu
|
||||
config="$HOME/.config/rofi/power-menu.rasi"
|
||||
|
||||
actions=$(echo -e " Lock\n Shutdown\n Reboot\n Suspend\n Hibernate\n Logout")
|
||||
|
||||
# Display logout menu
|
||||
selected_option=$(echo -e "$actions" | rofi -dmenu -i -p "Power Menu" -config "${config}" || pkill -x rofi)
|
||||
|
||||
# Perform actions based on the selected option
|
||||
case "$selected_option" in
|
||||
*Lock)
|
||||
loginctl lock-session
|
||||
;;
|
||||
*Shutdown)
|
||||
systemctl poweroff
|
||||
;;
|
||||
*Reboot)
|
||||
systemctl reboot
|
||||
;;
|
||||
*Suspend)
|
||||
systemctl suspend
|
||||
;;
|
||||
*Hibernate)
|
||||
systemctl hibernate
|
||||
;;
|
||||
*Logout)
|
||||
loginctl kill-session "$XDG_SESSION_ID"
|
||||
;;
|
||||
esac
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Define functions
|
||||
print_error() {
|
||||
cat <<"EOF"
|
||||
Usage: ./volumecontrol.sh -[device] <actions>
|
||||
...valid devices are...
|
||||
i -- input device
|
||||
o -- output device
|
||||
p -- player application
|
||||
...valid actions are...
|
||||
i -- increase volume [+2]
|
||||
d -- decrease volume [-2]
|
||||
m -- mute [x]
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
icon() {
|
||||
vol=$(pactl get-sink-volume @DEFAULT_SINK@ | awk '{print $5}' | sed 's/%//')
|
||||
mute=$(pactl get-sink-mute @DEFAULT_SINK@ | awk '{print $2}')
|
||||
|
||||
if [ "$mute" = "yes" ] || [ "$vol" -eq 0 ]; then
|
||||
icon="volume-level-muted"
|
||||
elif [ "$vol" -lt 33 ]; then
|
||||
icon="volume-level-low"
|
||||
elif [ "$vol" -lt 66 ]; then
|
||||
icon="volume-level-medium"
|
||||
else
|
||||
icon="volume-level-high"
|
||||
fi
|
||||
}
|
||||
|
||||
send_notification() {
|
||||
icon
|
||||
notify-send -a "state" -r 91190 -i "$icon" -h int:value:"$vol" "Volume: ${vol}%" -u low
|
||||
}
|
||||
|
||||
notify_mute() {
|
||||
mute=$(pactl get-sink-mute @DEFAULT_SINK@ | awk '{print $2}')
|
||||
if [ "$mute" = "yes" ]; then
|
||||
notify-send -a "state" -r 91190 -i "volume-level-muted" "Volume: Muted" -u low
|
||||
else
|
||||
icon
|
||||
notify-send -a "state" -r 91190 -i "$icon" "Volume: Unmuted" -u low
|
||||
fi
|
||||
}
|
||||
|
||||
action_volume() {
|
||||
case "${1}" in
|
||||
i)
|
||||
# Increase volume if below 100
|
||||
current_vol=$(pactl get-sink-volume @DEFAULT_SINK@ | awk '{print $5}' | sed 's/%//')
|
||||
if [ "$current_vol" -lt 100 ]; then
|
||||
new_vol=$((current_vol + 2))
|
||||
[ "$new_vol" -gt 100 ] && new_vol=100
|
||||
pactl set-sink-volume @DEFAULT_SINK@ "${new_vol}%"
|
||||
fi
|
||||
;;
|
||||
d)
|
||||
# Decrease volume if above 0
|
||||
current_vol=$(pactl get-sink-volume @DEFAULT_SINK@ | awk '{print $5}' | sed 's/%//')
|
||||
new_vol=$((current_vol - 2))
|
||||
[ "$new_vol" -lt 0 ] && new_vol=0
|
||||
pactl set-sink-volume @DEFAULT_SINK@ "${new_vol}%"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
select_output() {
|
||||
if [ "$@" ]; then
|
||||
desc="$*"
|
||||
device=$(pactl list sinks | grep -C2 -F "Description: $desc" | grep Name | cut -d: -f2 | xargs)
|
||||
if pactl set-default-sink "$device"; then
|
||||
notify-send -r 91190 "Activated: $desc"
|
||||
else
|
||||
notify-send -r 91190 "Error activating $desc"
|
||||
fi
|
||||
else
|
||||
pactl list sinks | grep -ie "Description:" | awk -F ': ' '{print $2}' | sort
|
||||
fi
|
||||
}
|
||||
|
||||
# Evaluate device option
|
||||
while getopts iops: DeviceOpt; do
|
||||
case "${DeviceOpt}" in
|
||||
i)
|
||||
nsink=$(pactl list sources short | awk '{print $2}')
|
||||
[ -z "${nsink}" ] && echo "ERROR: Input device not found..." && exit 0
|
||||
srce="--default-source"
|
||||
;;
|
||||
o)
|
||||
nsink=$(pactl list sinks short | awk '{print $2}')
|
||||
[ -z "${nsink}" ] && echo "ERROR: Output device not found..." && exit 0
|
||||
srce=""
|
||||
;;
|
||||
p)
|
||||
nsink=$(playerctl --list-all | grep -w "${OPTARG}")
|
||||
[ -z "${nsink}" ] && echo "ERROR: Player ${OPTARG} not active..." && exit 0
|
||||
# shellcheck disable=SC2034
|
||||
srce="${nsink}"
|
||||
;;
|
||||
s)
|
||||
# Select an output device
|
||||
select_output "$@"
|
||||
exit
|
||||
;;
|
||||
*) print_error ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Set default variables
|
||||
shift $((OPTIND - 1))
|
||||
|
||||
# Execute action
|
||||
case "${1}" in
|
||||
i) action_volume i ;;
|
||||
d) action_volume d ;;
|
||||
m) pactl set-sink-mute @DEFAULT_SINK@ toggle && notify_mute && exit 0 ;;
|
||||
*) print_error ;;
|
||||
esac
|
||||
|
||||
send_notification
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# This script gathers detailed Wi-Fi connection information.
|
||||
# It collects the following fields:
|
||||
#
|
||||
# - SSID (Service Set Identifier): The name of the Wi-Fi network you
|
||||
# are currently connected to. Example: "My_Network"
|
||||
#
|
||||
# - IP Address: The IP address assigned to the device by the router.
|
||||
# This is typically a private IP within the local network. Example:
|
||||
# "192.168.1.29/24" (with subnet mask)
|
||||
#
|
||||
# - Router (Gateway): The IP address of the router (default gateway)
|
||||
# that your device uses to communicate outside the local network.
|
||||
# Example: "192.168.1.1"
|
||||
#
|
||||
# - MAC Address: The unique Media Access Control address of the local
|
||||
# device's Wi-Fi adapter. Example: "F8:34:41:07:1B:65"
|
||||
#
|
||||
# - Security: The encryption protocol being used to secure your Wi-Fi
|
||||
# connection. Common security protocols include:
|
||||
# - WPA2 (Wi-Fi Protected Access 2): The most commonly used security
|
||||
# standard, offering strong encryption (AES).
|
||||
# - WPA3: The latest version, providing even stronger security,
|
||||
# especially in public or open networks.
|
||||
# - WEP (Wired Equivalent Privacy): An outdated and insecure protocol
|
||||
# that should not be used.
|
||||
# Example: "WPA2" indicates that the connection is secured using WPA2
|
||||
# with AES encryption.
|
||||
#
|
||||
# - BSSID (Basic Service Set Identifier): The MAC address of the Wi-Fi
|
||||
# access point you are connected to. Example: "A4:22:49:DA:91:A0"
|
||||
#
|
||||
# - Channel: The wireless channel your Wi-Fi network is using. This is
|
||||
# associated with the frequency band. Example: "100 (5500 MHz)"
|
||||
# indicates the channel number (100) and the frequency (5500 MHz),
|
||||
# which is within the 5 GHz band.
|
||||
#
|
||||
# - RSSI (Received Signal Strength Indicator): The strength of the
|
||||
# Wi-Fi signal, typically in dBm (decibels relative to 1 milliwatt).
|
||||
# Closer to 0 means stronger signal, with values like -40 dBm being
|
||||
# very good. Example: "-40 dBm"
|
||||
#
|
||||
# - Signal: The signal quality, which is represented as a percentage,
|
||||
# where higher numbers mean better signal. Example: "100"
|
||||
# indicates perfect signal strength.
|
||||
#
|
||||
# - Rx Rate (Receive Rate): The maximum data rate (in Mbit/s) at which
|
||||
# the device can receive data from the Wi-Fi access point. Example:
|
||||
# "866.7 MBit/s" indicates a high-speed connection on a modern
|
||||
# standard.
|
||||
#
|
||||
# - Tx Rate (Transmit Rate): The maximum data rate (in Mbit/s) at
|
||||
# which the device can send data to the Wi-Fi access point. Example:
|
||||
# "866.7 MBit/s"
|
||||
#
|
||||
# - PHY Mode (Physical Layer Mode): The Wi-Fi protocol or standard in
|
||||
# use. Common modes include 802.11n, 802.11ac, and 802.11ax (Wi-Fi
|
||||
# 6). Example: "802.11ac" indicates you're using the 5 GHz band with
|
||||
# a modern high-speed standard.
|
||||
|
||||
if ! command -v nmcli &>/dev/null; then
|
||||
echo "{\"text\": \"<span color='#f38ba8'></span>\", \"tooltip\": \"nmcli utility is missing\"}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if Wi-Fi is enabled
|
||||
wifi_status=$(nmcli radio wifi)
|
||||
|
||||
if [ "$wifi_status" = "disabled" ]; then
|
||||
echo "{\"text\": \"\", \"tooltip\": \"Wi-Fi Disabled\"}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
wifi_info=$(nmcli -t -f active,ssid,signal,security dev wifi | grep "^yes")
|
||||
|
||||
# If no ESSID is found, set a default value
|
||||
if [ -z "$wifi_info" ]; then
|
||||
essid="No Connection"
|
||||
signal=0
|
||||
tooltip="No Connection"
|
||||
else
|
||||
# Some defaults
|
||||
ip_address="127.0.0.1"
|
||||
# gateway="127.0.0.1"
|
||||
# mac_address="N/A"
|
||||
security=$(echo "$wifi_info" | awk -F: '{print $4}')
|
||||
# bssid="N/A"
|
||||
chan="N/A"
|
||||
# rssi="N/A"
|
||||
# rx_bitrate=""
|
||||
# tx_bitrate=""
|
||||
# phy_mode=""
|
||||
signal=$(echo "$wifi_info" | awk -F: '{print $3}')
|
||||
|
||||
active_device=$(nmcli -t -f DEVICE,STATE device status |
|
||||
grep -w "connected" |
|
||||
grep -v -E "^(dummy|lo:|virbr0)" |
|
||||
awk -F: '{print $1}' |
|
||||
head -n 1)
|
||||
|
||||
if [ -n "$active_device" ]; then
|
||||
output=$(nmcli -e no -g ip4.address,ip4.gateway,general.hwaddr device show "$active_device")
|
||||
|
||||
ip_address=$(echo "$output" | sed -n '1p')
|
||||
# gateway=$(echo "$output" | sed -n '2p')
|
||||
# mac_address=$(echo "$output" | sed -n '3p')
|
||||
|
||||
line=$(nmcli -e no -t -f active,bssid,chan,freq device wifi | grep "^yes")
|
||||
|
||||
# bssid=$(echo "$line" | awk -F':' '{print $2":"$3":"$4":"$5":"$6":"$7}')
|
||||
chan=$(echo "$line" | awk -F':' '{print $8}')
|
||||
freq=$(echo "$line" | awk -F':' '{print $9}')
|
||||
chan="$chan ($freq)"
|
||||
|
||||
# if command -v iw &>/dev/null; then
|
||||
# iw_output=$(iw dev "$active_device" station dump)
|
||||
# rssi=$(echo "$iw_output" | grep "signal:" | awk '{print $2 " dBm"}')
|
||||
|
||||
# Upload speed
|
||||
# rx_bitrate=$(echo "$iw_output" | grep "rx bitrate:" | awk '{print $3 " " $4}')
|
||||
|
||||
# Download speed
|
||||
# tx_bitrate=$(echo "$iw_output" | grep "tx bitrate:" | awk '{print $3 " " $4}')
|
||||
|
||||
# Physical Layer Mode
|
||||
# if echo "$iw_output" | grep -E -q "rx bitrate:.* VHT"; then
|
||||
# phy_mode="802.11ac" # Wi-Fi 5
|
||||
# elif echo "$iw_output" | grep -E -q "rx bitrate:.* HT"; then
|
||||
# phy_mode="802.11n" # Wi-Fi 4
|
||||
# elif echo "$iw_output" | grep -E -q "rx bitrate:.* HE"; then
|
||||
# phy_mode="802.11ax" # Wi-Fi 6
|
||||
# fi
|
||||
# fi
|
||||
|
||||
# Get the current Wi-Fi ESSID
|
||||
essid=$(echo "$wifi_info" | awk -F: '{print $2}')
|
||||
|
||||
tooltip=":: ${essid}"
|
||||
tooltip+="\nIP Address: ${ip_address}"
|
||||
# tooltip+="\nRouter: ${gateway}"
|
||||
# tooltip+="\nMAC Address: ${mac_address}"
|
||||
tooltip+="\nSecurity: ${security}"
|
||||
# tooltip+="\nBSSID: ${bssid}"
|
||||
tooltip+="\nChannel: ${chan}"
|
||||
# tooltip+="\nRSSI: ${rssi}"
|
||||
tooltip+="\nStrength: ${signal} / 100"
|
||||
|
||||
# if [ -n "$rx_bitrate" ]; then
|
||||
# tooltip+="\nRx Rate: ${rx_bitrate}"
|
||||
# fi
|
||||
|
||||
# if [ -n "$tx_bitrate" ]; then
|
||||
# tooltip+="\nTx Rate: ${tx_bitrate}"
|
||||
# fi
|
||||
|
||||
# if [ -n "$phy_mode" ]; then
|
||||
# tooltip+="\nPHY Mode: ${phy_mode}"
|
||||
# fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Determine Wi-Fi icon based on signal strength
|
||||
if [ "$signal" -ge 80 ]; then
|
||||
icon="" # Strong signal
|
||||
elif [ "$signal" -ge 60 ]; then
|
||||
icon="" # Good signal
|
||||
elif [ "$signal" -ge 40 ]; then
|
||||
icon="" # Weak signal
|
||||
elif [ "$signal" -ge 20 ]; then
|
||||
icon="" # Very weak signal
|
||||
else
|
||||
icon="<span color='#f38ba8'></span>" # No signal
|
||||
fi
|
||||
|
||||
# Module and tooltip
|
||||
echo "{\"text\": \"${icon}\", \"tooltip\": \"${tooltip}\"}"
|
||||
Reference in New Issue
Block a user