//! 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 = 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 { 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> { 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> { 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), } }