1use clap::{ArgAction, Args as ClapArgs, Parser, Subcommand, ValueEnum};
2use std::path::PathBuf;
3
4use crate::search::sources::git::GitSearchScope;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
7pub enum OutputFormat {
8 Plain,
9 Jsonl,
10}
11
12#[derive(Parser, Debug, Clone)]
13#[command(author, version, about, long_about = None)]
14pub struct Cli {
15 #[command(flatten)]
16 pub global: GlobalArgs,
17
18 #[command(subcommand)]
19 pub command: Option<Command>,
20}
21
22#[derive(ClapArgs, Debug, Clone)]
23pub struct GlobalArgs {
24 #[arg(short = 'H', long)]
26 pub headless: bool,
27
28 #[arg(long, value_enum, default_value_t = OutputFormat::Plain)]
30 pub output_format: OutputFormat,
31
32 #[arg(long)]
34 pub preview: Option<String>,
35
36 #[arg(long, default_value = ":")]
38 pub delimiter: String,
39
40 #[arg(short = 's', long)]
43 pub split: Option<String>,
44}
45
46#[derive(Subcommand, Debug, Clone)]
47pub enum Command {
48 Path(SearchCommandArgs),
50 Files(SearchCommandArgs),
52 #[command(alias = "grep")]
54 Content(ContentCommandArgs),
55 Dirs(SearchCommandArgs),
57 Log(LogCommandArgs),
59 Diff(DiffCommandArgs),
61 Git(GitCommandArgs),
63}
64
65#[derive(ClapArgs, Debug, Clone, Default)]
66pub struct SearchCommandArgs {
67 #[arg(index = 1)]
69 pub query: Option<String>,
70
71 #[arg(short = 'l', long, value_name = "DIR", action = ArgAction::Append)]
73 pub location: Vec<PathBuf>,
74
75 #[arg(short, long)]
77 pub exact: bool,
78
79 #[arg(long = "no-hidden")]
81 pub no_hidden: bool,
82
83 #[arg(long = "no-git-ignore")]
85 pub no_git_ignore: bool,
86
87 #[arg(long = "no-ignore")]
89 pub no_ignore: bool,
90
91 #[arg(long = "no-default-ignore-dirs")]
93 pub no_default_ignore_dirs: bool,
94}
95
96#[derive(ClapArgs, Debug, Clone, Default)]
97pub struct ContentCommandArgs {
98 #[command(flatten)]
99 pub search: SearchCommandArgs,
100
101 #[arg(long = "search-pdf")]
103 pub search_pdf: bool,
104}
105
106#[derive(ClapArgs, Debug, Clone, Default)]
107pub struct LogCommandArgs {
108 #[arg(value_name = "FILE")]
110 pub files: Vec<PathBuf>,
111}
112
113#[derive(ClapArgs, Debug, Clone)]
114pub struct DiffCommandArgs {
115 pub left: PathBuf,
116 pub right: PathBuf,
117}
118
119#[derive(ClapArgs, Debug, Clone)]
120pub struct GitCommandArgs {
121 #[command(subcommand)]
122 pub command: GitSubcommand,
123}
124
125#[derive(Subcommand, Debug, Clone)]
126pub enum GitSubcommand {
127 History(GitHistoryCommandArgs),
129 Branches(GitListCommandArgs),
131 #[command(alias = "logs")]
133 Commits(GitListCommandArgs),
134}
135
136#[derive(ClapArgs, Debug, Clone)]
137pub struct GitHistoryCommandArgs {
138 pub file: PathBuf,
139
140 #[arg(index = 2)]
142 pub query: Option<String>,
143
144 #[arg(short, long)]
146 pub exact: bool,
147}
148
149#[derive(ClapArgs, Debug, Clone, Default)]
150pub struct GitListCommandArgs {
151 #[arg(index = 1)]
153 pub query: Option<String>,
154
155 #[arg(short, long)]
157 pub exact: bool,
158}
159
160#[derive(Debug, Clone)]
161pub struct Args {
162 pub query: Option<String>,
163 pub location: Vec<PathBuf>,
164 pub dir_only: bool,
165 pub file_name: bool,
166 pub full_path: bool,
167 pub content: bool,
168 pub exact: bool,
169 pub no_hidden: bool,
170 pub no_git_ignore: bool,
171 pub no_ignore: bool,
172 pub no_default_ignore_dirs: bool,
173 pub search_pdf: bool,
174 pub git_history: Option<PathBuf>,
175 pub git_branches: bool,
176 pub git_commits: bool,
177 pub headless: bool,
178 pub diff: Option<Vec<PathBuf>>,
179 pub output_format: OutputFormat,
180 pub stdin: bool,
181 pub git_search_scope: Option<GitSearchScope>,
182 pub preview: Option<String>,
183 pub delimiter: String,
184 pub split: Option<String>,
185 pub log: bool,
186 pub log_files: Vec<PathBuf>,
187}
188
189impl Default for Args {
190 fn default() -> Self {
191 Self {
192 query: None,
193 location: vec![],
194 dir_only: false,
195 file_name: false,
196 full_path: false,
197 content: false,
198 exact: false,
199 no_hidden: false,
200 no_git_ignore: false,
201 no_ignore: false,
202 no_default_ignore_dirs: false,
203 search_pdf: false,
204 git_history: None,
205 git_branches: false,
206 git_commits: false,
207 headless: false,
208 diff: None,
209 output_format: OutputFormat::Plain,
210 stdin: false,
211 git_search_scope: None,
212 preview: None,
213 delimiter: ":".to_string(),
214 split: None,
215 log: false,
216 log_files: vec![],
217 }
218 }
219}
220
221impl Cli {
222 pub fn into_args(self) -> Args {
223 let mut args = Args {
224 headless: self.global.headless,
225 output_format: self.global.output_format,
226 preview: self.global.preview,
227 delimiter: self.global.delimiter,
228 split: self.global.split,
229 ..Args::default()
230 };
231
232 match self.command {
233 None => {
234 args.full_path = true;
235 }
236 Some(Command::Path(cmd)) => {
237 apply_search_command(&mut args, cmd);
238 args.full_path = true;
239 }
240 Some(Command::Files(cmd)) => {
241 apply_search_command(&mut args, cmd);
242 args.file_name = true;
243 }
244 Some(Command::Content(cmd)) => {
245 apply_search_command(&mut args, cmd.search);
246 args.content = true;
247 args.search_pdf = cmd.search_pdf;
248 }
249 Some(Command::Dirs(cmd)) => {
250 apply_search_command(&mut args, cmd);
251 args.dir_only = true;
252 }
253 Some(Command::Log(cmd)) => {
254 args.log = true;
255 args.log_files = cmd.files;
256 }
257 Some(Command::Diff(cmd)) => {
258 args.diff = Some(vec![cmd.left, cmd.right]);
259 }
260 Some(Command::Git(cmd)) => match cmd.command {
261 GitSubcommand::History(cmd) => {
262 args.git_history = Some(cmd.file);
263 args.query = cmd.query;
264 args.exact = cmd.exact;
265 }
266 GitSubcommand::Branches(cmd) => {
267 args.git_branches = true;
268 args.query = cmd.query;
269 args.exact = cmd.exact;
270 }
271 GitSubcommand::Commits(cmd) => {
272 args.git_commits = true;
273 args.query = cmd.query;
274 args.exact = cmd.exact;
275 }
276 },
277 }
278
279 args
280 }
281}
282
283fn apply_search_command(args: &mut Args, cmd: SearchCommandArgs) {
284 args.query = cmd.query;
285 args.location = cmd.location;
286 args.exact = cmd.exact;
287 args.no_hidden = cmd.no_hidden;
288 args.no_git_ignore = cmd.no_git_ignore;
289 args.no_ignore = cmd.no_ignore;
290 args.no_default_ignore_dirs = cmd.no_default_ignore_dirs;
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296 use clap::Parser;
297
298 #[test]
299 fn grep_alias_maps_to_content_command() {
300 let cli = Cli::parse_from(["binocular", "grep", "needle"]);
301 let args = cli.into_args();
302 assert!(args.content);
303 assert_eq!(args.query.as_deref(), Some("needle"));
304 }
305
306 #[test]
307 fn git_logs_alias_maps_to_commits_command() {
308 let cli = Cli::parse_from(["binocular", "git", "logs", "needle"]);
309 let args = cli.into_args();
310 assert!(args.git_commits);
311 assert_eq!(args.query.as_deref(), Some("needle"));
312 }
313}