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 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());

View File

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

View File

@ -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<Instant>,
/// 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<rodio::Sink>, width: usize, list: String) -> Self {
let width = 21 + width.min(32) * 2;
pub fn initial(sink: Arc<rodio::Sink>, 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(())

View File

@ -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<String> {
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<Clock>,
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<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(())
}
}