style: consolidate interface state into struct

This commit is contained in:
talwat 2025-12-17 11:08:13 +01:00
parent 0382a9dcbb
commit 89da41c9ff
4 changed files with 93 additions and 57 deletions

View File

@ -131,7 +131,7 @@ impl Player {
let list = List::load(args.track_list.as_ref()).await?; let list = List::load(args.track_list.as_ref()).await?;
let sink = Arc::new(rodio::Sink::connect_new(mixer)); 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?; let volume = PersistentVolume::load().await?;
sink.set_volume(volume.float()); sink.set_volume(volume.float());

View File

@ -128,17 +128,14 @@ mod interface {
download::PROGRESS, download::PROGRESS,
player::Current, player::Current,
tracks, tracks,
ui::{ ui::{Interface, State},
interface::{self, Params},
State,
},
}; };
#[test] #[test]
fn loading() { fn loading() {
let sink = Arc::new(rodio::Sink::new().0); let sink = Arc::new(rodio::Sink::new().0);
let mut state = State::initial(sink, 3, String::from("test")); let mut state = State::initial(sink, String::from("test"));
let menu = interface::menu(&mut state, Params::default()); let menu = Interface::default().menu(&mut state);
assert_eq!(menu[0], "loading "); assert_eq!(menu[0], "loading ");
assert_eq!(menu[1], " [ ] 00:00/00:00 "); assert_eq!(menu[1], " [ ] 00:00/00:00 ");
@ -157,11 +154,11 @@ mod interface {
fn volume() { fn volume() {
let sink = Arc::new(rodio::Sink::new().0); let sink = Arc::new(rodio::Sink::new().0);
sink.set_volume(0.5); 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()); 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[0], "loading ");
assert_eq!(menu[1], " volume: [///// ] 50% "); assert_eq!(menu[1], " volume: [///// ] 50% ");
assert_eq!( assert_eq!(
@ -179,10 +176,10 @@ mod interface {
fn progress() { fn progress() {
let sink = Arc::new(rodio::Sink::new().0); let sink = Arc::new(rodio::Sink::new().0);
PROGRESS.store(50, std::sync::atomic::Ordering::Relaxed); 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)); 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[0], format!("loading {} ", "50%".bold()));
assert_eq!(menu[1], " [ ] 00:00/00:00 "); assert_eq!(menu[1], " [ ] 00:00/00:00 ");
@ -207,9 +204,9 @@ mod interface {
duration: Some(Duration::from_secs(8)), 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()); state.current = Current::Track(track.clone());
let menu = interface::menu(&mut state, Params::default()); let menu = Interface::default().menu(&mut state);
assert_eq!( assert_eq!(
menu[0], menu[0],

View File

@ -11,6 +11,7 @@ pub mod environment;
pub use environment::Environment; pub use environment::Environment;
pub mod input; pub mod input;
pub mod interface; pub mod interface;
pub use interface::Interface;
#[cfg(feature = "mpris")] #[cfg(feature = "mpris")]
pub mod 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. /// The timer, which is used when the user changes volume to briefly display it.
pub(crate) timer: Option<Instant>, pub(crate) timer: Option<Instant>,
/// The full inner width of the terminal window.
pub(crate) width: usize,
/// The name of the playing tracklist, for MPRIS. /// The name of the playing tracklist, for MPRIS.
#[allow(dead_code)] #[allow(dead_code)]
list: String, list: String,
@ -75,10 +73,8 @@ pub struct State {
impl State { impl State {
/// Creates an initial UI state. /// Creates an initial UI state.
pub fn initial(sink: Arc<rodio::Sink>, width: usize, list: String) -> Self { pub fn initial(sink: Arc<rodio::Sink>, list: String) -> Self {
let width = 21 + width.min(32) * 2;
Self { Self {
width,
sink, sink,
list, list,
current: Current::default(), current: Current::default(),
@ -163,9 +159,7 @@ impl Handle {
mut state: State, mut state: State,
params: interface::Params, params: interface::Params,
) -> Result<()> { ) -> Result<()> {
let mut interval = tokio::time::interval(params.delta); let mut interface = Interface::new(params);
let mut window = interface::Window::new(state.width, params.borderless);
let mut clock = params.clock.then(|| interface::Clock::new(&mut window));
loop { loop {
if let Ok(message) = rx.try_recv() { 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).await?;
interface::draw(&mut state, &mut window, params)?;
interval.tick().await;
} }
Ok(()) Ok(())

View File

@ -1,4 +1,7 @@
use crate::{ui, Args}; use crate::{
ui::{self, State},
Args,
};
use std::{env, time::Duration}; use std::{env, time::Duration};
pub mod clock; pub mod clock;
@ -24,6 +27,9 @@ pub struct Params {
/// Whether to include the clock on the top bar. /// Whether to include the clock on the top bar.
pub clock: bool, 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 total delta between frames, which takes into account
/// the time it takes to actually render each frame. /// the time it takes to actually render each frame.
/// ///
@ -47,46 +53,87 @@ impl TryFrom<&Args> for Params {
delta, delta,
enabled: !disabled, enabled: !disabled,
clock: args.clock, clock: args.clock,
width: 21 + args.width.min(32) * 2,
minimalist: args.minimalist, minimalist: args.minimalist,
borderless: args.borderless, borderless: args.borderless,
}) })
} }
} }
/// Creates a full "menu" from the [`ui::State`], which can be /// All of the state related to the interface itself,
/// easily put into a window for display. /// which is displayed each frame to the standard output.
/// pub struct Interface {
/// The menu really is just a [`Vec`] of the different components, /// The interval to wait between frames.
/// with padding already added. interval: tokio::time::Interval,
pub(crate) fn menu(state: &mut ui::State, params: Params) -> Vec<String> {
let action = components::action(state, state.width);
let middle = match state.timer { /// The [`Window`] to render to.
Some(timer) => { window: Window,
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(state.width - 17, volume, &percentage) /// The visual clock, which is [`None`] if it has
} /// been disabled by the [`Params`].
None => components::progress_bar(state, state.width - 16), clock: Option<Clock>,
};
let controls = components::controls(state.width); /// The interface parameters that control smaller
if params.minimalist { /// aesthetic features and options.
vec![action, middle] params: Params,
} else { }
vec![action, middle, controls]
impl Default for Interface {
#[inline]
fn default() -> Self {
Self::new(Params::default())
} }
} }
/// The code for the terminal interface itself. impl Interface {
/// /// Creates a new interface.
/// * `minimalist` - All this does is hide the bottom control bar. pub fn new(params: Params) -> Self {
pub fn draw(state: &mut ui::State, window: &mut Window, params: Params) -> super::Result<()> { let mut window = Window::new(params.width, params.borderless);
let menu = menu(state, params);
window.draw(menu, false)?; Self {
Ok(()) 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<String> {
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(())
}
} }