From 17f8a131884925821715d67dabc0f85d24024784 Mon Sep 17 00:00:00 2001 From: kilyabin <65072190+kilyabin@users.noreply.github.com> Date: Thu, 18 Jun 2026 00:45:31 +0400 Subject: [PATCH] fix: resolve all clippy warnings breaking CI - introduce Color struct to reduce argument count in canvas drawing functions - remove unused write_to method - use is_multiple_of() per clippy suggestion --- src/canvas.rs | 108 ++++++++++++++++++++-------------------- src/screensaver/text.rs | 72 ++++++++++----------------- src/widget/clock.rs | 83 ++++++++++++------------------ src/widget/date.rs | 24 ++++----- src/widget/weather.rs | 73 ++++++++++----------------- 5 files changed, 144 insertions(+), 216 deletions(-) diff --git a/src/canvas.rs b/src/canvas.rs index 8ad76af..2f45ae8 100644 --- a/src/canvas.rs +++ b/src/canvas.rs @@ -1,6 +1,25 @@ use fontdue::{Font, FontSettings}; use image::RgbaImage; +// ── Color ───────────────────────────────────────────────────────────────────── + +#[derive(Clone, Copy, Debug)] +pub struct Color { + pub r: u8, + pub g: u8, + pub b: u8, + pub a: u8, +} + +impl Color { + pub const fn rgb(r: u8, g: u8, b: u8) -> Self { + Self { r, g, b, a: 255 } + } + pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self { + Self { r, g, b, a } + } +} + // ── Canvas ──────────────────────────────────────────────────────────────────── // Pixel buffer: each u32 = 0x00RRGGBB (softbuffer format) @@ -21,62 +40,59 @@ impl Canvas { self.buf.resize((width * height) as usize, 0); } - /// Clear to black (OLED off) pub fn clear(&mut self) { self.buf.fill(0); } #[inline] - pub fn set_pixel(&mut self, x: i32, y: i32, r: u8, g: u8, b: u8, a: u8) { + pub fn set_pixel(&mut self, x: i32, y: i32, c: Color) { if x < 0 || y < 0 || x >= self.width as i32 || y >= self.height as i32 { return; } let idx = y as usize * self.width as usize + x as usize; - if a == 255 { - self.buf[idx] = rgb(r, g, b); - } else if a > 0 { - // alpha-blend over current pixel + if c.a == 255 { + self.buf[idx] = pack(c.r, c.g, c.b); + } else if c.a > 0 { let dst = self.buf[idx]; let dr = ((dst >> 16) & 0xff) as u8; let dg = ((dst >> 8) & 0xff) as u8; let db = (dst & 0xff) as u8; - let af = a as u32; - let or_ = ((r as u32 * af + dr as u32 * (255 - af)) / 255) as u8; - let og = ((g as u32 * af + dg as u32 * (255 - af)) / 255) as u8; - let ob = ((b as u32 * af + db as u32 * (255 - af)) / 255) as u8; - self.buf[idx] = rgb(or_, og, ob); + let af = c.a as u32; + let or_ = ((c.r as u32 * af + dr as u32 * (255 - af)) / 255) as u8; + let og = ((c.g as u32 * af + dg as u32 * (255 - af)) / 255) as u8; + let ob = ((c.b as u32 * af + db as u32 * (255 - af)) / 255) as u8; + self.buf[idx] = pack(or_, og, ob); } } - pub fn fill_rect(&mut self, x: i32, y: i32, w: u32, h: u32, r: u8, g: u8, b: u8, a: u8) { + pub fn fill_rect(&mut self, x: i32, y: i32, w: u32, h: u32, c: Color) { for dy in 0..h as i32 { for dx in 0..w as i32 { - self.set_pixel(x + dx, y + dy, r, g, b, a); + self.set_pixel(x + dx, y + dy, c); } } } - pub fn draw_circle(&mut self, cx: i32, cy: i32, radius: i32, r: u8, g: u8, b: u8, a: u8) { + pub fn draw_circle(&mut self, cx: i32, cy: i32, radius: i32, c: Color) { for dy in -radius..=radius { for dx in -radius..=radius { if dx * dx + dy * dy <= radius * radius { - self.set_pixel(cx + dx, cy + dy, r, g, b, a); + self.set_pixel(cx + dx, cy + dy, c); } } } } - pub fn draw_ring(&mut self, cx: i32, cy: i32, outer: i32, inner: i32, r: u8, g: u8, b: u8, a: u8) { + pub fn draw_ring(&mut self, cx: i32, cy: i32, outer: i32, inner: i32, c: Color) { for dy in -outer..=outer { for dx in -outer..=outer { let d2 = dx * dx + dy * dy; if d2 <= outer * outer && d2 >= inner * inner { - self.set_pixel(cx + dx, cy + dy, r, g, b, a); + self.set_pixel(cx + dx, cy + dy, c); } } } } - /// Draw a line using Bresenham's algorithm - pub fn draw_line(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, thickness: u32, r: u8, g: u8, b: u8, a: u8) { + pub fn draw_line(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, thickness: u32, c: Color) { let dx = (x1 - x0).abs(); let dy = (y1 - y0).abs(); let sx = if x0 < x1 { 1 } else { -1 }; @@ -87,7 +103,7 @@ impl Canvas { loop { for ty in -(t / 2)..=(t / 2) { for tx in -(t / 2)..=(t / 2) { - self.set_pixel(x + tx, y + ty, r, g, b, a); + self.set_pixel(x + tx, y + ty, c); } } if x == x1 && y == y1 { break; } @@ -97,35 +113,28 @@ impl Canvas { } } - /// Draw an RGBA image at (x, y) pub fn draw_image(&mut self, x: i32, y: i32, img: &RgbaImage) { - for (px, py, pixel) in img.enumerate_pixels() { - self.set_pixel( - x + px as i32, - y + py as i32, - pixel[0], pixel[1], pixel[2], pixel[3], - ); + for (px, py, p) in img.enumerate_pixels() { + self.set_pixel(x + px as i32, y + py as i32, Color::rgba(p[0], p[1], p[2], p[3])); } } - /// Draw an RGBA image scaled to fill (w, h) at (x, y) pub fn draw_image_scaled(&mut self, x: i32, y: i32, w: u32, h: u32, img: &RgbaImage) { let sw = img.width(); let sh = img.height(); for dy in 0..h { for dx in 0..w { - let sx = (dx as f32 / w as f32 * sw as f32) as u32; - let sy = (dy as f32 / h as f32 * sh as f32) as u32; - let sx = sx.min(sw - 1); - let sy = sy.min(sh - 1); - let p = img.get_pixel(sx, sy); - self.set_pixel(x + dx as i32, y + dy as i32, p[0], p[1], p[2], p[3]); + let sx = ((dx as f32 / w as f32) * sw as f32) as u32; + let sy = ((dy as f32 / h as f32) * sh as f32) as u32; + let p = img.get_pixel(sx.min(sw - 1), sy.min(sh - 1)); + self.set_pixel(x + dx as i32, y + dy as i32, Color::rgba(p[0], p[1], p[2], p[3])); } } } - /// Render text, returns (pixel_width, pixel_height) - pub fn draw_text(&mut self, x: i32, y: i32, text: &str, font: &Font, size: f32, r: u8, g: u8, b: u8) -> (u32, u32) { + /// Render text; coverage from font bitmap is multiplied with `c.a`. + /// Returns (rendered_width, rendered_height). + pub fn draw_text(&mut self, x: i32, y: i32, text: &str, font: &Font, size: f32, c: Color) -> (u32, u32) { let mut cursor = x; let mut max_h = 0u32; for ch in text.chars() { @@ -136,7 +145,8 @@ impl Canvas { for px in 0..metrics.width { let cov = bitmap[py * metrics.width + px]; if cov > 0 { - self.set_pixel(bx + px as i32, by + py as i32, r, g, b, cov); + let a = ((cov as u32 * c.a as u32) / 255) as u8; + self.set_pixel(bx + px as i32, by + py as i32, Color::rgba(c.r, c.g, c.b, a)); } } } @@ -146,7 +156,6 @@ impl Canvas { ((cursor - x) as u32, max_h) } - /// Measure text without rendering pub fn measure_text(text: &str, font: &Font, size: f32) -> (u32, u32) { let mut w = 0u32; let mut h = 0u32; @@ -158,37 +167,29 @@ impl Canvas { (w, h) } - /// Draw rounded rectangle (Material card) - pub fn draw_rounded_rect(&mut self, x: i32, y: i32, w: u32, h: u32, radius: u32, r: u8, g: u8, b: u8, a: u8) { + pub fn draw_rounded_rect(&mut self, x: i32, y: i32, w: u32, h: u32, radius: u32, c: Color) { let rad = radius as i32; - // fill center - self.fill_rect(x + rad, y, w - 2 * radius, h, r, g, b, a); - self.fill_rect(x, y + rad, w, h - 2 * radius, r, g, b, a); - // four corner circles + self.fill_rect(x + rad, y, w - 2 * radius, h, c); + self.fill_rect(x, y + rad, w, h - 2 * radius, c); let corners = [ - (x + rad, y + rad), + (x + rad, y + rad), (x + w as i32 - rad - 1, y + rad), - (x + rad, y + h as i32 - rad - 1), + (x + rad, y + h as i32 - rad - 1), (x + w as i32 - rad - 1, y + h as i32 - rad - 1), ]; for (cx, cy) in corners { - self.draw_circle(cx, cy, rad, r, g, b, a); + self.draw_circle(cx, cy, rad, c); } } - - pub fn write_to(&self, target: &mut [u32]) { - target.copy_from_slice(&self.buf); - } } #[inline] -pub fn rgb(r: u8, g: u8, b: u8) -> u32 { +fn pack(r: u8, g: u8, b: u8) -> u32 { ((r as u32) << 16) | ((g as u32) << 8) | b as u32 } // ── Font loading ───────────────────────────────────────────────────────────── -/// Try common system font paths, return the first that loads pub fn load_system_font() -> Option { let candidates = [ "/usr/share/fonts/TTF/DejaVuSans.ttf", @@ -205,7 +206,6 @@ pub fn load_system_font() -> Option { "/usr/share/fonts/truetype/ubuntu/Ubuntu-R.ttf", ]; - // also check user-local fonts let home_fonts = dirs::home_dir() .map(|h| h.join(".local/share/fonts")) .unwrap_or_default(); diff --git a/src/screensaver/text.rs b/src/screensaver/text.rs index ca21416..c5e04c9 100644 --- a/src/screensaver/text.rs +++ b/src/screensaver/text.rs @@ -1,28 +1,26 @@ use fontdue::Font; -use crate::{canvas::Canvas, config::{TextAnimation, parse_hex_color}}; +use crate::{canvas::{Canvas, Color}, config::{TextAnimation, parse_hex_color}}; use super::Screensaver; pub struct TextScreensaver { content: String, font: Font, font_size: f32, - color: (u8, u8, u8), + color: Color, animation: TextAnimation, time: f64, - // Lissajous orbit for anti-burn-in floating - lx: f64, - ly: f64, + lx: f64, + ly: f64, } impl TextScreensaver { pub fn new(content: String, font: Font, font_size: f32, color: &str, animation: TextAnimation) -> Self { - let color = parse_hex_color(color); - // random-ish starting phase so it doesn't start center every time + let (r, g, b) = parse_hex_color(color); Self { content, font, font_size, - color, + color: Color::rgb(r, g, b), animation, time: 0.0, lx: 0.0, @@ -38,66 +36,46 @@ impl Screensaver for TextScreensaver { let (cw, ch) = (canvas.width as f32, canvas.height as f32); let (tw, th) = Canvas::measure_text(&self.content, &self.font, self.font_size); + let c = self.color; let (ox, oy) = match self.animation { TextAnimation::Float => { - // Lissajous curve: a=3, b=2 gives a nice non-repeating pattern let margin_x = (cw * 0.3) as f64; let margin_y = (ch * 0.3) as f64; let cx = cw as f64 / 2.0 - tw as f64 / 2.0; let cy = ch as f64 / 2.0 - th as f64 / 2.0; - let x = cx + margin_x * (3.0 * self.time * 0.05 + self.lx).sin(); - let y = cy + margin_y * (2.0 * self.time * 0.05 + self.ly).sin(); - (x as i32, y as i32) - } - TextAnimation::Typewriter => { - let cx = (cw / 2.0 - tw as f32 / 2.0) as i32; - let cy = (ch / 2.0 - th as f32 / 2.0) as i32; - (cx, cy) - } - TextAnimation::Fade => { - let cx = (cw / 2.0 - tw as f32 / 2.0) as i32; - let cy = (ch / 2.0 - th as f32 / 2.0) as i32; - (cx, cy) + ( + (cx + margin_x * (3.0 * self.time * 0.05 + self.lx).sin()) as i32, + (cy + margin_y * (2.0 * self.time * 0.05 + self.ly).sin()) as i32, + ) } + TextAnimation::Typewriter | TextAnimation::Fade => ( + (cw / 2.0 - tw as f32 / 2.0) as i32, + (ch / 2.0 - th as f32 / 2.0) as i32, + ), }; - let (r, g, b) = self.color; - match self.animation { TextAnimation::Float => { - canvas.draw_text(ox, oy + th as i32, &self.content, &self.font, self.font_size, r, g, b); + canvas.draw_text(ox, oy + th as i32, &self.content, &self.font, self.font_size, c); } TextAnimation::Typewriter => { - // show chars one by one, cycling every 3 seconds per char let chars: Vec = self.content.chars().collect(); let visible = ((self.time * 2.0) as usize % (chars.len() * 2 + 4)).min(chars.len()); let partial: String = chars[..visible].iter().collect(); - canvas.draw_text(ox, oy + th as i32, &partial, &self.font, self.font_size, r, g, b); - // blinking cursor - if (self.time * 2.0) as u32 % 2 == 0 && visible < chars.len() { + canvas.draw_text(ox, oy + th as i32, &partial, &self.font, self.font_size, c); + if ((self.time * 2.0) as u32).is_multiple_of(2) && visible < chars.len() { let (pw, _) = Canvas::measure_text(&partial, &self.font, self.font_size); - canvas.fill_rect(ox + pw as i32, oy, 3, th, r, g, b, 200); + canvas.fill_rect(ox + pw as i32, oy, 3, th, Color::rgba(c.r, c.g, c.b, 200)); } } TextAnimation::Fade => { - // fade in/out cycle: 3s in, 1s hold, 3s out, 1s black - let cycle = 8.0_f64; - let t = self.time % cycle; - let alpha: u8 = if t < 3.0 { - (t / 3.0 * 255.0) as u8 - } else if t < 4.0 { - 255 - } else if t < 7.0 { - ((7.0 - t) / 3.0 * 255.0) as u8 - } else { - 0 - }; - // draw to temp layer - let ar = (r as u32 * alpha as u32 / 255) as u8; - let ag = (g as u32 * alpha as u32 / 255) as u8; - let ab = (b as u32 * alpha as u32 / 255) as u8; - canvas.draw_text(ox, oy + th as i32, &self.content, &self.font, self.font_size, ar, ag, ab); + let t = self.time % 8.0; + let alpha: u8 = if t < 3.0 { (t / 3.0 * 255.0) as u8 } + else if t < 4.0 { 255 } + else if t < 7.0 { ((7.0 - t) / 3.0 * 255.0) as u8 } + else { 0 }; + canvas.draw_text(ox, oy + th as i32, &self.content, &self.font, self.font_size, Color::rgba(c.r, c.g, c.b, alpha)); } } } diff --git a/src/widget/clock.rs b/src/widget/clock.rs index 0025b35..64b4812 100644 --- a/src/widget/clock.rs +++ b/src/widget/clock.rs @@ -3,7 +3,7 @@ use fontdue::Font; use std::f64::consts::PI; use crate::{ - canvas::Canvas, + canvas::{Canvas, Color}, config::{ClockStyle, Position, parse_hex_color}, }; use super::Widget; @@ -11,7 +11,7 @@ use super::Widget; pub struct ClockWidget { font: Font, font_size: f32, - color: (u8, u8, u8), + color: Color, position: Position, style: ClockStyle, show_seconds: bool, @@ -19,30 +19,22 @@ pub struct ClockWidget { impl ClockWidget { pub fn new(font: Font, font_size: f32, color: &str, position: Position, style: ClockStyle, show_seconds: bool) -> Self { - Self { - font, - font_size, - color: parse_hex_color(color), - position, - style, - show_seconds, - } + let (r, g, b) = parse_hex_color(color); + Self { font, font_size, color: Color::rgb(r, g, b), position, style, show_seconds } } } impl Widget for ClockWidget { fn render(&mut self, canvas: &mut Canvas, _dt: f64) { let now = Local::now(); - let (r, g, b) = self.color; - match self.style { - ClockStyle::Digital => render_digital(self, canvas, &now, r, g, b), - ClockStyle::Analog => render_analog(self, canvas, &now, r, g, b), + ClockStyle::Digital => render_digital(self, canvas, &now), + ClockStyle::Analog => render_analog(self, canvas, &now), } } } -fn render_digital(w: &ClockWidget, canvas: &mut Canvas, now: &chrono::DateTime, r: u8, g: u8, b: u8) { +fn render_digital(w: &ClockWidget, canvas: &mut Canvas, now: &chrono::DateTime) { let text = if w.show_seconds { now.format("%H:%M:%S").to_string() } else { @@ -50,47 +42,46 @@ fn render_digital(w: &ClockWidget, canvas: &mut Canvas, now: &chrono::DateTime, r: u8, g: u8, b: u8) { +fn render_analog(w: &ClockWidget, canvas: &mut Canvas, now: &chrono::DateTime) { use chrono::Timelike; let size = (w.font_size * 3.0) as i32; let cx = w.position.x.resolve(canvas.width, size as u32 * 2) + size; let cy = w.position.y.resolve(canvas.height, size as u32 * 2) + size; let radius = size; + let c = w.color; - // Face ring - canvas.draw_ring(cx, cy, radius, radius - 4, r / 3, g / 3, b / 3, 180); + canvas.draw_ring(cx, cy, radius, radius - 4, Color::rgba(c.r / 3, c.g / 3, c.b / 3, 180)); - // Hour ticks for i in 0..12 { let angle = i as f64 * PI / 6.0 - PI / 2.0; let inner = (radius as f64 * 0.85) as i32; let outer = radius - 2; - let x0 = cx + (angle.cos() * inner as f64) as i32; - let y0 = cy + (angle.sin() * inner as f64) as i32; - let x1 = cx + (angle.cos() * outer as f64) as i32; - let y1 = cy + (angle.sin() * outer as f64) as i32; - canvas.draw_line(x0, y0, x1, y1, 2, r / 2, g / 2, b / 2, 255); + canvas.draw_line( + cx + (angle.cos() * inner as f64) as i32, + cy + (angle.sin() * inner as f64) as i32, + cx + (angle.cos() * outer as f64) as i32, + cy + (angle.sin() * outer as f64) as i32, + 2, + Color::rgba(c.r / 2, c.g / 2, c.b / 2, 255), + ); } - // Hands let h = now.hour() as f64 % 12.0; let m = now.minute() as f64; let s = now.second() as f64; @@ -99,28 +90,16 @@ fn render_analog(w: &ClockWidget, canvas: &mut Canvas, now: &chrono::DateTime Self { - Self { - font, - font_size, - color: parse_hex_color(color), - position, - format, - } + let (r, g, b) = parse_hex_color(color); + Self { font, font_size, color: Color::rgb(r, g, b), position, format } } } impl Widget for DateWidget { fn render(&mut self, canvas: &mut Canvas, _dt: f64) { - let text = Local::now().format(&self.format).to_string(); - let (r, g, b) = self.color; - let (tw, th) = Canvas::measure_text(&text, &self.font, self.font_size); - let x = self.position.x.resolve(canvas.width, tw); - let y = self.position.y.resolve(canvas.height, th) + th as i32; - canvas.draw_text(x, y, &text, &self.font, self.font_size, r, g, b); + let text = Local::now().format(&self.format).to_string(); + let (tw, th) = Canvas::measure_text(&text, &self.font, self.font_size); + let x = self.position.x.resolve(canvas.width, tw); + let y = self.position.y.resolve(canvas.height, th) + th as i32; + canvas.draw_text(x, y, &text, &self.font, self.font_size, self.color); } } diff --git a/src/widget/weather.rs b/src/widget/weather.rs index 6c910ac..095067e 100644 --- a/src/widget/weather.rs +++ b/src/widget/weather.rs @@ -3,13 +3,11 @@ use serde::Deserialize; use std::sync::{Arc, Mutex}; use crate::{ - canvas::Canvas, + canvas::{Canvas, Color}, config::{Position, WeatherUnits, parse_hex_color}, }; use super::Widget; -// ── OpenWeatherMap response structs ────────────────────────────────────────── - #[derive(Deserialize)] struct OWMResponse { main: OWMMain, @@ -29,8 +27,6 @@ struct OWMWeather { icon: String, } -// ── Widget ─────────────────────────────────────────────────────────────────── - struct WeatherData { temp: f32, humidity: u32, @@ -40,31 +36,24 @@ struct WeatherData { } pub struct WeatherWidget { - font: Font, - font_size: f32, - color: (u8, u8, u8), - position: Position, - api_key: String, - location: String, - units: WeatherUnits, - data: Arc>>, + font: Font, + font_size: f32, + color: Color, + position: Position, + api_key: String, + location: String, + units: WeatherUnits, + data: Arc>>, refresh_in: f64, } impl WeatherWidget { - pub fn new( - font: Font, - font_size: f32, - color: &str, - position: Position, - api_key: String, - location: String, - units: WeatherUnits, - ) -> Self { + pub fn new(font: Font, font_size: f32, color: &str, position: Position, api_key: String, location: String, units: WeatherUnits) -> Self { + let (r, g, b) = parse_hex_color(color); let widget = Self { font, font_size, - color: parse_hex_color(color), + color: Color::rgb(r, g, b), position, api_key, location, @@ -90,12 +79,11 @@ impl WeatherWidget { Ok(resp) => match resp.json::().await { Ok(json) => { let w = json.weather.into_iter().next(); - let mut lock = data.lock().unwrap(); - *lock = Some(WeatherData { + *data.lock().unwrap() = Some(WeatherData { temp: json.main.temp, humidity: json.main.humidity, - description: w.as_ref().map(|w| w.description.clone()).unwrap_or_default(), - icon: w.as_ref().map(|w| w.icon.clone()).unwrap_or_default(), + description: w.as_ref().map(|x| x.description.clone()).unwrap_or_default(), + icon: w.as_ref().map(|x| x.icon.clone()).unwrap_or_default(), city: json.name, }); } @@ -111,53 +99,42 @@ impl Widget for WeatherWidget { fn render(&mut self, canvas: &mut Canvas, dt: f64) { self.refresh_in -= dt; if self.refresh_in <= 0.0 { - self.refresh_in = 1800.0; // refresh every 30 min + self.refresh_in = 1800.0; self.fetch_async(); } - let lock = self.data.lock().unwrap(); - let (r, g, b) = self.color; - let unit_sym = match self.units { WeatherUnits::Metric => "°C", WeatherUnits::Imperial => "°F" }; - - let text = match lock.as_ref() { + let lock = self.data.lock().unwrap(); + let text = match lock.as_ref() { None => "Loading weather…".to_string(), Some(d) => format!( "{} {} {:.0}{unit_sym} H:{:.0}% {}", - d.city, - icon_char(&d.icon), - d.temp, - d.humidity, - d.description, + d.city, icon_char(&d.icon), d.temp, d.humidity, d.description, ), }; let (tw, th) = Canvas::measure_text(&text, &self.font, self.font_size); - let pad = (self.font_size * 0.4) as u32; - let x = self.position.x.resolve(canvas.width, tw); - let y = self.position.y.resolve(canvas.height, th) + th as i32; + let pad = (self.font_size * 0.4) as u32; + let x = self.position.x.resolve(canvas.width, tw); + let y = self.position.y.resolve(canvas.height, th) + th as i32; - // card background canvas.draw_rounded_rect( x - pad as i32, y - th as i32 - pad as i32, tw + pad * 2, th + pad * 2, (self.font_size * 0.25) as u32, - 10, 10, 10, 200, + Color::rgba(10, 10, 10, 200), ); - - canvas.draw_text(x, y, &text, &self.font, self.font_size, r, g, b); + canvas.draw_text(x, y, &text, &self.font, self.font_size, self.color); } } -/// Map OWM icon codes to Unicode weather symbols fn icon_char(icon: &str) -> &'static str { match icon { s if s.starts_with("01") => "☀", s if s.starts_with("02") => "⛅", - s if s.starts_with("03") => "☁", - s if s.starts_with("04") => "☁", + s if s.starts_with("03") | s.starts_with("04") => "☁", s if s.starts_with("09") => "🌧", s if s.starts_with("10") => "🌦", s if s.starts_with("11") => "⛈",