mirror of
https://github.com/talwat/lowfi
synced 2025-12-09 16:34:12 +00:00
chore: configure clippy & fix lints
This commit is contained in:
parent
1c8c788d76
commit
a87a8cc59e
14
Cargo.toml
14
Cargo.toml
@ -60,3 +60,17 @@ indicatif = { version = "0.18.0", optional = true }
|
|||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
libc = "0.2.167"
|
libc = "0.2.167"
|
||||||
|
|
||||||
|
[lints.clippy]
|
||||||
|
all = { level = "warn", priority = -1 }
|
||||||
|
pedantic = { level = "warn", priority = -1 }
|
||||||
|
nursery = { level = "warn", priority = -1 }
|
||||||
|
|
||||||
|
unwrap_in_result = "warn"
|
||||||
|
missing_docs_in_private_items = "warn"
|
||||||
|
|
||||||
|
missing_errors_doc = "allow"
|
||||||
|
missing_panics_doc = "allow"
|
||||||
|
must_use_candidate = "allow"
|
||||||
|
cast_precision_loss = "allow"
|
||||||
|
cast_sign_loss = "allow"
|
||||||
|
cast_possible_truncation = "allow"
|
||||||
@ -25,7 +25,7 @@ pub fn silent_get_output_stream() -> eyre::Result<rodio::OutputStream, crate::Er
|
|||||||
// SAFETY: Simple enough to be impossible to fail. Hopefully.
|
// SAFETY: Simple enough to be impossible to fail. Hopefully.
|
||||||
unsafe {
|
unsafe {
|
||||||
freopen(null.as_ptr(), mode.as_ptr(), stderr);
|
freopen(null.as_ptr(), mode.as_ptr(), stderr);
|
||||||
}
|
};
|
||||||
|
|
||||||
// Make the OutputStream while stderr is still redirected to /dev/null.
|
// Make the OutputStream while stderr is still redirected to /dev/null.
|
||||||
let stream = OutputStreamBuilder::open_default_stream()?;
|
let stream = OutputStreamBuilder::open_default_stream()?;
|
||||||
@ -36,7 +36,7 @@ pub fn silent_get_output_stream() -> eyre::Result<rodio::OutputStream, crate::Er
|
|||||||
// SAFETY: See the first call to `freopen`.
|
// SAFETY: See the first call to `freopen`.
|
||||||
unsafe {
|
unsafe {
|
||||||
freopen(tty.as_ptr(), mode.as_ptr(), stderr);
|
freopen(tty.as_ptr(), mode.as_ptr(), stderr);
|
||||||
}
|
};
|
||||||
|
|
||||||
Ok(stream)
|
Ok(stream)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,7 +23,7 @@ impl Handle {
|
|||||||
let notify = Arc::new(Notify::new());
|
let notify = Arc::new(Notify::new());
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
task: task::spawn(Self::waiter(sink, tx, notify.clone())),
|
task: task::spawn(Self::waiter(sink, tx, Arc::clone(¬ify))),
|
||||||
notify,
|
notify,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -40,7 +40,7 @@ impl Handle {
|
|||||||
time::sleep(Duration::from_millis(8)).await;
|
time::sleep(Duration::from_millis(8)).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Err(_) = tx.try_send(crate::Message::Next) {
|
if tx.try_send(crate::Message::Next).is_err() {
|
||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -46,7 +46,7 @@ impl Bookmarks {
|
|||||||
if x.is_empty() {
|
if x.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(x.to_string())
|
Some(x.to_owned())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@ -64,7 +64,7 @@ impl Bookmarks {
|
|||||||
/// Bookmarks a given track with a full path and optional custom name.
|
/// Bookmarks a given track with a full path and optional custom name.
|
||||||
///
|
///
|
||||||
/// Returns whether the track is now bookmarked, or not.
|
/// Returns whether the track is now bookmarked, or not.
|
||||||
pub async fn bookmark(&mut self, track: &tracks::Info) -> Result<bool> {
|
pub fn bookmark(&mut self, track: &tracks::Info) -> Result<bool> {
|
||||||
let entry = track.to_entry();
|
let entry = track.to_entry();
|
||||||
let idx = self.entries.iter().position(|x| **x == entry);
|
let idx = self.entries.iter().position(|x| **x == entry);
|
||||||
|
|
||||||
|
|||||||
@ -15,6 +15,8 @@ static LOADING: AtomicBool = AtomicBool::new(false);
|
|||||||
pub(crate) static PROGRESS: AtomicU8 = AtomicU8::new(0);
|
pub(crate) static PROGRESS: AtomicU8 = AtomicU8::new(0);
|
||||||
pub type Progress = &'static AtomicU8;
|
pub type Progress = &'static AtomicU8;
|
||||||
|
|
||||||
|
/// The downloader, which has all of the state necessary
|
||||||
|
/// to download tracks and add them to the queue.
|
||||||
pub struct Downloader {
|
pub struct Downloader {
|
||||||
queue: Sender<tracks::Queued>,
|
queue: Sender<tracks::Queued>,
|
||||||
tx: Sender<crate::Message>,
|
tx: Sender<crate::Message>,
|
||||||
@ -24,7 +26,10 @@ pub struct Downloader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Downloader {
|
impl Downloader {
|
||||||
pub async fn init(size: usize, tracks: tracks::List, tx: Sender<crate::Message>) -> Handle {
|
/// Initializes the downloader with a track list.
|
||||||
|
///
|
||||||
|
/// `tx` specifies the [`Sender`] to be notified with [`crate::Message::Loaded`].
|
||||||
|
pub fn init(size: usize, tracks: tracks::List, tx: Sender<crate::Message>) -> Handle {
|
||||||
let client = Client::new();
|
let client = Client::new();
|
||||||
|
|
||||||
let (qtx, qrx) = mpsc::channel(size - 1);
|
let (qtx, qrx) = mpsc::channel(size - 1);
|
||||||
@ -64,25 +69,30 @@ impl Downloader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Downloader handle, responsible for managing
|
||||||
|
/// the downloader task and internal buffer.
|
||||||
pub struct Handle {
|
pub struct Handle {
|
||||||
queue: Receiver<tracks::Queued>,
|
queue: Receiver<tracks::Queued>,
|
||||||
handle: JoinHandle<crate::Result<()>>,
|
handle: JoinHandle<crate::Result<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The output when a track is requested from the downloader.
|
||||||
pub enum Output {
|
pub enum Output {
|
||||||
Loading(Option<Progress>),
|
Loading(Option<Progress>),
|
||||||
Queued(tracks::Queued),
|
Queued(tracks::Queued),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Handle {
|
impl Handle {
|
||||||
pub async fn track(&mut self) -> Output {
|
/// Gets either a queued track, or a progress report,
|
||||||
match self.queue.try_recv() {
|
/// depending on the state of the internal download buffer.
|
||||||
Ok(queued) => Output::Queued(queued),
|
#[rustfmt::skip]
|
||||||
Err(_) => {
|
pub fn track(&mut self) -> Output {
|
||||||
|
self.queue.try_recv().map_or_else(|_| {
|
||||||
LOADING.store(true, atomic::Ordering::Relaxed);
|
LOADING.store(true, atomic::Ordering::Relaxed);
|
||||||
Output::Loading(Some(&PROGRESS))
|
Output::Loading(Some(&PROGRESS))
|
||||||
}
|
}, Output::Queued,
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
20
src/main.rs
20
src/main.rs
@ -1,6 +1,4 @@
|
|||||||
//! An extremely simple lofi player.
|
//! An extremely simple lofi player.
|
||||||
#![warn(clippy::all, clippy::pedantic, clippy::nursery)]
|
|
||||||
|
|
||||||
pub mod error;
|
pub mod error;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
@ -98,23 +96,21 @@ pub fn data_dir() -> crate::Result<PathBuf> {
|
|||||||
async fn main() -> eyre::Result<()> {
|
async fn main() -> eyre::Result<()> {
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
|
|
||||||
if let Some(command) = args.command {
|
#[cfg(feature = "scrape")]
|
||||||
|
if let Some(command) = &args.command {
|
||||||
match command {
|
match command {
|
||||||
#[cfg(feature = "scrape")]
|
|
||||||
Commands::Scrape { source } => match source {
|
Commands::Scrape { source } => match source {
|
||||||
Source::Archive => scrapers::archive::scrape().await?,
|
Source::Archive => scrapers::archive::scrape().await?,
|
||||||
Source::Lofigirl => scrapers::lofigirl::scrape().await?,
|
Source::Lofigirl => scrapers::lofigirl::scrape().await?,
|
||||||
Source::Chillhop => scrapers::chillhop::scrape().await?,
|
Source::Chillhop => scrapers::chillhop::scrape().await?,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
let player = Player::init(args).await?;
|
|
||||||
let environment = player.environment();
|
|
||||||
let result = player.run().await;
|
|
||||||
|
|
||||||
environment.cleanup(result.is_ok())?;
|
let player = Player::init(args).await?;
|
||||||
result?;
|
let environment = player.environment();
|
||||||
};
|
let result = player.run().await;
|
||||||
|
|
||||||
Ok(())
|
environment.cleanup(result.is_ok())?;
|
||||||
|
Ok(result?)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,13 +23,13 @@ pub enum Current {
|
|||||||
|
|
||||||
impl Default for Current {
|
impl Default for Current {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Current::Loading(None)
|
Self::Loading(None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Current {
|
impl Current {
|
||||||
pub fn loading(&self) -> bool {
|
pub const fn loading(&self) -> bool {
|
||||||
return matches!(self, Current::Loading(_));
|
matches!(self, Self::Loading(_))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,25 +53,25 @@ impl Drop for Player {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Player {
|
impl Player {
|
||||||
pub fn environment(&self) -> ui::Environment {
|
pub const fn environment(&self) -> ui::Environment {
|
||||||
self.ui.environment
|
self.ui.environment
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn set_current(&mut self, current: Current) -> crate::Result<()> {
|
pub fn set_current(&mut self, current: Current) -> crate::Result<()> {
|
||||||
self.current = current.clone();
|
self.current = current.clone();
|
||||||
self.update(ui::Update::Track(current)).await?;
|
self.update(ui::Update::Track(current))?;
|
||||||
|
|
||||||
let Current::Track(track) = &self.current else {
|
let Current::Track(track) = &self.current else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
|
||||||
let bookmarked = self.bookmarks.bookmarked(&track);
|
let bookmarked = self.bookmarks.bookmarked(track);
|
||||||
self.update(ui::Update::Bookmarked(bookmarked)).await?;
|
self.update(ui::Update::Bookmarked(bookmarked))?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update(&mut self, update: ui::Update) -> crate::Result<()> {
|
pub fn update(&mut self, update: ui::Update) -> crate::Result<()> {
|
||||||
self.broadcast.send(update)?;
|
self.broadcast.send(update)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -87,21 +87,19 @@ impl Player {
|
|||||||
let (tx, rx) = mpsc::channel(8);
|
let (tx, rx) = mpsc::channel(8);
|
||||||
tx.send(Message::Init).await?;
|
tx.send(Message::Init).await?;
|
||||||
let (utx, urx) = broadcast::channel(8);
|
let (utx, urx) = broadcast::channel(8);
|
||||||
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 =
|
let state = ui::State::initial(Arc::clone(&sink), args.width, list.name.clone());
|
||||||
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());
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
ui: ui::Handle::init(tx.clone(), urx, state, &args).await?,
|
ui: ui::Handle::init(tx.clone(), urx, state, &args).await?,
|
||||||
downloader: Downloader::init(args.buffer_size as usize, list, tx.clone()).await,
|
downloader: Downloader::init(args.buffer_size as usize, list, tx.clone()),
|
||||||
waiter: waiter::Handle::new(sink.clone(), tx.clone()),
|
waiter: waiter::Handle::new(Arc::clone(&sink), tx.clone()),
|
||||||
bookmarks: Bookmarks::load().await?,
|
bookmarks: Bookmarks::load().await?,
|
||||||
current,
|
current: Current::default(),
|
||||||
broadcast: utx,
|
broadcast: utx,
|
||||||
rx,
|
rx,
|
||||||
sink,
|
sink,
|
||||||
@ -112,15 +110,15 @@ impl Player {
|
|||||||
|
|
||||||
pub async fn close(&self) -> crate::Result<()> {
|
pub async fn close(&self) -> crate::Result<()> {
|
||||||
self.bookmarks.save().await?;
|
self.bookmarks.save().await?;
|
||||||
PersistentVolume::save(self.sink.volume() as f32).await?;
|
PersistentVolume::save(self.sink.volume()).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn play(&mut self, queued: tracks::Queued) -> crate::Result<()> {
|
pub fn play(&mut self, queued: tracks::Queued) -> crate::Result<()> {
|
||||||
let decoded = queued.decode()?;
|
let decoded = queued.decode()?;
|
||||||
self.sink.append(decoded.data);
|
self.sink.append(decoded.data);
|
||||||
self.set_current(Current::Track(decoded.info)).await?;
|
self.set_current(Current::Track(decoded.info))?;
|
||||||
self.waiter.notify();
|
self.waiter.notify();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -135,12 +133,12 @@ impl Player {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.sink.stop();
|
self.sink.stop();
|
||||||
match self.downloader.track().await {
|
match self.downloader.track() {
|
||||||
download::Output::Loading(progress) => {
|
download::Output::Loading(progress) => {
|
||||||
self.set_current(Current::Loading(progress)).await?;
|
self.set_current(Current::Loading(progress))?;
|
||||||
}
|
}
|
||||||
download::Output::Queued(queued) => {
|
download::Output::Queued(queued) => {
|
||||||
self.play(queued).await?;
|
self.play(queued)?;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -160,19 +158,19 @@ impl Player {
|
|||||||
Message::ChangeVolume(change) => {
|
Message::ChangeVolume(change) => {
|
||||||
self.sink
|
self.sink
|
||||||
.set_volume((self.sink.volume() + change).clamp(0.0, 1.0));
|
.set_volume((self.sink.volume() + change).clamp(0.0, 1.0));
|
||||||
self.update(ui::Update::Volume).await?;
|
self.update(ui::Update::Volume)?;
|
||||||
}
|
}
|
||||||
Message::SetVolume(set) => {
|
Message::SetVolume(set) => {
|
||||||
self.sink.set_volume(set.clamp(0.0, 1.0));
|
self.sink.set_volume(set.clamp(0.0, 1.0));
|
||||||
self.update(ui::Update::Volume).await?;
|
self.update(ui::Update::Volume)?;
|
||||||
}
|
}
|
||||||
Message::Bookmark => {
|
Message::Bookmark => {
|
||||||
let Current::Track(current) = &self.current else {
|
let Current::Track(current) = &self.current else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
let bookmarked = self.bookmarks.bookmark(current).await?;
|
let bookmarked = self.bookmarks.bookmark(current)?;
|
||||||
self.update(ui::Update::Bookmarked(bookmarked)).await?;
|
self.update(ui::Update::Bookmarked(bookmarked))?;
|
||||||
}
|
}
|
||||||
Message::Quit => break,
|
Message::Quit => break,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,8 +11,8 @@ mod bookmark {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[test]
|
||||||
async fn toggle_and_check() {
|
fn toggle_and_check() {
|
||||||
let mut bm = Bookmarks { entries: vec![] };
|
let mut bm = Bookmarks { entries: vec![] };
|
||||||
let info = test_info("p.mp3", "Nice Track");
|
let info = test_info("p.mp3", "Nice Track");
|
||||||
|
|
||||||
@ -20,37 +20,37 @@ mod bookmark {
|
|||||||
assert!(!bm.bookmarked(&info));
|
assert!(!bm.bookmarked(&info));
|
||||||
|
|
||||||
// bookmark it
|
// bookmark it
|
||||||
let added = bm.bookmark(&info).await.unwrap();
|
let added = bm.bookmark(&info).unwrap();
|
||||||
assert!(added);
|
assert!(added);
|
||||||
assert!(bm.bookmarked(&info));
|
assert!(bm.bookmarked(&info));
|
||||||
|
|
||||||
// un-bookmark it
|
// un-bookmark it
|
||||||
let removed = bm.bookmark(&info).await.unwrap();
|
let removed = bm.bookmark(&info).unwrap();
|
||||||
assert!(!removed);
|
assert!(!removed);
|
||||||
assert!(!bm.bookmarked(&info));
|
assert!(!bm.bookmarked(&info));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[test]
|
||||||
async fn multiple_bookmarks() {
|
fn multiple_bookmarks() {
|
||||||
let mut bm = Bookmarks { entries: vec![] };
|
let mut bm = Bookmarks { entries: vec![] };
|
||||||
let info1 = test_info("track1.mp3", "Track One");
|
let info1 = test_info("track1.mp3", "Track One");
|
||||||
let info2 = test_info("track2.mp3", "Track Two");
|
let info2 = test_info("track2.mp3", "Track Two");
|
||||||
|
|
||||||
bm.bookmark(&info1).await.unwrap();
|
bm.bookmark(&info1).unwrap();
|
||||||
bm.bookmark(&info2).await.unwrap();
|
bm.bookmark(&info2).unwrap();
|
||||||
|
|
||||||
assert!(bm.bookmarked(&info1));
|
assert!(bm.bookmarked(&info1));
|
||||||
assert!(bm.bookmarked(&info2));
|
assert!(bm.bookmarked(&info2));
|
||||||
assert_eq!(bm.entries.len(), 2);
|
assert_eq!(bm.entries.len(), 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[test]
|
||||||
async fn duplicate_bookmark_removes() {
|
fn duplicate_bookmark_removes() {
|
||||||
let mut bm = Bookmarks { entries: vec![] };
|
let mut bm = Bookmarks { entries: vec![] };
|
||||||
let info = test_info("x.mp3", "X");
|
let info = test_info("x.mp3", "X");
|
||||||
|
|
||||||
bm.bookmark(&info).await.unwrap();
|
bm.bookmark(&info).unwrap();
|
||||||
let is_added = bm.bookmark(&info).await.unwrap();
|
let is_added = bm.bookmark(&info).unwrap();
|
||||||
|
|
||||||
assert!(!is_added);
|
assert!(!is_added);
|
||||||
assert!(bm.entries.is_empty());
|
assert!(bm.entries.is_empty());
|
||||||
|
|||||||
@ -79,7 +79,7 @@ mod window {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn simple() {
|
fn simple() {
|
||||||
let mut w = Window::new(3, false);
|
let w = Window::new(3, false);
|
||||||
let (render, height) = w.render(vec![String::from("abc")], false, true).unwrap();
|
let (render, height) = w.render(vec![String::from("abc")], false, true).unwrap();
|
||||||
|
|
||||||
const MIDDLE: &str = "─────";
|
const MIDDLE: &str = "─────";
|
||||||
@ -89,7 +89,7 @@ mod window {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn spaced() {
|
fn spaced() {
|
||||||
let mut w = Window::new(3, false);
|
let w = Window::new(3, false);
|
||||||
let (render, height) = w
|
let (render, height) = w
|
||||||
.render(
|
.render(
|
||||||
vec![String::from("abc"), String::from(" b"), String::from("c")],
|
vec![String::from("abc"), String::from(" b"), String::from("c")],
|
||||||
@ -137,7 +137,7 @@ mod interface {
|
|||||||
#[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, Current::Loading(None), String::from("test"));
|
let mut state = State::initial(sink, 3, String::from("test"));
|
||||||
let menu = interface::menu(&mut state, Params::default());
|
let menu = interface::menu(&mut state, Params::default());
|
||||||
|
|
||||||
assert_eq!(menu[0], "loading ");
|
assert_eq!(menu[0], "loading ");
|
||||||
@ -157,7 +157,7 @@ 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, Current::Loading(None), String::from("test"));
|
let mut state = State::initial(sink, 3, String::from("test"));
|
||||||
state.timer = Some(Instant::now());
|
state.timer = Some(Instant::now());
|
||||||
|
|
||||||
let menu = interface::menu(&mut state, Params::default());
|
let menu = interface::menu(&mut state, Params::default());
|
||||||
@ -179,12 +179,9 @@ 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(
|
let mut state = State::initial(sink, 3, String::from("test"));
|
||||||
sink,
|
state.current = Current::Loading(Some(&PROGRESS));
|
||||||
3,
|
|
||||||
Current::Loading(Some(&PROGRESS)),
|
|
||||||
String::from("test"),
|
|
||||||
);
|
|
||||||
let menu = interface::menu(&mut state, Params::default());
|
let menu = interface::menu(&mut state, Params::default());
|
||||||
|
|
||||||
assert_eq!(menu[0], format!("loading {} ", "50%".bold()));
|
assert_eq!(menu[0], format!("loading {} ", "50%".bold()));
|
||||||
@ -210,8 +207,8 @@ mod interface {
|
|||||||
duration: Some(Duration::from_secs(8)),
|
duration: Some(Duration::from_secs(8)),
|
||||||
};
|
};
|
||||||
|
|
||||||
let current = Current::Track(track.clone());
|
let mut state = State::initial(sink, 3, String::from("test"));
|
||||||
let mut state = State::initial(sink, 3, current, String::from("test"));
|
state.current = Current::Track(track.clone());
|
||||||
let menu = interface::menu(&mut state, Params::default());
|
let menu = interface::menu(&mut state, Params::default());
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
@ -2,24 +2,18 @@
|
|||||||
//! of tracks, as well as downloading them & finding new ones.
|
//! of tracks, as well as downloading them & finding new ones.
|
||||||
//!
|
//!
|
||||||
//! There are several structs which represent the different stages
|
//! There are several structs which represent the different stages
|
||||||
//! that go on in downloading and playing tracks. The proccess for fetching tracks,
|
//! that go on in downloading and playing tracks. When first queued,
|
||||||
//! and what structs are relevant in each step, are as follows.
|
//! the downloader will return a [`Queued`] track.
|
||||||
//!
|
//!
|
||||||
//! First Stage, when a track is initially fetched.
|
//! Then, when it's time to play the track, it is decoded into
|
||||||
//! 1. Raw entry selected from track list.
|
//! a [`Decoded`] track, which includes all the information
|
||||||
//! 2. Raw entry split into path & display name.
|
//! in the form of [`Info`].
|
||||||
//! 3. Track data fetched, and [`QueuedTrack`] is created which includes a [`TrackName`] that may be raw.
|
|
||||||
//!
|
|
||||||
//! Second Stage, when a track is played.
|
|
||||||
//! 1. Track data is decoded.
|
|
||||||
//! 2. [`Info`] created from decoded data.
|
|
||||||
//! 3. [`Decoded`] made from [`Info`] and the original decoded data.
|
|
||||||
|
|
||||||
use std::{fmt::Debug, io::Cursor, time::Duration};
|
use std::{fmt::Debug, io::Cursor, time::Duration};
|
||||||
|
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use rodio::{Decoder, Source as _};
|
use rodio::{Decoder, Source as _};
|
||||||
use unicode_segmentation::UnicodeSegmentation;
|
use unicode_segmentation::UnicodeSegmentation as _;
|
||||||
|
|
||||||
pub mod list;
|
pub mod list;
|
||||||
pub use list::List;
|
pub use list::List;
|
||||||
@ -27,7 +21,7 @@ pub mod error;
|
|||||||
pub mod format;
|
pub mod format;
|
||||||
pub use error::{Error, Result};
|
pub use error::{Error, Result};
|
||||||
|
|
||||||
use crate::tracks::error::WithTrackContext;
|
use crate::tracks::error::WithTrackContext as _;
|
||||||
|
|
||||||
/// Just a shorthand for a decoded [Bytes].
|
/// Just a shorthand for a decoded [Bytes].
|
||||||
pub type DecodedData = Decoder<Cursor<Bytes>>;
|
pub type DecodedData = Decoder<Cursor<Bytes>>;
|
||||||
@ -35,7 +29,7 @@ pub type DecodedData = Decoder<Cursor<Bytes>>;
|
|||||||
/// Tracks which are still waiting in the queue, and can't be played yet.
|
/// Tracks which are still waiting in the queue, and can't be played yet.
|
||||||
///
|
///
|
||||||
/// This means that only the data & track name are included.
|
/// This means that only the data & track name are included.
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq, Eq)]
|
||||||
pub struct Queued {
|
pub struct Queued {
|
||||||
/// Display name of the track.
|
/// Display name of the track.
|
||||||
pub display: String,
|
pub display: String,
|
||||||
@ -66,6 +60,7 @@ impl Queued {
|
|||||||
Decoded::new(self)
|
Decoded::new(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a new queued track.
|
||||||
pub fn new(path: String, data: Bytes, display: Option<String>) -> Result<Self> {
|
pub fn new(path: String, data: Bytes, display: Option<String>) -> Result<Self> {
|
||||||
let display = match display {
|
let display = match display {
|
||||||
None => self::format::name(&path)?,
|
None => self::format::name(&path)?,
|
||||||
@ -73,8 +68,8 @@ impl Queued {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
path,
|
|
||||||
display,
|
display,
|
||||||
|
path,
|
||||||
data,
|
data,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -122,7 +117,7 @@ impl Info {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This struct is seperate from [Track] since it is generated lazily from
|
/// This struct is separate from [Track] since it is generated lazily from
|
||||||
/// a track, and not when the track is first downloaded.
|
/// a track, and not when the track is first downloaded.
|
||||||
pub struct Decoded {
|
pub struct Decoded {
|
||||||
/// Has both the formatted name and some information from the decoded data.
|
/// Has both the formatted name and some information from the decoded data.
|
||||||
@ -138,7 +133,7 @@ impl Decoded {
|
|||||||
pub fn new(track: Queued) -> Result<Self> {
|
pub fn new(track: Queued) -> Result<Self> {
|
||||||
let (path, display) = (track.path.clone(), track.display.clone());
|
let (path, display) = (track.path.clone(), track.display.clone());
|
||||||
let data = Decoder::builder()
|
let data = Decoder::builder()
|
||||||
.with_byte_len(track.data.len().try_into().unwrap())
|
.with_byte_len(track.data.len().try_into()?)
|
||||||
.with_data(Cursor::new(track.data))
|
.with_data(Cursor::new(track.data))
|
||||||
.build()
|
.build()
|
||||||
.track(track.display)?;
|
.track(track.display)?;
|
||||||
|
|||||||
@ -19,6 +19,9 @@ pub enum Kind {
|
|||||||
|
|
||||||
#[error("unable to fetch data: {0}")]
|
#[error("unable to fetch data: {0}")]
|
||||||
Request(#[from] reqwest::Error),
|
Request(#[from] reqwest::Error),
|
||||||
|
|
||||||
|
#[error("couldn't handle integer track length: {0}")]
|
||||||
|
Integer(#[from] std::num::TryFromIntError),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
use convert_case::{Case, Casing};
|
use convert_case::{Case, Casing as _};
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use url::form_urlencoded;
|
use url::form_urlencoded;
|
||||||
|
|
||||||
use super::error::WithTrackContext;
|
use super::error::WithTrackContext as _;
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref MASTER_PATTERNS: [Regex; 5] = [
|
static ref MASTER_PATTERNS: [Regex; 5] = [
|
||||||
@ -84,7 +84,7 @@ pub fn name(name: &str) -> super::Result<String> {
|
|||||||
|
|
||||||
// If the entire name of the track is a number, then just return it.
|
// If the entire name of the track is a number, then just return it.
|
||||||
if skip == name.len() {
|
if skip == name.len() {
|
||||||
Ok(name.trim().to_string())
|
Ok(name.trim().to_owned())
|
||||||
} else {
|
} else {
|
||||||
// We've already checked before that the bound is at an ASCII digit.
|
// We've already checked before that the bound is at an ASCII digit.
|
||||||
#[allow(clippy::string_slice)]
|
#[allow(clippy::string_slice)]
|
||||||
|
|||||||
@ -6,8 +6,8 @@ use std::{
|
|||||||
sync::atomic::{AtomicU8, Ordering},
|
sync::atomic::{AtomicU8, Ordering},
|
||||||
};
|
};
|
||||||
|
|
||||||
use bytes::{BufMut, Bytes, BytesMut};
|
use bytes::{BufMut as _, Bytes, BytesMut};
|
||||||
use futures::StreamExt;
|
use futures::StreamExt as _;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
|
|
||||||
@ -15,7 +15,7 @@ use crate::{
|
|||||||
data_dir,
|
data_dir,
|
||||||
tracks::{
|
tracks::{
|
||||||
self,
|
self,
|
||||||
error::{self, WithTrackContext},
|
error::{self, WithTrackContext as _},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -114,7 +114,7 @@ impl List {
|
|||||||
while let Some(item) = stream.next().await {
|
while let Some(item) = stream.next().await {
|
||||||
let chunk = item.track(track)?;
|
let chunk = item.track(track)?;
|
||||||
downloaded = min(downloaded + (chunk.len() as u64), total);
|
downloaded = min(downloaded + (chunk.len() as u64), total);
|
||||||
let rounded = ((downloaded as f32) / (total as f32) * 100.0).round() as u8;
|
let rounded = ((downloaded as f64) / (total as f64) * 100.0).round() as u8;
|
||||||
progress.store(rounded, Ordering::Relaxed);
|
progress.store(rounded, Ordering::Relaxed);
|
||||||
|
|
||||||
bytes.put(chunk);
|
bytes.put(chunk);
|
||||||
|
|||||||
39
src/ui.rs
39
src/ui.rs
@ -22,8 +22,8 @@ pub mod mpris;
|
|||||||
|
|
||||||
type Result<T> = std::result::Result<T, Error>;
|
type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
||||||
/// The error type for the UI, which is used to handle errors that occur
|
/// The error type for the UI, which is used to handle errors
|
||||||
/// while drawing the UI or handling input.
|
/// that occur while drawing the UI or handling input.
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error("unable to convert number")]
|
#[error("unable to convert number")]
|
||||||
@ -36,7 +36,7 @@ pub enum Error {
|
|||||||
CrateSend(#[from] tokio::sync::mpsc::error::SendError<crate::Message>),
|
CrateSend(#[from] tokio::sync::mpsc::error::SendError<crate::Message>),
|
||||||
|
|
||||||
#[error("sharing state between backend and frontend failed")]
|
#[error("sharing state between backend and frontend failed")]
|
||||||
UiSend(#[from] tokio::sync::broadcast::error::SendError<Update>),
|
Send(#[from] tokio::sync::broadcast::error::SendError<Update>),
|
||||||
|
|
||||||
#[cfg(feature = "mpris")]
|
#[cfg(feature = "mpris")]
|
||||||
#[error("mpris bus error")]
|
#[error("mpris bus error")]
|
||||||
@ -47,6 +47,11 @@ pub enum Error {
|
|||||||
Fdo(#[from] mpris_server::zbus::fdo::Error),
|
Fdo(#[from] mpris_server::zbus::fdo::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The UI state, which is all of the information that
|
||||||
|
/// the user interface needs to display to the user.
|
||||||
|
///
|
||||||
|
/// It should be noted that this is also used by MPRIS to keep
|
||||||
|
/// track of state.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct State {
|
pub struct State {
|
||||||
pub sink: Arc<rodio::Sink>,
|
pub sink: Arc<rodio::Sink>,
|
||||||
@ -60,19 +65,26 @@ pub struct State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
pub fn initial(sink: Arc<rodio::Sink>, width: usize, current: Current, list: String) -> Self {
|
/// Creates an initial UI state.
|
||||||
|
pub fn initial(sink: Arc<rodio::Sink>, width: usize, list: String) -> Self {
|
||||||
let width = 21 + width.min(32) * 2;
|
let width = 21 + width.min(32) * 2;
|
||||||
Self {
|
Self {
|
||||||
width,
|
width,
|
||||||
sink,
|
sink,
|
||||||
current,
|
|
||||||
list,
|
list,
|
||||||
|
current: Current::default(),
|
||||||
bookmarked: false,
|
bookmarked: false,
|
||||||
timer: None,
|
timer: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A UI update sent out by the main player thread, which may
|
||||||
|
/// not be immediately applied by the UI.
|
||||||
|
///
|
||||||
|
/// This corresponds to user actions, like bookmarking a track,
|
||||||
|
/// skipping, or changing the volume. The difference is that it also
|
||||||
|
/// contains the new information about the track.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Update {
|
pub enum Update {
|
||||||
Track(Current),
|
Track(Current),
|
||||||
@ -81,12 +93,16 @@ pub enum Update {
|
|||||||
Quit,
|
Quit,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Just a simple wrapper for the two primary tasks that the UI
|
||||||
|
/// requires to function.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct Tasks {
|
struct Tasks {
|
||||||
render: JoinHandle<Result<()>>,
|
render: JoinHandle<Result<()>>,
|
||||||
input: JoinHandle<Result<()>>,
|
input: JoinHandle<Result<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The UI handle for controlling the state of the UI, as well as
|
||||||
|
/// updating MPRIS information and other small interfacing tasks.
|
||||||
pub struct Handle {
|
pub struct Handle {
|
||||||
tasks: Tasks,
|
tasks: Tasks,
|
||||||
pub environment: Environment,
|
pub environment: Environment,
|
||||||
@ -102,6 +118,14 @@ impl Drop for Handle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Handle {
|
impl Handle {
|
||||||
|
/// The main UI process, which will both render the UI to the terminal
|
||||||
|
/// and also update state.
|
||||||
|
///
|
||||||
|
/// It does both of these things at a fixed interval, due to things
|
||||||
|
/// like the track duration changing too frequently.
|
||||||
|
///
|
||||||
|
/// `rx` is the receiver for state updates, `state` the initial state,
|
||||||
|
/// and `params` specifies aesthetic options that are specified by the user.
|
||||||
async fn ui(
|
async fn ui(
|
||||||
mut rx: broadcast::Receiver<Update>,
|
mut rx: broadcast::Receiver<Update>,
|
||||||
mut state: State,
|
mut state: State,
|
||||||
@ -111,8 +135,6 @@ 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)?;
|
|
||||||
|
|
||||||
if let Ok(message) = rx.try_recv() {
|
if let Ok(message) = rx.try_recv() {
|
||||||
match message {
|
match message {
|
||||||
Update::Track(track) => state.current = track,
|
Update::Track(track) => state.current = track,
|
||||||
@ -122,12 +144,15 @@ impl Handle {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface::draw(&mut state, &mut window, params)?;
|
||||||
interval.tick().await;
|
interval.tick().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Initializes the UI itself, along with all of the tasks that are related to it.
|
||||||
|
#[allow(clippy::unused_async)]
|
||||||
pub async fn init(
|
pub async fn init(
|
||||||
tx: Sender<crate::Message>,
|
tx: Sender<crate::Message>,
|
||||||
updater: broadcast::Receiver<ui::Update>,
|
updater: broadcast::Receiver<ui::Update>,
|
||||||
|
|||||||
@ -45,7 +45,7 @@ impl Environment {
|
|||||||
|
|
||||||
panic::set_hook(Box::new(move |info| {
|
panic::set_hook(Box::new(move |info| {
|
||||||
let _ = environment.cleanup(false);
|
let _ = environment.cleanup(false);
|
||||||
eprintln!("panic: {}", info);
|
eprintln!("panic: {info}");
|
||||||
}));
|
}));
|
||||||
|
|
||||||
Ok(environment)
|
Ok(environment)
|
||||||
|
|||||||
@ -26,7 +26,7 @@ impl From<&Args> for Params {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn menu(state: &mut ui::State, params: Params) -> Vec<String> {
|
pub(crate) fn menu(state: &mut ui::State, params: Params) -> Vec<String> {
|
||||||
let action = components::action(&state, state.width);
|
let action = components::action(state, state.width);
|
||||||
|
|
||||||
let middle = match state.timer {
|
let middle = match state.timer {
|
||||||
Some(timer) => {
|
Some(timer) => {
|
||||||
@ -38,7 +38,7 @@ pub(crate) fn menu(state: &mut ui::State, params: Params) -> Vec<String> {
|
|||||||
|
|
||||||
components::audio_bar(state.width - 17, volume, &percentage)
|
components::audio_bar(state.width - 17, volume, &percentage)
|
||||||
}
|
}
|
||||||
None => components::progress_bar(&state, state.width - 16),
|
None => components::progress_bar(state, state.width - 16),
|
||||||
};
|
};
|
||||||
|
|
||||||
let controls = components::controls(state.width);
|
let controls = components::controls(state.width);
|
||||||
|
|||||||
@ -263,8 +263,8 @@ pub struct Server {
|
|||||||
/// The inner MPRIS server.
|
/// The inner MPRIS server.
|
||||||
inner: mpris_server::Server<Player>,
|
inner: mpris_server::Server<Player>,
|
||||||
|
|
||||||
/// Broadcast reciever.
|
/// Broadcast receiver.
|
||||||
reciever: broadcast::Receiver<Update>,
|
receiver: broadcast::Receiver<Update>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Server {
|
impl Server {
|
||||||
@ -273,16 +273,17 @@ impl Server {
|
|||||||
&mut self,
|
&mut self,
|
||||||
properties: impl IntoIterator<Item = mpris_server::Property> + Send + Sync,
|
properties: impl IntoIterator<Item = mpris_server::Property> + Send + Sync,
|
||||||
) -> ui::Result<()> {
|
) -> ui::Result<()> {
|
||||||
while let Ok(update) = self.reciever.try_recv() {
|
while let Ok(update) = self.receiver.try_recv() {
|
||||||
if let Update::Track(current) = update {
|
if let Update::Track(current) = update {
|
||||||
self.player().current.swap(Arc::new(current));
|
self.player().current.swap(Arc::new(current));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.inner.properties_changed(properties).await?;
|
|
||||||
|
|
||||||
|
self.inner.properties_changed(properties).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Updates the volume with the latest information.
|
||||||
pub async fn update_volume(&mut self) -> ui::Result<()> {
|
pub async fn update_volume(&mut self) -> ui::Result<()> {
|
||||||
self.changed(vec![Property::Volume(self.player().sink.volume().into())])
|
self.changed(vec![Property::Volume(self.player().sink.volume().into())])
|
||||||
.await?;
|
.await?;
|
||||||
@ -290,7 +291,7 @@ impl Server {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shorthand to emit a `PropertiesChanged` signal, specifically about playback.
|
/// Updates the playback with the latest information.
|
||||||
pub async fn update_playback(&mut self) -> ui::Result<()> {
|
pub async fn update_playback(&mut self) -> ui::Result<()> {
|
||||||
let status = self.player().playback_status().await?;
|
let status = self.player().playback_status().await?;
|
||||||
self.changed(vec![Property::PlaybackStatus(status)]).await?;
|
self.changed(vec![Property::PlaybackStatus(status)]).await?;
|
||||||
@ -298,6 +299,7 @@ impl Server {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Updates the current track data with the current information.
|
||||||
pub async fn update_metadata(&mut self) -> ui::Result<()> {
|
pub async fn update_metadata(&mut self) -> ui::Result<()> {
|
||||||
let metadata = self.player().metadata().await?;
|
let metadata = self.player().metadata().await?;
|
||||||
self.changed(vec![Property::Metadata(metadata)]).await?;
|
self.changed(vec![Property::Metadata(metadata)]).await?;
|
||||||
@ -314,7 +316,7 @@ impl Server {
|
|||||||
pub async fn new(
|
pub async fn new(
|
||||||
state: ui::State,
|
state: ui::State,
|
||||||
sender: mpsc::Sender<Message>,
|
sender: mpsc::Sender<Message>,
|
||||||
reciever: broadcast::Receiver<Update>,
|
receiver: broadcast::Receiver<Update>,
|
||||||
) -> ui::Result<Server> {
|
) -> ui::Result<Server> {
|
||||||
let suffix = if env::var("LOWFI_FIXED_MPRIS_NAME").is_ok_and(|x| x == "1") {
|
let suffix = if env::var("LOWFI_FIXED_MPRIS_NAME").is_ok_and(|x| x == "1") {
|
||||||
String::from("lowfi")
|
String::from("lowfi")
|
||||||
@ -335,7 +337,7 @@ impl Server {
|
|||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
inner: server,
|
inner: server,
|
||||||
reciever,
|
receiver,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,11 +2,11 @@ use std::io::{stdout, Stdout};
|
|||||||
|
|
||||||
use crossterm::{
|
use crossterm::{
|
||||||
cursor::{MoveToColumn, MoveUp},
|
cursor::{MoveToColumn, MoveUp},
|
||||||
style::{Print, Stylize},
|
style::{Print, Stylize as _},
|
||||||
terminal::{Clear, ClearType},
|
terminal::{Clear, ClearType},
|
||||||
};
|
};
|
||||||
use std::fmt::Write;
|
use std::fmt::Write as _;
|
||||||
use unicode_segmentation::UnicodeSegmentation;
|
use unicode_segmentation::UnicodeSegmentation as _;
|
||||||
|
|
||||||
/// Represents an abstraction for drawing the actual lowfi window itself.
|
/// Represents an abstraction for drawing the actual lowfi window itself.
|
||||||
///
|
///
|
||||||
@ -52,7 +52,7 @@ impl Window {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn render(
|
pub(crate) fn render(
|
||||||
&mut self,
|
&self,
|
||||||
content: Vec<String>,
|
content: Vec<String>,
|
||||||
space: bool,
|
space: bool,
|
||||||
testing: bool,
|
testing: bool,
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
|
//! Persistent volume management.
|
||||||
use std::{num::ParseIntError, path::PathBuf};
|
use std::{num::ParseIntError, path::PathBuf};
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
|
|
||||||
|
/// Shorthand for a [`Result`] with a persistent volume error.
|
||||||
type Result<T> = std::result::Result<T, Error>;
|
type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
||||||
|
/// Errors which occur when loading/unloading persistent volume.
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error("couldn't find config directory")]
|
#[error("couldn't find config directory")]
|
||||||
@ -27,7 +30,7 @@ impl PersistentVolume {
|
|||||||
/// Retrieves the config directory.
|
/// Retrieves the config directory.
|
||||||
async fn config() -> Result<PathBuf> {
|
async fn config() -> Result<PathBuf> {
|
||||||
let config = dirs::config_dir()
|
let config = dirs::config_dir()
|
||||||
.ok_or_else(|| Error::Directory)?
|
.ok_or(Error::Directory)?
|
||||||
.join(PathBuf::from("lowfi"));
|
.join(PathBuf::from("lowfi"));
|
||||||
|
|
||||||
if !config.exists() {
|
if !config.exists() {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user