1use clap::{Parser, Subcommand, ValueEnum};
2use std::path::PathBuf;
3
4#[derive(Parser)]
6#[command(name = "fastpaper", version, about, long_about = None)]
7pub struct Cli {
8 #[command(subcommand)]
9 pub command: Commands,
10
11 #[command(flatten)]
12 pub global: GlobalOpts,
13}
14
15#[derive(clap::Args)]
16pub struct GlobalOpts {
17 #[arg(short, long, global = true, action = clap::ArgAction::Count)]
19 pub verbose: u8,
20
21 #[arg(short, long, global = true)]
23 pub quiet: bool,
24
25 #[arg(short, long, global = true, default_value = "table")]
27 pub format: OutputFormat,
28}
29
30#[derive(Subcommand)]
31pub enum Commands {
32 Search(SearchArgs),
34
35 Download(DownloadArgs),
37
38 Read(ReadArgs),
40
41 Get(GetArgs),
43
44 Sources(SourcesArgs),
46
47 Completions {
49 shell: clap_complete::Shell,
50 },
51}
52
53#[derive(clap::Args)]
56pub struct SearchArgs {
57 pub source: Source,
59
60 pub query: String,
62
63 #[arg(short = 'n', long, default_value = "10")]
65 pub limit: u32,
66
67 #[arg(long, default_value = "0")]
69 pub offset: u32,
70
71 #[arg(long, default_value = "relevance")]
73 pub sort: SortField,
74
75 #[arg(long, default_value = "desc")]
77 pub order: SortOrder,
78
79 #[arg(long)]
81 pub author: Option<String>,
82
83 #[arg(long)]
85 pub after: Option<String>,
86
87 #[arg(long)]
89 pub before: Option<String>,
90
91 #[arg(long)]
93 pub year: Option<u16>,
94
95 #[arg(long)]
97 pub field: Option<String>,
98
99 #[arg(long)]
101 pub open_access: bool,
102
103 #[arg(long)]
105 pub peer_reviewed: bool,
106
107 #[arg(long)]
109 pub fields: Option<String>,
110
111 #[arg(long)]
113 pub with_abstract: bool,
114
115 #[arg(short, long)]
117 pub output: Option<PathBuf>,
118}
119
120#[derive(clap::Args)]
123pub struct DownloadArgs {
124 pub source: Source,
126
127 pub identifier: String,
129
130 #[arg(short, long, env = "FASTPAPER_DOWNLOAD_DIR", default_value = "./papers")]
132 pub dir: PathBuf,
133
134 #[arg(long, default_value = "{id}.{title}")]
136 pub filename: String,
137
138 #[arg(long)]
140 pub overwrite: bool,
141
142 #[arg(long)]
144 pub source_files: bool,
145}
146
147#[derive(clap::Args)]
150pub struct ReadArgs {
151 pub source: Source,
153
154 pub identifier: String,
156
157 #[arg(long, default_value = "full")]
159 pub section: Section,
160
161 #[arg(long)]
163 pub metadata_only: bool,
164
165 #[arg(long)]
167 pub raw: bool,
168
169 #[arg(long)]
171 pub max_length: Option<usize>,
172
173 #[arg(short, long)]
175 pub output: Option<PathBuf>,
176}
177
178#[derive(clap::Args)]
181pub struct GetArgs {
182 pub identifier: String,
184
185 #[arg(long)]
187 pub resolve: bool,
188
189 #[arg(long)]
191 pub with_citations: bool,
192
193 #[arg(long)]
195 pub with_abstract: bool,
196
197 #[arg(long)]
199 pub with_related: bool,
200}
201
202#[derive(clap::Args)]
205pub struct SourcesArgs {
206 #[arg(long)]
208 pub check: bool,
209
210 #[arg(long)]
212 pub capabilities: bool,
213}
214
215#[derive(ValueEnum, Clone, Debug)]
218pub enum Source {
219 Arxiv,
220 Biorxiv,
221 Medrxiv,
222 Pubmed,
223 Pmc,
224 Europepmc,
225 Scholar,
226 Xueshu,
227 Semantic,
228 Crossref,
229 Openalex,
230 Dblp,
231 Core,
232 Openaire,
233 Doaj,
234 Unpaywall,
235 Zenodo,
236 Hal,
237 Local,
238}
239
240impl Source {
241 pub fn supports_search(&self) -> bool {
242 !matches!(self, Source::Local)
243 }
244
245 pub fn supports_download(&self) -> bool {
246 matches!(
247 self,
248 Source::Arxiv
249 | Source::Biorxiv
250 | Source::Medrxiv
251 | Source::Pmc
252 | Source::Semantic
253 | Source::Core
254 | Source::Doaj
255 | Source::Zenodo
256 | Source::Hal
257 | Source::Local
258 )
259 }
260
261 pub fn supports_read(&self) -> bool {
262 self.supports_download()
263 }
264
265 pub fn name(&self) -> &'static str {
266 match self {
267 Source::Arxiv => "arxiv",
268 Source::Biorxiv => "biorxiv",
269 Source::Medrxiv => "medrxiv",
270 Source::Pubmed => "pubmed",
271 Source::Pmc => "pmc",
272 Source::Europepmc => "europepmc",
273 Source::Scholar => "scholar",
274 Source::Xueshu => "xueshu",
275 Source::Semantic => "semantic",
276 Source::Crossref => "crossref",
277 Source::Openalex => "openalex",
278 Source::Dblp => "dblp",
279 Source::Core => "core",
280 Source::Openaire => "openaire",
281 Source::Doaj => "doaj",
282 Source::Unpaywall => "unpaywall",
283 Source::Zenodo => "zenodo",
284 Source::Hal => "hal",
285 Source::Local => "local",
286 }
287 }
288
289 pub fn download_hint(&self) -> Option<&'static str> {
290 match self {
291 Source::Pubmed => Some("Try: fastpaper download pmc <PMC_ID>"),
292 Source::Scholar => Some("Google Scholar does not provide PDFs directly"),
293 Source::Xueshu => {
294 Some("Baidu Xueshu aggregates external links only. Try: fastpaper get <DOI> --resolve")
295 }
296 Source::Crossref | Source::Openalex | Source::Dblp => {
297 Some("This source only provides metadata. Try: fastpaper get <ID> --resolve")
298 }
299 Source::Openaire | Source::Unpaywall => {
300 Some("This source only provides metadata. Try: fastpaper get <ID> --resolve")
301 }
302 _ => None,
303 }
304 }
305}
306
307#[derive(ValueEnum, Clone, Debug)]
308pub enum OutputFormat {
309 Table,
310 Json,
311 Jsonl,
312 Csv,
313 Bibtex,
314}
315
316#[derive(ValueEnum, Clone, Debug)]
317pub enum SortField {
318 Relevance,
319 Date,
320 Citations,
321}
322
323#[derive(ValueEnum, Clone, Debug)]
324pub enum SortOrder {
325 Asc,
326 Desc,
327}
328
329#[derive(ValueEnum, Clone, Debug)]
330pub enum Section {
331 Abstract,
332 Introduction,
333 Methods,
334 Results,
335 Discussion,
336 Conclusion,
337 References,
338 Full,
339}