aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md146
-rw-r--r--assets/demo.gifbin20821 -> 51713 bytes
-rwxr-xr-xinstall.sh128
-rw-r--r--src/.gitignore7
-rwxr-xr-xsrc/panahone461
-rwxr-xr-xuninstall.sh51
6 files changed, 739 insertions, 54 deletions
diff --git a/README.md b/README.md
index ef1144c..13d1cd3 100644
--- a/README.md
+++ b/README.md
@@ -1,27 +1,143 @@
-# README.txt
+# panahone β›…
-```plaintext
+simple yet *somewhat* 🀷 feature-rich GTK3 system tray weather applet using [wttr.in](https://wttr.in/)'s API, written in Python.
-panahone β›…
--------------
+## features
-simple gtk3 systray weather applet using wttr.in's API written in Python.
+- 🌑️ **semi-realtime weather information** with automatic updates
+- πŸ’Ύ **smart caching** to reduce API calls (10-minute cache by default)
+- πŸ”„ **auto-refresh** support (configurable interval)
+- 🎨 **dynamic weather icons** that change based on conditions
+- πŸ“ **location-based** weather (or auto-detect)
+- 🌑️ **temperature units** (celsius/fahrenheit) with easy toggle
+- πŸ’¨ **comprehensive weather data**:
+ - current temperature and "feels like" temperature
+ - weather conditions
+ - humidity levels
+ - wind speed and direction
+ - uv index
+ - visibility
+- βš™οΈ **persistent configuration** with JSON config file ***(EXPERIMENTAL)***
+- πŸ“ **detailed logging** for debugging
+- πŸ–±οΈ **interactive system tray**:
+ - left click: fetch/refresh weather
+ - middle click: quit
+ - right click: context menu
+- πŸ”” **desktop notifications** with customizable timeout
-(i really just wanted to practice using the `requests` and `json` modules in
-python so i made this).
+## install
-usage
--------------
+### system dependencies
-usage: panahone [-h] [-l LOCATION] [-f]
+- python 3.6+
+- gtk3
+- gobject
+- libnotify
-options:
- -h, --help show this help message and exit
- -l, --location LOCATION location for weather
- -f, --fahrenheit use Fahrenheit instead of Celsius
+on Arch Linux:
+```bash
+sudo pacman -S python python-gobject gtk3 libnotify
+```
+
+on Ubuntu/Debian:
+```bash
+sudo apt install python3 python3-gi gir1.2-gtk-3.0 gir1.2-notify-0.7
+```
+
+on Fedora:
+```bash
+sudo dnf install python3 python3-gobject gtk3 libnotify
+```
+
+### in-app/venv Dependencies
+
+```bash
+cd src
+pip install -r requirements.txt
+```
+
+## usage
+
+```bash
+# auto-detect location
+./panahone
+
+# specify location
+./panahone -l "New York"
+
+# use fahrenheit
+./panahone -f
+
+# enable debug logging
+./panahone --debug
+```
+
+### cli args
```
+-h, --help Show help message
+-l, --location LOCATION Specify location (city, coordinates, airport code)
+-f, --fahrenheit Use Fahrenheit instead of Celsius
+-v, --version Show version
+--debug Enable debug logging
+```
+
+### mouse
+
+- **left click**: fetch/refresh weather
+- **middle click**: quit application
+- **right click**: context menu (refresh, toggle units, about, quit)
+
+## config (honestly, still experimental I would still use overrides at this point)
+
+config file: `~/.config/panahone/config.json` (auto-created on first run)
+
+```json
+{
+ "location": "", // default location (empty = auto-detect)
+ "use_fahrenheit": false, // temperature unit
+ "auto_refresh": true, // enable auto-updates
+ "refresh_interval_minutes": 30, // update frequency
+ "show_wind": true, // show wind data
+ "show_humidity": true, // show humidity
+ "show_feels_like": true, // show "feels like" temp
+ "notification_timeout": 10000 // notification duration (ms)
+}
+```
+
+**file:**
+- config: `~/.config/panahone/config.json`
+- cache: `~/.cache/panahone/weather_cache.json`
+- logs: `~/.cache/panahone/panahone.log`
+
+## weather icons
+
+`panahone` uses system theme icons and automatically maps weather conditions to appropriate icons (yaru would have all of these, but some icon themes may have some missing):
+
+- β˜€οΈ clear/sunny β†’ `weather-clear`
+- 🌀️ partly cloudy β†’ `weather-few-clouds`
+- ☁️ cloudy/overcast β†’ `weather-overcast`
+- 🌫️ fog/mist β†’ `weather-fog`
+- 🌧️ rain β†’ `weather-showers`
+- β›ˆοΈ thunderstorm β†’ `weather-storm`
+- ❄️ snow/sleet β†’ `weather-snow`
+
+## troubleshooting
+
+**Debug mode:**
+```bash
+./panahone --debug
+cat ~/.cache/panahone/panahone.log
+```
+## changelog (2025.10.22)
+
+**improvements:**
+- added persistent configuration system with json config file
+- implemented smart caching to reduce api calls
+- added auto-refresh functionality with configurable intervals
+- enhanced weather display with humidity, wind, uv index, and visibility
+- added dynamic weather icons that change based on conditions
# πŸ“Έ
-![GIF animation of panahone β›…](assets/demo.gif)
+![GIF animation of panahone β›…](assets/demo.gif) \ No newline at end of file
diff --git a/assets/demo.gif b/assets/demo.gif
index 82fe807..a680f17 100644
--- a/assets/demo.gif
+++ b/assets/demo.gif
Binary files differ
diff --git a/install.sh b/install.sh
new file mode 100755
index 0000000..5aca202
--- /dev/null
+++ b/install.sh
@@ -0,0 +1,128 @@
+#!/usr/bin/env bash
+
+set -e
+
+echo "🌀️ panahone weather applet installer"
+echo "======================================"
+echo ""
+
+# checks
+if [[ "$OSTYPE" != "linux-gnu"* ]]; then
+ echo "❌ error: this app is designed for linux systems."
+ exit 1
+fi
+
+if ! command -v python3 &>/dev/null; then
+ echo "❌ error: python 3 is not installed."
+ echo "please install python 3 first."
+ exit 1
+fi
+
+PYTHON_VERSION=$(python3 --version | cut -d' ' -f2 | cut -d'.' -f1,2)
+echo "βœ… python 3 found: $(python3 --version)"
+
+if ! python3 -c "import gi; gi.require_version('Gtk', '3.0')" 2>/dev/null; then
+ echo "⚠️ gtk3 python bindings not found."
+ echo ""
+ echo "please install the required system packages:"
+ echo ""
+ echo "Arch:"
+ echo " sudo pacman -S python-gobject gtk3 libnotify"
+ echo ""
+ echo "Ubuntu/Debian:"
+ echo " sudo apt install python3-gi gir1.2-gtk-3.0 gir1.2-notify-0.7"
+ echo ""
+ echo "Fedora:"
+ echo " sudo dnf install python3-gobject gtk3 libnotify"
+ echo ""
+ read -p "press enter to continue after installing, or ctrl+c to exit..."
+fi
+
+# deps check
+echo ""
+echo "πŸ“¦ installing python dependencies..."
+cd src
+pip3 install --user -r requirements.txt
+
+if [ $? -eq 0 ]; then
+ echo "βœ… dependencies installed successfully"
+else
+ echo "❌ failed to install dependencies"
+ exit 1
+fi
+
+# +x check
+chmod +x panahone
+echo "βœ… made panahone executable"
+
+# symbolic link (my personal preferred)
+echo ""
+read -p "do you want to create a symbolic link in ~/.local/bin? (y/n) " -n 1 -r
+echo ""
+if [[ $REPLY =~ ^[Yy]$ ]]; then
+ mkdir -p ~/.local/bin
+ ln -sf "$(pwd)/panahone" ~/.local/bin/panahone
+ echo "βœ… symbolic link created at ~/.local/bin/panahone"
+ echo ""
+ echo "⚠️ make sure ~/.local/bin is in your path:"
+ echo " add this to your ~/.bashrc or ~/.zshrc:"
+ echo " export PATH=\"\$HOME/.local/bin:\$PATH\""
+fi
+
+# .desktop file (i do not prefer this, personally but y'all go ahead!)
+echo ""
+read -p "do you want to create a desktop entry? (y/n) " -n 1 -r
+echo ""
+if [[ $REPLY =~ ^[Yy]$ ]]; then
+ DESKTOP_FILE="$HOME/.local/share/applications/panahone.desktop"
+ mkdir -p ~/.local/share/applications
+
+ cat >"$DESKTOP_FILE" <<EOF
+[Desktop Entry]
+Name=Panahone
+Comment=GTK3 Weather Applet
+Exec=$(pwd)/panahone
+Icon=weather-overcast
+Terminal=false
+Type=Application
+Categories=Utility;
+StartupNotify=false
+EOF
+
+ echo "βœ… desktop entry created at $desktop_file"
+fi
+
+# xdg autostart
+echo ""
+read -p "do you want panahone to start automatically on login? (y/n) " -n 1 -r
+echo ""
+if [[ $REPLY =~ ^[Yy]$ ]]; then
+ AUTOSTART_FILE="$HOME/.config/autostart/panahone.desktop"
+ mkdir -p ~/.config/autostart
+
+ cat >"$AUTOSTART_FILE" <<EOF
+[Desktop Entry]
+Name=Panahone
+Comment=GTK3 Weather Applet
+Exec=$(pwd)/panahone
+Icon=weather-overcast
+Terminal=false
+Type=Application
+X-GNOME-Autostart-enabled=true
+EOF
+
+ echo "βœ… autostart entry created at $autostart_file"
+fi
+
+echo ""
+echo "πŸŽ‰ installation complete!"
+echo ""
+echo "you can now run panahone with:"
+echo " cd $(pwd) && ./panahone"
+if [[ -L ~/.local/bin/panahone ]]; then
+ echo " or simply: panahone (if ~/.local/bin is in PATH)"
+fi
+echo ""
+echo "for usage, run:"
+echo " ./panahone --help"
+echo "" \ No newline at end of file
diff --git a/src/.gitignore b/src/.gitignore
index 9c64fb6..ce03b6a 100644
--- a/src/.gitignore
+++ b/src/.gitignore
@@ -13,3 +13,10 @@ venv/
*.atom/
.DS_Store # macOS
Thumbs.db # Windows
+
+# Panahone specific
+*.cache
+cache/
+config/
+weather_cache.json
+
diff --git a/src/panahone b/src/panahone
index a4ef7ac..6a4d0f6 100755
--- a/src/panahone
+++ b/src/panahone
@@ -1,14 +1,20 @@
#!/usr/bin/env python3
import argparse
-import gi
-import requests
+import json
+import logging
+import os
import signal
+import sys
import warnings
-# import sys
-# import json
+from datetime import datetime, timedelta
+from pathlib import Path
+from typing import Optional, Dict, Any
+
+import gi
+import requests
-# will fix this when it breaks :D
+# suppress gtk statusicon FOR NOW
warnings.filterwarnings(
"ignore",
category=DeprecationWarning,
@@ -17,81 +23,458 @@ warnings.filterwarnings(
gi.require_version("Gtk", "3.0")
gi.require_version("Notify", "0.7")
-from gi.repository import Gtk, Notify
+from gi.repository import Gtk, GLib, Notify
+
+APP_NAME = "Panahone"
+APP_VERSION = "2025.10.22"
+CACHE_DIR = Path.home() / ".cache" / "panahone"
+CONFIG_DIR = Path.home() / ".config" / "panahone"
+CACHE_FILE = CACHE_DIR / "weather_cache.json"
+CONFIG_FILE = CONFIG_DIR / "config.json"
+LOG_FILE = CACHE_DIR / "panahone.log"
+CACHE_DURATION = timedelta(minutes=10) # cache weather data for 10 minutes
+AUTO_REFRESH_INTERVAL = 30 * 60 * 1000 # 30 minutes
+
+# weather icon mapping
+WEATHER_ICONS = {
+ "clear": "weather-clear",
+ "sunny": "weather-clear",
+ "partly cloudy": "weather-few-clouds",
+ "cloudy": "weather-overcast",
+ "overcast": "weather-overcast",
+ "mist": "weather-fog",
+ "fog": "weather-fog",
+ "patchy rain": "weather-showers-scattered",
+ "light rain": "weather-showers",
+ "rain": "weather-showers",
+ "heavy rain": "weather-storm",
+ "thunderstorm": "weather-storm",
+ "thunder": "weather-storm",
+ "snow": "weather-snow",
+ "sleet": "weather-snow",
+ "blizzard": "weather-snow",
+}
+
+# logging if desired
+CACHE_DIR.mkdir(parents=True, exist_ok=True)
+logging.basicConfig(
+ level=logging.INFO,
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
+ handlers=[logging.FileHandler(LOG_FILE), logging.StreamHandler(sys.stdout)],
+)
+logger = logging.getLogger(APP_NAME)
+
+
+class WeatherCache:
+ """Manages caching of weather data to reduce API calls"""
+
+ def __init__(self, cache_file: Path):
+ self.cache_file = cache_file
+ self.cache_file.parent.mkdir(parents=True, exist_ok=True)
+
+ def get(self, location: str) -> Optional[Dict[str, Any]]:
+ """Retrieve cached weather data if still valid"""
+ if not self.cache_file.exists():
+ return None
+
+ try:
+ with open(self.cache_file, "r") as f:
+ cache = json.load(f)
+
+ if cache.get("location") != location:
+ return None
+
+ cached_time = datetime.fromisoformat(cache.get("timestamp", ""))
+ if datetime.now() - cached_time > CACHE_DURATION:
+ return None
+
+ logger.info(f"Using cached weather data for {location}")
+ return cache.get("data")
+ except (json.JSONDecodeError, ValueError, KeyError) as e:
+ logger.warning(f"Cache read error: {e}")
+ return None
+
+ def set(self, location: str, data: Dict[str, Any]):
+ """Save weather data to cache"""
+ try:
+ cache = {
+ "location": location,
+ "timestamp": datetime.now().isoformat(),
+ "data": data,
+ }
+ with open(self.cache_file, "w") as f:
+ json.dump(cache, f, indent=2)
+ logger.info(f"Cached weather data for {location}")
+ except Exception as e:
+ logger.error(f"Cache write error: {e}")
+
+
+class ConfigManager:
+ """Manages application configuration"""
+
+ DEFAULT_CONFIG = {
+ "location": "",
+ "use_fahrenheit": False,
+ "auto_refresh": True,
+ "refresh_interval_minutes": 30,
+ "show_wind": True,
+ "show_humidity": True,
+ "show_feels_like": True,
+ "notification_timeout": 10000, # milliseconds
+ }
+
+ def __init__(self, config_file: Path):
+ self.config_file = config_file
+ self.config_file.parent.mkdir(parents=True, exist_ok=True)
+ self.config = self.load()
+
+ def load(self) -> Dict[str, Any]:
+ """Load configuration from file"""
+ if not self.config_file.exists():
+ self.save(self.DEFAULT_CONFIG)
+ return self.DEFAULT_CONFIG.copy()
+
+ try:
+ with open(self.config_file, "r") as f:
+ config = json.load(f)
+ # ensure all keys exist
+ merged = self.DEFAULT_CONFIG.copy()
+ merged.update(config)
+ return merged
+ except Exception as e:
+ logger.error(f"Config load error: {e}")
+ return self.DEFAULT_CONFIG.copy()
+
+ def save(self, config: Dict[str, Any]):
+ """Save configuration to file"""
+ try:
+ with open(self.config_file, "w") as f:
+ json.dump(config, f, indent=2)
+ logger.info("Configuration saved")
+ except Exception as e:
+ logger.error(f"Config save error: {e}")
+
+ def get(self, key: str, default=None):
+ """Get configuration value"""
+ return self.config.get(key, default)
+
+ def set(self, key: str, value: Any):
+ """Set configuration value"""
+ self.config[key] = value
+ self.save(self.config)
class PanahoneApplet:
- def __init__(self, location, use_fahrenheit):
- self.location = location or ""
- self.use_fahrenheit = use_fahrenheit
- Notify.init("Panahone")
+ """Main application class for the weather applet"""
+
+ def __init__(
+ self, location: Optional[str] = None, use_fahrenheit: Optional[bool] = None
+ ):
+ self.config = ConfigManager(CONFIG_FILE)
+ self.cache = WeatherCache(CACHE_FILE)
+
+ # args override config
+ self.location = (
+ location if location is not None else self.config.get("location", "")
+ )
+ self.use_fahrenheit = (
+ use_fahrenheit
+ if use_fahrenheit is not None
+ else self.config.get("use_fahrenheit", False)
+ )
+
+ Notify.init(APP_NAME)
+
+ # create systray icon
self.icon = Gtk.StatusIcon()
- self.icon.set_from_icon_name('weather-overcast')
- self.icon.set_tooltip_text("Panahone: click to get weather")
+ self.icon.set_from_icon_name("weather-overcast")
+ self.icon.set_tooltip_text(f"{APP_NAME}: Click to get weather")
self.icon.connect("button-press-event", self.on_click)
+
+ # refresh timer
+ self.auto_refresh_timer = None
+ if self.config.get("auto_refresh", True):
+ self.start_auto_refresh()
+
+ # sigint handling
signal.signal(signal.SIGINT, self.quit)
+ logger.info(f"{APP_NAME} v{APP_VERSION} started")
+
+ def start_auto_refresh(self):
+ """Start automatic weather refresh timer"""
+ interval = self.config.get("refresh_interval_minutes", 30) * 60 * 1000
+ self.auto_refresh_timer = GLib.timeout_add(interval, self.auto_refresh_callback)
+ logger.info(f"Auto-refresh enabled (every {interval // 60000} minutes)")
+
+ def auto_refresh_callback(self):
+ """Callback for auto-refresh timer"""
+ logger.info("Auto-refreshing weather data")
+ self.fetch_and_notify(silent=True)
+ return True # Continue timer
+
def on_click(self, icon, event):
- if event.button == 1:
- Notify.Notification.new(
- "Panahone",
- "Retrieving Weather Data…",
- "weather-overcast"
- ).show()
+ """Handle mouse clicks on the system tray icon"""
+ if event.button == 1: # left
+ self.show_notification(
+ "Retrieving Weather Data…", "weather-overcast", timeout=2000
+ )
self.fetch_and_notify()
- elif event.button == 2:
+ elif event.button == 2: # middle
self.quit()
+ elif event.button == 3: # right
+ self.show_menu(event)
+
+ def show_menu(self, event):
+ """Show context menu on right-click"""
+ menu = Gtk.Menu()
+
+ # refresh
+ refresh_item = Gtk.MenuItem(label="Refresh Weather")
+ refresh_item.connect("activate", lambda w: self.fetch_and_notify())
+ menu.append(refresh_item)
+ menu.append(Gtk.SeparatorMenuItem())
+
+ # f/c toggle
+ temp_unit = "Celsius" if self.use_fahrenheit else "Fahrenheit"
+ toggle_item = Gtk.MenuItem(label=f"Switch to {temp_unit}")
+ toggle_item.connect("activate", self.toggle_temperature_unit)
+ menu.append(toggle_item)
+ menu.append(Gtk.SeparatorMenuItem())
+
+ # about
+ about_item = Gtk.MenuItem(label="About")
+ about_item.connect("activate", self.show_about)
+ menu.append(about_item)
+ quit_item = Gtk.MenuItem(label="Quit")
+ quit_item.connect("activate", self.quit)
+ menu.append(quit_item)
+
+ menu.show_all()
+ menu.popup(None, None, None, None, event.button, event.time)
+
+ def toggle_temperature_unit(self, widget):
+ """Toggle between Fahrenheit and Celsius"""
+ self.use_fahrenheit = not self.use_fahrenheit
+ self.config.set("use_fahrenheit", self.use_fahrenheit)
+ unit = "Fahrenheit" if self.use_fahrenheit else "Celsius"
+ self.show_notification(
+ f"Temperature unit changed to {unit}", "weather-overcast"
+ )
+ logger.info(f"Temperature unit changed to {unit}")
+
+ def show_about(self, widget):
+ """Show about dialog"""
+ message = (
+ f"{APP_NAME} v{APP_VERSION}\n"
+ f"A GTK3 weather applet using wttr.in API\n\n"
+ f"Features:\n"
+ f"β€’ Real-time weather updates\n"
+ f"β€’ Automatic caching\n"
+ f"β€’ Auto-refresh support\n"
+ f"β€’ Customizable display\n\n"
+ f"Left click: Fetch weather\n"
+ f"Middle click: Quit\n"
+ f"Right click: Menu"
+ )
+ self.show_notification(message, "weather-overcast", timeout=15000)
+
+ def get_weather_icon(self, weather_desc: str) -> str:
+ """Map weather description to system icon"""
+ weather_lower = weather_desc.lower()
+ for keyword, icon in WEATHER_ICONS.items():
+ if keyword in weather_lower:
+ return icon
+ return "weather-overcast" # Default icon
+
+ def fetch_and_notify(self, silent: bool = False):
+ """Fetch weather data and show notification"""
+ # always use cache first
+ cached_data = self.cache.get(self.location)
+ if cached_data:
+ self.display_weather(cached_data, from_cache=True, silent=silent)
+ return
- def fetch_and_notify(self):
if not self.location:
url = "https://wttr.in/?format=j1"
else:
url = f"https://wttr.in/{self.location}?format=j1"
+
+ try:
+ logger.info(f"Fetching weather from {url}")
+ response = requests.get(url, timeout=15)
+ response.raise_for_status()
+ data = response.json()
+
+ self.cache.set(self.location, data)
+ self.display_weather(data, from_cache=False, silent=silent)
+
+ except requests.exceptions.Timeout:
+ error_msg = "Request timed out. Please check your internet connection."
+ logger.error(error_msg)
+ self.show_notification(f"Error: {error_msg}", "dialog-error")
+ except requests.exceptions.RequestException as e:
+ error_msg = f"Network error: {str(e)}"
+ logger.error(error_msg)
+ self.show_notification(f"Error: {error_msg}", "dialog-error")
+ except json.JSONDecodeError:
+ error_msg = "Invalid response from weather service"
+ logger.error(error_msg)
+ self.show_notification(f"Error: {error_msg}", "dialog-error")
+ except KeyError as e:
+ error_msg = f"Unexpected data format: {str(e)}"
+ logger.error(error_msg)
+ self.show_notification(f"Error: {error_msg}", "dialog-error")
+ except Exception as e:
+ error_msg = f"Unexpected error: {str(e)}"
+ logger.error(error_msg)
+ self.show_notification(f"Error: {error_msg}", "dialog-error")
+
+ def display_weather(
+ self, data: Dict[str, Any], from_cache: bool = False, silent: bool = False
+ ):
+ """Parse and display weather data"""
try:
- r = requests.get(url, timeout=10)
- data = r.json()
current = data["current_condition"][0]
- location = data["nearest_area"][0]["areaName"][0]["value"]
+ location_info = data["nearest_area"][0]
+ location = location_info["areaName"][0]["value"]
+ country = location_info["country"][0]["value"]
temp_c = current["temp_C"]
temp_f = current["temp_F"]
+ feels_c = current["FeelsLikeC"]
+ feels_f = current["FeelsLikeF"]
unit = "Β°F" if self.use_fahrenheit else "Β°C"
temp = temp_f if self.use_fahrenheit else temp_c
+ feels = feels_f if self.use_fahrenheit else feels_c
weather = current["weatherDesc"][0]["value"]
- message = f"Weather in {location}: {weather}, Temp: {temp}{unit}"
- except Exception as e:
- if not self.location:
- message = f"Error fetching weather: {e}"
- else:
- message = f"Error fetching weather for {self.location}: {e}"
+ humidity = current["humidity"]
+ wind_speed_kmph = current["windspeedKmph"]
+ wind_speed_mph = current["windspeedMiles"]
+ wind_speed = wind_speed_mph if self.use_fahrenheit else wind_speed_kmph
+ wind_unit = "mph" if self.use_fahrenheit else "km/h"
+ wind_dir = current["winddir16Point"]
+ uv_index = current.get("uvIndex", "N/A")
+ visibility_km = current.get("visibility", "N/A")
+ visibility_mi = current.get("visibilityMiles", "N/A")
+ visibility = visibility_mi if self.use_fahrenheit else visibility_km
+ visibility_unit = "mi" if self.use_fahrenheit else "km"
+
+ message_lines = [
+ f"πŸ“ {location}, {country}",
+ (
+ f"🌑️ {temp}{unit} (feels like {feels}{unit})"
+ if self.config.get("show_feels_like")
+ else f"🌑️ {temp}{unit}"
+ ),
+ f"☁️ {weather}",
+ ]
+
+ if self.config.get("show_humidity", True):
+ message_lines.append(f"πŸ’§ Humidity: {humidity}%")
+
+ if self.config.get("show_wind", True):
+ message_lines.append(f"πŸ’¨ Wind: {wind_speed} {wind_unit} {wind_dir}")
+
+ message_lines.extend(
+ [
+ f"β˜€οΈ UV Index: {uv_index}",
+ f"πŸ‘οΈ Visibility: {visibility} {visibility_unit}",
+ ]
+ )
+
+ if from_cache:
+ message_lines.append("\n(from cache)")
+
+ message = "\n".join(message_lines)
+
+ # systray icon update
+ icon_name = self.get_weather_icon(weather)
+ self.icon.set_from_icon_name(icon_name)
- Notify.Notification.new(
- "Panahone",
- message,
- "weather-overcast"
- ).show()
+ # tooltip update
+ tooltip = f"{location}: {weather}, {temp}{unit}"
+ self.icon.set_tooltip_text(tooltip)
+
+ # show notification (unless silent)
+ if not silent:
+ timeout = self.config.get("notification_timeout", 10000)
+ self.show_notification(message, icon_name, timeout=timeout)
+
+ logger.info(f"Weather displayed for {location}: {weather}, {temp}{unit}")
+
+ except (KeyError, IndexError) as e:
+ error_msg = f"Error parsing weather data: {str(e)}"
+ logger.error(error_msg)
+ self.show_notification(f"Error: {error_msg}", "dialog-error")
+
+ def show_notification(
+ self, message: str, icon: str = "weather-overcast", timeout: int = 10000
+ ):
+ """Display a desktop notification"""
+ try:
+ notification = Notify.Notification.new(APP_NAME, message, icon)
+ notification.set_timeout(timeout)
+ notification.show()
+ except Exception as e:
+ logger.error(f"Notification error: {e}")
def quit(self, *args):
+ """Clean up and quit the application"""
+ logger.info(f"{APP_NAME} shutting down")
+ if self.auto_refresh_timer:
+ GLib.source_remove(self.auto_refresh_timer)
+ Notify.uninit()
Gtk.main_quit()
def run(self):
+ """Start the GTK main loop"""
Gtk.main()
def parse_args():
- p = argparse.ArgumentParser(prog="panahone")
- p.add_argument("-l", "--location", help="location for weather")
- p.add_argument("-f", "--fahrenheit", action="store_true",
- help="use Fahrenheit instead of Celsius")
- return p.parse_args()
+ """Parse command line arguments"""
+ parser = argparse.ArgumentParser(
+ prog="panahone",
+ description=f"{APP_NAME} - A GTK3 weather applet using wttr.in API",
+ epilog="Left click: fetch weather | Middle click: quit | Right click: menu",
+ )
+ parser.add_argument(
+ "-l",
+ "--location",
+ help="Location for weather (city name, coordinates, etc.)",
+ metavar="LOCATION",
+ )
+ parser.add_argument(
+ "-f",
+ "--fahrenheit",
+ action="store_true",
+ help="Use Fahrenheit instead of Celsius",
+ )
+ parser.add_argument(
+ "-v", "--version", action="version", version=f"{APP_NAME} {APP_VERSION}"
+ )
+ parser.add_argument("--debug", action="store_true", help="Enable debug logging")
+ return parser.parse_args()
def main():
+ """Main entry point"""
args = parse_args()
+
+ # loglevel
+ if args.debug:
+ logger.setLevel(logging.DEBUG)
+ logger.debug("Debug logging enabled")
+
app = PanahoneApplet(args.location, args.fahrenheit)
+
try:
app.run()
except KeyboardInterrupt:
app.quit()
+ except Exception as e:
+ logger.critical(f"Fatal error: {e}", exc_info=True)
+ sys.exit(1)
if __name__ == "__main__":
diff --git a/uninstall.sh b/uninstall.sh
new file mode 100755
index 0000000..9a12a4e
--- /dev/null
+++ b/uninstall.sh
@@ -0,0 +1,51 @@
+#!/usr/bin/env bash
+
+# set -e
+
+echo "🌀️ panahone weather applet uninstaller"
+echo "========================================"
+echo ""
+
+read -p "are you sure you want to uninstall panahone? (y/n) " -n 1 -r
+echo ""
+if [[ ! $REPLY =~ ^[Yy]$ ]]; then
+ echo "uninstallation cancelled."
+ exit 0
+fi
+
+# rm symbolic link
+if [ -L ~/.local/bin/panahone ]; then
+ rm ~/.local/bin/panahone
+ echo "βœ… removed symbolic link from ~/.local/bin"
+fi
+
+# rm .desktop file
+if [ -f ~/.local/share/applications/panahone.desktop ]; then
+ rm ~/.local/share/applications/panahone.desktop
+ echo "βœ… removed desktop entry"
+fi
+
+# rm xdg autostart shenanigans
+if [ -f ~/.config/autostart/panahone.desktop ]; then
+ rm ~/.config/autostart/panahone.desktop
+ echo "βœ… removed autostart entry"
+fi
+
+echo ""
+read -p "do you want to remove configuration and cache files? (y/n) " -n 1 -r
+echo ""
+if [[ $REPLY =~ ^[Yy]$ ]]; then
+ if [ -d ~/.config/panahone ]; then
+ rm -rf ~/.config/panahone
+ echo "βœ… removed configuration directory"
+ fi
+
+ if [ -d ~/.cache/panahone ]; then
+ rm -rf ~/.cache/panahone
+ echo "βœ… removed cache directory"
+ fi
+fi
+
+echo ""
+echo "πŸŽ‰ panahone has been uninstalled."
+echo "you can manually remove the source directory if desired." \ No newline at end of file