use crate::engine::flags::GitFlags;
use crate::engine::lang::{LANGUAGE_SPECS, Language};
use crate::engine::output::ImageMode;
use crate::engine::target::{Target, TargetAddress};
use crate::engine::vision::VisionProfile;
use anyhow::{Context, Result, bail};
use argh::FromArgs;
use std::path::PathBuf;
pub(crate) mod run;
#[derive(Debug, FromArgs)]
#[argh(help_triggers("-h", "--help"))]
pub(crate) struct Cli {
#[argh(switch, short = 'V')]
pub(crate) version: bool,
#[argh(option, long = "output")]
pub(crate) output: Option<PathBuf>,
#[argh(option, long = "readseek-dir")]
pub(crate) readseek_dir: Option<PathBuf>,
#[argh(subcommand)]
pub(crate) command: Option<Command>,
}
#[derive(Debug, FromArgs)]
#[argh(subcommand)]
pub(crate) enum Command {
Detect(DetectCommand),
Read(ReadCommand),
View(ViewCommand),
Map(MapCommand),
Check(CheckCommand),
Symbol(SymbolCommand),
Identify(IdentifyCommand),
Def(DefCommand),
Refs(RefsCommand),
Rename(RenameCommand),
Search(SearchCommand),
Init(InitCommand),
}
#[derive(Debug, FromArgs)]
#[argh(subcommand, name = "detect")]
#[argh(help_triggers("-h", "--help"))]
pub(crate) struct DetectCommand {
#[argh(positional)]
pub(crate) target: Option<String>,
}
#[derive(Debug, FromArgs)]
#[argh(subcommand, name = "read")]
#[argh(help_triggers("-h", "--help"))]
pub(crate) struct ReadCommand {
#[argh(positional)]
pub(crate) target: Option<String>,
#[argh(option)]
pub(crate) end: Option<usize>,
#[argh(option)]
pub(crate) limit: Option<usize>,
#[argh(option)]
pub(crate) page: Option<usize>,
#[argh(option, from_str_fn(parse_language))]
pub(crate) language: Option<Language>,
#[argh(option, from_str_fn(parse_image_mode))]
pub(crate) image: Option<ImageMode>,
#[argh(option, from_str_fn(parse_vision_profile))]
pub(crate) vision_profile: Option<VisionProfile>,
#[argh(switch)]
pub(crate) vision_diagnostics: bool,
#[argh(option)]
pub(crate) vision_benchmark: Option<usize>,
}
#[derive(Debug, FromArgs)]
#[argh(subcommand, name = "view")]
#[argh(help_triggers("-h", "--help"))]
pub(crate) struct ViewCommand {
#[argh(positional)]
pub(crate) target: Option<String>,
#[argh(
option,
long = "format",
default = "crate::engine::output::Format::Plain"
)]
pub(crate) format: crate::engine::output::Format,
#[argh(option)]
pub(crate) node: Option<String>,
#[argh(option)]
pub(crate) page: Option<usize>,
#[argh(option)]
pub(crate) kind: Option<crate::engine::document::NodeKind>,
#[argh(option)]
pub(crate) depth: Option<usize>,
#[argh(switch)]
pub(crate) outline: bool,
}
#[derive(Debug, FromArgs)]
#[argh(subcommand, name = "map")]
#[argh(help_triggers("-h", "--help"))]
pub(crate) struct MapCommand {
#[argh(positional)]
pub(crate) target: Option<String>,
#[argh(option, from_str_fn(parse_language))]
pub(crate) language: Option<Language>,
}
#[derive(Debug, FromArgs)]
#[argh(subcommand, name = "check")]
#[argh(help_triggers("-h", "--help"))]
pub(crate) struct CheckCommand {
#[argh(positional)]
pub(crate) target: Option<String>,
#[argh(option, from_str_fn(parse_language))]
pub(crate) language: Option<Language>,
}
#[derive(Debug, FromArgs)]
#[argh(subcommand, name = "symbol")]
#[argh(help_triggers("-h", "--help"))]
pub(crate) struct SymbolCommand {
#[argh(positional)]
pub(crate) target: Option<String>,
#[argh(switch)]
pub(crate) name: bool,
#[argh(option, from_str_fn(parse_language))]
pub(crate) language: Option<Language>,
}
#[derive(Debug, FromArgs)]
#[argh(subcommand, name = "identify")]
#[argh(help_triggers("-h", "--help"))]
pub(crate) struct IdentifyCommand {
#[argh(positional)]
pub(crate) target: Option<String>,
#[argh(option)]
pub(crate) column: Option<usize>,
#[argh(option, from_str_fn(parse_language))]
pub(crate) language: Option<Language>,
}
#[derive(Debug, FromArgs)]
#[argh(subcommand, name = "def")]
#[argh(help_triggers("-h", "--help"))]
#[allow(clippy::struct_excessive_bools)]
pub(crate) struct DefCommand {
#[argh(positional)]
pub(crate) target: PathBuf,
#[argh(positional)]
pub(crate) name: String,
#[argh(
option,
long = "format",
default = "crate::engine::output::Format::Json"
)]
pub(crate) format: crate::engine::output::Format,
#[argh(option, from_str_fn(parse_language))]
pub(crate) language: Option<Language>,
#[argh(switch, short = 'c')]
pub(crate) cached: bool,
#[argh(switch, short = 'o')]
pub(crate) others: bool,
#[argh(switch, short = 'i')]
pub(crate) ignored: bool,
}
#[derive(Debug, FromArgs)]
#[argh(subcommand, name = "refs")]
#[argh(help_triggers("-h", "--help"))]
#[allow(clippy::struct_excessive_bools)]
pub(crate) struct RefsCommand {
#[argh(positional)]
pub(crate) target: PathBuf,
#[argh(positional)]
pub(crate) name: String,
#[argh(
option,
long = "format",
default = "crate::engine::output::Format::Json"
)]
pub(crate) format: crate::engine::output::Format,
#[argh(switch)]
pub(crate) scope: bool,
#[argh(option)]
pub(crate) line: Option<usize>,
#[argh(option)]
pub(crate) column: Option<usize>,
#[argh(option, from_str_fn(parse_language))]
pub(crate) language: Option<Language>,
#[argh(switch, short = 'c')]
pub(crate) cached: bool,
#[argh(switch, short = 'o')]
pub(crate) others: bool,
#[argh(switch, short = 'i')]
pub(crate) ignored: bool,
}
#[derive(Debug, FromArgs)]
#[argh(subcommand, name = "rename")]
#[argh(help_triggers("-h", "--help"))]
#[allow(clippy::struct_excessive_bools)]
pub(crate) struct RenameCommand {
#[argh(positional)]
pub(crate) target: PathBuf,
#[argh(option)]
pub(crate) line: usize,
#[argh(option)]
pub(crate) column: Option<usize>,
#[argh(option, long = "to")]
pub(crate) to: String,
#[argh(option)]
pub(crate) workspace: Option<PathBuf>,
#[argh(switch)]
pub(crate) apply: bool,
#[argh(option)]
pub(crate) plan_hash: Option<String>,
#[argh(option, from_str_fn(parse_language))]
pub(crate) language: Option<Language>,
#[argh(switch, short = 'c')]
pub(crate) cached: bool,
#[argh(switch, short = 'o')]
pub(crate) others: bool,
#[argh(switch, short = 'i')]
pub(crate) ignored: bool,
}
#[derive(Debug, FromArgs)]
#[argh(subcommand, name = "search")]
#[argh(help_triggers("-h", "--help"))]
#[allow(clippy::struct_excessive_bools)]
pub(crate) struct SearchCommand {
#[argh(positional)]
pub(crate) target: PathBuf,
#[argh(positional)]
pub(crate) pattern: String,
#[argh(option, from_str_fn(parse_language))]
pub(crate) language: Option<Language>,
#[argh(switch, short = 'c')]
pub(crate) cached: bool,
#[argh(switch, short = 'o')]
pub(crate) others: bool,
#[argh(switch, short = 'i')]
pub(crate) ignored: bool,
}
#[derive(Debug, FromArgs)]
#[argh(subcommand, name = "init")]
#[argh(help_triggers("-h", "--help"))]
pub(crate) struct InitCommand {
#[argh(positional)]
pub(crate) path: Option<PathBuf>,
}
pub(crate) fn parse_language(value: &str) -> std::result::Result<Language, String> {
if let Ok(language) = value.parse::<Language>() {
return Ok(language);
}
let alias = value.to_ascii_lowercase().replace(['-', '_'], "");
LANGUAGE_SPECS
.iter()
.find_map(|spec| {
spec.aliases
.contains(&alias.as_str())
.then_some(spec.language)
})
.ok_or_else(|| format!("unknown language `{value}`"))
}
pub(crate) fn parse_image_mode(value: &str) -> std::result::Result<ImageMode, String> {
match value {
"none" => Ok(ImageMode::None),
"all" => Ok(ImageMode::All),
"caption" => Ok(ImageMode::Caption),
"objects" => Ok(ImageMode::Objects),
"ocr" => Ok(ImageMode::Ocr),
_ => Err(format!(
"unknown image mode `{value}`; expected none, all, caption, objects, or ocr"
)),
}
}
pub(crate) fn parse_vision_profile(value: &str) -> std::result::Result<VisionProfile, String> {
match value {
"fast" => Ok(VisionProfile::Fast),
"balanced" => Ok(VisionProfile::Balanced),
"accurate" => Ok(VisionProfile::Accurate),
_ => Err(format!(
"unknown vision profile `{value}`; expected fast, balanced, or accurate"
)),
}
}
pub(crate) fn parse_target(value: &str, name_mode: bool) -> Result<Target> {
if value.is_empty() {
bail!("target must not be empty");
}
let (read_stdin, rest) = match value.strip_prefix("stdin:") {
Some(rest) => (true, rest),
None => (false, value),
};
let (path, address) = if rest.is_empty() {
(PathBuf::new(), None)
} else if let Some((path, suffix)) = rest.rsplit_once(':') {
let address = if suffix.is_empty() {
None
} else if name_mode {
Some(TargetAddress::Name(suffix.to_owned()))
} else if suffix.chars().all(|ch| ch.is_ascii_digit()) {
let line = suffix
.parse::<usize>()
.with_context(|| format!("invalid target line: {suffix}"))?;
if line == 0 {
bail!("target line must be greater than zero");
}
Some(TargetAddress::Line(line))
} else if is_line_hash(suffix) {
Some(TargetAddress::Hash(suffix.to_ascii_lowercase()))
} else {
None
};
if address.is_some() {
(PathBuf::from(path), address)
} else {
(PathBuf::from(rest), None)
}
} else {
(PathBuf::from(rest), None)
};
if !read_stdin && path.as_os_str().is_empty() {
bail!("target path must not be empty");
}
Ok(Target {
path,
address,
read_stdin,
})
}
fn is_line_hash(value: &str) -> bool {
value.len() == 3 && value.chars().all(|ch| ch.is_ascii_hexdigit())
}
pub(crate) trait GitSelection {
fn git_flags(&self) -> GitFlags;
}
macro_rules! impl_git_selection {
($($command:ty),+ $(,)?) => {
$(
impl GitSelection for $command {
fn git_flags(&self) -> GitFlags {
GitFlags {
cached: self.cached,
others: self.others,
ignored: self.ignored,
}
}
}
)+
};
}
impl_git_selection!(DefCommand, RefsCommand, RenameCommand, SearchCommand);