use clap::{Parser, ValueEnum};
use std::path::PathBuf;
#[cfg(feature = "unstable-dynamic")]
use clap_complete::engine::{ArgValueCompleter, CompletionCandidate, ValueCompleter};
#[cfg(feature = "unstable-dynamic")]
const LONG_ABOUT: &str = "treemd - A modern markdown viewer combining tree-based navigation with interactive TUI.\n\n\
Launch without flags for interactive mode with dual-pane interface, vim-style navigation,\n\
syntax highlighting, and real-time search. Use flags for CLI mode to extract, filter,\n\
and analyze markdown structure.\n\n\
Examples:\n \
treemd README.md # Interactive TUI mode\n \
treemd -l README.md # List all headings\n \
treemd --tree README.md # Show heading tree\n \
treemd -s Installation doc.md # Extract section\n \
treemd --setup-completions # Set up shell completions";
#[cfg(not(feature = "unstable-dynamic"))]
const LONG_ABOUT: &str = "treemd - A modern markdown viewer combining tree-based navigation with interactive TUI.\n\n\
Launch without flags for interactive mode with dual-pane interface, vim-style navigation,\n\
syntax highlighting, and real-time search. Use flags for CLI mode to extract, filter,\n\
and analyze markdown structure.\n\n\
Examples:\n \
treemd README.md # Interactive TUI mode\n \
treemd -l README.md # List all headings\n \
treemd --tree README.md # Show heading tree\n \
treemd -s Installation doc.md # Extract section";
#[derive(Parser, Debug)]
#[command(name = "treemd")]
#[command(version)]
#[command(about = "A markdown navigator with tree-based structural navigation")]
#[command(long_about = LONG_ABOUT)]
pub struct Cli {
#[arg(add = markdown_file_completer())]
pub file: Vec<PathBuf>,
#[arg(long = "at-line", value_name = "LINE")]
pub at_line: Option<usize>,
#[arg(short = 'l', long = "list")]
pub list: bool,
#[arg(long = "tree")]
pub tree: bool,
#[arg(long = "filter", value_name = "PATTERN")]
pub filter: Option<String>,
#[arg(short = 'L', long = "level", value_name = "LEVEL")]
pub level: Option<usize>,
#[arg(short = 'o', long = "output", default_value = "plain")]
pub output: OutputFormat,
#[arg(short = 's', long = "section", value_name = "HEADING")]
pub section: Option<String>,
#[arg(long = "count")]
pub count: bool,
#[cfg(feature = "unstable-dynamic")]
#[arg(long = "setup-completions")]
pub setup_completions: bool,
#[arg(long = "theme", value_name = "THEME")]
pub theme: Option<String>,
#[arg(long = "color-mode", value_name = "MODE")]
pub color_mode: Option<ColorModeArg>,
#[arg(long = "no-images")]
pub no_images: bool,
#[arg(long = "images", conflicts_with = "no_images")]
pub images: bool,
#[arg(short = 'q', long = "query", value_name = "EXPR")]
pub query: Option<String>,
#[arg(long = "query-help")]
pub query_help: bool,
#[arg(long = "query-output", value_name = "FORMAT")]
pub query_output: Option<String>,
}
#[derive(Debug, Clone, ValueEnum)]
pub enum ColorModeArg {
Auto,
Rgb,
#[value(name = "256")]
Color256,
}
#[derive(Debug, Clone, ValueEnum)]
pub enum OutputFormat {
Plain,
Json,
Tree,
}
#[cfg(feature = "unstable-dynamic")]
fn markdown_file_completer() -> ArgValueCompleter {
use std::ffi::OsStr;
use std::path::Path;
struct MarkdownCompleter;
impl ValueCompleter for MarkdownCompleter {
fn complete(&self, current: &OsStr) -> Vec<CompletionCandidate> {
let input_str = current.to_string_lossy();
let input_path = Path::new(input_str.as_ref());
let search_dir: &Path;
let prefix: String;
if input_str.is_empty() {
search_dir = Path::new(".");
prefix = String::new();
} else if input_str.ends_with('/') || input_str.ends_with('\\') {
search_dir = input_path;
prefix = String::new();
} else {
let parent = input_path.parent().unwrap_or(Path::new("."));
search_dir = if parent.as_os_str().is_empty() {
Path::new(".")
} else {
parent
};
prefix = input_path
.file_name()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_default();
};
let entries = match std::fs::read_dir(search_dir) {
Ok(entries) => entries,
Err(_) => return vec![],
};
entries
.filter_map(Result::ok)
.filter_map(|entry| {
let path = entry.path();
let is_dir = path.is_dir();
let file_name = path.file_name()?.to_string_lossy().to_string();
if !prefix.is_empty()
&& !file_name.to_lowercase().starts_with(&prefix.to_lowercase())
{
return None;
}
let completion_value = if search_dir == Path::new(".") {
file_name.clone()
} else {
search_dir.join(&file_name).to_string_lossy().to_string()
};
if is_dir {
let mut dir_completion = completion_value;
if !dir_completion.ends_with('/') {
dir_completion.push('/');
}
Some(
CompletionCandidate::new(dir_completion).help(Some("directory".into())),
)
} else if let Some(ext) = path.extension() {
let ext_lower = ext.to_string_lossy().to_lowercase();
if ext_lower == "md" || ext_lower == "markdown" {
Some(CompletionCandidate::new(completion_value))
} else {
None
}
} else {
None
}
})
.collect::<Vec<_>>()
}
}
ArgValueCompleter::new(MarkdownCompleter)
}
#[cfg(not(feature = "unstable-dynamic"))]
fn markdown_file_completer() -> clap::builder::ValueHint {
clap::ValueHint::FilePath
}