mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 15:25:46 +00:00
refactor(cli): extract human-format presenters into own submodule
Pull the JSON→stdout formatter layer out of cli/mod.rs into a new
cli/presenters.rs:
- print_library_human, print_server_list_human, print_search_human
- print_info_human + its helpers sorted_kv, value_inline
- print_audio_devices_human (kept as `pub` and re-exported from mod.rs)
Each takes a `serde_json::Value` and writes to stdout. No mutation,
no IO besides println, no inward calls back into mod.rs. cli/mod.rs
imports them via `use presenters::{...}` so the existing call sites
(print_*_cli_stdout wrappers, run_info_and_exit) read unchanged.
cli/mod.rs: 1151 → 895 LOC. Behaviour-preserving — same output
strings, same column separators, same scope handling for search.
This commit is contained in:
+4
-257
@@ -1,9 +1,12 @@
|
||||
//! CLI surface for scripting / compositor bindings (e.g. Hyprland `exec`).
|
||||
|
||||
mod parse;
|
||||
mod presenters;
|
||||
|
||||
pub use parse::*;
|
||||
use parse::{cli_registry_entry_by_command, cli_action_registry_entries};
|
||||
pub use presenters::print_audio_devices_human;
|
||||
use parse::{cli_action_registry_entries, cli_registry_entry_by_command};
|
||||
use presenters::{print_info_human, print_library_human, print_search_human, print_server_list_human};
|
||||
|
||||
// Bundled at compile time for `psysonic completions bash|zsh` (no extra files in packages).
|
||||
const COMPLETIONS_BASH: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../completions/psysonic.bash"));
|
||||
@@ -310,31 +313,6 @@ pub fn print_library_cli_stdout(text: &str, json_out: bool) {
|
||||
}
|
||||
}
|
||||
|
||||
fn print_library_human(v: &Value) {
|
||||
if let Some(sid) = v.get("active_server_id").and_then(|x| x.as_str()) {
|
||||
println!("active_server_id: {sid}");
|
||||
} else {
|
||||
println!("active_server_id: (none)");
|
||||
}
|
||||
match v.get("selected").and_then(|x| x.as_str()) {
|
||||
Some(s) => println!("selected: {s}"),
|
||||
None => println!("selected: (unknown)"),
|
||||
}
|
||||
println!("folders:");
|
||||
if let Some(Value::Array(rows)) = v.get("folders") {
|
||||
if rows.is_empty() {
|
||||
println!(" (none)");
|
||||
return;
|
||||
}
|
||||
for row in rows {
|
||||
let id = row.get("id").and_then(|x| x.as_str()).unwrap_or("?");
|
||||
let name = row.get("name").and_then(|x| x.as_str()).unwrap_or("?");
|
||||
println!(" - {id}\t{name}");
|
||||
}
|
||||
} else {
|
||||
println!(" (invalid JSON: missing folders array)");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_library_cli_response(payload: &Value) -> Result<(), String> {
|
||||
let path = cli_library_response_path();
|
||||
@@ -405,27 +383,6 @@ pub fn print_server_list_cli_stdout(text: &str, json_out: bool) {
|
||||
}
|
||||
}
|
||||
|
||||
fn print_server_list_human(v: &Value) {
|
||||
if let Some(sid) = v.get("active_server_id").and_then(|x| x.as_str()) {
|
||||
println!("active_server_id: {sid}");
|
||||
} else {
|
||||
println!("active_server_id: (none)");
|
||||
}
|
||||
println!("servers:");
|
||||
if let Some(Value::Array(rows)) = v.get("servers") {
|
||||
if rows.is_empty() {
|
||||
println!(" (none)");
|
||||
return;
|
||||
}
|
||||
for row in rows {
|
||||
let id = row.get("id").and_then(|x| x.as_str()).unwrap_or("?");
|
||||
let name = row.get("name").and_then(|x| x.as_str()).unwrap_or("?");
|
||||
println!(" - {id}\t{name}");
|
||||
}
|
||||
} else {
|
||||
println!(" (invalid JSON: missing servers array)");
|
||||
}
|
||||
}
|
||||
|
||||
/// Wait for `psysonic-cli-search.json` after `cli:search`.
|
||||
fn read_search_cli_response_blocking(max_wait: Duration) -> String {
|
||||
@@ -462,71 +419,6 @@ pub fn print_search_cli_stdout(text: &str, json_out: bool) {
|
||||
}
|
||||
}
|
||||
|
||||
fn print_search_human(v: &Value) {
|
||||
if let Some(err) = v.get("error").and_then(|x| x.as_str()) {
|
||||
if !err.is_empty() {
|
||||
println!("error: {err}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
let scope = v.get("scope").and_then(|x| x.as_str()).unwrap_or("?");
|
||||
let query = v.get("query").and_then(|x| x.as_str()).unwrap_or("");
|
||||
println!("scope: {scope}");
|
||||
println!("query: {query}");
|
||||
match scope {
|
||||
"track" => {
|
||||
println!("songs:");
|
||||
if let Some(Value::Array(rows)) = v.get("songs") {
|
||||
if rows.is_empty() {
|
||||
println!(" (none)");
|
||||
return;
|
||||
}
|
||||
for row in rows {
|
||||
let id = row.get("id").and_then(|x| x.as_str()).unwrap_or("?");
|
||||
let title = row.get("title").and_then(|x| x.as_str()).unwrap_or("?");
|
||||
let artist = row.get("artist").and_then(|x| x.as_str()).unwrap_or("?");
|
||||
println!(" - {id}\t{artist} — {title}");
|
||||
}
|
||||
} else {
|
||||
println!(" (missing songs array)");
|
||||
}
|
||||
}
|
||||
"album" => {
|
||||
println!("albums:");
|
||||
if let Some(Value::Array(rows)) = v.get("albums") {
|
||||
if rows.is_empty() {
|
||||
println!(" (none)");
|
||||
return;
|
||||
}
|
||||
for row in rows {
|
||||
let id = row.get("id").and_then(|x| x.as_str()).unwrap_or("?");
|
||||
let name = row.get("name").and_then(|x| x.as_str()).unwrap_or("?");
|
||||
let artist = row.get("artist").and_then(|x| x.as_str()).unwrap_or("?");
|
||||
println!(" - {id}\t{artist} — {name}");
|
||||
}
|
||||
} else {
|
||||
println!(" (missing albums array)");
|
||||
}
|
||||
}
|
||||
"artist" => {
|
||||
println!("artists:");
|
||||
if let Some(Value::Array(rows)) = v.get("artists") {
|
||||
if rows.is_empty() {
|
||||
println!(" (none)");
|
||||
return;
|
||||
}
|
||||
for row in rows {
|
||||
let id = row.get("id").and_then(|x| x.as_str()).unwrap_or("?");
|
||||
let name = row.get("name").and_then(|x| x.as_str()).unwrap_or("?");
|
||||
println!(" - {id}\t{name}");
|
||||
}
|
||||
} else {
|
||||
println!(" (missing artists array)");
|
||||
}
|
||||
}
|
||||
_ => println!("(unknown scope)"),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn tauri_identifier() -> &'static str {
|
||||
@@ -628,130 +520,7 @@ pub fn run_info_and_exit(args: &[String]) -> ! {
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
fn print_info_human(v: &Value) {
|
||||
let o = v.as_object();
|
||||
let o = match o {
|
||||
Some(m) => m,
|
||||
None => {
|
||||
println!("(snapshot is not a JSON object)");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let track = o.get("current_track").and_then(|x| x.as_object());
|
||||
println!("=== current_track ===");
|
||||
match track {
|
||||
None => println!("(none)"),
|
||||
Some(t) if t.is_empty() => println!("(none)"),
|
||||
Some(t) => {
|
||||
for (k, val) in sorted_kv(t) {
|
||||
println!(" {k}: {}", value_inline(val));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("=== current_radio ===");
|
||||
match o.get("current_radio") {
|
||||
None | Some(Value::Null) => println!("(none)"),
|
||||
Some(Value::Object(m)) if m.is_empty() => println!("(none)"),
|
||||
Some(Value::Object(m)) => {
|
||||
for (k, val) in sorted_kv(m) {
|
||||
println!(" {k}: {}", value_inline(val));
|
||||
}
|
||||
}
|
||||
Some(x) => println!(" {}", value_inline(x)),
|
||||
}
|
||||
|
||||
println!("=== music_library ===");
|
||||
match o.get("music_library").and_then(|x| x.as_object()) {
|
||||
None => println!("(none)"),
|
||||
Some(m) if m.is_empty() => println!("(none)"),
|
||||
Some(m) => {
|
||||
if let Some(v) = m.get("selected") {
|
||||
println!(" selected: {}", value_inline(v));
|
||||
}
|
||||
if let Some(v) = m.get("active_server_id") {
|
||||
println!(" active_server_id: {}", value_inline(v));
|
||||
}
|
||||
println!(" folders:");
|
||||
match m.get("folders").and_then(|x| x.as_array()) {
|
||||
None => println!(" (none loaded)"),
|
||||
Some(a) if a.is_empty() => println!(" (none loaded)"),
|
||||
Some(rows) => {
|
||||
for row in rows {
|
||||
let id = row.get("id").and_then(|x| x.as_str()).unwrap_or("?");
|
||||
let name = row.get("name").and_then(|x| x.as_str()).unwrap_or("?");
|
||||
println!(" - {id}\t{name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("=== playback ===");
|
||||
for key in [
|
||||
"is_playing",
|
||||
"current_time",
|
||||
"volume",
|
||||
"queue_index",
|
||||
"queue_length",
|
||||
"repeat_mode",
|
||||
"current_track_user_rating",
|
||||
"current_track_starred",
|
||||
] {
|
||||
if let Some(val) = o.get(key) {
|
||||
println!(" {key}: {}", value_inline(val));
|
||||
}
|
||||
}
|
||||
|
||||
println!("=== servers (saved) ===");
|
||||
match o.get("servers").and_then(|x| x.as_array()) {
|
||||
None => println!("(none)"),
|
||||
Some(rows) if rows.is_empty() => println!("(none)"),
|
||||
Some(rows) => {
|
||||
for row in rows {
|
||||
let id = row.get("id").and_then(|x| x.as_str()).unwrap_or("?");
|
||||
let name = row.get("name").and_then(|x| x.as_str()).unwrap_or("?");
|
||||
println!(" - {id}\t{name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("=== queue ({} items) ===", o.get("queue_length").and_then(|x| x.as_u64()).unwrap_or(0));
|
||||
if let Some(Value::Array(items)) = o.get("queue") {
|
||||
for (i, item) in items.iter().enumerate() {
|
||||
let line = match item {
|
||||
Value::Object(m) => {
|
||||
let title = m.get("title").and_then(|x| x.as_str()).unwrap_or("?");
|
||||
let artist = m.get("artist").and_then(|x| x.as_str()).unwrap_or("?");
|
||||
let id = m.get("id").and_then(|x| x.as_str()).unwrap_or("?");
|
||||
format!("[{i}] {artist} — {title} ({id})")
|
||||
}
|
||||
_ => format!("[{i}] {}", value_inline(item)),
|
||||
};
|
||||
println!("{line}");
|
||||
}
|
||||
} else {
|
||||
println!("(no queue array in snapshot)");
|
||||
}
|
||||
}
|
||||
|
||||
fn sorted_kv(m: &serde_json::Map<String, Value>) -> Vec<(&String, &Value)> {
|
||||
let mut v: Vec<_> = m.iter().collect();
|
||||
v.sort_by(|a, b| a.0.cmp(b.0));
|
||||
v
|
||||
}
|
||||
|
||||
fn value_inline(v: &Value) -> String {
|
||||
match v {
|
||||
Value::String(s) => s.clone(),
|
||||
Value::Number(n) => n.to_string(),
|
||||
Value::Bool(b) => b.to_string(),
|
||||
Value::Null => "(null)".into(),
|
||||
Value::Array(a) => format!("[{} elements]", a.len()),
|
||||
Value::Object(m) => format!("{{{} keys}}", m.len()),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn write_audio_device_cli_response(engine: &crate::audio::AudioEngine) -> Result<(), String> {
|
||||
@@ -774,28 +543,6 @@ pub fn write_audio_device_cli_response(engine: &crate::audio::AudioEngine) -> Re
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn print_audio_devices_human(v: &Value) {
|
||||
if let Some(def) = v.get("default").and_then(|x| x.as_str()) {
|
||||
println!("default_output: {def}");
|
||||
} else {
|
||||
println!("default_output: (unknown)");
|
||||
}
|
||||
if let Some(sel) = v.get("selected").and_then(|x| x.as_str()) {
|
||||
println!("selected: {sel}");
|
||||
} else {
|
||||
println!("selected: (host default)");
|
||||
}
|
||||
println!("devices:");
|
||||
if let Some(Value::Array(devs)) = v.get("devices") {
|
||||
for d in devs {
|
||||
if let Some(s) = d.as_str() {
|
||||
println!(" - {s}");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!(" (none)");
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle `--player` argv on the primary instance. Returns `true` if argv was a CLI action
|
||||
/// (do not raise/focus the main window).
|
||||
|
||||
Reference in New Issue
Block a user