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();
if args.iter().any(|a| a == "--internal-server") {
return run_internal_server(&args);
}
let is_hostless = match args.get(1).map(|s| s.as_str()) {
None | Some("--help" | "-h" | "--version" | "-V") => true,
Some("discover" | "completions") => true,
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 {
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,
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
}
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,
))
}
}
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");
}