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..2b07bc8 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 @@ -78,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/main.rs b/src/main.rs index 7ee1c53..27b42bd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -35,10 +35,14 @@ pub struct Args { #[clap(long, short)] minimalist: bool, - /// Exclude borders in UI. + /// Exclude window borders. #[clap(long, short)] borderless: bool, + /// Include a clock. + #[clap(long, short)] + clock: bool, + /// Start lowfi paused. #[clap(long, short)] paused: 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 675d921..7e8ea9d 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,21 +1,16 @@ use std::sync::Arc; -use crate::{ - player::Current, - ui::{self, 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,7 +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 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() { @@ -181,6 +177,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 7befea8..f7a1a08 100644 --- a/src/ui/interface.rs +++ b/src/ui/interface.rs @@ -1,9 +1,12 @@ +use crate::{ui, Args}; use std::{env, time::Duration}; -use crate::{ - ui::{self, components, window::Window}, - Args, -}; +pub mod clock; +pub mod components; +pub mod window; + +pub use clock::Clock; +pub use window::Window; /// UI-specific parameters and options. #[derive(Copy, Clone, Debug, Default)] @@ -18,6 +21,9 @@ pub struct Params { /// This only applies if the MPRIS feature is enabled. pub enabled: bool, + /// Whether to include the clock on the top bar. + pub clock: bool, + /// The total delta between frames, which takes into account /// the time it takes to actually render each frame. /// @@ -40,6 +46,7 @@ impl TryFrom<&Args> for Params { Ok(Self { delta, enabled: !disabled, + clock: args.clock, minimalist: args.minimalist, borderless: args.borderless, }) 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 89% rename from src/ui/window.rs rename to src/ui/interface/window.rs index 746249c..eaecc77 100644 --- a/src/ui/window.rs +++ b/src/ui/interface/window.rs @@ -1,4 +1,7 @@ -use std::io::{stdout, Stdout}; +use std::{ + fmt::Display, + io::{stdout, Stdout}, +}; use crossterm::{ cursor::{MoveToColumn, MoveUp}, @@ -8,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, @@ -17,7 +22,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 +56,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 @@ -63,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()?; @@ -101,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!(