diff --git a/.gitignore b/.gitignore
index b6e4761..094812e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,7 +2,7 @@
__pycache__/
*.py[cod]
*$py.class
-
+.idea/
# C extensions
*.so
diff --git a/.idea/.gitignore b/.idea/.gitignore
deleted file mode 100644
index 13566b8..0000000
--- a/.idea/.gitignore
+++ /dev/null
@@ -1,8 +0,0 @@
-# Default ignored files
-/shelf/
-/workspace.xml
-# Editor-based HTTP Client requests
-/httpRequests/
-# Datasource local storage ignored files
-/dataSources/
-/dataSources.local.xml
diff --git a/.idea/Weather.iml b/.idea/Weather.iml
deleted file mode 100644
index 26cf95d..0000000
--- a/.idea/Weather.iml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
deleted file mode 100644
index ffe444e..0000000
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
deleted file mode 100644
index 105ce2d..0000000
--- a/.idea/inspectionProfiles/profiles_settings.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
deleted file mode 100644
index d517855..0000000
--- a/.idea/misc.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
deleted file mode 100644
index ea03d89..0000000
--- a/.idea/modules.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
deleted file mode 100644
index 35eb1dd..0000000
--- a/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index f4db013..f105d01 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,4 +4,6 @@ requests==2.32.4
python-dotenv==1.0.0
PyGithub>=2.1.0
-pyyaml>=6.0
\ No newline at end of file
+pyyaml>=6.0
+
+textual
diff --git a/weather-tui/css/weather.tcss b/weather-tui/css/weather.tcss
new file mode 100644
index 0000000..f2478b8
--- /dev/null
+++ b/weather-tui/css/weather.tcss
@@ -0,0 +1,29 @@
+/* css/weather.tcss */
+
+Sidebar {
+ width: 30%;
+ border-right: solid $accent;
+ padding: 1;
+}
+
+.main-content {
+ width: 70%;
+ padding: 1;
+}
+
+.sidebar-title {
+ text-style: bold;
+ background: $accent;
+ padding: 1;
+ margin-bottom: 1;
+}
+
+ListView {
+ height: 80%;
+}
+
+WeatherWidget {
+ margin: 1;
+ padding: 1;
+ border: solid $primary;
+}
diff --git a/weather-tui/weather_app.py b/weather-tui/weather_app.py
new file mode 100644
index 0000000..a1bfcf5
--- /dev/null
+++ b/weather-tui/weather_app.py
@@ -0,0 +1,87 @@
+from textual.app import App, ComposeResult
+from textual.containers import Container, Horizontal, Vertical
+from textual.widgets import Header, Footer, ListView, ListItem, Label, Static
+from textual.reactive import reactive
+from textual import work
+from textual.message import Message
+from weather_service import WeatherService
+
+class WeatherWidget(Static):
+ """A custom widget to display weather information."""
+
+ weather_data = reactive("")
+
+ def on_mount(self):
+ """Start a timer to refresh weather data every 30 seconds."""
+ self.set_interval(30, self.refresh_weather)
+
+ @work
+ async def refresh_weather(self):
+ """Perform the weather API call in a worker thread."""
+ weather_text = await self.run_worker(self.fetch_weather_data, thread=True)
+ self.weather_data = weather_text
+
+ def fetch_weather_data(self):
+ city_name = self.id if self.id else "Unknown"
+ return weather_service.get_full_weather(city_name)
+
+ def render(self):
+ return self.weather_data
+
+class CityListView(ListView):
+ """A list view that emits a custom message when an item is selected."""
+
+ class CitySelected(Message):
+ def __init__(self, city: str) -> None:
+ self.city = city
+ super().__init__()
+
+ def on_list_view_selected(self, event: ListView.Selected):
+ """Send a CitySelected message when a city is chosen."""
+ # The city name is stored in the ListItem's id attribute
+ city_name = event.item.id
+ self.post_message(self.CitySelected(city_name))
+
+class WeatherApp(App):
+ """The main Textual application class."""
+
+ CSS_PATH = "css/weather.tcss"
+ BINDINGS = [("q", "quit", "Quit")]
+
+ def __init__(self):
+ super().__init__()
+ self.weather_service = WeatherService()
+ self.default_cities = ['tehran', 'london', 'new-york', 'tokyo', 'sydney',
+ 'cape-town', 'mumbai', 'beijing', 'rio-de-janeiro', 'cairo']
+ self.current_city = "tehran"
+
+ def compose(self) -> ComposeResult:
+ """Create the UI layout."""
+ yield Header()
+ with Container():
+ with Horizontal():
+ with Vertical(classes="sidebar"):
+ yield Label("Cities", classes="sidebar-title")
+ # Create ListItems with id set to the city name (lowercase)
+ city_items = [ListItem(Label(city.capitalize()), id=city) for city in self.default_cities]
+ yield CityListView(*city_items, id="city-list")
+ with Vertical(classes="main-content"):
+ yield WeatherWidget(id=self.current_city)
+ yield Footer()
+
+ def on_city_list_view_city_selected(self, message: CityListView.CitySelected):
+ """Handle city selection by updating the weather widget."""
+ self.current_city = message.city
+ # Remove the old widget
+ old_widget = self.query_one(f"#{self.current_city}")
+ old_widget.remove()
+ # Mount a new widget for the selected city
+ new_widget = WeatherWidget(id=self.current_city)
+ self.query_one(".main-content").mount(new_widget)
+
+if __name__ == "__main__":
+ # Create the global weather_service instance used by WeatherWidget
+ global weather_service
+ weather_service = WeatherService()
+ app = WeatherApp()
+ app.run()
diff --git a/weather-tui/weather_service.py b/weather-tui/weather_service.py
new file mode 100644
index 0000000..10cd42a
--- /dev/null
+++ b/weather-tui/weather_service.py
@@ -0,0 +1,72 @@
+import os
+import requests
+from datetime import datetime, timedelta
+from dotenv import load_dotenv
+
+load_dotenv()
+
+class WeatherService:
+ """Handles fetching and caching weather data from OpenWeatherMap."""
+
+ def __init__(self):
+ self.api_key = os.getenv("OPENWEATHER_API_KEY")
+ if not self.api_key:
+ raise ValueError("OPENWEATHER_API_KEY not found in .env file")
+ self.base_url = "https://api.openweathermap.org/data/2.5/weather"
+ self.cache = {}
+ self.cache_duration = timedelta(minutes=10) # Cache for 10 minutes
+
+ def get_cached_weather(self, city: str):
+ """Retrieves weather data from cache if it's still valid."""
+ if city in self.cache:
+ data, timestamp = self.cache[city]
+ if datetime.now() - timestamp < self.cache_duration:
+ return data
+ return None
+
+ def fetch_weather(self, city: str):
+ """Fetches fresh weather data from the API for a given city."""
+ cached_data = self.get_cached_weather(city)
+ if cached_data:
+ return cached_data
+
+ try:
+ params = {
+ "q": city,
+ "appid": self.api_key,
+ "units": "metric" # Use metric units
+ }
+ response = requests.get(self.base_url, params=params)
+ response.raise_for_status()
+ weather_data = response.json()
+ self.cache[city] = (weather_data, datetime.now())
+ return weather_data
+ except requests.exceptions.RequestException as e:
+ print(f"Error fetching weather for {city}: {e}")
+ return None
+
+ def get_temperature(self, city: str):
+ """Returns the temperature in Celsius for a given city."""
+ weather_data = self.fetch_weather(city)
+ if weather_data:
+ return weather_data['main']['temp']
+ return None
+
+ def get_full_weather(self, city: str):
+ """Returns a formatted string with all relevant weather information."""
+ weather_data = self.fetch_weather(city)
+ if not weather_data:
+ return f"Could not retrieve weather for {city}."
+
+ temp = weather_data['main']['temp']
+ feels_like = weather_data['main']['feels_like']
+ humidity = weather_data['main']['humidity']
+ description = weather_data['weather'][0]['description']
+ wind_speed = weather_data['wind']['speed']
+
+ return (f"Location: {city}\n"
+ f"Temperature: {temp}°C\n"
+ f"Feels like: {feels_like}°C\n"
+ f"Conditions: {description}\n"
+ f"Humidity: {humidity}%\n"
+ f"Wind Speed: {wind_speed} m/s")