//! CLI argument definitions using clap derive. use std::path::PathBuf; use clap::{Parser, Subcommand}; #[derive(Parser)] #[command(name = "ptyctl")] #[command(about = "Run commands in PTY and control them via HTTP")] #[command(version)] #[command(arg_required_else_help = true)] pub struct Cli { #[command(subcommand)] pub command: Commands, } #[derive(Subcommand)] pub enum Commands { /// Spawn a command in a PTY and start an HTTP control server #[command(arg_required_else_help = true)] Run { /// Command to run (use -- before command) #[arg(required = true, trailing_var_arg = true)] command: Vec, /// Terminal width in columns #[arg(short = 'W', long, default_value = "80")] width: u16, /// Terminal height in rows #[arg(short = 'H', long, default_value = "24")] height: u16, /// Working directory #[arg(short = 'c', long)] cwd: Option, /// Environment variable (KEY=VAL, repeatable) #[arg(short = 'e', long = "env", value_name = "KEY=VAL")] env: Vec, /// TCP port to listen on (0 = auto-assign) #[arg(short, long, default_value = "0")] port: u16, /// Session name (registers in ~/.local/state/ptyctl/sessions/) #[arg(short, long)] name: Option, /// Take over an existing session name (replaces the registration; does not stop the old server) #[arg(long)] force: bool, /// Shutdown after N seconds #[arg(short, long, value_name = "SECS")] timeout: Option, /// Keep server running after process exits #[arg(short, long)] linger: bool, /// Suppress output (just print port number) #[arg(short, long)] quiet: bool, }, /// Send keystrokes to a running session #[command(arg_required_else_help = true)] Send { #[command(flatten)] target: Target, /// Keys to send (vim notation) keys: String, /// Append Enter () after keys #[arg(short = 'e', long)] enter: bool, }, /// Query terminal screen content #[command(arg_required_else_help = true)] Screen { #[command(flatten)] target: Target, /// Row range, 1-indexed (e.g. "1:5") #[arg(short, long)] rows: Option, /// Column range, 1-indexed #[arg(short = 'C', long)] cols: Option, /// Output as JSON #[arg(short, long, conflicts_with_all = ["ansi", "styled", "html"])] json: bool, /// Show cursor position with this character #[arg(short = 'c', long)] cursor: Option, /// Include ANSI escape codes #[arg(short, long, conflicts_with_all = ["json", "styled", "html"])] ansi: bool, /// Output as styled JSON (LLM-friendly) #[arg(short = 's', long, conflicts_with_all = ["json", "ansi", "html"])] styled: bool, /// Output as HTML #[arg(long, conflicts_with_all = ["json", "ansi", "styled"])] html: bool, /// Include trailing empty lines #[arg(long)] full: bool, /// Show line numbers #[arg(short = 'l', long)] line_numbers: bool, }, /// Query process status #[command(arg_required_else_help = true)] Status { #[command(flatten)] target: Target, }, /// Stop a running session #[command(arg_required_else_help = true)] Stop { #[command(flatten)] target: Target, }, /// Resize terminal dimensions #[command(arg_required_else_help = true)] Resize { #[command(flatten)] target: Target, /// New size as COLSxROWS (e.g. "120x40") size: String, }, /// Query cursor position #[command(arg_required_else_help = true)] Cursor { #[command(flatten)] target: Target, }, /// Wait for a screen condition (event-driven, no polling). /// Exit codes: 0 matched, 1 timeout (failure JSON on stdout), 2 usage/connection error #[command(arg_required_else_help = true)] #[command(group(clap::ArgGroup::new("condition").required(true)))] Wait { #[command(flatten)] target: Target, /// Wait until this text appears on screen #[arg(long, group = "condition")] text: Option, /// Wait until this regex matches the screen text #[arg(long, group = "condition")] regex: Option, /// Wait until this text is absent from the screen #[arg(long, group = "condition")] gone: Option, /// Wait until the screen has been unchanged for this many milliseconds #[arg(long, value_name = "MS", group = "condition")] stable_ms: Option, /// Timeout in seconds (server caps at 120) #[arg(short, long, default_value = "10")] timeout: u64, }, /// List registered sessions List { /// Output as JSON #[arg(short, long)] json: bool, }, } /// Target session — exactly one of host, port, or name must be provided. #[derive(clap::Args)] #[group(required = true, multiple = false)] pub struct Target { /// Remote host address (e.g. 127.0.0.1:8080) #[arg(short = 'H', long)] pub host: Option, /// Local server port #[arg(short, long)] pub port: Option, /// Session name (from registry) #[arg(short, long)] pub name: Option, } impl Target { /// Resolve target to a base URL. pub fn to_url(&self) -> anyhow::Result { if let Some(ref h) = self.host { if h.starts_with("http") { return Ok(h.clone()); } return Ok(format!("http://{h}")); } if let Some(p) = self.port { return Ok(format!("http://127.0.0.1:{p}")); } if let Some(ref n) = self.name { let info = crate::registry::lookup_session(n)?; return Ok(format!("http://127.0.0.1:{}", info.port)); } anyhow::bail!("no target specified") } }