From abc88b2c86cdfdccc6b39d85362275609c02863b Mon Sep 17 00:00:00 2001 From: talwat <83217276+talwat@users.noreply.github.com> Date: Sun, 7 Dec 2025 23:05:31 +0100 Subject: [PATCH 1/4] feat: add clock feature --- Cargo.lock | 102 +++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 1 + src/main.rs | 4 ++ src/ui.rs | 4 +- src/ui/interface.rs | 36 +++++++++++++++- src/ui/window.rs | 13 +++++- 6 files changed, 154 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3dc4e08..e829e72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,6 +24,15 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.21" @@ -312,6 +321,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "num-traits", + "windows-link", +] + [[package]] name = "clap" version = "4.5.53" @@ -1032,6 +1052,30 @@ dependencies = [ "tracing", ] +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "2.1.1" @@ -1284,6 +1328,7 @@ version = "2.0.0-dev" dependencies = [ "arc-swap", "bytes", + "chrono", "clap", "crossterm", "dirs", @@ -2993,7 +3038,7 @@ version = "0.54.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" dependencies = [ - "windows-core", + "windows-core 0.54.0", "windows-targets 0.52.6", ] @@ -3003,10 +3048,45 @@ version = "0.54.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" dependencies = [ - "windows-result", + "windows-result 0.1.2", "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result 0.4.1", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" version = "0.2.1" @@ -3022,6 +3102,24 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.45.0" diff --git a/Cargo.toml b/Cargo.toml index 9d06d3e..aa24bdb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ arc-swap = "1.7.1" # Data reqwest = { version = "0.12.9", features = ["stream", "http2", "default-tls"], default-features = false } +chrono = { version = "0.4.42", features = ["clock"], default-features = false } bytes = "1.9.0" # I/O diff --git a/src/main.rs b/src/main.rs index 2a50596..4cccc4e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -41,6 +41,10 @@ pub struct Args { #[clap(long, short)] borderless: bool, + /// Include a small clock in the UI. + #[clap(long, short)] + clock: bool, + /// Start lowfi paused. #[clap(long, short)] paused: bool, diff --git a/src/ui.rs b/src/ui.rs index 675d921..98b6235 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use crate::{ player::Current, - ui::{self, window::Window}, + ui::{self, interface::Clock, window::Window}, Args, }; use tokio::{ @@ -170,6 +170,7 @@ impl Handle { ) -> Result<()> { let mut interval = tokio::time::interval(params.delta); let mut window = Window::new(state.width, params.borderless); + let mut clock = params.clock.then(|| Clock::new(&mut window)); loop { if let Ok(message) = rx.try_recv() { @@ -181,6 +182,7 @@ impl Handle { } } + clock.as_mut().map(|x| x.update(&mut window)); interface::draw(&mut state, &mut window, params)?; interval.tick().await; } diff --git a/src/ui/interface.rs b/src/ui/interface.rs index 4e7bdef..26588b0 100644 --- a/src/ui/interface.rs +++ b/src/ui/interface.rs @@ -1,15 +1,48 @@ use std::{env, time::Duration}; +use tokio::time::Instant; + use crate::{ ui::{self, components, window::Window}, Args, }; +/// An extremely simple clock to be used alongside the [`Window`]. +pub struct Clock(Instant); + +impl Clock { + /// Small shorthand for getting the local time now, and formatting it. + #[inline] + fn now() -> chrono::format::DelayedFormat> { + chrono::Local::now().format("%H:%M:%S") + } + + /// Checks if the last update was long enough ago, and if so, + /// updates the displayed clock. + /// + /// This is to avoid constant calls to [`chrono::Local::now`], which + /// is somewhat expensive because of timezones. + pub fn update(&mut self, window: &mut Window) { + if self.0.elapsed().as_secs() >= 1 { + window.display(Self::now(), 8); + self.0 = Instant::now(); + } + } + + /// Simply creates a new clock, and renders it's initial state to the window top. + pub fn new(window: &mut Window) -> Self { + window.display(Self::now(), 8); + + Self(Instant::now()) + } +} + #[derive(Copy, Clone, Debug, Default)] pub struct Params { pub borderless: bool, pub minimalist: bool, pub enabled: bool, + pub clock: bool, pub delta: Duration, } @@ -28,6 +61,7 @@ impl TryFrom<&Args> for Params { Ok(Self { delta, enabled: !disabled, + clock: args.clock, minimalist: args.minimalist, borderless: args.borderless, }) @@ -46,7 +80,7 @@ pub(crate) fn menu(state: &mut ui::State, params: Params) -> Vec { Some(timer) => { let volume = state.sink.volume(); let percentage = format!("{}%", (volume * 100.0).round().abs()); - if timer.elapsed() > Duration::from_secs(1) { + if timer.elapsed() > Duration::from_millis(500) { state.timer = None; } diff --git a/src/ui/window.rs b/src/ui/window.rs index 746249c..2413e33 100644 --- a/src/ui/window.rs +++ b/src/ui/window.rs @@ -1,4 +1,7 @@ -use std::io::{stdout, Stdout}; +use std::{ + fmt::Display, + io::{stdout, Stdout}, +}; use crossterm::{ cursor::{MoveToColumn, MoveUp}, @@ -17,7 +20,7 @@ pub struct Window { borderless: bool, /// The top & bottom borders, which are here since they can be - /// prerendered, as they don't change from window to window. + /// prerendered, as they don't change every single draw. /// /// If the option to not include borders is set, these will just be empty [String]s. pub(crate) borders: [String; 2], @@ -51,6 +54,12 @@ impl Window { } } + /// Adds text to the top of the window. + pub fn display(&mut self, display: impl Display, len: usize) { + let new = format!("┌─ {} {}─┐", display, "─".repeat(self.width - len - 2)); + self.borders[0] = new; + } + /// Renders the window itself, but doesn't actually draw it. /// /// `testing` just determines whether to add special features From 9e1edc06eb94471872442f61e9d761fd6ef4c5fa Mon Sep 17 00:00:00 2001 From: talwat <83217276+talwat@users.noreply.github.com> Date: Sun, 7 Dec 2025 23:08:07 +0100 Subject: [PATCH 2/4] fix: make clock updates more frequent --- src/ui/interface.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ui/interface.rs b/src/ui/interface.rs index 26588b0..705087a 100644 --- a/src/ui/interface.rs +++ b/src/ui/interface.rs @@ -23,7 +23,7 @@ impl Clock { /// This is to avoid constant calls to [`chrono::Local::now`], which /// is somewhat expensive because of timezones. pub fn update(&mut self, window: &mut Window) { - if self.0.elapsed().as_secs() >= 1 { + if self.0.elapsed().as_millis() >= 500 { window.display(Self::now(), 8); self.0 = Instant::now(); } @@ -80,7 +80,7 @@ pub(crate) fn menu(state: &mut ui::State, params: Params) -> Vec { Some(timer) => { let volume = state.sink.volume(); let percentage = format!("{}%", (volume * 100.0).round().abs()); - if timer.elapsed() > Duration::from_millis(500) { + if timer.elapsed() > Duration::from_secs(1) { state.timer = None; } From 556e6881d1783666ebf774145839e60cb673c7f9 Mon Sep 17 00:00:00 2001 From: talwat <83217276+talwat@users.noreply.github.com> Date: Sun, 7 Dec 2025 23:11:38 +0100 Subject: [PATCH 3/4] style: reorder imports --- Cargo.toml | 1 + src/ui.rs | 4 +++- src/ui/interface.rs | 6 ++---- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index aa24bdb..2b07bc8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -79,3 +79,4 @@ must_use_candidate = "allow" cast_precision_loss = "allow" cast_sign_loss = "allow" cast_possible_truncation = "allow" +struct_excessive_bools = "allow" diff --git a/src/ui.rs b/src/ui.rs index 98b6235..1520e68 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -182,7 +182,9 @@ impl Handle { } } - clock.as_mut().map(|x| x.update(&mut window)); + if let Some(x) = clock.as_mut() { + x.update(&mut window) + } interface::draw(&mut state, &mut window, params)?; interval.tick().await; } diff --git a/src/ui/interface.rs b/src/ui/interface.rs index 705087a..80b0e97 100644 --- a/src/ui/interface.rs +++ b/src/ui/interface.rs @@ -1,11 +1,9 @@ -use std::{env, time::Duration}; - -use tokio::time::Instant; - use crate::{ ui::{self, components, window::Window}, Args, }; +use std::{env, time::Duration}; +use tokio::time::Instant; /// An extremely simple clock to be used alongside the [`Window`]. pub struct Clock(Instant); From ea24b7d8b31bd75bed0453f306a1f622c97be017 Mon Sep 17 00:00:00 2001 From: talwat <83217276+talwat@users.noreply.github.com> Date: Wed, 17 Dec 2025 10:31:30 +0100 Subject: [PATCH 4/4] style: improve structure of clock code --- src/main.rs | 4 +-- src/tests/ui.rs | 18 ++++++------ src/ui.rs | 17 ++++-------- src/ui/interface.rs | 41 +++++----------------------- src/ui/interface/clock.rs | 33 ++++++++++++++++++++++ src/ui/{ => interface}/components.rs | 0 src/ui/{ => interface}/window.rs | 6 ++-- 7 files changed, 60 insertions(+), 59 deletions(-) create mode 100644 src/ui/interface/clock.rs rename src/ui/{ => interface}/components.rs (100%) rename src/ui/{ => interface}/window.rs (98%) diff --git a/src/main.rs b/src/main.rs index 81e8816..27b42bd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -35,11 +35,11 @@ pub struct Args { #[clap(long, short)] minimalist: bool, - /// Exclude borders in UI. + /// Exclude window borders. #[clap(long, short)] borderless: bool, - /// Include a small clock in the UI. + /// Include a clock. #[clap(long, short)] clock: bool, diff --git a/src/tests/ui.rs b/src/tests/ui.rs index 85d39e7..cef20be 100644 --- a/src/tests/ui.rs +++ b/src/tests/ui.rs @@ -8,50 +8,50 @@ #[cfg(test)] mod components { - use crate::ui; + use crate::ui::interface; use std::time::Duration; #[test] fn format_duration_works() { let d = Duration::from_secs(62); - assert_eq!(ui::components::format_duration(&d), "01:02"); + assert_eq!(interface::components::format_duration(&d), "01:02"); } #[test] fn format_duration_zero() { let d = Duration::from_secs(0); - assert_eq!(ui::components::format_duration(&d), "00:00"); + assert_eq!(interface::components::format_duration(&d), "00:00"); } #[test] fn format_duration_hours_wrap() { let d = Duration::from_secs(3661); // 1:01:01 - assert_eq!(ui::components::format_duration(&d), "61:01"); + assert_eq!(interface::components::format_duration(&d), "61:01"); } #[test] fn audio_bar_contains_percentage() { - let s = ui::components::audio_bar(10, 0.5, "50%"); + let s = interface::components::audio_bar(10, 0.5, "50%"); assert!(s.contains("50%")); assert!(s.starts_with(" volume:")); } #[test] fn audio_bar_muted_volume() { - let s = ui::components::audio_bar(8, 0.0, "0%"); + let s = interface::components::audio_bar(8, 0.0, "0%"); assert!(s.contains("0%")); } #[test] fn audio_bar_full_volume() { - let s = ui::components::audio_bar(10, 1.0, "100%"); + let s = interface::components::audio_bar(10, 1.0, "100%"); assert!(s.contains("100%")); } #[test] fn controls_has_items() { - let s = ui::components::controls(30); + let s = interface::components::controls(30); assert!(s.contains("[s]")); assert!(s.contains("[p]")); assert!(s.contains("[q]")); @@ -60,7 +60,7 @@ mod components { #[cfg(test)] mod window { - use crate::ui::window::Window; + use crate::ui::interface::Window; #[test] fn new_border_strings() { diff --git a/src/ui.rs b/src/ui.rs index 1520e68..7e8ea9d 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,21 +1,16 @@ use std::sync::Arc; -use crate::{ - player::Current, - ui::{self, interface::Clock, window::Window}, - Args, -}; +use crate::{player::Current, ui, Args}; use tokio::{ sync::{broadcast, mpsc::Sender}, task::JoinHandle, time::Instant, }; -pub mod components; + pub mod environment; pub use environment::Environment; pub mod input; pub mod interface; -pub mod window; #[cfg(feature = "mpris")] pub mod mpris; @@ -169,8 +164,8 @@ impl Handle { params: interface::Params, ) -> Result<()> { let mut interval = tokio::time::interval(params.delta); - let mut window = Window::new(state.width, params.borderless); - let mut clock = params.clock.then(|| Clock::new(&mut window)); + let mut window = interface::Window::new(state.width, params.borderless); + let mut clock = params.clock.then(|| interface::Clock::new(&mut window)); loop { if let Ok(message) = rx.try_recv() { @@ -182,9 +177,7 @@ impl Handle { } } - if let Some(x) = clock.as_mut() { - x.update(&mut window) - } + clock.as_mut().map(|x| x.update(&mut window)); interface::draw(&mut state, &mut window, params)?; interval.tick().await; } diff --git a/src/ui/interface.rs b/src/ui/interface.rs index db86b92..f7a1a08 100644 --- a/src/ui/interface.rs +++ b/src/ui/interface.rs @@ -1,39 +1,12 @@ -use crate::{ - ui::{self, components, window::Window}, - Args, -}; +use crate::{ui, Args}; use std::{env, time::Duration}; -use tokio::time::Instant; -/// An extremely simple clock to be used alongside the [`Window`]. -pub struct Clock(Instant); +pub mod clock; +pub mod components; +pub mod window; -impl Clock { - /// Small shorthand for getting the local time now, and formatting it. - #[inline] - fn now() -> chrono::format::DelayedFormat> { - chrono::Local::now().format("%H:%M:%S") - } - - /// Checks if the last update was long enough ago, and if so, - /// updates the displayed clock. - /// - /// This is to avoid constant calls to [`chrono::Local::now`], which - /// is somewhat expensive because of timezones. - pub fn update(&mut self, window: &mut Window) { - if self.0.elapsed().as_millis() >= 500 { - window.display(Self::now(), 8); - self.0 = Instant::now(); - } - } - - /// Simply creates a new clock, and renders it's initial state to the window top. - pub fn new(window: &mut Window) -> Self { - window.display(Self::now(), 8); - - Self(Instant::now()) - } -} +pub use clock::Clock; +pub use window::Window; /// UI-specific parameters and options. #[derive(Copy, Clone, Debug, Default)] @@ -47,7 +20,7 @@ pub struct Params { /// Whether the visual part of the UI should be enabled. /// This only applies if the MPRIS feature is enabled. pub enabled: bool, - + /// Whether to include the clock on the top bar. pub clock: bool, diff --git a/src/ui/interface/clock.rs b/src/ui/interface/clock.rs new file mode 100644 index 0000000..64d57d1 --- /dev/null +++ b/src/ui/interface/clock.rs @@ -0,0 +1,33 @@ +use tokio::time::Instant; + +use super::window::Window; + +/// An extremely simple clock to be used alongside the [`Window`]. +pub struct Clock(Instant); + +impl Clock { + /// Small shorthand for getting the local time now, and formatting it. + #[inline] + fn now() -> chrono::format::DelayedFormat> { + chrono::Local::now().format("%H:%M:%S") + } + + /// Checks if the last update was long enough ago, and if so, + /// updates the displayed clock. + /// + /// This is to avoid constant calls to [`chrono::Local::now`], which + /// is somewhat expensive because of timezones. + pub fn update(&mut self, window: &mut Window) { + if self.0.elapsed().as_millis() >= 200 { + window.display(Self::now(), 8); + self.0 = Instant::now(); + } + } + + /// Simply creates a new clock, and renders it's initial state to the top of the window. + pub fn new(window: &mut Window) -> Self { + window.display(Self::now(), 8); + + Self(Instant::now()) + } +} diff --git a/src/ui/components.rs b/src/ui/interface/components.rs similarity index 100% rename from src/ui/components.rs rename to src/ui/interface/components.rs diff --git a/src/ui/window.rs b/src/ui/interface/window.rs similarity index 98% rename from src/ui/window.rs rename to src/ui/interface/window.rs index 2413e33..eaecc77 100644 --- a/src/ui/window.rs +++ b/src/ui/interface/window.rs @@ -11,6 +11,8 @@ use crossterm::{ use std::fmt::Write as _; use unicode_segmentation::UnicodeSegmentation as _; +use crate::ui; + /// Represents an abstraction for drawing the actual lowfi window itself. /// /// The main purpose of this struct is just to add the fancy border, @@ -72,7 +74,7 @@ impl Window { content: Vec, space: bool, testing: bool, - ) -> super::Result<(String, u16)> { + ) -> ui::Result<(String, u16)> { let linefeed = if testing { "\n" } else { "\r\n" }; let len: u16 = content.len().try_into()?; @@ -110,7 +112,7 @@ impl Window { } /// Actually draws the window, with each element in `content` being on a new line. - pub fn draw(&mut self, content: Vec, space: bool) -> super::Result<()> { + pub fn draw(&mut self, content: Vec, space: bool) -> ui::Result<()> { let (rendered, height) = self.render(content, space, false)?; crossterm::execute!(