weathernow.pyraw
#!/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()