mirror of
https://github.com/talwat/lowfi
synced 2026-04-28 04:43:26 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d46c41df7 | ||
|
|
4891e626e1 | ||
|
|
5efecea14e | ||
|
|
64b7694fb9 | ||
|
|
c0ebdb057f | ||
|
|
1d7963fdc8 | ||
|
|
767f8e8017 | ||
|
|
4e9ff033d8 | ||
|
|
68766b8607 | ||
|
|
fb75cc1a28 |
16
Cargo.toml
16
Cargo.toml
@ -66,19 +66,3 @@ indicatif = { version = "0.18.4", optional = true }
|
|||||||
|
|
||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
libc = "0.2.182"
|
libc = "0.2.182"
|
||||||
|
|
||||||
[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"
|
|
||||||
struct_excessive_bools = "allow"
|
|
||||||
|
|||||||
37
README.md
37
README.md
@ -30,14 +30,28 @@ and as such it buffers 5 whole songs at a time instead of parts of the same song
|
|||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
>
|
>
|
||||||
> If you're interested in maintaining a package for `lowfi`
|
> Distro repositories may be slightly behind cargo, but lowfi is not security critical
|
||||||
> on package managers such as homebrew and the like, open an issue.
|
> so they are still perfectly fine to use.
|
||||||
|
|
||||||
### Dependencies
|
<a href="https://repology.org/project/lowfi/versions">
|
||||||
|
<img src="https://repology.org/badge/vertical-allrepos/lowfi.svg" alt="Packaging status" align="right">
|
||||||
|
</a>
|
||||||
|
|
||||||
You'll need Rust 1.83.0+.
|
### Methods
|
||||||
|
|
||||||
On MacOS & Windows, no extra dependencies are needed.
|
- [Cargo](#cargo)
|
||||||
|
- [Release Binaries](#release-binaries)
|
||||||
|
- [AUR](#aur)
|
||||||
|
- [openSUSE](#opensuse)
|
||||||
|
- [Debian and Ubuntu](#debian-and-ubuntu)
|
||||||
|
- [Fedora (COPR)](#fedora-copr)
|
||||||
|
- [Manual](#manual)
|
||||||
|
|
||||||
|
### Cargo
|
||||||
|
|
||||||
|
Installing with cargo is universal, but carries a few dependencies with it.
|
||||||
|
|
||||||
|
Firstly, you'll need Rust 1.83.0+. On MacOS & Windows, no extra dependencies are needed.
|
||||||
|
|
||||||
On Linux, you'll also need openssl & alsa, as well as their headers.
|
On Linux, you'll also need openssl & alsa, as well as their headers.
|
||||||
|
|
||||||
@ -46,9 +60,7 @@ On Linux, you'll also need openssl & alsa, as well as their headers.
|
|||||||
|
|
||||||
Make sure to also install `pulseaudio-alsa` if you're using PulseAudio.
|
Make sure to also install `pulseaudio-alsa` if you're using PulseAudio.
|
||||||
|
|
||||||
### Cargo
|
Then, simply run:
|
||||||
|
|
||||||
The recommended installation method is to use cargo:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cargo install lowfi
|
cargo install lowfi
|
||||||
@ -57,7 +69,7 @@ cargo install lowfi
|
|||||||
cargo install lowfi --features mpris
|
cargo install lowfi --features mpris
|
||||||
```
|
```
|
||||||
|
|
||||||
and making sure `$HOME/.cargo/bin` is added to `$PATH`.
|
And make sure `$HOME/.cargo/bin` is added to `$PATH`.
|
||||||
Also see [Extra Features](#extra-features) for extended functionality.
|
Also see [Extra Features](#extra-features) for extended functionality.
|
||||||
|
|
||||||
### Release Binaries
|
### Release Binaries
|
||||||
@ -159,10 +171,11 @@ slightly tweak the UI or behavior of the menu. The flags can be viewed with `low
|
|||||||
| `-a`, `--alternate` | Use an alternate terminal screen |
|
| `-a`, `--alternate` | Use an alternate terminal screen |
|
||||||
| `-m`, `--minimalist` | Hide the bottom control bar |
|
| `-m`, `--minimalist` | Hide the bottom control bar |
|
||||||
| `-b`, `--borderless` | Exclude borders in UI |
|
| `-b`, `--borderless` | Exclude borders in UI |
|
||||||
|
| `-c`, `--clock` | Include a clock |
|
||||||
| `-p`, `--paused` | Start lowfi paused |
|
| `-p`, `--paused` | Start lowfi paused |
|
||||||
| `-f`, `--fps` | FPS of the UI [default: 12] |
|
| `-f`, `--fps` | FPS of the UI [default: 12] |
|
||||||
| `--timeout` | Timeout in seconds for music downloads [default: 3] |
|
| `--timeout` | Timeout in seconds for music downloads [default: 3] |
|
||||||
| `-d`, `--debug` | Include ALSA & other logs |
|
| `-d`, `--debug` | Include ALSA & other logs meant for debugging |
|
||||||
| `-w`, `--width <WIDTH>` | Width of the player, from 0 to 32 [default: 3] |
|
| `-w`, `--width <WIDTH>` | Width of the player, from 0 to 32 [default: 3] |
|
||||||
| `-t`, `--track-list <TRACK_LIST>` | Use a [custom track list](#custom-track-lists) |
|
| `-t`, `--track-list <TRACK_LIST>` | Use a [custom track list](#custom-track-lists) |
|
||||||
| `-s`, `--buffer-size <BUFFER_SIZE>` | Internal song buffer size [default: 5] |
|
| `-s`, `--buffer-size <BUFFER_SIZE>` | Internal song buffer size [default: 5] |
|
||||||
@ -204,7 +217,7 @@ If you are dealing with the 1% using another audio format which is in
|
|||||||
> Feel free to contribute your own list with a PR.
|
> Feel free to contribute your own list with a PR.
|
||||||
|
|
||||||
lowfi also supports custom track lists, although the default one from chillhop
|
lowfi also supports custom track lists, although the default one from chillhop
|
||||||
is embedded into the binary.
|
is embedded into the binary by default.
|
||||||
|
|
||||||
To use a custom list, use the `--track-list` flag. This can either be a path to some file,
|
To use a custom list, use the `--track-list` flag. This can either be a path to some file,
|
||||||
or it could also be the name of a file (without the `.txt` extension) in the data
|
or it could also be the name of a file (without the `.txt` extension) in the data
|
||||||
@ -231,7 +244,7 @@ Each track will be first appended to the header, and then use the combination to
|
|||||||
the track.
|
the track.
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> lowfi *will not* put a `/` between the base & track for added flexibility,
|
> lowfi _will not_ put a `/` between the base & track for added flexibility,
|
||||||
> so for most cases you should have a trailing `/` in your header.
|
> so for most cases you should have a trailing `/` in your header.
|
||||||
|
|
||||||
The exception to this is if the track name begins with a protocol like `https://`,
|
The exception to this is if the track name begins with a protocol like `https://`,
|
||||||
|
|||||||
@ -5,7 +5,7 @@ use std::{
|
|||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::tracks;
|
use crate::{tracks, ui::interface::Logger};
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
@ -68,20 +68,60 @@ pub struct Downloader {
|
|||||||
/// The list of tracks to download from.
|
/// The list of tracks to download from.
|
||||||
tracks: tracks::List,
|
tracks: tracks::List,
|
||||||
|
|
||||||
|
/// Whether to log more debug information.
|
||||||
|
debug: bool,
|
||||||
|
|
||||||
/// The [`reqwest`] client to use for downloads.
|
/// The [`reqwest`] client to use for downloads.
|
||||||
client: Client,
|
client: Client,
|
||||||
|
|
||||||
/// The RNG generator to use.
|
/// The RNG generator to use.
|
||||||
rng: fastrand::Rng,
|
rng: fastrand::Rng,
|
||||||
|
|
||||||
|
/// Logger, to report any download errors to the UI.
|
||||||
|
logger: Logger,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Downloader {
|
impl Downloader {
|
||||||
|
/// Handles an error encountered by the downloader.
|
||||||
|
///
|
||||||
|
/// If the error isn't a timeout, it will also stall for some arbitrary error timeout.
|
||||||
|
async fn handle_error(&mut self, error: tracks::Error) -> crate::Result<()> {
|
||||||
|
const ERROR_TIMEOUT: Duration = Duration::from_secs(1);
|
||||||
|
|
||||||
|
PROGRESS.store(0, atomic::Ordering::Relaxed);
|
||||||
|
|
||||||
|
let track_suffix = if let Some(x) = error.track.as_ref() {
|
||||||
|
format!(" track: {x}")
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
match &error.kind {
|
||||||
|
tracks::error::Kind::Request(x) => {
|
||||||
|
if let Some(status) = x.status() {
|
||||||
|
let message =
|
||||||
|
format!("track list error code: {}{track_suffix}", status.as_u16());
|
||||||
|
self.logger.info(message).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
error if self.debug => {
|
||||||
|
let message = format!("debug: error: {}{track_suffix}", error);
|
||||||
|
self.logger.info(message).await?;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !error.timeout() {
|
||||||
|
tokio::time::sleep(ERROR_TIMEOUT).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Actually runs the downloader, consuming it and beginning
|
/// Actually runs the downloader, consuming it and beginning
|
||||||
/// the cycle of downloading tracks and reporting to the
|
/// the cycle of downloading tracks and reporting to the
|
||||||
/// rest of the program.
|
/// rest of the program.
|
||||||
async fn run(mut self) -> crate::Result<()> {
|
async fn run(mut self) -> crate::Result<()> {
|
||||||
const ERROR_TIMEOUT: Duration = Duration::from_secs(1);
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let result = self
|
let result = self
|
||||||
.tracks
|
.tracks
|
||||||
@ -90,18 +130,19 @@ impl Downloader {
|
|||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(track) => {
|
Ok(track) => {
|
||||||
|
if self.debug {
|
||||||
|
self.logger
|
||||||
|
.info(format!("debug: adding {} to queue", track.display))
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
self.queue.send(track).await?;
|
self.queue.send(track).await?;
|
||||||
if LOADING.load(atomic::Ordering::Relaxed) {
|
if LOADING.load(atomic::Ordering::Relaxed) {
|
||||||
self.tx.send(crate::Message::Loaded).await?;
|
self.tx.send(crate::Message::Loaded).await?;
|
||||||
LOADING.store(false, atomic::Ordering::Relaxed);
|
LOADING.store(false, atomic::Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => self.handle_error(error).await?,
|
||||||
PROGRESS.store(0, atomic::Ordering::Relaxed);
|
|
||||||
if !error.timeout() {
|
|
||||||
tokio::time::sleep(ERROR_TIMEOUT).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -146,8 +187,8 @@ impl crate::Tasks {
|
|||||||
/// `tx` specifies the [`Sender`] to be notified with [`crate::Message::Loaded`].
|
/// `tx` specifies the [`Sender`] to be notified with [`crate::Message::Loaded`].
|
||||||
pub fn downloader(
|
pub fn downloader(
|
||||||
&mut self,
|
&mut self,
|
||||||
size: usize,
|
args: &crate::Args,
|
||||||
timeout: u64,
|
logger: Logger,
|
||||||
tracks: tracks::List,
|
tracks: tracks::List,
|
||||||
) -> crate::Result<Handle> {
|
) -> crate::Result<Handle> {
|
||||||
let client = Client::builder()
|
let client = Client::builder()
|
||||||
@ -156,11 +197,13 @@ impl crate::Tasks {
|
|||||||
"/",
|
"/",
|
||||||
env!("CARGO_PKG_VERSION")
|
env!("CARGO_PKG_VERSION")
|
||||||
))
|
))
|
||||||
.timeout(Duration::from_secs(timeout))
|
.timeout(Duration::from_secs(args.timeout))
|
||||||
.build()?;
|
.build()?;
|
||||||
|
|
||||||
let (qtx, qrx) = mpsc::channel(size - 1);
|
let (qtx, qrx) = mpsc::channel(args.buffer_size as usize - 1);
|
||||||
let downloader = Downloader {
|
let downloader = Downloader {
|
||||||
|
debug: args.debug,
|
||||||
|
logger,
|
||||||
queue: qtx,
|
queue: qtx,
|
||||||
tx: self.tx(),
|
tx: self.tx(),
|
||||||
tracks,
|
tracks,
|
||||||
|
|||||||
@ -111,9 +111,10 @@ impl Player {
|
|||||||
let volume = PersistentVolume::load().await?;
|
let volume = PersistentVolume::load().await?;
|
||||||
sink.set_volume(volume.float());
|
sink.set_volume(volume.float());
|
||||||
|
|
||||||
|
let ui = tasks.ui(state, &args).await?;
|
||||||
let player = Self {
|
let player = Self {
|
||||||
ui: tasks.ui(state, &args).await?,
|
downloader: tasks.downloader(&args, ui.logger(), list)?,
|
||||||
downloader: tasks.downloader(args.buffer_size as usize, args.timeout, list)?,
|
ui,
|
||||||
waiter: tasks.waiter(Arc::clone(&sink)),
|
waiter: tasks.waiter(Arc::clone(&sink)),
|
||||||
bookmarks: Bookmarks::load().await?,
|
bookmarks: Bookmarks::load().await?,
|
||||||
current: Current::default(),
|
current: Current::default(),
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
//! The module containing all of the logic behind track lists,
|
//! The module containing all of the logic behind track lists,
|
||||||
//! as well as obtaining track names & downloading the raw audio data
|
//! as well as obtaining track names & downloading the raw audio data
|
||||||
|
|
||||||
use std::cmp::min;
|
use std::{cmp::min, path::Path};
|
||||||
|
|
||||||
use bytes::{BufMut as _, Bytes, BytesMut};
|
use bytes::{BufMut as _, Bytes, BytesMut};
|
||||||
use futures_util::StreamExt as _;
|
use futures_util::StreamExt as _;
|
||||||
@ -28,8 +28,8 @@ pub struct List {
|
|||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|
||||||
/// Just the raw file, but seperated by `/n` (newlines).
|
/// Just the raw file, but separated by `/n` (newlines).
|
||||||
/// `lines[0]` is the base/heaeder, with the rest being tracks.
|
/// `lines[0]` is the base/header, with the rest being tracks.
|
||||||
pub lines: Vec<String>,
|
pub lines: Vec<String>,
|
||||||
|
|
||||||
/// The file path which the list was read from.
|
/// The file path which the list was read from.
|
||||||
@ -95,7 +95,13 @@ impl List {
|
|||||||
let result = tokio::fs::read(path.clone()).await.track(x)?;
|
let result = tokio::fs::read(path.clone()).await.track(x)?;
|
||||||
result.into()
|
result.into()
|
||||||
} else {
|
} else {
|
||||||
let response = client.get(path.clone()).send().await.track(track)?;
|
let response = client
|
||||||
|
.get(path.clone())
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()
|
||||||
|
.track(track)?;
|
||||||
|
|
||||||
let Some(progress) = progress else {
|
let Some(progress) = progress else {
|
||||||
let bytes = response.bytes().await.track(track)?;
|
let bytes = response.bytes().await.track(track)?;
|
||||||
return Ok((bytes, path));
|
return Ok((bytes, path));
|
||||||
@ -139,8 +145,27 @@ impl List {
|
|||||||
Queued::new(path, data, display)
|
Queued::new(path, data, display)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reads a file and parses it into a list.
|
||||||
|
pub async fn from_file(path: impl AsRef<Path>, name: Option<&str>) -> tracks::Result<Self> {
|
||||||
|
let path = path.as_ref();
|
||||||
|
let text = fs::read_to_string(path).await?;
|
||||||
|
|
||||||
|
let name = match name {
|
||||||
|
Some(name) => name,
|
||||||
|
None => path
|
||||||
|
.file_stem()
|
||||||
|
.and_then(|x| x.to_str())
|
||||||
|
.ok_or(tracks::error::Kind::InvalidName)?,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self::new(name, &text, path.to_str()))
|
||||||
|
}
|
||||||
|
|
||||||
/// Parses text into a [List].
|
/// Parses text into a [List].
|
||||||
pub fn new(name: &str, text: &str, path: Option<&str>) -> Self {
|
pub fn new(name: &str, text: &str, path: Option<&str>) -> Self {
|
||||||
|
// Get rid of special noheader case for tracklists without a header.
|
||||||
|
let text = text.strip_prefix("noheader").unwrap_or(text);
|
||||||
|
|
||||||
let lines: Vec<String> = text
|
let lines: Vec<String> = text
|
||||||
.trim_end()
|
.trim_end()
|
||||||
.lines()
|
.lines()
|
||||||
@ -156,6 +181,15 @@ impl List {
|
|||||||
|
|
||||||
/// Reads a [List] from the filesystem using the CLI argument provided.
|
/// Reads a [List] from the filesystem using the CLI argument provided.
|
||||||
pub async fn load(tracks: &str) -> tracks::Result<Self> {
|
pub async fn load(tracks: &str) -> tracks::Result<Self> {
|
||||||
|
// Check if the track is in ~/.local/share/lowfi, in which case we'll load that.
|
||||||
|
let path = data_dir()
|
||||||
|
.map_err(|_| error::Kind::InvalidPath)?
|
||||||
|
.join(format!("{tracks}.txt"));
|
||||||
|
|
||||||
|
if path.exists() {
|
||||||
|
return Self::from_file(path, Some(tracks)).await;
|
||||||
|
}
|
||||||
|
|
||||||
if tracks == "chillhop" {
|
if tracks == "chillhop" {
|
||||||
#[cfg(feature = "default-tracklist")]
|
#[cfg(feature = "default-tracklist")]
|
||||||
return Ok(Self::new(
|
return Ok(Self::new(
|
||||||
@ -168,23 +202,6 @@ impl List {
|
|||||||
return Err(tracks::error::Kind::NoTrackList.into());
|
return Err(tracks::error::Kind::NoTrackList.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the track is in ~/.local/share/lowfi, in which case we'll load that.
|
Self::from_file(tracks, None).await
|
||||||
let path = data_dir()
|
|
||||||
.map_err(|_| error::Kind::InvalidPath)?
|
|
||||||
.join(format!("{tracks}.txt"));
|
|
||||||
let path = if path.exists() { path } else { tracks.into() };
|
|
||||||
|
|
||||||
let raw = fs::read_to_string(path.clone()).await?;
|
|
||||||
|
|
||||||
// Get rid of special noheader case for tracklists without a header.
|
|
||||||
let raw = raw.strip_prefix("noheader").unwrap_or_else(|| raw.as_ref());
|
|
||||||
|
|
||||||
let name = path
|
|
||||||
.file_stem()
|
|
||||||
.and_then(|x| x.to_str())
|
|
||||||
.ok_or(tracks::error::Kind::InvalidName)
|
|
||||||
.track(tracks)?;
|
|
||||||
|
|
||||||
Ok(Self::new(name, raw, path.to_str()))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
17
src/ui.rs
17
src/ui.rs
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::player::Current;
|
use crate::{player::Current, ui::interface::Logger};
|
||||||
use tokio::{sync::broadcast, time::Instant};
|
use tokio::{sync::broadcast, time::Instant};
|
||||||
|
|
||||||
pub mod environment;
|
pub mod environment;
|
||||||
@ -34,6 +34,9 @@ pub enum Error {
|
|||||||
#[error("sharing state between backend and frontend failed: {0}")]
|
#[error("sharing state between backend and frontend failed: {0}")]
|
||||||
StateSend(#[from] tokio::sync::broadcast::error::SendError<Update>),
|
StateSend(#[from] tokio::sync::broadcast::error::SendError<Update>),
|
||||||
|
|
||||||
|
#[error("error sending a log: {0}")]
|
||||||
|
LogSend(#[from] tokio::sync::mpsc::error::SendError<String>),
|
||||||
|
|
||||||
#[error("you can't disable the UI without MPRIS!")]
|
#[error("you can't disable the UI without MPRIS!")]
|
||||||
RejectedDisable,
|
RejectedDisable,
|
||||||
|
|
||||||
@ -114,6 +117,9 @@ pub struct Handle {
|
|||||||
/// The MPRIS server, which is more or less a handle to the actual MPRIS thread.
|
/// The MPRIS server, which is more or less a handle to the actual MPRIS thread.
|
||||||
#[cfg(feature = "mpris")]
|
#[cfg(feature = "mpris")]
|
||||||
pub mpris: mpris::Server,
|
pub mpris: mpris::Server,
|
||||||
|
|
||||||
|
/// Logger which can be used to log important events.
|
||||||
|
logger: Logger,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Handle {
|
impl Handle {
|
||||||
@ -122,6 +128,11 @@ impl Handle {
|
|||||||
self.updater.send(update)?;
|
self.updater.send(update)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a new handle to log events.
|
||||||
|
pub fn logger(&self) -> Logger {
|
||||||
|
self.logger.clone()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The main UI process, which will both render the UI to the terminal
|
/// The main UI process, which will both render the UI to the terminal
|
||||||
@ -134,11 +145,9 @@ impl Handle {
|
|||||||
/// and `params` specifies aesthetic options that are specified by the user.
|
/// and `params` specifies aesthetic options that are specified by the user.
|
||||||
pub async fn run(
|
pub async fn run(
|
||||||
mut updater: broadcast::Receiver<Update>,
|
mut updater: broadcast::Receiver<Update>,
|
||||||
|
mut interface: Interface,
|
||||||
mut state: State,
|
mut state: State,
|
||||||
params: interface::Params,
|
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let mut interface = Interface::new(params)?;
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if let Ok(message) = updater.try_recv() {
|
if let Ok(message) = updater.try_recv() {
|
||||||
match message {
|
match message {
|
||||||
|
|||||||
@ -13,13 +13,17 @@ impl crate::Tasks {
|
|||||||
let mpris = ui::mpris::Server::new(state.clone(), self.tx(), urx.resubscribe()).await?;
|
let mpris = ui::mpris::Server::new(state.clone(), self.tx(), urx.resubscribe()).await?;
|
||||||
|
|
||||||
let params = interface::Params::try_from(args)?;
|
let params = interface::Params::try_from(args)?;
|
||||||
|
let interface = interface::Interface::new(params)?;
|
||||||
|
let logger = interface.logger.clone();
|
||||||
|
|
||||||
if params.enabled {
|
if params.enabled {
|
||||||
self.spawn(ui::run(urx, state, params));
|
self.spawn(ui::run(urx, interface, state));
|
||||||
self.spawn(input::listen(self.tx()));
|
self.spawn(input::listen(self.tx()));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(ui::Handle {
|
Ok(ui::Handle {
|
||||||
updater: utx,
|
updater: utx,
|
||||||
|
logger,
|
||||||
#[cfg(feature = "mpris")]
|
#[cfg(feature = "mpris")]
|
||||||
mpris,
|
mpris,
|
||||||
})
|
})
|
||||||
|
|||||||
@ -13,8 +13,21 @@ pub mod window;
|
|||||||
|
|
||||||
pub use clock::Clock;
|
pub use clock::Clock;
|
||||||
pub use titlebar::TitleBar;
|
pub use titlebar::TitleBar;
|
||||||
|
use tokio::sync::mpsc;
|
||||||
pub use window::Window;
|
pub use window::Window;
|
||||||
|
|
||||||
|
/// Wrapper around [`mpsc::Sender`] which provides a nice API for logging.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Logger(mpsc::Sender<String>);
|
||||||
|
|
||||||
|
impl Logger {
|
||||||
|
/// Send an informational log.
|
||||||
|
pub async fn info(&self, message: String) -> ui::Result<()> {
|
||||||
|
self.0.send(message).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// UI-specific parameters and options.
|
/// UI-specific parameters and options.
|
||||||
#[derive(Copy, Clone, Debug)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
pub struct Params {
|
pub struct Params {
|
||||||
@ -93,6 +106,13 @@ pub struct Interface {
|
|||||||
/// The interface parameters that control smaller
|
/// The interface parameters that control smaller
|
||||||
/// aesthetic features and options.
|
/// aesthetic features and options.
|
||||||
params: Params,
|
params: Params,
|
||||||
|
|
||||||
|
/// Receiver for any truly import live time logs about tracks.
|
||||||
|
pub logs: mpsc::Receiver<String>,
|
||||||
|
|
||||||
|
/// An instance of the logger which can be cloned to give tasks
|
||||||
|
/// access to logging.
|
||||||
|
pub(super) logger: Logger,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Interface {
|
impl Default for Interface {
|
||||||
@ -106,10 +126,13 @@ impl Interface {
|
|||||||
/// Creates a new interface.
|
/// Creates a new interface.
|
||||||
pub fn new(params: Params) -> ui::Result<Self> {
|
pub fn new(params: Params) -> ui::Result<Self> {
|
||||||
let mut window = Window::new(params.width, params.borderless, false, true);
|
let mut window = Window::new(params.width, params.borderless, false, true);
|
||||||
|
let (sender, logs) = mpsc::channel(8);
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
clock: params.clock.then(|| Clock::new(&mut window)),
|
clock: params.clock.then(|| Clock::new(&mut window)),
|
||||||
interval: tokio::time::interval(params.delta),
|
interval: tokio::time::interval(params.delta),
|
||||||
|
logger: Logger(sender),
|
||||||
|
logs,
|
||||||
window,
|
window,
|
||||||
params,
|
params,
|
||||||
})
|
})
|
||||||
@ -148,7 +171,8 @@ impl Interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let menu = self.menu(state);
|
let menu = self.menu(state);
|
||||||
self.window.draw(stdout().lock(), menu)?;
|
self.window
|
||||||
|
.draw(stdout().lock(), self.logs.try_recv().ok(), menu)?;
|
||||||
self.interval.tick().await;
|
self.interval.tick().await;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@ -99,14 +99,22 @@ impl Window {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Actually draws the window, with each element in `content` being on a new line.
|
/// Actually draws the window, with each element in `content` being on a new line.
|
||||||
|
///
|
||||||
|
/// If `log` is [`Some`], then it will also print it after clearing, but before the lowfi window.
|
||||||
pub fn draw(
|
pub fn draw(
|
||||||
&mut self,
|
&mut self,
|
||||||
mut writer: impl std::io::Write,
|
mut writer: impl std::io::Write,
|
||||||
|
log: Option<String>,
|
||||||
content: Vec<String>,
|
content: Vec<String>,
|
||||||
) -> ui::Result<()> {
|
) -> ui::Result<()> {
|
||||||
let (rendered, height) = self.render(content)?;
|
let (rendered, height) = self.render(content)?;
|
||||||
|
crossterm::queue!(writer, Clear(ClearType::FromCursorDown), MoveToColumn(0))?;
|
||||||
|
|
||||||
crossterm::execute!(
|
if let Some(log) = log {
|
||||||
|
crossterm::queue!(writer, Print(log), Print("\n"), MoveToColumn(0))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
crossterm::queue!(
|
||||||
writer,
|
writer,
|
||||||
Clear(ClearType::FromCursorDown),
|
Clear(ClearType::FromCursorDown),
|
||||||
MoveToColumn(0),
|
MoveToColumn(0),
|
||||||
@ -115,6 +123,7 @@ impl Window {
|
|||||||
MoveUp(height - 1),
|
MoveUp(height - 1),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
writer.flush()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user