Skip to main content

fastpaper/
cli.rs

1use clap::{Parser, Subcommand, ValueEnum};
2use std::path::PathBuf;
3
4/// Fast academic paper search, download & read
5#[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    /// Increase verbosity (-v, -vv, -vvv)
18    #[arg(short, long, global = true, action = clap::ArgAction::Count)]
19    pub verbose: u8,
20
21    /// Suppress non-essential output
22    #[arg(short, long, global = true)]
23    pub quiet: bool,
24
25    /// Output format
26    #[arg(short, long, global = true, default_value = "table")]
27    pub format: OutputFormat,
28}
29
30#[derive(Subcommand)]
31pub enum Commands {
32    /// Search papers from a single academic source
33    Search(SearchArgs),
34
35    /// Download paper PDF/source files
36    Download(DownloadArgs),
37
38    /// Extract and display paper content
39    Read(ReadArgs),
40
41    /// Fetch a single paper by identifier (auto-detect source)
42    Get(GetArgs),
43
44    /// List available sources and capabilities
45    Sources(SourcesArgs),
46
47    /// Generate shell completions
48    Completions {
49        shell: clap_complete::Shell,
50    },
51}
52
53// ── search ──────────────────────────────────────
54
55#[derive(clap::Args)]
56pub struct SearchArgs {
57    /// Academic source to search
58    pub source: Source,
59
60    /// Search query string
61    pub query: String,
62
63    /// Max results
64    #[arg(short = 'n', long, default_value = "10")]
65    pub limit: u32,
66
67    /// Skip first N results
68    #[arg(long, default_value = "0")]
69    pub offset: u32,
70
71    /// Sort by field
72    #[arg(long, default_value = "relevance")]
73    pub sort: SortField,
74
75    /// Sort direction
76    #[arg(long, default_value = "desc")]
77    pub order: SortOrder,
78
79    /// Filter by author
80    #[arg(long)]
81    pub author: Option<String>,
82
83    /// Papers after date (YYYY-MM-DD)
84    #[arg(long)]
85    pub after: Option<String>,
86
87    /// Papers before date (YYYY-MM-DD)
88    #[arg(long)]
89    pub before: Option<String>,
90
91    /// Papers in specific year
92    #[arg(long)]
93    pub year: Option<u16>,
94
95    /// Field of study / category
96    #[arg(long)]
97    pub field: Option<String>,
98
99    /// Only open access papers
100    #[arg(long)]
101    pub open_access: bool,
102
103    /// Only peer-reviewed papers
104    #[arg(long)]
105    pub peer_reviewed: bool,
106
107    /// Comma-separated fields to include in output
108    #[arg(long)]
109    pub fields: Option<String>,
110
111    /// Include abstract in table output
112    #[arg(long)]
113    pub with_abstract: bool,
114
115    /// Write results to file
116    #[arg(short, long)]
117    pub output: Option<PathBuf>,
118}
119
120// ── download ────────────────────────────────────
121
122#[derive(clap::Args)]
123pub struct DownloadArgs {
124    /// Source to download from
125    pub source: Source,
126
127    /// Paper identifier
128    pub identifier: String,
129
130    /// Download directory
131    #[arg(short, long, env = "FASTPAPER_DOWNLOAD_DIR", default_value = "./papers")]
132    pub dir: PathBuf,
133
134    /// Filename template: {id}, {title}, {authors}, {year}, {doi}
135    #[arg(long, default_value = "{id}.{title}")]
136    pub filename: String,
137
138    /// Overwrite existing files
139    #[arg(long)]
140    pub overwrite: bool,
141
142    /// Download source/LaTeX instead of PDF (arXiv only)
143    #[arg(long)]
144    pub source_files: bool,
145}
146
147// ── read ────────────────────────────────────────
148
149#[derive(clap::Args)]
150pub struct ReadArgs {
151    /// Source to read from (use "local" for local files)
152    pub source: Source,
153
154    /// Paper identifier or file path
155    pub identifier: String,
156
157    /// Extract specific section
158    #[arg(long, default_value = "full")]
159    pub section: Section,
160
161    /// Only show metadata
162    #[arg(long)]
163    pub metadata_only: bool,
164
165    /// Raw text without formatting
166    #[arg(long)]
167    pub raw: bool,
168
169    /// Truncate output to N characters
170    #[arg(long)]
171    pub max_length: Option<usize>,
172
173    /// Write content to file
174    #[arg(short, long)]
175    pub output: Option<PathBuf>,
176}
177
178// ── get ─────────────────────────────────────────
179
180#[derive(clap::Args)]
181pub struct GetArgs {
182    /// DOI, arXiv ID, PMID, PMC ID, URL, etc.
183    pub identifier: String,
184
185    /// Resolve all available OA versions
186    #[arg(long)]
187    pub resolve: bool,
188
189    /// Include citation count and references
190    #[arg(long)]
191    pub with_citations: bool,
192
193    /// Include abstract
194    #[arg(long)]
195    pub with_abstract: bool,
196
197    /// Include related/recommended papers
198    #[arg(long)]
199    pub with_related: bool,
200}
201
202// ── sources ─────────────────────────────────────
203
204#[derive(clap::Args)]
205pub struct SourcesArgs {
206    /// Test connectivity to each source
207    #[arg(long)]
208    pub check: bool,
209
210    /// Show detailed capability matrix
211    #[arg(long)]
212    pub capabilities: bool,
213}
214
215// ── enums ───────────────────────────────────────
216
217#[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}