From ed0eb532db735fe58dfc7e84e1634fd70713507e Mon Sep 17 00:00:00 2001 From: server2 Date: Fri, 16 Oct 2026 02:51:51 +0200 Subject: [PATCH] Model list arrow flips when open; temperatures in a fixed order; bigger avatar; clock font, weight and digit width as settings; Cancel at the top of the event form, Esc cancels it --- src/ai.rs | 4 +++- src/config.rs | 7 +++++-- src/glyph.rs | 2 ++ src/home.rs | 47 ++++++++++++++++++++++++++++++++++++----------- src/pods.rs | 3 ++- src/stats.rs | 16 ++++++++++++---- 6 files changed, 60 insertions(+), 19 deletions(-) diff --git a/src/ai.rs b/src/ai.rs index 5f037a7..38f4e21 100644 --- a/src/ai.rs +++ b/src/ai.rs @@ -321,7 +321,9 @@ pub fn card(width: f32, height: f32) -> Rectangle { let theme = theme::now(); // the model, as a chip that opens the list of the others - let mut buttons: Vec> = vec![Box::new(pods::chip(&settings::model(), Some(Glyph::Chevron), pick))]; + // the model, as a chip that opens the list of the others; its arrow points up while the list is open + let arrow = if ai.models.is_some() { Glyph::ChevronUp } else { Glyph::Chevron }; + let mut buttons: Vec> = vec![Box::new(pods::chip(&settings::model(), Some(arrow), pick))]; if ai.busy { buttons.push(Box::new(pods::chip("Stop", None, stop))); diff --git a/src/config.rs b/src/config.rs index aecc6cc..9e69c0a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -518,7 +518,7 @@ settings! { interval_ms: u64 = 1000, 200..=10000; "How often the system monitor reads the system" history: usize = 60, 10..=600; "How many readings the graphs show" graph_height: f32 = 34.0, 10..=200; "Height of each graph" - temperatures: usize = 6, 0..=20; "How many temperature sensors to show, hottest first" + temperatures: usize = 6, 0..=20; "How many temperature sensors to show, always in the same order: CPU, iGPU, eGPU, NVMe, memory, board, then the rest" warm: f32 = 65.0, 20..=120; "From this temperature (°C) a sensor shows in the warm colour" hot: f32 = 85.0, 20..=130; "From this temperature (°C) a sensor shows in the hot colour" cool_color: Color = Color::rgb(120, 200, 255); "Colour of a sensor below the warm temperature" @@ -532,9 +532,12 @@ settings! { home { width: f32 = 820.0, 300..=1600; "Width of the home card. It opens rightwards from beside the island, so on a 1920 px wide monitor about 820 fits" clock_size: f32 = 64.0, 20..=200; "Size of the big clock" + clock_font: String = String::new(); "The big clock's font family, any installed one (fc-list : family lists them); empty takes the shell's font" + clock_weight: u64 = 700, 100..=900; "The big clock's weight, 100 (thin) to 900 (black); a font without that weight shows its nearest one" + clock_letter_width: f32 = 0.0, 0..=2; "Width of one of the clock's digits as a share of its size, so the weather beside it makes room; 0 takes the shell font's" seconds: bool = false; "Show seconds on the big clock" avatar: String = String::from("~/.face"); "The picture shown as the user's avatar; a drawn person without one" - avatar_size: f32 = 48.0, 24..=120; "Size of the avatar; the greeting row is as tall" + avatar_size: f32 = 84.0, 24..=200; "Size of the avatar; the greeting row is as tall" gap: f32 = 12.0, 0..=60; "Space between the home card's parts" city: String = String::from("Munich"); "The weather's place, as shown" latitude: f32 = 48.137, -90.0..=90.0; "The weather's latitude" diff --git a/src/glyph.rs b/src/glyph.rs index 799bfb7..44497a4 100644 --- a/src/glyph.rs +++ b/src/glyph.rs @@ -12,6 +12,7 @@ pub enum Glyph { Muted, Bluetooth, Chevron, + ChevronUp, // pointing left, for a menu's way back Back, Note, @@ -138,6 +139,7 @@ pub fn glyph_shapes(glyph: Glyph, size: f32, color: Color, weight: f32, origin: .cap(Cap::Round) ], Glyph::Chevron => shapes![Path::new().move_to(x(0.12), y(0.32)).line_to(x(0.5), y(0.7)).line_to(x(0.88), y(0.32)).stroke(1.5, color).cap(Cap::Round)], + Glyph::ChevronUp => shapes![Path::new().move_to(x(0.12), y(0.68)).line_to(x(0.5), y(0.3)).line_to(x(0.88), y(0.68)).stroke(1.5, color).cap(Cap::Round)], Glyph::Back => shapes![Path::new().move_to(x(0.64), y(0.14)).line_to(x(0.3), y(0.5)).line_to(x(0.64), y(0.86)).stroke(thin.max(1.6), color).cap(Cap::Round)], // an eighth note: round head, stem, a curved flag Glyph::Note => shapes![ diff --git a/src/home.rs b/src/home.rs index 0010fd3..0d31b7f 100644 --- a/src/home.rs +++ b/src/home.rs @@ -350,6 +350,22 @@ fn sync_settings() -> (String, String, String, bool) { } // whether the card has a form to type into: then it keeps the keyboard and stays open +// the event form closes without saving +fn cancel() { + Agenda::write().form = None; +} + +// Esc in the card: closes the event form if it's open (then true), else leaves it to the card +pub fn escape() -> bool { + let open = Agenda::read().form.is_some(); + + if open { + cancel(); + } + + open +} + pub fn typing() -> bool { Agenda::read().form.is_some() } @@ -1197,18 +1213,18 @@ fn top_row(clock: &Clock, width: f32, shapes: &mut Vec>) -> Box>) -> Box { - let theme = theme::now(); let gap = settings::gap(); let seconds_today = clock.now.rem_euclid(86_400); let (hour, minute, second) = (seconds_today / 3_600, seconds_today % 3_600 / 60, seconds_today % 60); let time = if settings::seconds() { format!("{hour:02}:{minute:02}:{second:02}") } else { format!("{hour:02}:{minute:02}") }; - let clock_w = (settings::clock_size() * font::letter_width() * time.chars().count() as f32).min(width / 2.0); + let letter = if settings::clock_letter_width() > 0.0 { settings::clock_letter_width() } else { font::letter_width() }; + let clock_w = (settings::clock_size() * letter * time.chars().count() as f32).min(width / 2.0); let weather_w = (width - clock_w - gap).max(0.0); let height = middle_h(); - let big = Rectangle::new().width(clock_w).height(height).align_child(Start, Center).child(Text::new(time).size(settings::clock_size()).weight(Weight::Bold).color(theme.text).tight()); + let big = Rectangle::new().width(clock_w).height(height).align_child(Start, Center).child(clock_text(time)); let weather = weather(forecast, weather_w, today, (clock_w + gap, y + (height - weather_h()) / 2.0), shapes); Box::new( @@ -1219,6 +1235,14 @@ fn middle_row(clock: &Clock, forecast: &Forecast, width: f32, today: i64, y: f32 ) } +// the big clock's digits in its own font and weight +fn clock_text(time: String) -> Text { + let digits = Text::new(time).size(settings::clock_size()).weight(settings::clock_weight().min(1000) as u16).color(theme::now().text).tight(); + let family = settings::clock_font(); + + if family.trim().is_empty() { digits } else { digits.font(family.trim()) } +} + // the weather now over the coming days, at `origin` in the card fn weather(forecast: &Forecast, width: f32, today: i64, origin: (f32, f32), shapes: &mut Vec>) -> Rectangle { let theme = theme::now(); @@ -1744,6 +1768,7 @@ fn event_form(agenda: &Agenda, form: &Form, width: f32, origin: (f32, f32), shap }; let mut top: Vec> = vec![Box::new(Text::new(heading).size(text::heading()).weight(Weight::Bold).color(theme.text))]; + let mut actions: Vec> = Vec::new(); if form.editing.is_some() { let words = match (form.confirm, series) { @@ -1753,11 +1778,16 @@ fn event_form(agenda: &Agenda, form: &Form, width: f32, origin: (f32, f32), shap }; let button = button_size(); - let at = (origin.0 + width - choice_width(words, button / 2.0 + 6.0), origin.1 + (pods::header_height() - button) / 2.0); + let cancel_w = choice_width("Cancel", 0.0); + let at = (origin.0 + width - cancel_w - gap - choice_width(words, button / 2.0 + 6.0), origin.1 + (pods::header_height() - button) / 2.0); - top.push(Box::new(icon_chip(words, form.confirm, Glyph::Trash, at, shapes, remove))); + actions.push(Box::new(icon_chip(words, form.confirm, Glyph::Trash, at, shapes, remove))); } + // Cancel at the top, where it's always in sight (Esc does the same) + actions.push(Box::new(pods::chip("Cancel", None, cancel))); + top.push(Box::new(Row::new(actions).gap(gap).align(Center))); + let mut parts: Vec> = vec![ Box::new(Rectangle::new().width(width).height(pods::header_height()).child(Row::new(top).width(width).justify(SpaceBetween).align(Center))), Box::new(field(title, width)), @@ -1834,12 +1864,7 @@ fn event_form(agenda: &Agenda, form: &Form, width: f32, origin: (f32, f32), shap parts.push(Box::new(wrapped(problem.clone(), overlays::low_battery(), width))); } - let buttons: Vec> = vec![ - Box::new(choice(if form.saving { "Saving…" } else { "Save" }, true, None, save)), - Box::new(pods::chip("Cancel", None, || Agenda::write().form = None)), - ]; - - parts.push(Box::new(Row::new(buttons).gap(gap).align(Center))); + parts.push(Box::new(choice(if form.saving { "Saving…" } else { "Save" }, true, None, save))); Box::new(Rectangle::new().width(width).height(calendar_h()).child(Column::new(parts).gap(gap))) } diff --git a/src/pods.rs b/src/pods.rs index a9edaf7..bfc514d 100644 --- a/src/pods.rs +++ b/src/pods.rs @@ -334,7 +334,8 @@ fn side(monitor: &Monitor, left: bool) -> LayerWindow { let window = if typing { window.keyboard(Keyboard::OnDemand).on_key(|key| { - if key == Key::Escape { + // Esc closes the home card's event form first, then the card + if key == Key::Escape && !home::escape() { close(); } }) diff --git a/src/stats.rs b/src/stats.rs index d1bf329..e9e0afe 100644 --- a/src/stats.rs +++ b/src/stats.rs @@ -156,12 +156,19 @@ fn gpu_clock() -> (f32, String) { (0.0, String::from("no clock to read")) } -// every hwmon device's hottest sensor, named for what it is, hottest first +// what the temperatures are shown in, always: a sensor keeps its place whatever its reading +const ORDER: [&str; 8] = ["CPU", "iGPU", "eGPU", "NVMe", "Memory", "Board", "ThinkPad", "Wi-Fi"]; + +// every hwmon device's hottest sensor, named for what it is, in ORDER (then by name, then by device) fn temperatures() -> Vec<(String, Glyph, f32)> { let mut found: Vec<(String, Glyph, f32)> = Vec::new(); - for entry in fs::read_dir("/sys/class/hwmon").into_iter().flatten().flatten() { - let device = entry.path(); + // the devices in their numbers' order (hwmon2 before hwmon10), so two of a kind keep theirs too + let mut devices: Vec = fs::read_dir("/sys/class/hwmon").into_iter().flatten().flatten().map(|entry| entry.path()).collect(); + + devices.sort_by_key(|path| path.file_name().and_then(|name| name.to_str()).and_then(|name| name.trim_start_matches("hwmon").parse::().ok()).unwrap_or(u32::MAX)); + + for device in devices { let kind = read(device.join("name")).trim().to_string(); let hottest = fs::read_dir(&device) @@ -191,7 +198,8 @@ fn temperatures() -> Vec<(String, Glyph, f32)> { found.push((name.to_string(), glyph, celsius)); } - found.sort_by(|a, b| b.2.total_cmp(&a.2)); + // a stable sort: same kinds keep the devices' order + found.sort_by_key(|(name, ..)| (ORDER.iter().position(|kind| kind == name).unwrap_or(ORDER.len()), name.clone())); found.truncate(settings::temperatures()); // a kind shown more than once is numbered, in the order shown