use std::path::PathBuf;
use clap::{Parser, Subcommand};
#[derive(Debug, Parser)]
#[command(
name = "tl",
about = "macOS clipboard + OCR daemon, MCP server for Claude Code",
version
)]
pub struct Cli {
#[arg(long, global = true, env = "TEXTLOG_CONFIG_DIR")]
pub config_dir: Option<PathBuf>,
#[command(subcommand)]
pub command: 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,
}
#[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, Command::Mcp));
}
#[test]
fn parses_version_subcommand() {
let cli = Cli::try_parse_argv(&["tl", "version"]).unwrap();
assert!(matches!(cli.command, Command::Version));
}
#[test]
fn parses_config_show() {
let cli = Cli::try_parse_argv(&["tl", "config", "show"]).unwrap();
match cli.command {
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 {
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 {
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 {
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 {
Command::Start { foreground } => assert!(foreground),
other => panic!("expected Start, got {other:?}"),
}
}
#[test]
fn missing_subcommand_errors() {
assert!(Cli::try_parse_argv(&["tl"]).is_err());
}
}