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    Semantic,
227    Crossref,
228    Openalex,
229    Dblp,
230    Core,
231    Openaire,
232    Doaj,
233    Unpaywall,
234    Zenodo,
235    Hal,
236    Local,
237}
238
239impl Source {
240    pub fn supports_search(&self) -> bool {
241        !matches!(self, Source::Local)
242    }
243
244    pub fn supports_download(&self) -> bool {
245        matches!(
246            self,
247            Source::Arxiv
248                | Source::Biorxiv
249                | Source::Medrxiv
250                | Source::Pmc
251                | Source::Semantic
252                | Source::Core
253                | Source::Doaj
254                | Source::Zenodo
255                | Source::Hal
256                | Source::Local
257        )
258    }
259
260    pub fn supports_read(&self) -> bool {
261        self.supports_download()
262    }
263
264    pub fn name(&self) -> &'static str {
265        match self {
266            Source::Arxiv => "arxiv",
267            Source::Biorxiv => "biorxiv",
268            Source::Medrxiv => "medrxiv",
269            Source::Pubmed => "pubmed",
270            Source::Pmc => "pmc",
271            Source::Europepmc => "europepmc",
272            Source::Scholar => "scholar",
273            Source::Semantic => "semantic",
274            Source::Crossref => "crossref",
275            Source::Openalex => "openalex",
276            Source::Dblp => "dblp",
277            Source::Core => "core",
278            Source::Openaire => "openaire",
279            Source::Doaj => "doaj",
280            Source::Unpaywall => "unpaywall",
281            Source::Zenodo => "zenodo",
282            Source::Hal => "hal",
283            Source::Local => "local",
284        }
285    }
286
287    pub fn download_hint(&self) -> Option<&'static str> {
288        match self {
289            Source::Pubmed => Some("Try: fastpaper download pmc <PMC_ID>"),
290            Source::Scholar => Some("Google Scholar does not provide PDFs directly"),
291            Source::Crossref | Source::Openalex | Source::Dblp => {
292                Some("This source only provides metadata. Try: fastpaper get <ID> --resolve")
293            }
294            Source::Openaire | Source::Unpaywall => {
295                Some("This source only provides metadata. Try: fastpaper get <ID> --resolve")
296            }
297            _ => None,
298        }
299    }
300}
301
302#[derive(ValueEnum, Clone, Debug)]
303pub enum OutputFormat {
304    Table,
305    Json,
306    Jsonl,
307    Csv,
308    Bibtex,
309}
310
311#[derive(ValueEnum, Clone, Debug)]
312pub enum SortField {
313    Relevance,
314    Date,
315    Citations,
316}
317
318#[derive(ValueEnum, Clone, Debug)]
319pub enum SortOrder {
320    Asc,
321    Desc,
322}
323
324#[derive(ValueEnum, Clone, Debug)]
325pub enum Section {
326    Abstract,
327    Introduction,
328    Methods,
329    Results,
330    Discussion,
331    Conclusion,
332    References,
333    Full,
334}