git/grrs/main

build: optimization, formatting configs, dependency trimming

added release profile optimizations. trimmed unused features from tokio and image (drastically reduces number of crates). added the pretty_assertions dev-dependency. added some formatting configs. updated readme a tiny bit
diff --git a/.taplo.toml b/.taplo.toml
new file mode 100644
index 0000000..9325585
--- /dev/null
+++ b/.taplo.toml
@@ -0,0 +1,6 @@
+[[rule]]
+include = ["Cargo.toml"]
+
+[rule.formatting]
+column_width = 120
+inline_table_expand = false
diff --git a/Cargo.lock b/Cargo.lock
index 59e452a..69a3925 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -361,6 +361,12 @@ dependencies = [
"shlex",
]
+[[package]]
+name = "cesu8"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
+
[[package]]
name = "cfg-if"
version = "1.0.4"
@@ -741,6 +747,12 @@ dependencies = [
"syn 2.0.119",
]
+[[package]]
+name = "diff"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
+
[[package]]
name = "digest"
version = "0.10.7"
@@ -1146,9 +1158,9 @@ dependencies = [
[[package]]
name = "h2"
-version = "0.4.18"
+version = "0.4.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "839c0e8a181239723652be9062bb56ca5bf5f64011f73b623f6f4fc59086a228"
+checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16"
dependencies = [
"atomic-waker",
"bytes",
@@ -1550,6 +1562,22 @@ version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+[[package]]
+name = "jni"
+version = "0.21.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
+dependencies = [
+ "cesu8",
+ "cfg-if",
+ "combine",
+ "jni-sys 0.3.1",
+ "log",
+ "thiserror 1.0.69",
+ "walkdir",
+ "windows-sys 0.45.0",
+]
+
[[package]]
name = "jni"
version = "0.22.4"
@@ -1559,7 +1587,7 @@ dependencies = [
"cfg-if",
"combine",
"jni-macros",
- "jni-sys",
+ "jni-sys 0.4.1",
"log",
"simd_cesu8",
"thiserror 2.0.20",
@@ -1580,6 +1608,15 @@ dependencies = [
"syn 2.0.119",
]
+[[package]]
+name = "jni-sys"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258"
+dependencies = [
+ "jni-sys 0.4.1",
+]
+
[[package]]
name = "jni-sys"
version = "0.4.1"
@@ -1854,6 +1891,7 @@ dependencies = [
"crossterm",
"futures-util",
"image",
+ "pretty_assertions",
"ratatui",
"ratatui-image",
"reqwest",
@@ -2419,6 +2457,16 @@ dependencies = [
"zerocopy",
]
+[[package]]
+name = "pretty_assertions"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d"
+dependencies = [
+ "diff",
+ "yansi",
+]
+
[[package]]
name = "proc-macro2"
version = "1.0.107"
@@ -2572,7 +2620,7 @@ dependencies = [
"once_cell",
"socket2",
"tracing",
- "windows-sys 0.61.2",
+ "windows-sys 0.52.0",
]
[[package]]
@@ -2960,9 +3008,9 @@ checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
[[package]]
name = "reqwest"
-version = "0.13.4"
+version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3"
+checksum = "04e9018c9d814e5f30cc16a0f03271aeab3571e609612d9fe78c1aa8d11c2f62"
dependencies = [
"base64",
"bytes",
@@ -3097,13 +3145,13 @@ dependencies = [
[[package]]
name = "rustls-platform-verifier"
-version = "0.7.0"
+version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0"
+checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784"
dependencies = [
"core-foundation 0.10.1",
"core-foundation-sys",
- "jni",
+ "jni 0.21.1",
"log",
"once_cell",
"rustls",
@@ -3113,7 +3161,7 @@ dependencies = [
"security-framework",
"security-framework-sys",
"webpki-root-certs",
- "windows-sys 0.61.2",
+ "windows-sys 0.52.0",
]
[[package]]
@@ -3534,10 +3582,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
- "getrandom 0.4.3",
+ "getrandom 0.3.4",
"once_cell",
"rustix 1.1.4",
- "windows-sys 0.61.2",
+ "windows-sys 0.52.0",
]
[[package]]
@@ -3725,9 +3773,7 @@ dependencies = [
"bytes",
"libc",
"mio",
- "parking_lot",
"pin-project-lite",
- "signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys 0.61.2",
@@ -4241,7 +4287,7 @@ version = "1.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62c35be770821a214dbc362fc26908c853e776c0004294d0b10b8a6bad582f94"
dependencies = [
- "jni",
+ "jni 0.22.4",
"log",
"ndk-context",
"objc2",
@@ -4487,6 +4533,15 @@ dependencies = [
"windows-link",
]
+[[package]]
+name = "windows-sys"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
+dependencies = [
+ "windows-targets 0.42.2",
+]
+
[[package]]
name = "windows-sys"
version = "0.52.0"
@@ -4777,6 +4832,12 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448"
+[[package]]
+name = "yansi"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
+
[[package]]
name = "yoke"
version = "0.8.3"
diff --git a/Cargo.toml b/Cargo.toml
index 0368508..c496d4d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -6,22 +6,31 @@ repository = "https://git.myn.sh/grrs.git"
description = "a minimal tui for gensokyo radio"
license = "Unlicense"
+[profile.release]
+opt-level = "z"
+lto = true
+strip = true
+panic = "abort"
+
[dependencies]
ratatui = "0.30"
+ratatui-image = { version = "11.0", features = ["crossterm"] }
crossterm = "0.29"
-souvlaki = "0.8.3"
-tokio = { version = "1.37", features = ["full"] }
+tokio = { version = "1.37", features = ["rt-multi-thread", "macros", "net", "time", "fs", "io-util"] }
+futures-util = "0.3"
reqwest = { version = "0.13", features = ["json"] }
tokio-tungstenite = { version = "0.30", features = ["native-tls"] }
-futures-util = "0.3"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
-image = "0.25"
-ratatui-image = { version = "11.0", features = ["crossterm"] }
+toml = "1.1.4"
+image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp"] }
+souvlaki = "0.8.3"
arboard = { version = "3.6.1", features = ["wayland-data-control"] }
webbrowser = "1.2.4"
urlencoding = "2.1.3"
-toml = "1.1.4"
+
+[dev-dependencies]
+pretty_assertions = "1.4.1"
[[bin]]
name = "grrs"
diff --git a/README.md b/README.md
index 14acc4d..327814c 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@ A minimal terminal user interface for [Gensokyo Radio](https://gensokyoradio.net
- Streams Gensokyo Radio with album art in your terminal
- Lets you save liked tracks and keeps local copies of their metadata and covers
- Supports media keys (MPRIS) and volume controls
-- Rudimentary theming support via base16 cycling accents
+- Rudimentary theming support via config file
## Dependencies
- `mpv`: Required for the underlying audio streaming and playback.
diff --git a/rustfmt.toml b/rustfmt.toml
new file mode 100644
index 0000000..88ccf2a
--- /dev/null
+++ b/rustfmt.toml
@@ -0,0 +1,13 @@
+edition = "2024"
+max_width = 120
+use_small_heuristics = "Max"
+imports_granularity = "Crate"
+group_imports = "StdExternalCrate"
+format_code_in_doc_comments = true
+wrap_comments = true
+reorder_impl_items = true
+normalize_comments = true
+reorder_modules = true
+match_block_trailing_comma = true
+blank_lines_upper_bound = 1
+newline_style = "Unix"
diff --git a/src/app.rs b/src/app.rs
index c56e5d2..38d1915 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -1,11 +1,3 @@
-use crate::{
- config::AppConfig,
- models::{LikedSong, MpvSong, SongData},
-};
-use crossterm::event::Event as CEvent;
-use ratatui::{style::Color, widgets::ListState};
-use ratatui_image::protocol::StatefulProtocol;
-use souvlaki::MediaControlEvent;
use std::{
collections::HashMap,
env, fs,
@@ -13,6 +5,16 @@ use std::{
time::{Instant, SystemTime, UNIX_EPOCH},
};
+use crossterm::event::Event as CEvent;
+use ratatui::{style::Color, widgets::ListState};
+use ratatui_image::protocol::StatefulProtocol;
+use souvlaki::MediaControlEvent;
+
+use crate::{
+ config::AppConfig,
+ models::{LikedSong, MpvSong, SongData},
+};
+
pub const REFRESH_WINDOW: std::time::Duration = std::time::Duration::from_secs(30);
#[derive(PartialEq)]
@@ -30,12 +32,7 @@ pub enum AppEvent {
ConnectionStatus(String),
LocalArtLoaded(i64, Option<StatefulProtocol>),
MediaControl(MediaControlEvent),
- ApiUpdate {
- listeners: u64,
- circle_link: String,
- rating: String,
- times_rated: u64,
- },
+ ApiUpdate { listeners: u64, circle_link: String, rating: String, times_rated: u64 },
MpvUpdate(MpvSong),
KosuzuBusy(bool),
}
@@ -86,7 +83,7 @@ impl AppState {
Ok(cfg) => {
let w = cfg.get_warnings();
(cfg, w)
- }
+ },
Err(e) => (AppConfig::default(), vec![e]),
};
@@ -130,10 +127,8 @@ impl AppState {
let _ = fs::remove_file(cache_dir.join("theme.txt"));
let likes_file = new_likes;
- let likes = fs::read_to_string(&likes_file)
- .ok()
- .and_then(|d| serde_json::from_str(&d).ok())
- .unwrap_or_default();
+ let likes =
+ fs::read_to_string(&likes_file).ok().and_then(|d| serde_json::from_str(&d).ok()).unwrap_or_default();
let mode = if !warnings.is_empty() {
AppMode::ConfigError(warnings)
@@ -188,10 +183,7 @@ impl AppState {
return;
}
if self.likes.remove(&self.song.id).is_none() {
- let ts = SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .unwrap()
- .as_secs();
+ let ts = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
self.likes.insert(
self.song.id,
LikedSong {
@@ -220,11 +212,7 @@ impl AppState {
}
pub fn art_path(&self, id: i64) -> PathBuf {
- self.likes_file
- .parent()
- .unwrap()
- .join("art")
- .join(format!("{}.jpg", id))
+ self.likes_file.parent().unwrap().join("art").join(format!("{}.jpg", id))
}
pub fn matches_mpv(&self) -> bool {
@@ -247,8 +235,6 @@ impl AppState {
}
pub fn is_refreshing(&self) -> bool {
- self.refresh_until
- .map(|t| Instant::now() < t)
- .unwrap_or(false)
+ self.refresh_until.map(|t| Instant::now() < t).unwrap_or(false)
}
}
diff --git a/src/config.rs b/src/config.rs
index 21c6136..08651f0 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -1,6 +1,7 @@
+use std::{fs, path::PathBuf};
+
use ratatui::style::Color;
use serde::{Deserialize, Serialize};
-use std::{fs, path::PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
@@ -19,10 +20,7 @@ pub struct PlayerConfig {
impl Default for PlayerConfig {
fn default() -> Self {
- Self {
- startup_volume: 100,
- stream_url: "https://stream.gensokyoradio.net/3".to_string(),
- }
+ Self { startup_volume: 100, stream_url: "https://stream.gensokyoradio.net/3".to_string() }
}
}
@@ -62,29 +60,20 @@ pub struct SystemConfig {
impl Default for SystemConfig {
fn default() -> Self {
- Self {
- mpris_enabled: true,
- socket_path: "/tmp/grrs.sock".to_string(),
- data_dir: String::new(),
- }
+ Self { mpris_enabled: true, socket_path: "/tmp/grrs.sock".to_string(), data_dir: String::new() }
}
}
impl Default for AppConfig {
fn default() -> Self {
- Self {
- player: PlayerConfig::default(),
- ui: UiConfig::default(),
- system: SystemConfig::default(),
- }
+ Self { player: PlayerConfig::default(), ui: UiConfig::default(), system: SystemConfig::default() }
}
}
impl AppConfig {
pub fn load_or_create(config_path: &PathBuf) -> Result<Self, String> {
if config_path.exists() {
- let contents = fs::read_to_string(config_path)
- .map_err(|e| format!("Failed to read file: {}", e))?;
+ let contents = fs::read_to_string(config_path).map_err(|e| format!("Failed to read file: {}", e))?;
match toml::from_str::<AppConfig>(&contents) {
Ok(cfg) => return Ok(cfg),
diff --git a/src/input.rs b/src/input.rs
index d24f943..bf84066 100644
--- a/src/input.rs
+++ b/src/input.rs
@@ -1,9 +1,11 @@
-use crate::app::{AppEvent, AppMode, AppState};
+use std::{fs, io::Write, os::unix::net::UnixStream};
+
use crossterm::event::{Event as CEvent, KeyCode, KeyModifiers, MouseButton, MouseEventKind};
use ratatui_image::picker::Picker;
-use std::{fs, io::Write, os::unix::net::UnixStream};
use tokio::sync::mpsc;
+use crate::app::{AppEvent, AppMode, AppState};
+
pub struct InputOutcome {
pub should_quit: bool,
pub should_clear: bool,
@@ -25,32 +27,25 @@ pub async fn handle_input(
CEvent::Key(key) => {
if key.code == KeyCode::Char('q')
|| key.code == KeyCode::Esc
- || (key.code == KeyCode::Char('c')
- && key.modifiers.contains(KeyModifiers::CONTROL))
+ || (key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL))
{
- return InputOutcome {
- should_quit: true,
- should_clear: false,
- };
+ return InputOutcome { should_quit: true, should_clear: false };
} else if key.code == KeyCode::Down || key.code == KeyCode::Char('j') {
state.config_scroll = state.config_scroll.saturating_add(1);
} else if key.code == KeyCode::Up || key.code == KeyCode::Char('k') {
state.config_scroll = state.config_scroll.saturating_sub(1);
}
- }
+ },
CEvent::Mouse(mouse) => {
if mouse.kind == MouseEventKind::ScrollDown {
state.config_scroll = state.config_scroll.saturating_add(1);
} else if mouse.kind == MouseEventKind::ScrollUp {
state.config_scroll = state.config_scroll.saturating_sub(1);
}
- }
- _ => {}
+ },
+ _ => {},
}
- return InputOutcome {
- should_quit: false,
- should_clear: false,
- };
+ return InputOutcome { should_quit: false, should_clear: false };
}
match evt {
@@ -58,10 +53,7 @@ pub async fn handle_input(
if key.code == KeyCode::Char('q')
|| (key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL))
{
- return InputOutcome {
- should_quit: true,
- should_clear: false,
- };
+ return InputOutcome { should_quit: true, should_clear: false };
}
if state.show_unlike_confirm.is_some() {
@@ -84,30 +76,15 @@ pub async fn handle_input(
state.liked_list_state.select(None);
state.selected_art = None;
} else {
- let next = state
- .liked_list_state
- .selected()
- .unwrap_or(0)
- .min(sorted.len().saturating_sub(1));
+ let next = state.liked_list_state.selected().unwrap_or(0).min(sorted.len().saturating_sub(1));
state.liked_list_state.select(Some(next));
- load_local_art(
- sorted[next].id,
- state.art_path(sorted[next].id),
- tx.clone(),
- picker.clone(),
- );
+ load_local_art(sorted[next].id, state.art_path(sorted[next].id), tx.clone(), picker.clone());
}
- } else if key.code == KeyCode::Char('n')
- || key.code == KeyCode::Char('N')
- || key.code == KeyCode::Esc
- {
+ } else if key.code == KeyCode::Char('n') || key.code == KeyCode::Char('N') || key.code == KeyCode::Esc {
state.show_unlike_confirm = None;
clear = true;
}
- return InputOutcome {
- should_quit: false,
- should_clear: clear,
- };
+ return InputOutcome { should_quit: false, should_clear: clear };
}
if state.show_help_overlay {
@@ -118,17 +95,11 @@ pub async fn handle_input(
} else if key.code == KeyCode::Tab {
state.help_tab = (state.help_tab + 1) % 2;
}
- return InputOutcome {
- should_quit: false,
- should_clear: clear,
- };
+ return InputOutcome { should_quit: false, should_clear: clear };
}
if key.code == KeyCode::Char('?') {
state.show_help_overlay = true;
- return InputOutcome {
- should_quit: false,
- should_clear: clear,
- };
+ return InputOutcome { should_quit: false, should_clear: clear };
}
// GLOBAL KEYBINDS
@@ -137,38 +108,23 @@ pub async fn handle_input(
if let Ok(mut stream) = UnixStream::connect(socket_path) {
let _ = stream.write_all(b"{\"command\": [\"cycle\", \"mute\"]}\n");
}
- return InputOutcome {
- should_quit: false,
- should_clear: clear,
- };
+ return InputOutcome { should_quit: false, should_clear: clear };
} else if key.code == KeyCode::Char('-') {
state.volume = state.volume.saturating_sub(5);
state.volume_time = Some(std::time::Instant::now());
if let Ok(mut stream) = UnixStream::connect(socket_path) {
- let cmd = format!(
- "{{\"command\": [\"set_property\", \"volume\", {}]}}\n",
- state.volume
- );
+ let cmd = format!("{{\"command\": [\"set_property\", \"volume\", {}]}}\n", state.volume);
let _ = stream.write_all(cmd.as_bytes());
}
- return InputOutcome {
- should_quit: false,
- should_clear: clear,
- };
+ return InputOutcome { should_quit: false, should_clear: clear };
} else if key.code == KeyCode::Char('=') || key.code == KeyCode::Char('+') {
state.volume = state.volume.saturating_add(5).min(100);
state.volume_time = Some(std::time::Instant::now());
if let Ok(mut stream) = UnixStream::connect(socket_path) {
- let cmd = format!(
- "{{\"command\": [\"set_property\", \"volume\", {}]}}\n",
- state.volume
- );
+ let cmd = format!("{{\"command\": [\"set_property\", \"volume\", {}]}}\n", state.volume);
let _ = stream.write_all(cmd.as_bytes());
}
- return InputOutcome {
- should_quit: false,
- should_clear: clear,
- };
+ return InputOutcome { should_quit: false, should_clear: clear };
}
// MODE SPECIFIC KEYBINDS
@@ -178,20 +134,12 @@ pub async fn handle_input(
let sorted = state.get_sorted_likes();
if !sorted.is_empty() {
state.liked_list_state.select(Some(0));
- load_local_art(
- sorted[0].id,
- state.art_path(sorted[0].id),
- tx.clone(),
- picker.clone(),
- );
+ load_local_art(sorted[0].id, state.art_path(sorted[0].id), tx.clone(), picker.clone());
}
} else if key.code == KeyCode::Char('o') || key.code == KeyCode::Enter {
if !state.song.album.is_empty() {
let query = urlencoding::encode(&state.song.album);
- let url = format!(
- "https://touhoudb.com/Search?searchType=Album&filter={}",
- query
- );
+ let url = format!("https://touhoudb.com/Search?searchType=Album&filter={}", query);
if webbrowser::open(&url).is_ok() {
state.status_message = "opened touhoudb in browser!".to_string();
} else {
@@ -202,10 +150,7 @@ pub async fn handle_input(
} else if key.code == KeyCode::Char('l') {
let was_liked = state.likes.contains_key(&state.song.id);
state.toggle_like();
- if state.likes.contains_key(&state.song.id)
- && !was_liked
- && !state.song.art_url.is_empty()
- {
+ if state.likes.contains_key(&state.song.id) && !was_liked && !state.song.art_url.is_empty() {
let art_dir = state.likes_file.parent().unwrap().join("art");
fs::create_dir_all(&art_dir).ok();
let url = if state.song.art_url.starts_with("http") {
@@ -259,11 +204,7 @@ pub async fn handle_input(
let sorted = state.get_sorted_likes();
if !sorted.is_empty() {
let current = state.liked_list_state.selected().unwrap_or(0);
- let next = if current == 0 {
- sorted.len() - 1
- } else {
- current - 1
- };
+ let next = if current == 0 { sorted.len() - 1 } else { current - 1 };
state.liked_list_state.select(Some(next));
if current != next {
load_local_art(
@@ -278,11 +219,7 @@ pub async fn handle_input(
let sorted = state.get_sorted_likes();
if !sorted.is_empty() {
let current = state.liked_list_state.selected().unwrap_or(0);
- let next = if current == sorted.len() - 1 {
- 0
- } else {
- current + 1
- };
+ let next = if current == sorted.len() - 1 { 0 } else { current + 1 };
state.liked_list_state.select(Some(next));
if current != next {
load_local_art(
@@ -328,10 +265,7 @@ pub async fn handle_input(
let sorted_likes = state.get_sorted_likes();
if let Some(song) = sorted_likes.get(idx) {
let query = urlencoding::encode(&song.album);
- let url = format!(
- "https://touhoudb.com/Search?searchType=Album&filter={}",
- query
- );
+ let url = format!("https://touhoudb.com/Search?searchType=Album&filter={}", query);
if webbrowser::open(&url).is_ok() {
state.status_message = "opened touhoudb in browser!".to_string();
} else {
@@ -342,14 +276,11 @@ pub async fn handle_input(
}
}
}
- }
+ },
CEvent::Mouse(mouse) => {
if mouse.kind == MouseEventKind::Down(MouseButton::Left) {
if let Ok(size) = crossterm::terminal::size() {
- if mouse.row == size.1.saturating_sub(3)
- && mouse.column >= 2
- && mouse.column <= 10
- {
+ if mouse.row == size.1.saturating_sub(3) && mouse.column >= 2 && mouse.column <= 10 {
state.show_help_overlay = !state.show_help_overlay;
if !state.show_help_overlay {
clear = true;
@@ -357,14 +288,11 @@ pub async fn handle_input(
}
}
}
- }
- _ => {}
+ },
+ _ => {},
}
- InputOutcome {
- should_quit: false,
- should_clear: clear,
- }
+ InputOutcome { should_quit: false, should_clear: clear }
}
fn load_local_art(id: i64, path: std::path::PathBuf, tx: mpsc::Sender<AppEvent>, picker: Picker) {
diff --git a/src/main.rs b/src/main.rs
index 6096275..ab6da14 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -4,12 +4,14 @@ mod input;
mod models;
mod ui;
-use crate::{
- app::{AppEvent, AppMode, AppState, REFRESH_WINDOW},
- input::handle_input,
- models::{ApiInfo, SongData, WsPayload, parse_mpv_media_title},
- ui::draw_ui,
+use std::{
+ fs,
+ io::{Write, stdout},
+ os::unix::net::UnixStream,
+ process::{Command, Stdio},
+ time::{Duration, Instant},
};
+
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event as CEvent},
execute,
@@ -19,23 +21,23 @@ use futures_util::{SinkExt, StreamExt};
use ratatui::{Terminal, backend::CrosstermBackend};
use ratatui_image::picker::Picker;
use souvlaki::{MediaControlEvent, MediaControls, MediaMetadata, MediaPlayback, PlatformConfig};
-use std::{
- fs,
- io::{Write, stdout},
- os::unix::net::UnixStream,
- process::{Command, Stdio},
- time::{Duration, Instant},
+use tokio::{
+ io::{AsyncBufReadExt, AsyncWriteExt},
+ sync::{mpsc, watch},
};
-use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
-use tokio::sync::{mpsc, watch};
use tokio_tungstenite::{connect_async, tungstenite::protocol::Message};
+use crate::{
+ app::{AppEvent, AppMode, AppState, REFRESH_WINDOW},
+ input::handle_input,
+ models::{ApiInfo, SongData, WsPayload, parse_mpv_media_title},
+ ui::draw_ui,
+};
+
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
if Command::new("mpv").arg("--version").output().is_err() {
- eprintln!(
- "error: missing required dependency 'mpv'. please install it using your system package manager."
- );
+ eprintln!("error: missing required dependency 'mpv'. please install it using your system package manager.");
std::process::exit(1);
}
@@ -78,12 +80,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut controls = None;
if !is_config_error && state.config.system.mpris_enabled {
- controls = MediaControls::new(PlatformConfig {
- dbus_name: "grrs",
- display_name: "Gensokyo Radio TUI",
- hwnd: None,
- })
- .ok();
+ controls =
+ MediaControls::new(PlatformConfig { dbus_name: "grrs", display_name: "Gensokyo Radio TUI", hwnd: None })
+ .ok();
}
if let Some(ref mut c) = controls {
@@ -92,16 +91,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let _ = tx_media.blocking_send(AppEvent::MediaControl(event));
})
.ok();
- c.set_playback(MediaPlayback::Playing { progress: None })
- .ok();
+ c.set_playback(MediaPlayback::Playing { progress: None }).ok();
}
let picker = Picker::from_query_stdio().expect("failed to query terminal for image support");
let loop_picker = picker.clone();
- let http_client = reqwest::Client::builder()
- .timeout(Duration::from_secs(5))
- .build()
- .unwrap();
+ let http_client = reqwest::Client::builder().timeout(Duration::from_secs(5)).build().unwrap();
let tx_input = tx.clone();
std::thread::spawn(move || {
@@ -121,12 +116,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
}
});
- let (art_tx, mut art_rx) = watch::channel::<(i64, String, String, String)>((
- 0,
- String::new(),
- String::new(),
- String::new(),
- ));
+ let (art_tx, mut art_rx) =
+ watch::channel::<(i64, String, String, String)>((0, String::new(), String::new(), String::new()));
let (ws_nudge_tx, mut ws_nudge_rx) = watch::channel(());
// Hard stop: Only spawn tokio tasks if the config is valid
@@ -135,11 +126,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let api_client = http_client.clone();
tokio::spawn(async move {
loop {
- if let Ok(resp) = api_client
- .get("https://gensokyoradio.net/api/station/playing/")
- .send()
- .await
- {
+ if let Ok(resp) = api_client.get("https://gensokyoradio.net/api/station/playing/").send().await {
if let Ok(text) = resp.text().await {
if let Ok(info) = serde_json::from_str::<ApiInfo>(&text) {
let _ = tx_api
@@ -163,18 +150,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
loop {
match connect_async("wss://gensokyoradio.net/wss").await {
Ok((mut ws_stream, _)) => {
- if tx_ws
- .send(AppEvent::ConnectionStatus("".to_string()))
- .await
- .is_err()
- {
+ if tx_ws.send(AppEvent::ConnectionStatus("".to_string())).await.is_err() {
return;
}
let _ = ws_stream
.send(Message::Text(
- serde_json::json!({"message": "grInitialConnection"})
- .to_string()
- .into(),
+ serde_json::json!({"message": "grInitialConnection"}).to_string().into(),
))
.await;
@@ -221,19 +202,17 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
}
}
}
- }
+ },
Err(_) => {
if tx_ws
- .send(AppEvent::ConnectionStatus(
- "websocket dropped. reconnecting...".to_string(),
- ))
+ .send(AppEvent::ConnectionStatus("websocket dropped. reconnecting...".to_string()))
.await
.is_err()
{
return;
}
tokio::time::sleep(Duration::from_secs(5)).await;
- }
+ },
}
}
});
@@ -274,9 +253,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.await;
if let Ok(Some((protocol, original_bytes))) = decode_result {
- let _ = tx_art_local
- .send(AppEvent::ArtUpdate(protocol, original_bytes))
- .await;
+ let _ = tx_art_local.send(AppEvent::ArtUpdate(protocol, original_bytes)).await;
ok = true;
break;
}
@@ -315,15 +292,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
}
if let Ok(value) = serde_json::from_str::<serde_json::Value>(&line) {
- if value.get("event").and_then(|v| v.as_str())
- == Some("property-change")
- {
- if let Some(title) = value.get("data").and_then(|v| v.as_str())
- {
+ if value.get("event").and_then(|v| v.as_str()) == Some("property-change") {
+ if let Some(title) = value.get("data").and_then(|v| v.as_str()) {
if let Some(mpv_song) = parse_mpv_media_title(title) {
- let _ = tx_patchouli
- .send(AppEvent::MpvUpdate(mpv_song))
- .await;
+ let _ = tx_patchouli.send(AppEvent::MpvUpdate(mpv_song)).await;
}
}
}
@@ -346,16 +318,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
if let Some(event) = rx.recv().await {
match event {
AppEvent::Input(evt) => {
- let outcome = handle_input(
- evt,
- &mut state,
- &tx,
- &picker,
- &loop_picker,
- &socket_path,
- &http_client,
- )
- .await;
+ let outcome =
+ handle_input(evt, &mut state, &tx, &picker, &loop_picker, &socket_path, &http_client).await;
if outcome.should_quit {
break;
}
@@ -365,14 +329,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let _ = terminal.draw(|f| draw_ui(f, &mut state, hide_art));
hide_art = false;
}
- }
+ },
AppEvent::MediaControl(m_event) => {
- if matches!(
- m_event,
- MediaControlEvent::Toggle
- | MediaControlEvent::Play
- | MediaControlEvent::Pause
- ) {
+ if matches!(m_event, MediaControlEvent::Toggle | MediaControlEvent::Play | MediaControlEvent::Pause)
+ {
state.is_muted = !state.is_muted;
if let Ok(mut stream) = UnixStream::connect(&socket_path) {
let _ = stream.write_all(b"{\"command\": [\"cycle\", \"mute\"]}\n");
@@ -380,14 +340,13 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
} else if matches!(m_event, MediaControlEvent::Quit) {
break;
}
- }
+ },
AppEvent::SongUpdate(song) => {
let cached_listeners = state.song.listeners;
if song.id != state.song.id {
state.art_protocol = None;
- state.song_start_time = Instant::now()
- .checked_sub(Duration::from_secs(song.played))
- .unwrap_or_else(Instant::now);
+ state.song_start_time =
+ Instant::now().checked_sub(Duration::from_secs(song.played)).unwrap_or_else(Instant::now);
state.song.rating = String::new();
state.song.times_rated = 0;
state.song.circle_link = String::new();
@@ -395,10 +354,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let tx_api_quick = tx.clone();
let quick_client = http_client.clone();
tokio::spawn(async move {
- if let Ok(resp) = quick_client
- .get("https://gensokyoradio.net/api/station/playing/")
- .send()
- .await
+ if let Ok(resp) =
+ quick_client.get("https://gensokyoradio.net/api/station/playing/").send().await
{
if let Ok(text) = resp.text().await {
if let Ok(info) = serde_json::from_str::<ApiInfo>(&text) {
@@ -434,18 +391,13 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
})
.ok();
}
- }
- AppEvent::ApiUpdate {
- listeners,
- circle_link,
- rating,
- times_rated,
- } => {
+ },
+ AppEvent::ApiUpdate { listeners, circle_link, rating, times_rated } => {
state.song.listeners = listeners;
state.song.circle_link = circle_link;
state.song.rating = rating;
state.song.times_rated = times_rated;
- }
+ },
AppEvent::ArtUpdate(protocol, bytes) => {
state.live_art_bytes = Some(bytes.clone());
state.art_protocol = Some(protocol);
@@ -453,7 +405,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
tokio::spawn(async move {
let _ = tokio::fs::write(path, bytes).await;
});
- }
+ },
AppEvent::LocalArtLoaded(id, protocol) => {
if let Some(idx) = state.liked_list_state.selected() {
let sorted_likes = state.get_sorted_likes();
@@ -463,11 +415,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
}
}
}
- }
+ },
AppEvent::ConnectionStatus(msg) => state.status_message = msg,
AppEvent::MpvUpdate(mpv) => {
- let changed =
- state.mpv_song.title != mpv.title || state.mpv_song.artist != mpv.artist;
+ let changed = state.mpv_song.title != mpv.title || state.mpv_song.artist != mpv.artist;
state.mpv_song = mpv;
let elapsed = state.song_start_time.elapsed().as_secs();
let expired = state.song.duration > 0 && elapsed >= state.song.duration;
@@ -479,14 +430,14 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
last_nudge = Instant::now();
}
}
- }
+ },
AppEvent::KosuzuBusy(busy) => {
if busy {
state.refresh_until = Some(Instant::now() + REFRESH_WINDOW);
} else {
state.refresh_until = None;
}
- }
+ },
AppEvent::Tick => {
if let Some(t) = state.status_message_time {
if t.elapsed().as_secs() >= 3 {
@@ -499,17 +450,13 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
state.volume_time = None;
}
}
- }
+ },
}
}
}
disable_raw_mode()?;
- execute!(
- terminal.backend_mut(),
- LeaveAlternateScreen,
- DisableMouseCapture
- )?;
+ execute!(terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture)?;
if let Some(mut child) = mpv_process {
let _ = child.kill();
diff --git a/src/models.rs b/src/models.rs
index 3d5933a..b3565f7 100644
--- a/src/models.rs
+++ b/src/models.rs
@@ -66,10 +66,7 @@ pub struct MpvSong {
pub fn parse_mpv_media_title(title: &str) -> Option<MpvSong> {
let trimmed = title.trim();
if let Some(idx) = trimmed.find(" - ") {
- Some(MpvSong {
- artist: trimmed[..idx].trim().to_string(),
- title: trimmed[idx + 3..].trim().to_string(),
- })
+ Some(MpvSong { artist: trimmed[..idx].trim().to_string(), title: trimmed[idx + 3..].trim().to_string() })
} else {
None
}
diff --git a/src/ui.rs b/src/ui.rs
index 09359bd..13fdb36 100644
--- a/src/ui.rs
+++ b/src/ui.rs
@@ -1,4 +1,5 @@
-use crate::app::{AppMode, AppState};
+use std::rc::Rc;
+
use ratatui::{
Frame,
layout::{Alignment, Constraint, Direction, Layout, Rect},
@@ -7,7 +8,8 @@ use ratatui::{
widgets::{Block, Borders, Clear, Gauge, List, ListItem, Paragraph, Row, Table, Tabs, Wrap},
};
use ratatui_image::StatefulImage;
-use std::rc::Rc;
+
+use crate::app::{AppMode, AppState};
fn centered_rect(width: u16, height: u16, r: Rect) -> Rect {
let popup_layout = Layout::default()
@@ -30,22 +32,13 @@ fn centered_rect(width: u16, height: u16, r: Rect) -> Rect {
}
fn build_status_bar<'a>(state: &'a AppState) -> Line<'a> {
- let mut status_spans = vec![Span::styled(
- "[?] Help",
- Style::default().fg(state.secondary_color),
- )];
+ let mut status_spans = vec![Span::styled("[?] Help", Style::default().fg(state.secondary_color))];
if state.song.id != 0 && state.likes.contains_key(&state.song.id) {
- status_spans.push(Span::styled(
- " [Liked]",
- Style::default().fg(state.theme_color),
- ));
+ status_spans.push(Span::styled(" [Liked]", Style::default().fg(state.theme_color)));
}
if state.is_muted || state.volume == 0 {
- status_spans.push(Span::styled(
- " [Muted]",
- Style::default().fg(state.error_color),
- ));
+ status_spans.push(Span::styled(" [Muted]", Style::default().fg(state.error_color)));
}
if !state.status_message.is_empty() {
@@ -57,10 +50,7 @@ fn build_status_bar<'a>(state: &'a AppState) -> Line<'a> {
} else {
state.theme_color
};
- status_spans.push(Span::styled(
- format!(" [{}]", state.status_message),
- Style::default().fg(msg_color),
- ));
+ status_spans.push(Span::styled(format!(" [{}]", state.status_message), Style::default().fg(msg_color)));
} else if !(state.is_muted || state.volume == 0) && state.volume_time.is_some() {
let vol_color = if state.volume >= 66 {
state.theme_color
@@ -69,10 +59,7 @@ fn build_status_bar<'a>(state: &'a AppState) -> Line<'a> {
} else {
state.error_color
};
- status_spans.push(Span::styled(
- format!(" [Vol: {}%]", state.volume),
- Style::default().fg(vol_color),
- ));
+ status_spans.push(Span::styled(format!(" [Vol: {}%]", state.volume), Style::default().fg(vol_color)));
}
Line::from(status_spans)
@@ -85,10 +72,7 @@ pub fn draw_ui(f: &mut Frame, state: &mut AppState, hide_art: bool) {
let mut lines = vec![
Line::from(""),
- Line::from(vec![Span::styled(
- " Invalid Configuration Detected ",
- Style::default().fg(Color::Red).bold(),
- )]),
+ Line::from(vec![Span::styled(" Invalid Configuration Detected ", Style::default().fg(Color::Red).bold())]),
Line::from(""),
Line::from(vec![Span::styled(
" The following issues were found in config.toml:",
@@ -102,10 +86,7 @@ pub fn draw_ui(f: &mut Frame, state: &mut AppState, hide_art: bool) {
if w.contains("valid color") {
has_color_error = true;
}
- lines.push(Line::from(vec![Span::styled(
- format!(" - {}", w),
- Style::default().fg(Color::Yellow),
- )]));
+ lines.push(Line::from(vec![Span::styled(format!(" - {}", w), Style::default().fg(Color::Yellow))]));
}
if has_color_error {
@@ -125,20 +106,14 @@ pub fn draw_ui(f: &mut Frame, state: &mut AppState, hide_art: bool) {
}
lines.push(Line::from(""));
- lines.push(Line::from(vec![Span::styled(
- " Press 'q' or 'Esc' to quit.",
- Style::default().fg(Color::DarkGray),
- )]));
+ lines
+ .push(Line::from(vec![Span::styled(" Press 'q' or 'Esc' to quit.", Style::default().fg(Color::DarkGray))]));
f.render_widget(
Paragraph::new(lines)
.wrap(Wrap { trim: true })
.scroll((state.config_scroll, 0))
- .block(
- Block::default()
- .borders(Borders::ALL)
- .style(Style::default().fg(Color::Red)),
- )
+ .block(Block::default().borders(Borders::ALL).style(Style::default().fg(Color::Red)))
.alignment(Alignment::Center),
area,
);
@@ -153,28 +128,17 @@ pub fn draw_ui(f: &mut Frame, state: &mut AppState, hide_art: bool) {
let dynamic_width = (main_chunks[0].height * 2).clamp(10, 45);
let content_chunks = Layout::default()
.direction(Direction::Horizontal)
- .constraints([
- Constraint::Length(dynamic_width),
- Constraint::Length(2),
- Constraint::Min(20),
- ])
+ .constraints([Constraint::Length(dynamic_width), Constraint::Length(2), Constraint::Min(20)])
.split(main_chunks[0]);
match state.mode {
AppMode::Playing => {
- draw_player(
- f,
- state,
- content_chunks.clone(),
- main_chunks[1],
- main_chunks[0].height,
- hide_art,
- );
- }
+ draw_player(f, state, content_chunks.clone(), main_chunks[1], main_chunks[0].height, hide_art);
+ },
AppMode::LikedSongs => {
draw_library(f, state, content_chunks, main_chunks[1], hide_art);
- }
- _ => {}
+ },
+ _ => {},
}
if state.show_help_overlay {
@@ -204,39 +168,21 @@ fn draw_player(
} else if let Some(ref mut protocol) = state.art_protocol {
f.render_stateful_widget(StatefulImage::new(), left_col[0], protocol);
} else {
- f.render_widget(
- Paragraph::new("Loading Art...").alignment(Alignment::Center),
- left_col[0],
- );
+ f.render_widget(Paragraph::new("Loading Art...").alignment(Alignment::Center), left_col[0]);
}
let basic_text = vec![
- Line::from(vec![Span::styled(
- &state.song.title,
- Style::default().fg(state.theme_color).bold(),
- )]),
- Line::from(vec![
- Span::styled("Artist: ", Style::default().bold()),
- Span::raw(&state.song.artist),
- ]),
- Line::from(vec![
- Span::styled("Album: ", Style::default().bold()),
- Span::raw(&state.song.album),
- ]),
+ Line::from(vec![Span::styled(&state.song.title, Style::default().fg(state.theme_color).bold())]),
+ Line::from(vec![Span::styled("Artist: ", Style::default().bold()), Span::raw(&state.song.artist)]),
+ Line::from(vec![Span::styled("Album: ", Style::default().bold()), Span::raw(&state.song.album)]),
];
let mut ext_text = vec![];
if available_height >= 16 {
if !state.song.circle.is_empty() && state.song.circle != "Unknown" {
let mut spans = vec![
- Span::styled(
- "Circle: ",
- Style::default().bold().fg(state.secondary_color),
- ),
- Span::styled(
- &state.song.circle,
- Style::default().fg(state.secondary_color),
- ),
+ Span::styled("Circle: ", Style::default().bold().fg(state.secondary_color)),
+ Span::styled(&state.song.circle, Style::default().fg(state.secondary_color)),
];
if !state.song.circle_link.is_empty() {
spans.push(Span::styled(
@@ -246,29 +192,16 @@ fn draw_player(
}
ext_text.push(Line::from(spans));
}
- if !state.song.vocal.is_empty() && state.song.vocal != "Unknown" && state.song.vocal != "-"
- {
+ if !state.song.vocal.is_empty() && state.song.vocal != "Unknown" && state.song.vocal != "-" {
ext_text.push(Line::from(vec![
Span::styled("Vocal: ", Style::default().bold().fg(state.secondary_color)),
- Span::styled(
- &state.song.vocal,
- Style::default().fg(state.secondary_color),
- ),
+ Span::styled(&state.song.vocal, Style::default().fg(state.secondary_color)),
]));
}
- if !state.song.original.is_empty()
- && state.song.original != "Unknown"
- && state.song.original != "-"
- {
+ if !state.song.original.is_empty() && state.song.original != "Unknown" && state.song.original != "-" {
ext_text.push(Line::from(vec![
- Span::styled(
- "Original: ",
- Style::default().bold().fg(state.secondary_color),
- ),
- Span::styled(
- &state.song.original,
- Style::default().fg(state.secondary_color),
- ),
+ Span::styled("Original: ", Style::default().bold().fg(state.secondary_color)),
+ Span::styled(&state.song.original, Style::default().fg(state.secondary_color)),
]));
}
if !state.song.year.is_empty() && state.song.year != "Unknown" {
@@ -279,10 +212,7 @@ fn draw_player(
}
if !state.song.rating.is_empty() && state.song.rating != "Unknown" {
ext_text.push(Line::from(vec![
- Span::styled(
- "Rating: ",
- Style::default().bold().fg(state.secondary_color),
- ),
+ Span::styled("Rating: ", Style::default().bold().fg(state.secondary_color)),
Span::styled(
format!("{} ({})", state.song.rating, state.song.times_rated),
Style::default().fg(state.secondary_color),
@@ -312,27 +242,14 @@ fn draw_player(
let (ratio, label) = if total > 0 {
(
(elapsed as f64 / total as f64).clamp(0.0, 1.0),
- format!(
- "{:02}:{:02} / {:02}:{:02}",
- elapsed / 60,
- elapsed % 60,
- total / 60,
- total % 60
- ),
+ format!("{:02}:{:02} / {:02}:{:02}", elapsed / 60, elapsed % 60, total / 60, total % 60),
)
} else {
- (
- 1.0,
- format!("{:02}:{:02} / LIVE", elapsed / 60, elapsed % 60),
- )
+ (1.0, format!("{:02}:{:02} / LIVE", elapsed / 60, elapsed % 60))
};
f.render_widget(
Gauge::default()
- .gauge_style(
- Style::default()
- .fg(state.theme_color)
- .bg(state.secondary_color),
- )
+ .gauge_style(Style::default().fg(state.theme_color).bg(state.secondary_color))
.ratio(ratio)
.label(label),
right_chunks[2],
@@ -343,10 +260,7 @@ fn draw_player(
if refresh_height > 0 {
f.render_widget(
- Paragraph::new(Line::from(vec![Span::styled(
- "少女祈祷中…",
- Style::default().fg(state.theme_color),
- )])),
+ Paragraph::new(Line::from(vec![Span::styled("少女祈祷中…", Style::default().fg(state.theme_color))])),
right_chunks[6],
);
}
@@ -356,17 +270,10 @@ fn draw_player(
.constraints([Constraint::Min(40), Constraint::Length(25)])
.split(bottom);
- f.render_widget(
- Paragraph::new(build_status_bar(state)).alignment(Alignment::Left),
- bottom_layout[0],
- );
+ f.render_widget(Paragraph::new(build_status_bar(state)).alignment(Alignment::Left), bottom_layout[0]);
f.render_widget(
Paragraph::new(Line::from(vec![Span::styled(
- if state.song.listeners > 0 {
- format!("{} Listeners ", state.song.listeners)
- } else {
- "LIVE ".to_string()
- },
+ if state.song.listeners > 0 { format!("{} Listeners ", state.song.listeners) } else { "LIVE ".to_string() },
Style::default().fg(state.theme_color).bold(),
)]))
.alignment(Alignment::Right),
@@ -374,13 +281,7 @@ fn draw_player(
);
}
-fn draw_library(
- f: &mut Frame,
- state: &mut AppState,
- content: Rc<[Rect]>,
- bottom: Rect,
- hide_art: bool,
-) {
+fn draw_library(f: &mut Frame, state: &mut AppState, content: Rc<[Rect]>, bottom: Rect, hide_art: bool) {
let image_height = content[0].width / 2;
let left_col = Layout::default()
.direction(Direction::Vertical)
@@ -392,10 +293,7 @@ fn draw_library(
} else if let Some(ref mut protocol) = state.selected_art {
f.render_stateful_widget(StatefulImage::new(), left_col[0], protocol);
} else {
- f.render_widget(
- Paragraph::new("No Art Saved").alignment(Alignment::Center),
- left_col[0],
- );
+ f.render_widget(Paragraph::new("No Art Saved").alignment(Alignment::Center), left_col[0]);
}
let sorted = state.get_sorted_likes();
@@ -410,14 +308,8 @@ fn draw_library(
)]))
} else {
ListItem::new(Line::from(vec![
- Span::styled(
- format!("{} ", s.title),
- Style::default().fg(state.theme_color).bold(),
- ),
- Span::styled(
- format!("- {}", s.artist),
- Style::default().fg(state.secondary_color),
- ),
+ Span::styled(format!("{} ", s.title), Style::default().fg(state.theme_color).bold()),
+ Span::styled(format!("- {}", s.artist), Style::default().fg(state.secondary_color)),
]))
}
})
@@ -430,16 +322,11 @@ fn draw_library(
)])));
}
f.render_stateful_widget(
- List::new(items)
- .highlight_style(Style::default().bg(state.highlight_bg_color))
- .highlight_symbol("> "),
+ List::new(items).highlight_style(Style::default().bg(state.highlight_bg_color)).highlight_symbol("> "),
content[2],
&mut state.liked_list_state,
);
- f.render_widget(
- Paragraph::new(build_status_bar(state)).alignment(Alignment::Left),
- bottom,
- );
+ f.render_widget(Paragraph::new(build_status_bar(state)).alignment(Alignment::Left), bottom);
}
fn draw_help_popup(f: &mut Frame, state: &AppState) {
@@ -462,18 +349,10 @@ fn draw_help_popup(f: &mut Frame, state: &AppState) {
.margin(1)
.split(inner_area);
- let tab_titles: Vec<_> = vec![" General ", " Library "]
- .into_iter()
- .map(Line::from)
- .collect();
+ let tab_titles: Vec<_> = vec![" General ", " Library "].into_iter().map(Line::from).collect();
let tabs = Tabs::new(tab_titles)
.select(state.help_tab)
- .highlight_style(
- Style::default()
- .fg(Color::Black)
- .bg(state.theme_color)
- .bold(),
- )
+ .highlight_style(Style::default().fg(Color::Black).bg(state.theme_color).bold())
.divider("│");
f.render_widget(tabs, chunks[0]);
@@ -542,12 +421,7 @@ fn draw_confirm_popup(f: &mut Frame, state: &AppState) {
Span::raw("No"),
]),
])
- .block(
- Block::default()
- .title(" Confirm ")
- .borders(Borders::ALL)
- .style(Style::default().fg(state.error_color)),
- )
+ .block(Block::default().title(" Confirm ").borders(Borders::ALL).style(Style::default().fg(state.error_color)))
.alignment(Alignment::Center),
area,
);