use anyhow::{anyhow, Result};
use clap::Parser;
use tracing_appender::rolling;
use tracing_subscriber::EnvFilter;
use huddle_core::network::NetworkMode;
use huddle_core::storage::keychain;
mod app;
mod input;
mod ui;
#[derive(Parser)]
#[command(name = "huddle", version, about = "Huddle — decentralized encrypted chat (TUI)")]
struct Cli {
#[arg(long, help = "Override data directory")]
data_dir: Option<String>,
#[arg(long, value_parser = parse_mode)]
mode: Option<NetworkMode>,
#[arg(long, default_value_t = 0u16)]
port: u16,
#[arg(long)]
name: Option<String>,
#[arg(long)]
no_master_passphrase: bool,
}
fn parse_mode(s: &str) -> std::result::Result<NetworkMode, String> {
NetworkMode::from_str(s).ok_or_else(|| format!("unknown mode `{s}` (try mdns or direct)"))
}
#[tokio::main]
async fn main() -> Result<()> {
let cli = Cli::parse();
let log_path = huddle_core::config::log_path();
if let Some(parent) = log_path.parent() {
std::fs::create_dir_all(parent)?;
}
let file_appender = rolling::never(
log_path.parent().unwrap(),
log_path.file_name().unwrap(),
);
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env().add_directive("huddle=debug".parse()?))
.with_writer(file_appender)
.with_ansi(false)
.init();
let mode = cli.mode.unwrap_or(NetworkMode::Mdns);
if cli.mode.is_none() && !app::show_welcome()? {
return Ok(());
}
let (master_key, signup_name) = if cli.no_master_passphrase {
(None, None)
} else {
let salt_path = keychain::keychain_salt_path();
let is_new = !salt_path.exists();
let prompt = app::prompt_master_passphrase(is_new)?;
if prompt.passphrase.is_empty() {
return Ok(());
}
let salt = keychain::load_or_create_salt().map_err(|e| anyhow!(e))?;
let key =
keychain::derive_master_key(&prompt.passphrase, &salt).map_err(|e| anyhow!(e))?;
(Some(key), prompt.username)
};
let handle = huddle_core::app::AppHandle::start_with_options(
mode,
cli.port,
master_key.as_ref(),
)
.await
.map_err(|e| anyhow!(e))?;
let name_to_set = cli.name.clone().or(signup_name);
if let Some(name) = name_to_set {
let trimmed = name.trim();
if !trimmed.is_empty() {
handle
.set_display_name(Some(trimmed))
.map_err(|e| anyhow!(e))?;
}
}
app::run_tui(handle).await
}