use crate::time_utils;
use ratatui::{prelude::Stylize, style::Color, text::Span};
use serde::Deserialize;
use std::sync::{Arc, Mutex};
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Default, Clone)]
pub struct WeatherData {
pub temperature: String,
pub condition: String,
pub icon: String,
}
#[derive(Debug)]
pub struct Weather {
data: Arc<Mutex<WeatherData>>,
cached_span_content: Arc<Mutex<String>>,
last_update: Arc<Mutex<u64>>,
day_start: u8,
night_start: u8,
_update_handle: tokio::task::JoinHandle<()>,
}
#[derive(Debug, Deserialize)]
struct WeatherResponse {
main: Main,
weather: Vec<WeatherCondition>,
}
#[derive(Debug, Deserialize)]
struct Main {
temp: f64,
}
#[derive(Debug, Deserialize)]
struct WeatherCondition {
main: String,
}
impl Default for Weather {
fn default() -> Self {
Self::new()
}
}
impl Weather {
pub fn new() -> Self {
Self::with_config(
time_utils::default_day_start(),
time_utils::default_night_start(),
)
}
pub fn with_config(day_start: u8, night_start: u8) -> Self {
let data = Arc::new(Mutex::new(WeatherData {
temperature: "--".to_string(),
condition: "Unknown".to_string(),
icon: "".to_string(),
}));
let cached_span_content = Arc::new(Mutex::new(" --°C".to_string()));
let last_update = Arc::new(Mutex::new(0u64));
let data_clone = data.clone();
let cached_span_content_clone = cached_span_content.clone();
let last_update_clone = last_update.clone();
let day_start_clone = day_start;
let night_start_clone = night_start;
let update_handle = tokio::spawn(async move {
let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(600));
loop {
interval.tick().await;
if let Ok(weather_data) = Self::fetch_weather_async().await {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let is_nighttime = time_utils::is_nighttime(day_start_clone, night_start_clone);
if let Ok(mut data_guard) = data_clone.lock() {
data_guard.temperature = format!("{:.0}", weather_data.main.temp);
data_guard.condition = weather_data.weather[0].main.clone();
data_guard.icon =
Self::get_weather_icon(&data_guard.condition, is_nighttime);
let new_content =
format!("{} {}°C", data_guard.icon, data_guard.temperature);
if let Ok(mut cached_guard) = cached_span_content_clone.lock() {
*cached_guard = new_content;
}
}
if let Ok(mut last_update_guard) = last_update_clone.lock() {
*last_update_guard = now;
}
}
}
});
Self {
data,
cached_span_content,
last_update,
day_start,
night_start,
_update_handle: update_handle,
}
}
pub fn update(&mut self) {
let _now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
if let Ok(last_update_guard) = self.last_update.lock()
&& *last_update_guard == 0
{
let data_clone = self.data.clone();
let cached_span_content_clone = self.cached_span_content.clone();
let last_update_clone = self.last_update.clone();
let day_start_clone = self.day_start;
let night_start_clone = self.night_start;
tokio::spawn(async move {
if let Ok(weather_data) = Self::fetch_weather_async().await {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let is_nighttime = time_utils::is_nighttime(day_start_clone, night_start_clone);
if let Ok(mut data_guard) = data_clone.lock() {
data_guard.temperature = format!("{:.0}", weather_data.main.temp);
data_guard.condition = weather_data.weather[0].main.clone();
data_guard.icon =
Self::get_weather_icon(&data_guard.condition, is_nighttime);
let new_content =
format!("{} {}°C", data_guard.icon, data_guard.temperature);
if let Ok(mut cached_guard) = cached_span_content_clone.lock() {
*cached_guard = new_content;
}
}
if let Ok(mut last_update_guard) = last_update_clone.lock() {
*last_update_guard = now;
}
}
});
}
}
pub fn get_weather_data(&self) -> WeatherData {
self.data
.lock()
.unwrap_or_else(|_| panic!("Weather data mutex poisoned"))
.clone()
}
pub fn render_as_spans(&self, colorize: bool) -> Vec<Span<'_>> {
let cached_content = if let Ok(guard) = self.cached_span_content.lock() {
guard.clone()
} else {
" --°C".to_string()
};
let span = Span::raw(cached_content);
if colorize {
let data = self.get_weather_data();
let color = {
let condition_lower = data.condition.to_lowercase();
if condition_lower.contains("clear") || condition_lower.contains("sunny") {
time_utils::get_time_based_color(
Color::Yellow, Color::LightCyan, self.day_start,
self.night_start,
)
} else if condition_lower.contains("cloud") || condition_lower.contains("overcast")
{
Color::Gray } else if condition_lower.contains("rain") || condition_lower.contains("drizzle") {
Color::Blue } else if condition_lower.contains("snow") || condition_lower.contains("sleet") {
Color::Cyan } else if condition_lower.contains("thunder") || condition_lower.contains("storm") {
Color::Magenta } else if condition_lower.contains("fog") || condition_lower.contains("mist") {
Color::DarkGray } else if condition_lower.contains("wind") {
Color::LightGreen } else {
Color::White }
};
vec![span.fg(color)]
} else {
vec![span]
}
}
async fn fetch_weather_async() -> color_eyre::Result<WeatherResponse> {
let url = "http://wttr.in/?format=j1";
let response = reqwest::get(url).await?;
let json: serde_json::Value = response.json().await?;
if let Some(current) = json["current_condition"].get(0) {
let temp = current["temp_C"]
.as_str()
.unwrap_or("--")
.parse::<f64>()
.unwrap_or(0.0);
let condition = current["weatherDesc"][0]["value"]
.as_str()
.unwrap_or("Unknown");
return Ok(WeatherResponse {
main: Main { temp },
weather: vec![WeatherCondition {
main: condition.to_string(),
}],
});
}
Err(color_eyre::eyre::eyre!("Failed to parse weather data"))
}
fn get_weather_icon(condition: &str, is_nighttime: bool) -> String {
let condition_lower = condition.to_lowercase();
match condition_lower.as_str() {
cond if cond.contains("clear") || cond.contains("sunny") => {
if is_nighttime {
"".to_string() } else {
"".to_string() }
}
cond if cond.contains("cloud") || cond.contains("overcast") => "".to_string(),
cond if cond.contains("rain") || cond.contains("drizzle") => "".to_string(),
cond if cond.contains("snow") || cond.contains("sleet") => "".to_string(),
cond if cond.contains("thunder") || cond.contains("storm") => "".to_string(),
cond if cond.contains("fog") || cond.contains("mist") => "".to_string(),
cond if cond.contains("wind") => "".to_string(),
_ => "".to_string(),
}
}
}