use std::path::PathBuf;
use clap::{Parser, Subcommand};
#[derive(Debug, Parser)]
#[command(
name = "tl",
about = "macOS clipboard + OCR daemon, MCP server for Claude Code",
// `disable_version_flag` lets us register both -v and --version
// (clap's default is uppercase -V; users expect lowercase -v).
disable_version_flag = true,
)]
pub struct Cli {
#[arg(long, global = true, env = "TEXTLOG_CONFIG_DIR")]
pub config_dir: Option<PathBuf>,
#[arg(short = 'v', short_alias = 'V', long = "version", global = true)]
pub version_flag: bool,
#[command(subcommand)]
pub command: Option<Command>,
}
#[derive(Debug, Subcommand)]
pub enum Command {
Mcp,
Version,
Config {
#[command(subcommand)]
cmd: ConfigCmd,
},
Logs {
#[command(subcommand)]
cmd: LogsCmd,
},
Doctor,
Install,
Uninstall,
Start {
#[arg(long)]
foreground: bool,
},
Stop,
Status,
Update,
Perf {
#[arg(long, default_value_t = 10)]
duration: u64,
#[arg(long = "interval-ms", default_value_t = 1000)]
interval_ms: u64,
},
}
#[derive(Debug, Subcommand)]
pub enum ConfigCmd {
Show,
Path,
Reset,
}
#[derive(Debug, Subcommand)]
pub enum LogsCmd {
Today,
Search {
query: String,
#[arg(long, default_value_t = 20)]
limit: u32,
},
Path,
}
impl Cli {
#[cfg(test)]
pub fn try_parse_argv(argv: &[&str]) -> std::result::Result<Self, clap::Error> {
<Self as clap::Parser>::try_parse_from(argv)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_mcp_subcommand() {
let cli = Cli::try_parse_argv(&["tl", "mcp"]).unwrap();
assert!(matches!(cli.command, Some(Command::Mcp)));
}
#[test]
fn parses_version_subcommand() {
let cli = Cli::try_parse_argv(&["tl", "version"]).unwrap();
assert!(matches!(cli.command, Some(Command::Version)));
}
#[test]
fn version_short_flag_lowercase_works() {
let cli = Cli::try_parse_argv(&["tl", "-v"]).unwrap();
assert!(cli.version_flag);
assert!(cli.command.is_none());
}
#[test]
fn version_short_flag_uppercase_alias_works() {
let cli = Cli::try_parse_argv(&["tl", "-V"]).unwrap();
assert!(cli.version_flag);
}
#[test]
fn version_long_flag_works() {
let cli = Cli::try_parse_argv(&["tl", "--version"]).unwrap();
assert!(cli.version_flag);
}
#[test]
fn parses_update_subcommand() {
let cli = Cli::try_parse_argv(&["tl", "update"]).unwrap();
assert!(matches!(cli.command, Some(Command::Update)));
}
#[test]
fn parses_config_show() {
let cli = Cli::try_parse_argv(&["tl", "config", "show"]).unwrap();
match cli.command {
Some(Command::Config { cmd: ConfigCmd::Show }) => {}
other => panic!("expected Config Show, got {other:?}"),
}
}
#[test]
fn parses_config_reset() {
let cli = Cli::try_parse_argv(&["tl", "config", "reset"]).unwrap();
match cli.command {
Some(Command::Config { cmd: ConfigCmd::Reset }) => {}
other => panic!("expected Config Reset, got {other:?}"),
}
}
#[test]
fn parses_logs_search_with_default_limit() {
let cli = Cli::try_parse_argv(&["tl", "logs", "search", "panic"]).unwrap();
match cli.command {
Some(Command::Logs {
cmd: LogsCmd::Search { query, limit },
}) => {
assert_eq!(query, "panic");
assert_eq!(limit, 20);
}
other => panic!("expected Logs Search, got {other:?}"),
}
}
#[test]
fn parses_logs_search_with_custom_limit() {
let cli = Cli::try_parse_argv(&["tl", "logs", "search", "x", "--limit", "5"]).unwrap();
match cli.command {
Some(Command::Logs {
cmd: LogsCmd::Search { query, limit },
}) => {
assert_eq!(query, "x");
assert_eq!(limit, 5);
}
other => panic!("expected Logs Search, got {other:?}"),
}
}
#[test]
fn config_dir_flag_overrides() {
let cli =
Cli::try_parse_argv(&["tl", "--config-dir", "/tmp/foo", "config", "path"]).unwrap();
assert_eq!(cli.config_dir, Some(PathBuf::from("/tmp/foo")));
}
#[test]
fn parses_start_foreground() {
let cli = Cli::try_parse_argv(&["tl", "start", "--foreground"]).unwrap();
match cli.command {
Some(Command::Start { foreground }) => assert!(foreground),
other => panic!("expected Start, got {other:?}"),
}
}
#[test]
fn bare_tl_parses_with_neither_command_nor_version() {
let cli = Cli::try_parse_argv(&["tl"]).unwrap();
assert!(cli.command.is_none());
assert!(!cli.version_flag);
}
}