From 89da41c9ffd6ffd708090ca9188079772c718600 Mon Sep 17 00:00:00 2001 From: talwat <83217276+talwat@users.noreply.github.com> Date: Wed, 17 Dec 2025 11:08:13 +0100 Subject: [PATCH] style: consolidate interface state into struct --- src/player.rs | 2 +- src/tests/ui.rs | 23 ++++------ src/ui.rs | 16 ++----- src/ui/interface.rs | 109 +++++++++++++++++++++++++++++++------------- 4 files changed, 93 insertions(+), 57 deletions(-) diff --git a/src/player.rs b/src/player.rs index 84e318a..7d58690 100644 --- a/src/player.rs +++ b/src/player.rs @@ -131,7 +131,7 @@ impl Player { let list = List::load(args.track_list.as_ref()).await?; let sink = Arc::new(rodio::Sink::connect_new(mixer)); - let state = ui::State::initial(Arc::clone(&sink), args.width, list.name.clone()); + let state = ui::State::initial(Arc::clone(&sink), list.name.clone()); let volume = PersistentVolume::load().await?; sink.set_volume(volume.float()); diff --git a/src/tests/ui.rs b/src/tests/ui.rs index cef20be..8755aed 100644 --- a/src/tests/ui.rs +++ b/src/tests/ui.rs @@ -128,17 +128,14 @@ mod interface { download::PROGRESS, player::Current, tracks, - ui::{ - interface::{self, Params}, - State, - }, + ui::{Interface, State}, }; #[test] fn loading() { let sink = Arc::new(rodio::Sink::new().0); - let mut state = State::initial(sink, 3, String::from("test")); - let menu = interface::menu(&mut state, Params::default()); + let mut state = State::initial(sink, String::from("test")); + let menu = Interface::default().menu(&mut state); assert_eq!(menu[0], "loading "); assert_eq!(menu[1], " [ ] 00:00/00:00 "); @@ -157,11 +154,11 @@ mod interface { fn volume() { let sink = Arc::new(rodio::Sink::new().0); sink.set_volume(0.5); - let mut state = State::initial(sink, 3, String::from("test")); + + let mut state = State::initial(sink, String::from("test")); state.timer = Some(Instant::now()); - let menu = interface::menu(&mut state, Params::default()); - + let menu = Interface::default().menu(&mut state); assert_eq!(menu[0], "loading "); assert_eq!(menu[1], " volume: [///// ] 50% "); assert_eq!( @@ -179,10 +176,10 @@ mod interface { fn progress() { let sink = Arc::new(rodio::Sink::new().0); PROGRESS.store(50, std::sync::atomic::Ordering::Relaxed); - let mut state = State::initial(sink, 3, String::from("test")); + let mut state = State::initial(sink, String::from("test")); state.current = Current::Loading(Some(&PROGRESS)); - let menu = interface::menu(&mut state, Params::default()); + let menu = Interface::default().menu(&mut state); assert_eq!(menu[0], format!("loading {} ", "50%".bold())); assert_eq!(menu[1], " [ ] 00:00/00:00 "); @@ -207,9 +204,9 @@ mod interface { duration: Some(Duration::from_secs(8)), }; - let mut state = State::initial(sink, 3, String::from("test")); + let mut state = State::initial(sink, String::from("test")); state.current = Current::Track(track.clone()); - let menu = interface::menu(&mut state, Params::default()); + let menu = Interface::default().menu(&mut state); assert_eq!( menu[0], diff --git a/src/ui.rs b/src/ui.rs index 7e8ea9d..74689e0 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -11,6 +11,7 @@ pub mod environment; pub use environment::Environment; pub mod input; pub mod interface; +pub use interface::Interface; #[cfg(feature = "mpris")] pub mod mpris; @@ -65,9 +66,6 @@ pub struct State { /// The timer, which is used when the user changes volume to briefly display it. pub(crate) timer: Option, - /// The full inner width of the terminal window. - pub(crate) width: usize, - /// The name of the playing tracklist, for MPRIS. #[allow(dead_code)] list: String, @@ -75,10 +73,8 @@ pub struct State { impl State { /// Creates an initial UI state. - pub fn initial(sink: Arc, width: usize, list: String) -> Self { - let width = 21 + width.min(32) * 2; + pub fn initial(sink: Arc, list: String) -> Self { Self { - width, sink, list, current: Current::default(), @@ -163,9 +159,7 @@ impl Handle { mut state: State, params: interface::Params, ) -> Result<()> { - let mut interval = tokio::time::interval(params.delta); - let mut window = interface::Window::new(state.width, params.borderless); - let mut clock = params.clock.then(|| interface::Clock::new(&mut window)); + let mut interface = Interface::new(params); loop { if let Ok(message) = rx.try_recv() { @@ -177,9 +171,7 @@ impl Handle { } } - clock.as_mut().map(|x| x.update(&mut window)); - interface::draw(&mut state, &mut window, params)?; - interval.tick().await; + interface.draw(&mut state).await?; } Ok(()) diff --git a/src/ui/interface.rs b/src/ui/interface.rs index f7a1a08..a09add1 100644 --- a/src/ui/interface.rs +++ b/src/ui/interface.rs @@ -1,4 +1,7 @@ -use crate::{ui, Args}; +use crate::{ + ui::{self, State}, + Args, +}; use std::{env, time::Duration}; pub mod clock; @@ -24,6 +27,9 @@ pub struct Params { /// Whether to include the clock on the top bar. pub clock: bool, + /// The full inner width of the terminal window. + pub(crate) width: usize, + /// The total delta between frames, which takes into account /// the time it takes to actually render each frame. /// @@ -47,46 +53,87 @@ impl TryFrom<&Args> for Params { delta, enabled: !disabled, clock: args.clock, + width: 21 + args.width.min(32) * 2, minimalist: args.minimalist, borderless: args.borderless, }) } } -/// Creates a full "menu" from the [`ui::State`], which can be -/// easily put into a window for display. -/// -/// The menu really is just a [`Vec`] of the different components, -/// with padding already added. -pub(crate) fn menu(state: &mut ui::State, params: Params) -> Vec { - let action = components::action(state, state.width); +/// All of the state related to the interface itself, +/// which is displayed each frame to the standard output. +pub struct Interface { + /// The interval to wait between frames. + interval: tokio::time::Interval, - let middle = match state.timer { - Some(timer) => { - let volume = state.sink.volume(); - let percentage = format!("{}%", (volume * 100.0).round().abs()); - if timer.elapsed() > Duration::from_secs(1) { - state.timer = None; - } + /// The [`Window`] to render to. + window: Window, - components::audio_bar(state.width - 17, volume, &percentage) - } - None => components::progress_bar(state, state.width - 16), - }; + /// The visual clock, which is [`None`] if it has + /// been disabled by the [`Params`]. + clock: Option, - let controls = components::controls(state.width); - if params.minimalist { - vec![action, middle] - } else { - vec![action, middle, controls] + /// The interface parameters that control smaller + /// aesthetic features and options. + params: Params, +} + +impl Default for Interface { + #[inline] + fn default() -> Self { + Self::new(Params::default()) } } -/// The code for the terminal interface itself. -/// -/// * `minimalist` - All this does is hide the bottom control bar. -pub fn draw(state: &mut ui::State, window: &mut Window, params: Params) -> super::Result<()> { - let menu = menu(state, params); - window.draw(menu, false)?; - Ok(()) +impl Interface { + /// Creates a new interface. + pub fn new(params: Params) -> Self { + let mut window = Window::new(params.width, params.borderless); + + Self { + clock: params.clock.then(|| Clock::new(&mut window)), + interval: tokio::time::interval(params.delta), + window, + params, + } + } + + /// Creates a full "menu" from the [`ui::State`], which can be + /// easily put into a window for display. + /// + /// The menu really is just a [`Vec`] of the different components, + /// with padding already added. + pub(crate) fn menu(&self, state: &mut State) -> Vec { + let action = components::action(state, self.params.width); + + let middle = match state.timer { + Some(timer) => { + let volume = state.sink.volume(); + let percentage = format!("{}%", (volume * 100.0).round().abs()); + if timer.elapsed() > Duration::from_secs(1) { + state.timer = None; + } + + components::audio_bar(self.params.width - 17, volume, &percentage) + } + None => components::progress_bar(state, self.params.width - 16), + }; + + let controls = components::controls(self.params.width); + if self.params.minimalist { + vec![action, middle] + } else { + vec![action, middle, controls] + } + } + + /// Draws the terminal. This will also wait for the specified + /// delta to pass before completing. + pub async fn draw(&mut self, state: &mut State) -> super::Result<()> { + self.clock.as_mut().map(|x| x.update(&mut self.window)); + self.window.draw(self.menu(state), false)?; + self.interval.tick().await; + + Ok(()) + } }