feat: merge pull request #111 from talwat/clock

feat: add clock feature
This commit is contained in:
Tal 2025-12-17 10:33:14 +01:00 committed by GitHub
commit 0382a9dcbb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 180 additions and 28 deletions

102
Cargo.lock generated
View File

@ -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"

View File

@ -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"

View File

@ -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,

View File

@ -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() {

View File

@ -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;
}

View File

@ -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,
})

33
src/ui/interface/clock.rs Normal file
View File

@ -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::format::StrftimeItems<'static>> {
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())
}
}

View File

@ -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<String>,
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<String>, space: bool) -> super::Result<()> {
pub fn draw(&mut self, content: Vec<String>, space: bool) -> ui::Result<()> {
let (rendered, height) = self.render(content, space, false)?;
crossterm::execute!(