crabidy/cbd-tui/src/main.rs

131 lines
4.7 KiB
Rust

//! The standalone TUI binary: a clap-derive CLI ([`cbd_cli::TuiCli`]). With
//! no subcommand it loads the client config (writing defaults on first run),
//! applies the `--address/--user/--password/--spectrum` overrides, and runs
//! the TUI — exactly as before. Subcommands cover `auth` (write credentials
//! into the config), remote control (`library`/`queue`/`global`), and shell
//! completions. All client logic lives in the library so the bundled `cbd`
//! binary can host it too (architecture/cbd-bundle.md D1).
use std::sync::OnceLock;
use cbd_cli::{RemoteCmd, TuiCli, TuiCommand};
use cbd_tui::config::{self, Config};
use clap::{CommandFactory, Parser};
/// The config file name for the standalone TUI.
const CONFIG_FILE: &str = "cbd-tui.toml";
static CONFIG: OnceLock<Config> = OnceLock::new();
/// Logs to a file: the terminal is owned by the TUI, so writing log lines to
/// stdout/stderr would corrupt the interface.
fn init_tracing() -> Option<tracing_appender::non_blocking::WorkerGuard> {
use tracing_subscriber::{prelude::*, EnvFilter};
let log_dir = dirs::state_dir()
.or_else(dirs::cache_dir)
.unwrap_or_else(std::env::temp_dir)
.join("crabidy");
if let Err(err) = std::fs::create_dir_all(&log_dir) {
eprintln!(
"could not create log directory {}: {err}",
log_dir.display()
);
return None;
}
let file_appender = tracing_appender::rolling::daily(&log_dir, "cbd-tui.log");
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
let env_filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("info,cbd_tui=debug,crabidy_core=debug"));
tracing_subscriber::registry()
.with(env_filter)
.with(
tracing_subscriber::fmt::layer()
.with_writer(non_blocking)
.with_ansi(false)
.with_target(true),
)
.init();
Some(guard)
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let cli = TuiCli::parse();
match cli.command {
// No subcommand: load config, apply overrides, run the TUI.
None => {
let _log_guard = init_tracing();
let mut config = config::load_first_run(CONFIG_FILE);
config::apply_overrides(
&mut config,
cli.remote.address,
cli.remote.user,
cli.remote.password,
cli.spectrum,
);
let config = CONFIG.get_or_init(|| config);
cbd_tui::run(config).await
}
// Subcommands are one-shot CLI actions; a failure prints a short
// message and exits non-zero.
Some(command) => {
if let Err(err) = run_command(&cli.remote, command).await {
eprintln!("error: {err}");
std::process::exit(1);
}
Ok(())
}
}
}
/// Dispatches a TUI subcommand.
async fn run_command(
remote: &cbd_cli::RemoteArgs,
command: TuiCommand,
) -> Result<(), Box<dyn std::error::Error>> {
match command {
TuiCommand::Auth(args) => {
let password = args
.password
.ok_or("missing password (pass it as an argument)")?;
let path = config::write_auth(
CONFIG_FILE,
args.role.user_name(),
&password,
args.address.as_deref(),
)?;
println!(
"wrote credentials for {} to {}",
args.role.user_name(),
path.display()
);
Ok(())
}
TuiCommand::Library(cmd) => {
cbd_cli::run_remote(&connection(remote), RemoteCmd::Library(cmd)).await
}
TuiCommand::Queue(cmd) => {
cbd_cli::run_remote(&connection(remote), RemoteCmd::Queue(cmd)).await
}
TuiCommand::Global(cmd) => {
cbd_cli::run_remote(&connection(remote), RemoteCmd::Global(cmd)).await
}
TuiCommand::Completions(args) => {
cbd_cli::print_completions(args.shell, &mut TuiCli::command(), "cbd-tui");
Ok(())
}
}
}
/// Resolves the connection for a remote command: the CLI flags win over the
/// client config, which supplies the fallback (address and credentials).
fn connection(remote: &cbd_cli::RemoteArgs) -> cbd_cli::Connection {
let config = config::load_first_run(CONFIG_FILE);
cbd_cli::Connection {
address: remote.address.clone().unwrap_or(config.server.address),
user: remote.user.clone().unwrap_or(config.server.user),
password: remote.password.clone().unwrap_or(config.server.password),
}
}