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