use clap::Parser;
use std::path::PathBuf;
#[derive(Parser, Debug)]
#[command(
name = "sd",
author = "Streamdown Contributors",
version,
about = "A streaming markdown renderer for modern terminals",
after_help = "Repository: https://github.com/fed-stew/streamdown-rs\n\n\
Examples:\n \
cat README.md | sd\n \
sd document.md\n \
sd -w 100 -c theme.toml input.md\n \
sd --exec 'ollama run llama3'"
)]
pub struct Cli {
#[arg(value_name = "FILE")]
pub files: Vec<PathBuf>,
#[arg(short = 'l', long = "loglevel", default_value = "warn")]
pub log_level: String,
#[arg(short = 'b', long = "base")]
pub base: Option<String>,
#[arg(short = 'c', long = "config")]
pub config: Option<String>,
#[arg(short = 'w', long = "width", default_value = "0")]
pub width: u16,
#[arg(short = 'e', long = "exec", value_name = "CMD")]
pub exec_cmd: Option<String>,
#[arg(short = 'p', long = "prompt", default_value = r"^.*>\s+$")]
pub prompt: String,
#[arg(short = 's', long = "scrape", value_name = "DIR")]
pub scrape: Option<PathBuf>,
#[arg(long = "no-highlight")]
pub no_highlight: bool,
#[arg(long = "no-pretty-pad")]
pub no_pretty_pad: bool,
#[arg(long = "pretty-broken")]
pub pretty_broken: bool,
#[arg(long = "clipboard")]
pub clipboard: bool,
#[arg(long = "savebrace")]
pub savebrace: bool,
#[arg(long = "paths")]
pub show_paths: bool,
#[arg(long = "theme", default_value = "base16-ocean.dark")]
pub theme: String,
}
impl Cli {
pub fn effective_width(&self) -> usize {
if self.width == 0 {
crossterm::terminal::size()
.map(|(cols, _)| cols as usize)
.unwrap_or(80)
} else {
self.width as usize
}
}
pub fn should_read_stdin(&self) -> bool {
self.files.is_empty() && self.exec_cmd.is_none()
}
pub fn parse_base(&self) -> Option<(f32, f32, f32)> {
self.base.as_ref().and_then(|b| {
let parts: Vec<&str> = b.split(',').collect();
if parts.len() == 3 {
let h = parts[0].parse().ok()?;
let s = parts[1].parse().ok()?;
let v = parts[2].parse().ok()?;
Some((h, s, v))
} else {
None
}
})
}
}
pub fn show_paths() {
use streamdown_config::Config;
let config_path = Config::config_path()
.map(|p| p.display().to_string())
.unwrap_or_else(|| "(not found)".to_string());
let log_dir = std::env::temp_dir()
.join("sd")
.join(std::env::var("UID").unwrap_or_else(|_| "unknown".to_string()));
println!("paths:");
println!(" config {}", config_path);
println!(" logs {}", log_dir.display());
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cli_parse_default() {
let cli = Cli::parse_from(["sd"]);
assert!(cli.files.is_empty());
assert_eq!(cli.width, 0);
assert_eq!(cli.log_level, "warn");
assert!(!cli.clipboard);
}
#[test]
fn test_cli_parse_with_file() {
let cli = Cli::parse_from(["sd", "test.md"]);
assert_eq!(cli.files.len(), 1);
assert_eq!(cli.files[0], PathBuf::from("test.md"));
}
#[test]
fn test_cli_parse_with_options() {
let cli = Cli::parse_from([
"sd",
"-w",
"100",
"-l",
"debug",
"--clipboard",
"--savebrace",
"file.md",
]);
assert_eq!(cli.width, 100);
assert_eq!(cli.log_level, "debug");
assert!(cli.clipboard);
assert!(cli.savebrace);
}
#[test]
fn test_cli_parse_exec() {
let cli = Cli::parse_from(["sd", "-e", "ollama run llama3", "-p", ">>> "]);
assert_eq!(cli.exec_cmd, Some("ollama run llama3".to_string()));
assert_eq!(cli.prompt, ">>> ");
}
#[test]
fn test_cli_parse_base() {
let cli = Cli::parse_from(["sd", "-b", "0.6,0.5,0.5"]);
let base = cli.parse_base();
assert!(base.is_some());
let (h, s, v) = base.unwrap();
assert!((h - 0.6).abs() < 0.01);
assert!((s - 0.5).abs() < 0.01);
assert!((v - 0.5).abs() < 0.01);
}
#[test]
fn test_should_read_stdin() {
let cli = Cli::parse_from(["sd"]);
assert!(cli.should_read_stdin());
let cli = Cli::parse_from(["sd", "file.md"]);
assert!(!cli.should_read_stdin());
let cli = Cli::parse_from(["sd", "-e", "cmd"]);
assert!(!cli.should_read_stdin());
}
}