framesmith-cli 0.1.0

CLI tool for controlling Samsung Frame TVs over the local network
mod cli;
mod client;
mod commands;
mod connect;
mod executor;
mod ipc;
mod server;

use std::env;
use std::path::PathBuf;
use std::process::ExitCode;

use clap::Parser;

use cli::{HostCli, HostCommand, HostlessCli, HostlessCommand};

fn main() -> ExitCode {
    let args: Vec<String> = env::args().collect();

    // Detect hidden --internal-server flag before clap parsing
    if args.iter().any(|a| a == "--internal-server") {
        return run_internal_server(&args);
    }

    // Determine if this is a hostless command or host-based command
    let is_hostless = match args.get(1).map(|s| s.as_str()) {
        None | Some("--help" | "-h" | "--version" | "-V") => true,
        Some("discover" | "completions") => true,
        // Check if flags come before a hostless command
        Some(arg) if arg.starts_with('-') => args
            .iter()
            .any(|a| matches!(a.as_str(), "discover" | "completions")),
        _ => false,
    };

    if args.len() == 1 {
        print_combined_help();
        return ExitCode::SUCCESS;
    }

    if is_hostless {
        let cli = match HostlessCli::try_parse() {
            Ok(cli) => cli,
            Err(e) => {
                e.print().ok();
                return if e.use_stderr() {
                    ExitCode::FAILURE
                } else {
                    ExitCode::SUCCESS
                };
            }
        };

        if cli.verbose {
            init_tracing();
        }

        match run_hostless(cli) {
            Ok(()) => ExitCode::SUCCESS,
            Err(e) => {
                eprintln!("Error: {e:#}");
                ExitCode::FAILURE
            }
        }
    } else {
        let cli = match HostCli::try_parse() {
            Ok(cli) => cli,
            Err(e) => {
                e.print().ok();
                return if e.use_stderr() {
                    ExitCode::FAILURE
                } else {
                    ExitCode::SUCCESS
                };
            }
        };

        if cli.verbose {
            init_tracing();
        }

        let json = cli.json;
        let rt = tokio::runtime::Runtime::new().unwrap();
        match rt.block_on(run_host(cli)) {
            Ok(()) => ExitCode::SUCCESS,
            Err(e) => {
                if json {
                    eprintln!("{}", serde_json::json!({"error": format!("{e:#}")}));
                } else {
                    eprintln!("Error: {e:#}");
                }
                ExitCode::FAILURE
            }
        }
    }
}

fn run_hostless(cli: HostlessCli) -> anyhow::Result<()> {
    match cli.command {
        HostlessCommand::Discover => {
            let rt = tokio::runtime::Runtime::new()?;
            rt.block_on(commands::discover::run(cli.json))
        }
        HostlessCommand::Completions { shell } => commands::completions::run(shell),
    }
}

async fn run_host(cli: HostCli) -> anyhow::Result<()> {
    match &cli.command {
        // Commands that bypass the server
        HostCommand::Auth { sub } => {
            commands::auth::run(
                &cli.host,
                &cli.auth_token_file,
                cli.timeout,
                sub.clone(),
                cli.json,
            )
            .await
        }
        HostCommand::Info => commands::info::run(&cli.host, cli.json).await,

        // Commands that go through the executor (server or direct)
        HostCommand::Art { sub } => {
            let exec = make_executor(&cli);
            commands::art::run(&exec, sub, cli.json).await
        }
        HostCommand::Display { sub } => {
            let exec = make_executor(&cli);
            commands::display::run(&exec, sub, cli.json).await
        }
        HostCommand::Motion { sub } => {
            let exec = make_executor(&cli);
            commands::motion::run(&exec, sub, cli.json).await
        }
        HostCommand::Remote { button } => {
            let exec = make_executor(&cli);
            commands::remote::run(&exec, button, cli.json).await
        }

        // Server management
        HostCommand::Server { .. } if cli.direct => {
            anyhow::bail!("--direct cannot be used with server management commands");
        }
        HostCommand::Server { sub } => {
            use cli::ServerCommand;
            match sub {
                ServerCommand::Start => {
                    commands::server::start(&cli.host, &cli.auth_token_file, cli.timeout, cli.json)
                        .await
                }
                ServerCommand::Stop => commands::server::stop(&cli.host, cli.json).await,
                ServerCommand::Restart => {
                    commands::server::restart(
                        &cli.host,
                        &cli.auth_token_file,
                        cli.timeout,
                        cli.json,
                    )
                    .await
                }
                ServerCommand::Status => commands::server::status(&cli.host, cli.json).await,
                ServerCommand::TailLogs => commands::server::tail_logs(&cli.host).await,
            }
        }
    }
}

