#!/usr/bin/env python3 """WeatherNow — current weather and forecast in your terminal.""" import argparse import json import sys import urllib.request class C: RED = "\033[91m" GREEN = "\033[92m" YELLOW = "\033[93m" BLUE = "\033[94m" CYAN = "\033[96m" WHITE = "\033[97m" GRAY = "\033[90m" BOLD = "\033[1m" DIM = "\033[2m" RESET = "\033[0m" BANNER = f""" {C.CYAN}{C.BOLD} ██╗ ██╗███████╗ █████╗ ████████╗██╗ ██╗███████╗██████╗ ██║ ██║██╔════╝██╔══██╗╚══██╔══╝██║ ██║██╔════╝██╔══██╗ ██║ █╗ ██║█████╗ ███████║ ██║ ███████║█████╗ ██████╔╝ ██║███╗██║██╔══╝ ██╔══██║ ██║ ██╔══██║██╔══╝ ██╔══██╗ ╚███╔███╔╝███████╗██║ ██║ ██║ ██║ ██║███████╗██║ ██║ ╚══╝╚══╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝{C.RESET} {C.GRAY} ──────────────────────────────────────────────────────────{C.RESET} {C.DIM} Terminal Weather v1.0 {C.GRAY}by mainstarkov{C.RESET} {C.GRAY} ──────────────────────────────────────────────────────────{C.RESET} """ W = 56 WEATHER_ICONS = { 0: ("☀️", "Clear sky"), 1: ("🌤", "Mainly clear"), 2: ("⛅", "Partly cloudy"), 3: ("☁️", "Overcast"), 45: ("🌫", "Foggy"), 48: ("🌫", "Icy fog"), 51: ("🌦", "Light drizzle"), 53: ("🌦", "Drizzle"), 55: ("🌧", "Heavy drizzle"), 61: ("🌧", "Light rain"), 63: ("🌧", "Rain"), 65: ("🌧", "Heavy rain"), 71: ("🌨", "Light snow"), 73: ("🌨", "Snow"), 75: ("❄️", "Heavy snow"), 77: ("❄️", "Snow grains"), 80: ("🌧", "Rain showers"), 81: ("🌧", "Moderate showers"), 82: ("⛈", "Violent showers"), 85: ("🌨", "Snow showers"), 86: ("🌨", "Heavy snow showers"), 95: ("⛈", "Thunderstorm"), 96: ("⛈", "Thunderstorm + hail"), 99: ("⛈", "Severe thunderstorm"), } def fetch_json(url: str) -> dict | None: try: req = urllib.request.Request(url, headers={"User-Agent": "WeatherNow/1.0"}) with urllib.request.urlopen(req, timeout=10) as r: return json.loads(r.read().decode()) except Exception: return None def geocode(city: str) -> dict | None: encoded = urllib.request.quote(city) data = fetch_json( f"https://geocoding-api.open-meteo.com/v1/search?name={encoded}&count=1&language=en" ) if data and data.get("results"): r = data["results"][0] return { "name": r.get("name", city), "country": r.get("country", ""), "cc": r.get("country_code", ""), "lat": r["latitude"], "lon": r["longitude"], "tz": r.get("timezone", ""), } return None def get_weather(lat: float, lon: float) -> dict | None: return fetch_json( f"https://api.open-meteo.com/v1/forecast?" f"latitude={lat}&longitude={lon}" f"¤t=temperature_2m,relative_humidity_2m,apparent_temperature," f"wind_speed_10m,wind_gusts_10m,wind_direction_10m," f"weather_code,pressure_msl,cloud_cover,uv_index" f"&daily=weather_code,temperature_2m_max,temperature_2m_min," f"precipitation_sum,wind_speed_10m_max" f"&timezone=auto&forecast_days=5" ) def box_top(title: str, icon: str, color: str): w = W - 2 pad = max(0, w - 3 - len(title)) print(f"\n{color} {icon} ╔{'═' * w}╗{C.RESET}") print(f"{color} ║ {C.BOLD}{title}{C.RESET}{color}{' ' * pad}║{C.RESET}") print(f"{color} ╠{'═' * w}╣{C.RESET}") def box_row(label: str, value: str, color: str, vc: str = ""): vc = vc or C.WHITE padded = f"{label:<16}" pad = max(0, W - 3 - 16 - 3 - len(value)) print(f"{color} ║ {C.YELLOW}{padded}{C.RESET} {vc}{value}{C.RESET}{' ' * pad}{color}║{C.RESET}") def box_sep(color: str): print(f"{color} ╟{'─' * (W - 2)}╢{C.RESET}") def box_end(color: str): print(f"{color} ╚{'═' * (W - 2)}╝{C.RESET}") def flag(cc: str) -> str: if not cc or len(cc) != 2: return "🌐" return chr(0x1F1E6 + ord(cc[0]) - ord("A")) + chr(0x1F1E6 + ord(cc[1]) - ord("A")) def wind_direction(deg: int) -> str: dirs = ["N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"] return dirs[round(deg / 22.5) % 16] def temp_color(temp: float) -> str: if temp <= -10: return C.BLUE if temp <= 0: return C.CYAN if temp <= 15: return C.WHITE if temp <= 25: return C.YELLOW if temp <= 35: return C.RED return f"\033[91m" def display_weather(location: dict, weather: dict): cur = weather.get("current", {}) daily = weather.get("daily", {}) temp = cur.get("temperature_2m", 0) feels = cur.get("apparent_temperature", 0) code = cur.get("weather_code", 0) icon, desc = WEATHER_ICONS.get(code, ("❓", "Unknown")) tc = temp_color(temp) # Current weather box_top(f"{flag(location['cc'])} {location['name']}, {location['country']}", "📍", C.CYAN) box_row("Condition", f"{icon} {desc}", C.CYAN) box_row("Temperature", f"{tc}{temp}°C{C.RESET}", C.CYAN) box_row("Feels like", f"{tc}{feels}°C{C.RESET}", C.CYAN) box_sep(C.CYAN) box_row("Humidity", f"{cur.get('relative_humidity_2m', 0)}%", C.CYAN) box_row("Pressure", f"{cur.get('pressure_msl', 0)} hPa", C.CYAN) box_row("Cloud cover", f"{cur.get('cloud_cover', 0)}%", C.CYAN) box_row("UV Index", f"{cur.get('uv_index', 0)}", C.CYAN) box_sep(C.CYAN) wind = cur.get("wind_speed_10m", 0) gusts = cur.get("wind_gusts_10m", 0) wdir = wind_direction(cur.get("wind_direction_10m", 0)) box_row("Wind", f"{wind} km/h {wdir}", C.CYAN) box_row("Gusts", f"{gusts} km/h", C.CYAN) box_end(C.CYAN) # 5-day forecast dates = daily.get("time", []) maxs = daily.get("temperature_2m_max", []) mins = daily.get("temperature_2m_min", []) codes = daily.get("weather_code", []) precip = daily.get("precipitation_sum", []) winds = daily.get("wind_speed_10m_max", []) if dates: box_top("5-DAY FORECAST", "📅", C.GREEN) for i in range(min(5, len(dates))): d_icon, d_desc = WEATHER_ICONS.get(codes[i] if i < len(codes) else 0, ("❓", "?")) hi = maxs[i] if i < len(maxs) else "?" lo = mins[i] if i < len(mins) else "?" rain = precip[i] if i < len(precip) else 0 w = winds[i] if i < len(winds) else 0 hi_c = temp_color(hi) if isinstance(hi, (int, float)) else "" lo_c = temp_color(lo) if isinstance(lo, (int, float)) else "" forecast_str = f"{d_icon} {hi_c}{hi}°{C.RESET}/{lo_c}{lo}°{C.RESET}" if rain and rain > 0: forecast_str += f" {C.BLUE}💧{rain}mm{C.RESET}" forecast_str += f" 💨{w}km/h" box_row(dates[i], forecast_str, C.GREEN) box_end(C.GREEN) print() def main(): parser = argparse.ArgumentParser(prog="weathernow", description="Terminal weather app") parser.add_argument("city", nargs="+", help="city name (e.g. Moscow, 'New York')") parser.add_argument("--json", action="store_true", help="output raw JSON") args = parser.parse_args() print(BANNER) city = " ".join(args.city) location = geocode(city) if not location: print(f" {C.RED}✗ City not found: {city}{C.RESET}\n") sys.exit(1) weather = get_weather(location["lat"], location["lon"]) if not weather: print(f" {C.RED}✗ Could not fetch weather data{C.RESET}\n") sys.exit(1) if args.json: print(json.dumps({"location": location, "weather": weather}, indent=2, ensure_ascii=False)) else: display_weather(location, weather) if __name__ == "__main__": main()