use std::path::PathBuf;
use anyhow::{Context, Result};
use clap::Parser;
use crate::paths;
#[derive(Parser, Debug, Clone)]
#[command(name = "recorder-for-jetkvm", about = "Record JetKVM screen changes")]
pub struct Config {
#[arg(long)]
pub host: String,
#[arg(long, env = "JETKVM_PASSWORD")]
pub password: Option<String>,
#[arg(long)]
pub password_file: Option<PathBuf>,
#[arg(long)]
pub output_dir: Option<PathBuf>,
#[arg(long, default_value_t = 0.02, value_parser = parse_sensitivity)]
pub sensitivity: f64,
#[arg(long, default_value_t = 5)]
pub pre_buffer: u64,
#[arg(long, default_value_t = 10)]
pub cooldown: u64,
#[arg(long, default_value_t = 500)]
pub check_interval: u64,
#[arg(long, default_value_t = 3)]
pub pli_interval: u64,
#[arg(long, default_value_t = false)]
pub no_tls_verify: bool,
#[arg(long, default_value_t = false)]
pub screenshot: bool,
#[arg(long)]
pub screenshot_output: Option<PathBuf>,
}
fn parse_sensitivity(input: &str) -> std::result::Result<f64, String> {
let value = input
.parse::<f64>()
.map_err(|_| format!("sensitivity must be a finite number in [0.0, 1.0], got '{input}'"))?;
if !value.is_finite() || !(0.0..=1.0).contains(&value) {
return Err(format!(
"sensitivity must be a finite number in [0.0, 1.0], got '{input}'"
));
}
Ok(value)
}
impl Config {
pub fn recordings_dir(&self) -> PathBuf {
self.output_dir
.clone()
.unwrap_or_else(paths::default_recordings_dir)
}
pub fn screenshot_output_path(&self) -> PathBuf {
self.screenshot_output
.clone()
.unwrap_or_else(paths::default_screenshot_path)
}
pub fn resolve_password(&self) -> Result<String> {
if let Some(ref password) = self.password {
return Ok(password.clone());
}
if let Some(ref path) = self.password_file {
let contents = std::fs::read_to_string(path)
.with_context(|| format!("failed to read password file: {}", path.display()))?;
let password = contents.trim().to_string();
if password.is_empty() {
anyhow::bail!("password file is empty: {}", path.display());
}
return Ok(password);
}
anyhow::bail!(
"no password provided; use --password, JETKVM_PASSWORD env var, or --password-file"
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use clap::error::ErrorKind;
#[test]
fn test_parse_with_required_host() {
let cfg = Config::try_parse_from(["recorder-for-jetkvm", "--host", "192.168.1.130"])
.expect("expected config parsing to succeed");
assert_eq!(cfg.host, "192.168.1.130");
assert!(cfg.output_dir.is_none());
assert_eq!(cfg.sensitivity, 0.02);
assert_eq!(cfg.check_interval, 500);
assert!(!cfg.screenshot);
assert!(cfg.screenshot_output.is_none());
}
#[test]
fn test_screenshot_flag_enables_single_capture_mode() {
let cfg = Config::try_parse_from([
"recorder-for-jetkvm",
"--host",
"192.168.1.130",
"--screenshot",
])
.expect("expected screenshot mode parsing to succeed");
assert!(cfg.screenshot);
}
#[test]
fn test_explicit_output_paths_override_defaults() {
let cfg = Config::try_parse_from([
"recorder-for-jetkvm",
"--host",
"192.168.1.130",
"--output-dir",
"/tmp/recordings",
"--screenshot-output",
"/tmp/capture.png",
])
.expect("expected explicit output paths to parse");
assert_eq!(cfg.recordings_dir(), PathBuf::from("/tmp/recordings"));
assert_eq!(cfg.screenshot_output_path(), PathBuf::from("/tmp/capture.png"));
}
#[test]
fn test_noise_threshold_flag_is_rejected() {
let err = Config::try_parse_from([
"recorder-for-jetkvm",
"--host",
"192.168.1.130",
"--noise-threshold",
"25",
])
.expect_err("expected removed --noise-threshold flag to fail parsing");
assert_eq!(err.kind(), ErrorKind::UnknownArgument);
}
#[test]
fn test_sensitivity_valid_values() {
for (raw, expected) in [("0.0", 0.0), ("0.02", 0.02), ("1.0", 1.0)] {
let sensitivity_arg = format!("--sensitivity={raw}");
let cfg = Config::try_parse_from(vec![
"recorder-for-jetkvm".to_string(),
"--host".to_string(),
"192.168.1.130".to_string(),
sensitivity_arg,
])
.expect("expected sensitivity to parse");
assert_eq!(cfg.sensitivity, expected);
}
}
#[test]
fn test_sensitivity_invalid_values_are_rejected() {
for raw in ["-0.1", "1.1", "NaN", "inf"] {
let sensitivity_arg = format!("--sensitivity={raw}");
let err = Config::try_parse_from(vec![
"recorder-for-jetkvm".to_string(),
"--host".to_string(),
"192.168.1.130".to_string(),
sensitivity_arg,
])
.expect_err("expected invalid sensitivity to fail parsing");
let message = err.to_string();
assert!(
message.contains("sensitivity must be a finite number in [0.0, 1.0]"),
"unexpected clap error for --sensitivity={raw}: {message}"
);
}
}
}