use clap::builder::Styles;
use clap::builder::styling::{AnsiColor, Effects};
use clap::{Parser, Subcommand, ValueEnum};
use std::path::PathBuf;
const STYLES: Styles = Styles::styled()
.header(AnsiColor::Green.on_default().effects(Effects::BOLD))
.usage(AnsiColor::Green.on_default().effects(Effects::BOLD))
.literal(AnsiColor::Cyan.on_default().effects(Effects::BOLD))
.placeholder(AnsiColor::Cyan.on_default());
#[derive(Parser)]
#[command(name = "panache")]
#[command(author, version)]
#[command(
about = "Panache: A language server, formatter, and linter for Pandoc, Quarto and R Markdown"
)]
#[command(styles = STYLES)]
#[command(
long_about = "Panache is a CLI formatter and LSP for Quarto (.qmd), Pandoc, and Markdown files \
written in Rust. It understands Quarto/Pandoc-specific syntax that other formatters like \
Prettier and mdformat struggle with, including fenced divs, tables, and math formatting."
)]
#[command(after_help = "For help with a specific command, see: `panache help <command>`.")]
#[command(arg_required_else_help = true)]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
#[arg(long, global = true, help_heading = "Global options")]
#[arg(help = "Path to configuration file")]
#[arg(
long_help = "Path to a custom configuration file. If not specified, Panache will \
search for .panache.toml or panache.toml in the current directory and its parents, \
then fall back to ~/.config/panache/config.toml."
)]
pub config: Option<PathBuf>,
#[arg(
long,
global = true,
value_name = "PATH",
help_heading = "Global options"
)]
#[arg(help = "Synthetic filename for stdin input (used for flavor detection)")]
#[arg(
long_help = "Synthetic filename to associate with stdin input. This is useful for editor \
integrations that pipe content via stdin but still need Panache to infer flavor/extensions \
from file extension (for example: --stdin-filename doc.qmd)."
)]
pub stdin_filename: Option<PathBuf>,
#[arg(
long,
global = true,
value_enum,
default_value = "auto",
value_name = "WHEN",
help_heading = "Global options"
)]
#[arg(help = "Control when colored output is used")]
pub color: ColorMode,
#[arg(long, global = true, help_heading = "Global options")]
#[arg(help = "Disable colored output (equivalent to --color never)")]
pub no_color: bool,
#[arg(long, global = true, help_heading = "Global options")]
#[arg(help = "Ignore all discovered configuration files")]
pub isolated: bool,
#[arg(
long,
global = true,
env = "PANACHE_NO_CACHE",
help_heading = "Global options"
)]
#[arg(help = "Disable all lint/format cache reads and writes for this run")]
#[arg(
long_help = "Disable all lint/format cache reads and writes for this run. Can also be enabled with PANACHE_NO_CACHE."
)]
pub no_cache: bool,
#[arg(
long,
global = true,
value_name = "CACHE_DIR",
env = "PANACHE_CACHE_DIR",
help_heading = "Global options"
)]
#[arg(help = "Path to the cache directory (overrides config cache-dir)")]
#[arg(
long_help = "Path to the cache directory for this invocation. Overrides config `cache-dir`. Can also be set with PANACHE_CACHE_DIR."
)]
pub cache_dir: Option<PathBuf>,
}
#[derive(Subcommand)]
pub enum Commands {
#[command(
long_about = "Format a Quarto, Pandoc, or R Markdown document according to Panache's \
formatting rules. By default, formats files in place. Use --check to verify formatting \
without making changes. With --verify, Panache runs parser/formatter invariants without \
writing changes to disk. Stdin input always outputs to stdout."
)]
Format {
#[arg(help = "Input file path(s) or directories")]
#[arg(
long_help = "Path(s) to the input file(s) or directories to format. If not provided, reads from stdin. \
Supports .qmd, .md, .Rmd/.Rmarkdown, and other Markdown-based formats. When file paths are \
provided, the files are formatted in place by default. Stdin input always outputs \
to stdout. Supports glob patterns (e.g., *.md) and directories (e.g., . or docs/). \
Directories are traversed recursively, respecting .gitignore files."
)]
files: Vec<PathBuf>,
#[arg(long)]
#[arg(help = "Check if file is formatted (exit code 1 if not)")]
#[arg(
long_help = "Check if the file is already formatted according to Panache's rules \
without making any changes. If the file is not formatted, displays a diff and exits \
with code 1. If formatted, exits with code 0. Useful for CI/CD pipelines."
)]
check: bool,
#[arg(long, value_name = "START:END")]
#[arg(help = "Format only lines START:END (e.g., --range 5:10) [Experimental]")]
#[arg(
long_help = "Format only the specified line range. Lines are 1-indexed and inclusive. \
The range will be expanded to complete block boundaries to ensure well-formed output. \
For example, if you select part of a list, the entire list will be formatted. \
Format: --range START:END (e.g., --range 5:10 formats lines 5 through 10). \
\n\nNote: This feature is experimental. Range filtering may not work correctly in all cases."
)]
range: Option<String>,
#[arg(long)]
#[arg(help = "[Deprecated] Verify losslessness and idempotency invariants")]
#[arg(long_help = "Run smoke-check invariants: \
(1) parser losslessness (input == parsed CST text) and \
(2) formatter idempotency (format(format(x)) == format(x)). \
When formatting files by path (including directories), --verify does not write any changes \
to disk. Exits with code 1 when verification fails. \
\n\nDeprecated: prefer `panache debug format --checks all`.")]
verify: bool,
#[arg(long)]
#[arg(help = "Apply exclude patterns to explicitly provided files")]
force_exclude: bool,
},
#[command(
long_about = "Parse a document and display its Concrete Syntax Tree (CST) for debugging \
and understanding how Panache interprets the document structure. The CST shows all block \
and inline elements detected by the parser."
)]
Parse {
#[arg(help = "Input file path")]
#[arg(
long_help = "Path to the input file to parse. If not provided, reads from stdin. \
The parser respects extension flags from the configuration file."
)]
file: Option<PathBuf>,
#[arg(long, value_name = "PATH")]
#[arg(help = "Write CST JSON output to PATH")]
#[arg(
long_help = "Write the parsed CST to the given JSON file instead of printing the debug tree. \
The output includes node kinds, text ranges, and token text."
)]
json: Option<PathBuf>,
#[arg(long)]
#[arg(help = "Do not print CST output")]
#[arg(
long_help = "Suppress CST output to stdout. Useful with --verify for smoke-testing \
large files without terminal spam. Verification failures still print diagnostics and exit non-zero."
)]
quiet: bool,
#[arg(long)]
#[arg(help = "[Deprecated] Verify parser losslessness invariant")]
#[arg(
long_help = "Run parser losslessness verification (input == parsed CST text). \
Exits with code 1 when verification fails. \
\n\nDeprecated: prefer `panache debug format --checks losslessness`."
)]
verify: bool,
},
#[command(
long_about = "Start the Panache language server protocol (LSP) server for editor \
integration. The LSP server provides formatting capabilities to editors like VS Code, \
Neovim, and others that support LSP."
)]
#[command(after_help = "\
The LSP server communicates via stdin/stdout and is typically launched automatically by your \
editor's LSP client. You generally don't need to run this command manually.
For editor configuration examples, see: https://github.com/jolars/panache#editor-integration")]
Lsp {
#[arg(long)]
#[arg(help = "Enable LSP debug logging to ~/.local/state/panache/lsp-debug.log")]
#[arg(
long_help = "Enable verbose LSP debug logging to ~/.local/state/panache/lsp-debug.log \
(or $XDG_STATE_HOME/panache/lsp-debug.log when XDG_STATE_HOME is set). \
Logs are written to file to avoid interfering with the LSP protocol over stdout."
)]
debug: bool,
},
#[command(
long_about = "Lint a document to check for correctness issues and best practice \
violations. Unlike the formatter which handles style, the linter catches semantic \
problems like syntax errors, heading hierarchy issues, and broken references."
)]
#[command(after_help = "Configure rules in panache.toml with [lint] section.")]
Lint {
#[arg(help = "Input file path(s) or directories")]
#[arg(
long_help = "Path(s) to the input file(s) or directories to check. If not provided, reads from stdin. \
Supports .qmd, .md, .Rmd/.Rmarkdown, and other Markdown-based formats. Supports glob patterns \
(e.g., *.md) and directories (e.g., . or docs/). Directories are traversed recursively, \
respecting .gitignore files."
)]
files: Vec<PathBuf>,
#[arg(long)]
#[arg(help = "Exit with code 1 if violations found (CI mode)")]
check: bool,
#[arg(long)]
#[arg(help = "Automatically fix violations where possible")]
fix: bool,
#[arg(long, value_enum, default_value = "human")]
#[arg(help = "Diagnostic rendering format")]
message_format: MessageFormat,
#[arg(long)]
#[arg(help = "Apply exclude patterns to explicitly provided files")]
force_exclude: bool,
},
#[command(
long_about = "Debugging utilities for parse/format workflows. These commands are intended \
for diagnosing parser losslessness and formatter idempotency failures in repositories."
)]
Debug {
#[command(subcommand)]
command: DebugCommands,
},
}
#[derive(Subcommand)]
pub enum DebugCommands {
#[command(name = "format")]
Format {
#[arg(help = "Input file path(s) or directories")]
files: Vec<PathBuf>,
#[arg(long, value_enum, default_value = "all")]
checks: DebugChecks,
#[arg(long)]
json: bool,
#[arg(long)]
report: bool,
#[arg(long, value_name = "DIR")]
dump_dir: Option<PathBuf>,
#[arg(long)]
#[arg(help = "Write input/parse/format pass artifacts to --dump-dir for every file")]
dump_passes: bool,
#[arg(long)]
#[arg(help = "Apply exclude patterns to explicitly provided files")]
force_exclude: bool,
},
}
#[derive(Clone, Copy, Debug, ValueEnum)]
pub enum DebugChecks {
Idempotency,
Losslessness,
All,
}
#[derive(Clone, Copy, Debug, ValueEnum)]
pub enum ColorMode {
Auto,
Always,
Never,
}
#[derive(Clone, Copy, Debug, ValueEnum)]
pub enum MessageFormat {
Human,
Short,
}