use std::path::PathBuf;
use clap::Parser;
#[derive(Parser, Debug, Clone)]
#[command(name = "tess", version, about = "A less-style terminal pager.")]
pub struct Args {
#[arg(short = 'S', long = "chop-long-lines", display_order = 1)]
pub chop: bool,
#[arg(long = "content-type", value_name = "TYPE", display_order = 2)]
pub content_type: Option<String>,
#[arg(long = "dim", display_order = 3)]
pub dim: bool,
#[arg(long = "display", value_name = "TEMPLATE", display_order = 4)]
pub display: Option<String>,
#[arg(long = "examples", display_order = 5)]
pub examples: bool,
#[arg(long = "filter", value_name = "FIELD<op>VALUE", display_order = 6)]
pub filter: Vec<String>,
#[arg(short = 'f', long = "follow", display_order = 7)]
pub follow: bool,
#[arg(long = "format", value_name = "NAME", display_order = 8)]
pub format: Option<String>,
#[arg(long = "head", value_name = "N", conflicts_with = "tail", display_order = 9)]
pub head: Option<usize>,
#[arg(short = 'N', long = "LINE-NUMBERS", display_order = 10)]
pub line_numbers: bool,
#[arg(long = "list-formats", display_order = 11)]
pub list_formats: bool,
#[arg(long = "live", conflicts_with = "follow", display_order = 12)]
pub live: bool,
#[arg(long = "manual", display_order = 13)]
pub manual: bool,
#[arg(short = 'o', long = "output", value_name = "FILE", display_order = 14)]
pub output: Option<String>,
#[arg(long = "prettify", display_order = 15)]
pub prettify: bool,
#[arg(long = "stdout", conflicts_with = "output", display_order = 16)]
pub stdout: bool,
#[arg(long = "tab-width", default_value_t = 8, display_order = 17)]
pub tab_width: u8,
#[arg(long = "tail", value_name = "N", conflicts_with = "head", display_order = 18)]
pub tail: Option<usize>,
pub files: Vec<PathBuf>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_no_flags_no_files() {
let a = Args::parse_from(["tess"]);
assert!(!a.line_numbers);
assert!(!a.chop);
assert_eq!(a.tab_width, 8);
assert!(a.files.is_empty());
}
#[test]
fn parses_short_flags_and_file() {
let a = Args::parse_from(["tess", "-N", "-S", "foo.txt"]);
assert!(a.line_numbers);
assert!(a.chop);
assert_eq!(a.files, vec![PathBuf::from("foo.txt")]);
}
#[test]
fn parses_tab_width() {
let a = Args::parse_from(["tess", "--tab-width", "4", "x"]);
assert_eq!(a.tab_width, 4);
}
#[test]
fn collects_multiple_files() {
let a = Args::parse_from(["tess", "a", "b", "c"]);
assert_eq!(a.files.len(), 3);
}
#[test]
fn parses_follow_short_flag() {
let a = Args::parse_from(["tess", "-f", "log.txt"]);
assert!(a.follow);
assert_eq!(a.files, vec![PathBuf::from("log.txt")]);
}
#[test]
fn parses_follow_long_flag() {
let a = Args::parse_from(["tess", "--follow"]);
assert!(a.follow);
}
#[test]
fn follow_defaults_off() {
let a = Args::parse_from(["tess", "x"]);
assert!(!a.follow);
}
#[test]
fn parses_head() {
let a = Args::parse_from(["tess", "--head", "100", "x"]);
assert_eq!(a.head, Some(100));
assert_eq!(a.tail, None);
}
#[test]
fn parses_tail() {
let a = Args::parse_from(["tess", "--tail", "50", "x"]);
assert_eq!(a.tail, Some(50));
assert_eq!(a.head, None);
}
#[test]
fn head_and_tail_are_mutually_exclusive() {
let r = Args::try_parse_from(["tess", "--head", "10", "--tail", "20", "x"]);
assert!(r.is_err(), "clap should reject combining --head and --tail");
}
#[test]
fn head_tail_default_to_none() {
let a = Args::parse_from(["tess", "x"]);
assert!(a.head.is_none());
assert!(a.tail.is_none());
}
#[test]
fn parses_format_and_filter() {
let a = Args::parse_from([
"tess", "--format", "apache-combined",
"--filter", "status=500",
"--filter", "ip~^10\\.",
"log",
]);
assert_eq!(a.format.as_deref(), Some("apache-combined"));
assert_eq!(a.filter.len(), 2);
assert_eq!(a.filter[0], "status=500");
}
#[test]
fn parses_dim() {
let a = Args::parse_from(["tess", "--format", "x", "--filter", "y=z", "--dim", "f"]);
assert!(a.dim);
}
#[test]
fn parses_list_formats() {
let a = Args::parse_from(["tess", "--list-formats"]);
assert!(a.list_formats);
}
#[test]
fn parses_manual() {
let a = Args::parse_from(["tess", "--manual"]);
assert!(a.manual);
}
#[test]
fn parses_examples() {
let a = Args::parse_from(["tess", "--examples"]);
assert!(a.examples);
}
#[test]
fn parses_live() {
let a = Args::parse_from(["tess", "--live", "f"]);
assert!(a.live);
assert!(!a.follow);
}
#[test]
fn live_and_follow_are_mutually_exclusive() {
let r = Args::try_parse_from(["tess", "--live", "--follow", "f"]);
assert!(r.is_err(), "clap should reject combining --live and --follow");
}
#[test]
fn parses_prettify() {
let a = Args::parse_from(["tess", "--prettify", "f.json"]);
assert!(a.prettify);
assert_eq!(a.content_type, None);
}
#[test]
fn parses_content_type() {
let a = Args::parse_from(["tess", "--content-type", "json", "f"]);
assert_eq!(a.content_type.as_deref(), Some("json"));
}
#[test]
fn parses_output_long_and_short() {
let a = Args::parse_from(["tess", "-o", "/tmp/out.txt", "f"]);
assert_eq!(a.output.as_deref(), Some("/tmp/out.txt"));
let b = Args::parse_from(["tess", "--output", "/tmp/out.txt", "f"]);
assert_eq!(b.output.as_deref(), Some("/tmp/out.txt"));
}
#[test]
fn parses_stdout_flag() {
let a = Args::parse_from(["tess", "--stdout", "f"]);
assert!(a.stdout);
assert_eq!(a.output, None);
}
#[test]
fn output_and_stdout_are_mutually_exclusive() {
let r = Args::try_parse_from(["tess", "-o", "x", "--stdout", "f"]);
assert!(r.is_err(), "clap should reject combining --output and --stdout");
}
#[test]
fn help_lists_flags_in_alphabetical_order() {
use clap::CommandFactory;
let mut cmd = Args::command();
let help = cmd.render_help().to_string();
let expected = [
"--chop-long-lines",
"--content-type",
"--dim",
"--display",
"--examples",
"--filter",
"--follow",
"--format",
"--head",
"--LINE-NUMBERS",
"--list-formats",
"--live",
"--manual",
"--output",
"--prettify",
"--stdout",
"--tab-width",
"--tail",
];
let listed: Vec<&str> = help
.lines()
.map(str::trim_start)
.filter(|l| l.starts_with('-'))
.filter_map(|l| {
l.split(|c: char| c.is_whitespace() || c == ',')
.find(|tok| expected.contains(tok))
})
.collect();
assert_eq!(listed, expected, "help long-flag order should be alphabetical");
}
}