mirror of
https://github.com/talwat/lowfi
synced 2025-12-09 16:34:12 +00:00
chore: add more tests
This commit is contained in:
parent
b0a1a1e399
commit
95ce4f2352
@ -12,7 +12,7 @@ use tokio::{
|
|||||||
use crate::tracks;
|
use crate::tracks;
|
||||||
|
|
||||||
static LOADING: AtomicBool = AtomicBool::new(false);
|
static LOADING: AtomicBool = AtomicBool::new(false);
|
||||||
static PROGRESS: AtomicU8 = AtomicU8::new(0);
|
pub(crate) static PROGRESS: AtomicU8 = AtomicU8::new(0);
|
||||||
pub type Progress = &'static AtomicU8;
|
pub type Progress = &'static AtomicU8;
|
||||||
|
|
||||||
pub struct Downloader {
|
pub struct Downloader {
|
||||||
|
|||||||
@ -90,7 +90,8 @@ impl Player {
|
|||||||
let current = Current::Loading(None);
|
let current = Current::Loading(None);
|
||||||
|
|
||||||
let list = List::load(args.track_list.as_ref()).await?;
|
let list = List::load(args.track_list.as_ref()).await?;
|
||||||
let state = ui::State::initial(sink.clone(), &args, current.clone(), list.name.clone());
|
let state =
|
||||||
|
ui::State::initial(sink.clone(), args.width, current.clone(), list.name.clone());
|
||||||
|
|
||||||
let volume = PersistentVolume::load().await?;
|
let volume = PersistentVolume::load().await?;
|
||||||
sink.set_volume(volume.float());
|
sink.set_volume(volume.float());
|
||||||
|
|||||||
@ -2,4 +2,3 @@ mod bookmark;
|
|||||||
mod player;
|
mod player;
|
||||||
mod tracks;
|
mod tracks;
|
||||||
mod ui;
|
mod ui;
|
||||||
mod volume;
|
|
||||||
|
|||||||
@ -89,7 +89,7 @@ mod info {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn info_width_counts_graphemes() {
|
fn width_counts_graphemes() {
|
||||||
// We cannot create a valid decoder for arbitrary bytes here, so test width through constructor logic directly.
|
// We cannot create a valid decoder for arbitrary bytes here, so test width through constructor logic directly.
|
||||||
let display = "a̐é"; // multiple-grapheme clusters
|
let display = "a̐é"; // multiple-grapheme clusters
|
||||||
let width = display.graphemes(true).count();
|
let width = display.graphemes(true).count();
|
||||||
@ -126,17 +126,18 @@ mod decoded {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod list {
|
mod list {
|
||||||
use crate::tracks::List;
|
use crate::{download::PROGRESS, tracks::List};
|
||||||
|
use reqwest::Client;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn list_base_works() {
|
fn base_works() {
|
||||||
let text = "http://base/\ntrack1\ntrack2";
|
let text = "http://base/\ntrack1\ntrack2";
|
||||||
let list = List::new("test", text, None);
|
let list = List::new("test", text, None);
|
||||||
assert_eq!(list.base(), "http://base/");
|
assert_eq!(list.header(), "http://base/");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn list_random_path_parses_custom_display() {
|
fn random_path_parses_custom_display() {
|
||||||
let text = "http://x/\npath!Display";
|
let text = "http://x/\npath!Display";
|
||||||
let list = List::new("t", text, None);
|
let list = List::new("t", text, None);
|
||||||
|
|
||||||
@ -146,7 +147,7 @@ mod list {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn list_random_path_no_display() {
|
fn random_path_no_display() {
|
||||||
let text = "http://x/\ntrackA";
|
let text = "http://x/\ntrackA";
|
||||||
let list = List::new("t", text, None);
|
let list = List::new("t", text, None);
|
||||||
|
|
||||||
@ -160,21 +161,13 @@ mod list {
|
|||||||
let text = "base\na \nb ";
|
let text = "base\na \nb ";
|
||||||
let list = List::new("name", text, None);
|
let list = List::new("name", text, None);
|
||||||
|
|
||||||
assert_eq!(list.base(), "base");
|
assert_eq!(list.header(), "base");
|
||||||
assert_eq!(list.lines[1], "a");
|
assert_eq!(list.lines[1], "a");
|
||||||
assert_eq!(list.lines[2], "b");
|
assert_eq!(list.lines[2], "b");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn list_noheader_base() {
|
fn custom_display_with_exclamation() {
|
||||||
let text = "noheader\nhttps://example.com/track.mp3";
|
|
||||||
let list = List::new("test", text, None);
|
|
||||||
// noheader means the first line should be treated as base
|
|
||||||
assert_eq!(list.base(), "noheader");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn list_custom_display_with_exclamation() {
|
|
||||||
let text = "http://base/\nfile.mp3!My Custom Name";
|
let text = "http://base/\nfile.mp3!My Custom Name";
|
||||||
let list = List::new("t", text, None);
|
let list = List::new("t", text, None);
|
||||||
let (path, display) = list.random_path();
|
let (path, display) = list.random_path();
|
||||||
@ -183,10 +176,25 @@ mod list {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn list_single_track() {
|
fn single_track() {
|
||||||
let text = "base\nonly_track.mp3";
|
let text = "base\nonly_track.mp3";
|
||||||
let list = List::new("name", text, None);
|
let list = List::new("name", text, None);
|
||||||
let (path, _) = list.random_path();
|
let (path, _) = list.random_path();
|
||||||
assert_eq!(path, "only_track.mp3");
|
assert_eq!(path, "only_track.mp3");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn download() {
|
||||||
|
let text = "https://stream.chillhop.com/mp3/\n9476!Apple Juice";
|
||||||
|
let list = List::new("name", text, None);
|
||||||
|
|
||||||
|
let client = Client::new();
|
||||||
|
let track = list.random(&client, &PROGRESS).await.unwrap();
|
||||||
|
assert_eq!(track.display, "Apple Juice");
|
||||||
|
assert_eq!(track.path, "https://stream.chillhop.com/mp3/9476");
|
||||||
|
assert_eq!(track.data.len(), 3150424);
|
||||||
|
|
||||||
|
let decoded = track.decode().unwrap();
|
||||||
|
assert_eq!(decoded.info.duration.unwrap().as_secs(), 143);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
222
src/tests/ui.rs
222
src/tests/ui.rs
@ -1,3 +1,11 @@
|
|||||||
|
/* The lowfi UI:
|
||||||
|
┌─────────────────────────────┐
|
||||||
|
│ loading │
|
||||||
|
│ [ ] 00:00/00:00 │
|
||||||
|
│ [s]kip [p]ause [q]uit │
|
||||||
|
└─────────────────────────────┘
|
||||||
|
*/
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod components {
|
mod components {
|
||||||
use crate::ui;
|
use crate::ui;
|
||||||
@ -65,21 +73,164 @@ mod window {
|
|||||||
assert!(w2.borders[1].is_empty());
|
assert!(w2.borders[1].is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn sided(text: &str) -> String {
|
||||||
|
return format!("│ {text} │");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn border_width_consistency() {
|
fn simple() {
|
||||||
let w = Window::new(20, false);
|
let mut w = Window::new(3, false);
|
||||||
// borders should have consistent format with width encoded
|
let (render, height) = w.render(vec![String::from("abc")], false, true).unwrap();
|
||||||
assert!(w.borders[0].len() > 0);
|
|
||||||
|
const MIDDLE: &str = "─────";
|
||||||
|
assert_eq!(format!("┌{MIDDLE}┐\n{}\n└{MIDDLE}┘", sided("abc")), render);
|
||||||
|
assert_eq!(height, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spaced() {
|
||||||
|
let mut w = Window::new(3, false);
|
||||||
|
let (render, height) = w
|
||||||
|
.render(
|
||||||
|
vec![String::from("abc"), String::from(" b"), String::from("c")],
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
const MIDDLE: &str = "─────";
|
||||||
|
assert_eq!(
|
||||||
|
format!(
|
||||||
|
"┌{MIDDLE}┐\n{}\n{}\n{}\n└{MIDDLE}┘",
|
||||||
|
sided("abc"),
|
||||||
|
sided(" b "),
|
||||||
|
sided("c "),
|
||||||
|
),
|
||||||
|
render
|
||||||
|
);
|
||||||
|
assert_eq!(height, 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn zero_width_window() {
|
fn zero_width_window() {
|
||||||
let w = Window::new(0, false);
|
let w = Window::new(0, false);
|
||||||
// Should handle zero-width gracefully
|
|
||||||
assert!(!w.borders[0].is_empty());
|
assert!(!w.borders[0].is_empty());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod interface {
|
||||||
|
use crossterm::style::Stylize;
|
||||||
|
use std::{sync::Arc, time::Duration};
|
||||||
|
use tokio::time::Instant;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
download::PROGRESS,
|
||||||
|
player::Current,
|
||||||
|
tracks,
|
||||||
|
ui::{
|
||||||
|
interface::{self, Params},
|
||||||
|
State,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn loading() {
|
||||||
|
let sink = Arc::new(rodio::Sink::new().0);
|
||||||
|
let mut state = State::initial(sink, 3, Current::Loading(None), String::from("test"));
|
||||||
|
let menu = interface::menu(&mut state, Params::default());
|
||||||
|
|
||||||
|
assert_eq!(menu[0], "loading ");
|
||||||
|
assert_eq!(menu[1], " [ ] 00:00/00:00 ");
|
||||||
|
assert_eq!(
|
||||||
|
menu[2],
|
||||||
|
format!(
|
||||||
|
"{}kip {}ause {}uit",
|
||||||
|
"[s]".bold(),
|
||||||
|
"[p]".bold(),
|
||||||
|
"[q]".bold()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn volume() {
|
||||||
|
let sink = Arc::new(rodio::Sink::new().0);
|
||||||
|
sink.set_volume(0.5);
|
||||||
|
let mut state = State::initial(sink, 3, Current::Loading(None), String::from("test"));
|
||||||
|
state.timer = Some(Instant::now());
|
||||||
|
|
||||||
|
let menu = interface::menu(&mut state, Params::default());
|
||||||
|
|
||||||
|
assert_eq!(menu[0], "loading ");
|
||||||
|
assert_eq!(menu[1], " volume: [///// ] 50% ");
|
||||||
|
assert_eq!(
|
||||||
|
menu[2],
|
||||||
|
format!(
|
||||||
|
"{}kip {}ause {}uit",
|
||||||
|
"[s]".bold(),
|
||||||
|
"[p]".bold(),
|
||||||
|
"[q]".bold()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
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,
|
||||||
|
Current::Loading(Some(&PROGRESS)),
|
||||||
|
String::from("test"),
|
||||||
|
);
|
||||||
|
let menu = interface::menu(&mut state, Params::default());
|
||||||
|
|
||||||
|
assert_eq!(menu[0], format!("loading {} ", "50%".bold()));
|
||||||
|
assert_eq!(menu[1], " [ ] 00:00/00:00 ");
|
||||||
|
assert_eq!(
|
||||||
|
menu[2],
|
||||||
|
format!(
|
||||||
|
"{}kip {}ause {}uit",
|
||||||
|
"[s]".bold(),
|
||||||
|
"[p]".bold(),
|
||||||
|
"[q]".bold()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn track() {
|
||||||
|
let sink = Arc::new(rodio::Sink::new().0);
|
||||||
|
let track = tracks::Info {
|
||||||
|
path: "/path".to_owned(),
|
||||||
|
display: "Test Track".to_owned(),
|
||||||
|
width: 4 + 1 + 5,
|
||||||
|
duration: Some(Duration::from_secs(8)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let current = Current::Track(track.clone());
|
||||||
|
let mut state = State::initial(sink, 3, current, String::from("test"));
|
||||||
|
let menu = interface::menu(&mut state, Params::default());
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
menu[0],
|
||||||
|
format!("playing {} ", track.display.bold())
|
||||||
|
);
|
||||||
|
assert_eq!(menu[1], " [ ] 00:00/00:08 ");
|
||||||
|
assert_eq!(
|
||||||
|
menu[2],
|
||||||
|
format!(
|
||||||
|
"{}kip {}ause {}uit",
|
||||||
|
"[s]".bold(),
|
||||||
|
"[p]".bold(),
|
||||||
|
"[q]".bold()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod environment {
|
mod environment {
|
||||||
use crate::ui::Environment;
|
use crate::ui::Environment;
|
||||||
@ -101,64 +252,3 @@ mod environment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod integration {
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use rodio::OutputStreamBuilder;
|
|
||||||
|
|
||||||
use crate::{player::Current, Args};
|
|
||||||
|
|
||||||
fn try_make_state() -> Option<crate::ui::State> {
|
|
||||||
let stream = OutputStreamBuilder::open_default_stream();
|
|
||||||
if stream.is_err() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut stream = stream.unwrap();
|
|
||||||
stream.log_on_drop(false);
|
|
||||||
let sink = Arc::new(rodio::Sink::connect_new(stream.mixer()));
|
|
||||||
|
|
||||||
let args = Args {
|
|
||||||
alternate: false,
|
|
||||||
minimalist: false,
|
|
||||||
borderless: false,
|
|
||||||
paused: false,
|
|
||||||
fps: 12,
|
|
||||||
timeout: 3,
|
|
||||||
debug: false,
|
|
||||||
width: 3,
|
|
||||||
track_list: String::from("chillhop"),
|
|
||||||
buffer_size: 5,
|
|
||||||
command: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let current = Current::default();
|
|
||||||
Some(crate::ui::State::initial(
|
|
||||||
sink,
|
|
||||||
&args,
|
|
||||||
current,
|
|
||||||
String::from("list"),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn progress_bar_runs() -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
if let Some(state) = try_make_state() {
|
|
||||||
// ensure we can call progress_bar without panic
|
|
||||||
let _ = crate::ui::components::progress_bar(&state, state.width);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn action_runs() -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
if let Some(state) = try_make_state() {
|
|
||||||
let _ = crate::ui::components::action(&state, state.width);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,28 +0,0 @@
|
|||||||
#[cfg(test)]
|
|
||||||
mod volume {
|
|
||||||
use crate::volume::PersistentVolume;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn float_converts_percent() {
|
|
||||||
let pv = PersistentVolume { inner: 75 };
|
|
||||||
assert!((pv.float() - 0.75).abs() < f32::EPSILON);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn float_zero_volume() {
|
|
||||||
let pv = PersistentVolume { inner: 0 };
|
|
||||||
assert_eq!(pv.float(), 0.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn float_full_volume() {
|
|
||||||
let pv = PersistentVolume { inner: 100 };
|
|
||||||
assert_eq!(pv.float(), 1.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn float_mid_range() {
|
|
||||||
let pv = PersistentVolume { inner: 50 };
|
|
||||||
assert!((pv.float() - 0.5).abs() < f32::EPSILON);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -41,7 +41,7 @@ pub struct List {
|
|||||||
|
|
||||||
impl List {
|
impl List {
|
||||||
/// Gets the base URL of the [List].
|
/// Gets the base URL of the [List].
|
||||||
pub fn base(&self) -> &str {
|
pub fn header(&self) -> &str {
|
||||||
self.lines[0].trim()
|
self.lines[0].trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,7 +66,7 @@ impl List {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Downloads a raw track, but doesn't decode it.
|
/// Downloads a raw track, but doesn't decode it.
|
||||||
async fn download(
|
pub(crate) async fn download(
|
||||||
&self,
|
&self,
|
||||||
track: &str,
|
track: &str,
|
||||||
client: &Client,
|
client: &Client,
|
||||||
@ -76,7 +76,7 @@ impl List {
|
|||||||
let path = if track.contains("://") {
|
let path = if track.contains("://") {
|
||||||
track.to_owned()
|
track.to_owned()
|
||||||
} else {
|
} else {
|
||||||
format!("{}{}", self.base(), track)
|
format!("{}{}", self.header(), track)
|
||||||
};
|
};
|
||||||
|
|
||||||
let data: Bytes = if let Some(x) = path.strip_prefix("file://") {
|
let data: Bytes = if let Some(x) = path.strip_prefix("file://") {
|
||||||
|
|||||||
@ -52,7 +52,7 @@ pub struct State {
|
|||||||
pub sink: Arc<rodio::Sink>,
|
pub sink: Arc<rodio::Sink>,
|
||||||
pub current: Current,
|
pub current: Current,
|
||||||
pub bookmarked: bool,
|
pub bookmarked: bool,
|
||||||
timer: Option<Instant>,
|
pub(crate) timer: Option<Instant>,
|
||||||
pub(crate) width: usize,
|
pub(crate) width: usize,
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
@ -60,8 +60,8 @@ pub struct State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
pub fn initial(sink: Arc<rodio::Sink>, args: &Args, current: Current, list: String) -> Self {
|
pub fn initial(sink: Arc<rodio::Sink>, width: usize, current: Current, list: String) -> Self {
|
||||||
let width = 21 + args.width.min(32) * 2;
|
let width = 21 + width.min(32) * 2;
|
||||||
Self {
|
Self {
|
||||||
width,
|
width,
|
||||||
sink,
|
sink,
|
||||||
@ -111,7 +111,7 @@ impl Handle {
|
|||||||
let mut window = Window::new(state.width, params.borderless);
|
let mut window = Window::new(state.width, params.borderless);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
interface::draw(&mut state, &mut window, params).await?;
|
interface::draw(&mut state, &mut window, params)?;
|
||||||
|
|
||||||
if let Ok(message) = rx.try_recv() {
|
if let Ok(message) = rx.try_recv() {
|
||||||
match message {
|
match message {
|
||||||
|
|||||||
@ -5,7 +5,7 @@ use crate::{
|
|||||||
Args,
|
Args,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug)]
|
#[derive(Copy, Clone, Debug, Default)]
|
||||||
pub struct Params {
|
pub struct Params {
|
||||||
pub borderless: bool,
|
pub borderless: bool,
|
||||||
pub minimalist: bool,
|
pub minimalist: bool,
|
||||||
@ -25,10 +25,7 @@ impl From<&Args> for Params {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The code for the terminal interface itself.
|
pub(crate) fn menu(state: &mut ui::State, params: Params) -> Vec<String> {
|
||||||
///
|
|
||||||
/// * `minimalist` - All this does is hide the bottom control bar.
|
|
||||||
pub async fn draw(state: &mut ui::State, window: &mut Window, params: Params) -> super::Result<()> {
|
|
||||||
let action = components::action(&state, state.width);
|
let action = components::action(&state, state.width);
|
||||||
|
|
||||||
let middle = match state.timer {
|
let middle = match state.timer {
|
||||||
@ -45,12 +42,18 @@ pub async fn draw(state: &mut ui::State, window: &mut Window, params: Params) ->
|
|||||||
};
|
};
|
||||||
|
|
||||||
let controls = components::controls(state.width);
|
let controls = components::controls(state.width);
|
||||||
let menu = if params.minimalist {
|
if params.minimalist {
|
||||||
vec![action, middle]
|
vec![action, middle]
|
||||||
} else {
|
} else {
|
||||||
vec![action, middle, controls]
|
vec![action, middle, controls]
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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)?;
|
window.draw(menu, false)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,7 +22,7 @@ pub struct Window {
|
|||||||
/// If the option to not include borders is set, these will just be empty [String]s.
|
/// If the option to not include borders is set, these will just be empty [String]s.
|
||||||
pub(crate) borders: [String; 2],
|
pub(crate) borders: [String; 2],
|
||||||
|
|
||||||
/// The width of the window.
|
/// The inner width of the window.
|
||||||
width: usize,
|
width: usize,
|
||||||
|
|
||||||
/// The output, currently just an [`Stdout`].
|
/// The output, currently just an [`Stdout`].
|
||||||
@ -32,7 +32,7 @@ pub struct Window {
|
|||||||
impl Window {
|
impl Window {
|
||||||
/// Initializes a new [Window].
|
/// Initializes a new [Window].
|
||||||
///
|
///
|
||||||
/// * `width` - Width of the windows.
|
/// * `width` - Inner width of the window.
|
||||||
/// * `borderless` - Whether to include borders in the window, or not.
|
/// * `borderless` - Whether to include borders in the window, or not.
|
||||||
pub fn new(width: usize, borderless: bool) -> Self {
|
pub fn new(width: usize, borderless: bool) -> Self {
|
||||||
let borders = if borderless {
|
let borders = if borderless {
|
||||||
@ -51,8 +51,13 @@ impl Window {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Actually draws the window, with each element in `content` being on a new line.
|
pub(crate) fn render(
|
||||||
pub fn draw(&mut self, content: Vec<String>, space: bool) -> super::Result<()> {
|
&mut self,
|
||||||
|
content: Vec<String>,
|
||||||
|
space: bool,
|
||||||
|
testing: bool,
|
||||||
|
) -> super::Result<(String, u16)> {
|
||||||
|
let linefeed = if testing { "\n" } else { "\r\n" };
|
||||||
let len: u16 = content.len().try_into()?;
|
let len: u16 = content.len().try_into()?;
|
||||||
|
|
||||||
// Note that this will have a trailing newline, which we use later.
|
// Note that this will have a trailing newline, which we use later.
|
||||||
@ -64,7 +69,9 @@ impl Window {
|
|||||||
} else {
|
} else {
|
||||||
String::new()
|
String::new()
|
||||||
};
|
};
|
||||||
write!(output, "{padding} {}{space} {padding}\r\n", x.reset()).unwrap();
|
|
||||||
|
let center = if testing { x } else { x.reset().to_string() };
|
||||||
|
write!(output, "{padding} {center}{space} {padding}{linefeed}").unwrap();
|
||||||
|
|
||||||
output
|
output
|
||||||
});
|
});
|
||||||
@ -72,12 +79,23 @@ impl Window {
|
|||||||
// We're doing this because Windows is stupid and can't stand
|
// We're doing this because Windows is stupid and can't stand
|
||||||
// writing to the last line repeatedly.
|
// writing to the last line repeatedly.
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
let (height, suffix) = (len + 2, "\r\n");
|
let (height, suffix) = (len + 3, linefeed);
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
let (height, suffix) = (len + 1, "");
|
let (height, suffix) = (len + 2, "");
|
||||||
|
|
||||||
// There's no need for another newline after the main menu content, because it already has one.
|
// There's no need for another newline after the main menu content, because it already has one.
|
||||||
let rendered = format!("{}\r\n{menu}{}{suffix}", self.borders[0], self.borders[1]);
|
Ok((
|
||||||
|
format!(
|
||||||
|
"{}{linefeed}{menu}{}{suffix}",
|
||||||
|
self.borders[0], self.borders[1]
|
||||||
|
),
|
||||||
|
height,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<()> {
|
||||||
|
let (rendered, height) = self.render(content, space, false)?;
|
||||||
|
|
||||||
crossterm::execute!(
|
crossterm::execute!(
|
||||||
self.out,
|
self.out,
|
||||||
@ -85,7 +103,7 @@ impl Window {
|
|||||||
MoveToColumn(0),
|
MoveToColumn(0),
|
||||||
Print(rendered),
|
Print(rendered),
|
||||||
MoveToColumn(0),
|
MoveToColumn(0),
|
||||||
MoveUp(height),
|
MoveUp(height - 1),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user