fn make_executor(cli: &HostCli) -> executor::Executor {
    if cli.direct {
        executor::Executor::Direct {
            host: cli.host.clone(),
            auth_token_file: cli.auth_token_file.clone(),
            timeout: cli.timeout,
        }
    } else {
        executor::Executor::Server(client::Client::new(
            &cli.host,
            &cli.auth_token_file,
            cli.timeout,
        ))
    }
}

/// Handle the --internal-server flag: parse server args and run the server.
fn run_internal_server(args: &[String]) -> ExitCode {
    let mut host: Option<String> = None;
    let mut auth_token_file: Option<PathBuf> = None;
    let mut timeout: u64 = 5;

    let mut i = 1;
    while i < args.len() {
        match args[i].as_str() {
            "--internal-server" => {}
            "--host" => {
                i += 1;
                host = args.get(i).cloned();
            }
            "--auth-token-file" => {
                i += 1;
                auth_token_file = args.get(i).map(PathBuf::from);
            }
            "--timeout" => {
                i += 1;
                timeout = args.get(i).and_then(|s| s.parse().ok()).unwrap_or(5);
            }
            _ => {}
        }
        i += 1;
    }

    let Some(host) = host else {
        eprintln!("--internal-server requires --host");
        return ExitCode::FAILURE;
    };
    let token_file = auth_token_file.unwrap_or_else(|| {
        dirs::home_dir()
            .unwrap_or_else(|| PathBuf::from("."))
            .join(".framesmith")
            .join("token")
    });

    let rt = tokio::runtime::Runtime::new().unwrap();
    match rt.block_on(server::run_server(&host, &token_file, timeout)) {
        Ok(()) => ExitCode::SUCCESS,
        Err(e) => {
            eprintln!("Server error: {e:#}");
            ExitCode::FAILURE
        }
    }
}

fn init_tracing() {
    tracing_subscriber::fmt()
        .with_env_filter(
            tracing_subscriber::EnvFilter::builder()
                .with_default_directive(tracing_subscriber::filter::LevelFilter::DEBUG.into())
                .from_env_lossy(),
        )
        .init();
}

fn print_combined_help() {
    println!("Control Samsung Frame TVs from the command line");
    println!();
    println!("Usage:");
    println!("  framesmith <HOST> <COMMAND> [ARGS...]");
    println!("  framesmith <COMMAND> [ARGS...]");
    println!();
    println!("Commands (no host required):");
    println!("  discover       Find Samsung Frame TVs on the local network");
    println!("  completions    Generate shell completions");
    println!();
    println!("Commands (require TV host):");
    println!("  auth           Manage TV authentication");
    println!("  info           Show device information");
    println!("  art            Art images, art mode, and slideshow");
    println!("  display        Display settings");
    println!("  motion         Motion sensor settings");
    println!("  remote         Send remote control button press");
    println!("  server         Manage background server process");
    println!();
    println!("Options:");
    println!("  --token-file <PATH>    Path to auth token file [default: ~/.framesmith/token]");
    println!("  --timeout <SECONDS>    Connection timeout in seconds [default: 5]");
    println!("  --direct               Connect directly to TV, bypass background server");
    println!("  --json                 Emit all output as JSON");
    println!("  -v, --verbose          Enable verbose/debug output");
    println!("  -h, --help             Print help");
    println!("  -V, --version          Print version");
}