lixplore-cli 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lixplore/__init__.py +0 -0
- lixplore/__main__.py +10 -0
- lixplore/cli.py +57 -0
- lixplore/commands.py +1543 -0
- lixplore/dispatcher.py +814 -0
- lixplore/search.py +33 -0
- lixplore/sources/__init__.py +0 -0
- lixplore/sources/arxiv.py +108 -0
- lixplore/sources/crossref.py +102 -0
- lixplore/sources/doaj.py +108 -0
- lixplore/sources/europepmc.py +96 -0
- lixplore/sources/pubmed.py +131 -0
- lixplore/templates/__init__.py +1 -0
- lixplore/templates/ieee.json +15 -0
- lixplore/templates/nature.json +15 -0
- lixplore/templates/science.json +15 -0
- lixplore/utils/annotations.py +486 -0
- lixplore/utils/cache.py +20 -0
- lixplore/utils/citations.py +269 -0
- lixplore/utils/custom_apis.py +291 -0
- lixplore/utils/enhanced_tui.py +849 -0
- lixplore/utils/enrichment.py +338 -0
- lixplore/utils/export.py +833 -0
- lixplore/utils/interactive_tui.py +425 -0
- lixplore/utils/pdf_downloader.py +495 -0
- lixplore/utils/profiles.py +213 -0
- lixplore/utils/reference_managers.py +302 -0
- lixplore/utils/shell_mode.py +563 -0
- lixplore/utils/statistics.py +355 -0
- lixplore/utils/template_engine.py +187 -0
- lixplore/utils/terminal.py +246 -0
- lixplore/utils/wizard_mode.py +334 -0
- lixplore_cli-1.0.0.dist-info/METADATA +619 -0
- lixplore_cli-1.0.0.dist-info/RECORD +38 -0
- lixplore_cli-1.0.0.dist-info/WHEEL +5 -0
- lixplore_cli-1.0.0.dist-info/entry_points.txt +2 -0
- lixplore_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
- lixplore_cli-1.0.0.dist-info/top_level.txt +1 -0
lixplore/commands.py
ADDED
|
@@ -0,0 +1,1543 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from . import dispatcher
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def add_commands(parser: argparse.ArgumentParser):
|
|
10
|
+
"""Register CLI arguments for lixplore."""
|
|
11
|
+
|
|
12
|
+
# Custom description and epilog for detailed help
|
|
13
|
+
parser.description = """
|
|
14
|
+
Lixplore - Academic Literature Search & Export Tool
|
|
15
|
+
|
|
16
|
+
Search across multiple academic databases (PubMed, Crossref, DOAJ, EuropePMC, arXiv)
|
|
17
|
+
and export results in various formats (CSV, Excel, JSON, BibTeX, RIS, EndNote, XML).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
parser.epilog = """
|
|
21
|
+
EXAMPLES:
|
|
22
|
+
Basic search:
|
|
23
|
+
lixplore -P -q "cancer treatment" -m 10
|
|
24
|
+
|
|
25
|
+
Multi-source search with export:
|
|
26
|
+
lixplore -s PX -q "machine learning" -m 20 -X xlsx -o ml_papers.xlsx
|
|
27
|
+
|
|
28
|
+
Search all sources with deduplication:
|
|
29
|
+
lixplore -A -q "COVID-19" -m 50 -D -X csv
|
|
30
|
+
|
|
31
|
+
Search with date filter and abstracts:
|
|
32
|
+
lixplore -P -q "diabetes" -d 2020-01-01 2024-12-31 -m 15 -a
|
|
33
|
+
|
|
34
|
+
Search by author:
|
|
35
|
+
lixplore -P -au "Smith J" -m 10 -a
|
|
36
|
+
|
|
37
|
+
Export to EndNote:
|
|
38
|
+
lixplore -P -q "neuroscience" -m 30 -X enw -o neuro_papers.enw
|
|
39
|
+
|
|
40
|
+
Review articles in separate terminal (two-step):
|
|
41
|
+
lixplore -P -q "paracetamol" -m 10 # Step 1: Search
|
|
42
|
+
lixplore -R 1 5 9 # Step 2: Review articles #1, #5, #9
|
|
43
|
+
# In review window: Press 'q' or Ctrl+C to close
|
|
44
|
+
|
|
45
|
+
SOURCE CODES (for -s flag):
|
|
46
|
+
P = PubMed C = Crossref J = DOAJ
|
|
47
|
+
E = EuropePMC X = arXiv A = All sources
|
|
48
|
+
|
|
49
|
+
Examples: -s PX (PubMed+arXiv), -s PCE (PubMed+Crossref+EuropePMC)
|
|
50
|
+
|
|
51
|
+
EXPORT FORMATS:
|
|
52
|
+
csv - CSV format (Excel, Google Sheets)
|
|
53
|
+
xlsx - Microsoft Excel format with formatting
|
|
54
|
+
json - JSON structured data
|
|
55
|
+
bibtex - BibTeX format for LaTeX citations
|
|
56
|
+
ris - RIS format (Zotero, Mendeley, RefWorks)
|
|
57
|
+
enw - EndNote Tagged format (recommended for EndNote)
|
|
58
|
+
endnote - EndNote XML format
|
|
59
|
+
xml - Generic XML format
|
|
60
|
+
|
|
61
|
+
EXPORT LOCATIONS:
|
|
62
|
+
All exports are automatically saved to organized folders:
|
|
63
|
+
exports/csv/ - CSV files
|
|
64
|
+
exports/excel/ - Excel files
|
|
65
|
+
exports/json/ - JSON files
|
|
66
|
+
exports/bibtex/ - BibTeX files
|
|
67
|
+
exports/ris/ - RIS files
|
|
68
|
+
exports/endnote_tagged/ - EndNote .enw files
|
|
69
|
+
exports/endnote_xml/ - EndNote XML files
|
|
70
|
+
exports/xml/ - Generic XML files
|
|
71
|
+
|
|
72
|
+
For more information, visit: https://github.com/yourusername/lixplore
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
# ===== SOURCE SELECTION =====
|
|
76
|
+
source_group = parser.add_argument_group(
|
|
77
|
+
'[SOURCE SELECTION]',
|
|
78
|
+
'Choose which academic databases to search'
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
source_group.add_argument(
|
|
82
|
+
"-P", "--pubmed", action="store_true",
|
|
83
|
+
help="Search PubMed (biomedical and life sciences literature)"
|
|
84
|
+
)
|
|
85
|
+
source_group.add_argument(
|
|
86
|
+
"-C", "--crossref", action="store_true",
|
|
87
|
+
help="Search Crossref (scholarly works with DOIs)"
|
|
88
|
+
)
|
|
89
|
+
source_group.add_argument(
|
|
90
|
+
"-J", "--doaj", action="store_true",
|
|
91
|
+
help="Search DOAJ - Directory of Open Access Journals"
|
|
92
|
+
)
|
|
93
|
+
source_group.add_argument(
|
|
94
|
+
"-E", "--europepmc", action="store_true",
|
|
95
|
+
help="Search EuropePMC (Europe PubMed Central)"
|
|
96
|
+
)
|
|
97
|
+
source_group.add_argument(
|
|
98
|
+
"-x", "--arxiv", action="store_true",
|
|
99
|
+
help="Search arXiv (preprint repository for physics, math, CS, etc.)"
|
|
100
|
+
)
|
|
101
|
+
source_group.add_argument(
|
|
102
|
+
"-A", "--all", action="store_true",
|
|
103
|
+
help="Search ALL sources at once (PubMed, Crossref, DOAJ, EuropePMC, arXiv)"
|
|
104
|
+
)
|
|
105
|
+
source_group.add_argument(
|
|
106
|
+
"-s", "--sources", type=str, metavar="CODES",
|
|
107
|
+
help="Combined source selection using codes: P=PubMed, C=Crossref, J=DOAJ, E=EuropePMC, X=arXiv, A=All. Example: -s PX for PubMed+arXiv, -s PCJE for multiple"
|
|
108
|
+
)
|
|
109
|
+
source_group.add_argument(
|
|
110
|
+
"--custom-api", type=str, metavar="NAME",
|
|
111
|
+
help="Use a custom API source (Springer, BASE, etc.). Must be configured in ~/.lixplore/apis/ or ~/.lixplore/custom_apis.json. Example: --custom-api springer"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# ===== SEARCH PARAMETERS =====
|
|
115
|
+
search_group = parser.add_argument_group(
|
|
116
|
+
'[SEARCH PARAMETERS]',
|
|
117
|
+
'Define what and how to search'
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
search_group.add_argument(
|
|
121
|
+
"-q", "--query", type=str, metavar="TEXT",
|
|
122
|
+
help="Search query string. Supports Boolean operators: AND, OR, NOT, parentheses for grouping. Examples: -q \"cancer treatment\" | -q \"cancer AND treatment\" | -q \"(cancer OR tumor) AND treatment\" | -q \"diabetes NOT type1\""
|
|
123
|
+
)
|
|
124
|
+
search_group.add_argument(
|
|
125
|
+
"-au", "--author", type=str, metavar="NAME",
|
|
126
|
+
help="Search by author name. Example: -au \"Smith J\" or -au \"Einstein A\""
|
|
127
|
+
)
|
|
128
|
+
search_group.add_argument(
|
|
129
|
+
"-DOI", "--doi", type=str, metavar="DOI",
|
|
130
|
+
help="Search for a specific article by DOI. Example: -DOI \"10.1038/nature12345\""
|
|
131
|
+
)
|
|
132
|
+
search_group.add_argument(
|
|
133
|
+
"-m", "--max_results", type=int, default=10, metavar="N",
|
|
134
|
+
help="Maximum number of results to fetch per source (default: 10). Example: -m 50"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# ===== FILTERING & PROCESSING =====
|
|
138
|
+
filter_group = parser.add_argument_group(
|
|
139
|
+
'[FILTERING & PROCESSING]',
|
|
140
|
+
'Filter and process search results'
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
filter_group.add_argument(
|
|
144
|
+
"-d", "--date", nargs=2, metavar=("FROM", "TO"),
|
|
145
|
+
help="Filter by publication date range in YYYY-MM-DD format. Example: -d 2020-01-01 2024-12-31"
|
|
146
|
+
)
|
|
147
|
+
filter_group.add_argument(
|
|
148
|
+
"-D", "--deduplicate", nargs="?", const="auto", metavar="STRATEGY",
|
|
149
|
+
choices=["auto", "doi_only", "title_only", "strict", "loose"],
|
|
150
|
+
help="Remove duplicate results. Strategies: auto (default, multi-level), doi_only (DOI matching only), title_only (title similarity only), strict (high threshold 0.95), loose (low threshold 0.75). Example: -D or -D strict"
|
|
151
|
+
)
|
|
152
|
+
filter_group.add_argument(
|
|
153
|
+
"--dedup-threshold", type=float, default=0.85, metavar="FLOAT",
|
|
154
|
+
help="Title similarity threshold for deduplication (0.0-1.0). Default: 0.85. Example: --dedup-threshold 0.9"
|
|
155
|
+
)
|
|
156
|
+
filter_group.add_argument(
|
|
157
|
+
"--dedup-keep", type=str,
|
|
158
|
+
choices=["first", "most_complete", "prefer_doi"],
|
|
159
|
+
default="most_complete", metavar="STRATEGY",
|
|
160
|
+
help="Which duplicate to keep: first (chronological), most_complete (most metadata), prefer_doi (prefer entries with DOI). Default: most_complete"
|
|
161
|
+
)
|
|
162
|
+
filter_group.add_argument(
|
|
163
|
+
"--dedup-merge", action="store_true",
|
|
164
|
+
help="Merge metadata from duplicates instead of discarding. Combines best data from all duplicates."
|
|
165
|
+
)
|
|
166
|
+
filter_group.add_argument(
|
|
167
|
+
"--sort", type=str, choices=["relevant", "newest", "oldest", "journal", "author"],
|
|
168
|
+
default="relevant", metavar="ORDER",
|
|
169
|
+
help="Sort results by: relevant (default/original order), newest (latest first), oldest (earliest first), journal (alphabetical), author (by first author). Example: --sort newest"
|
|
170
|
+
)
|
|
171
|
+
filter_group.add_argument(
|
|
172
|
+
"--enrich", nargs="*", metavar="API",
|
|
173
|
+
choices=["crossref", "pubmed", "arxiv", "all"],
|
|
174
|
+
help="Enrich metadata using external APIs. Use without arguments for all, or specify: crossref, pubmed, arxiv. Automatically validates and finds missing DOIs. Example: --enrich or --enrich crossref pubmed"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# ===== DISPLAY OPTIONS =====
|
|
178
|
+
display_group = parser.add_argument_group(
|
|
179
|
+
'[DISPLAY OPTIONS]',
|
|
180
|
+
'Control how results are displayed'
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
display_group.add_argument(
|
|
184
|
+
"-a", "--abstract", action="store_true",
|
|
185
|
+
help="Display abstracts along with titles in the results"
|
|
186
|
+
)
|
|
187
|
+
display_group.add_argument(
|
|
188
|
+
"-i", "--interactive", action="store_true",
|
|
189
|
+
help="Launch simple interactive TUI mode for searching and browsing (can be used standalone: lixplore -i)"
|
|
190
|
+
)
|
|
191
|
+
display_group.add_argument(
|
|
192
|
+
"-N", "--number", type=int, nargs="+", default=[], metavar="N",
|
|
193
|
+
help="View detailed information for specific article(s) by number. Example: -N 1 or -N 1 2 3"
|
|
194
|
+
)
|
|
195
|
+
display_group.add_argument(
|
|
196
|
+
"-R", "--review", type=int, nargs="+", default=[], metavar="N",
|
|
197
|
+
help="Open article(s) in separate terminal window for detailed review. Two modes: 1) With search: -P -q 'query' -R 1 2, or 2) Standalone: lixplore -R 1 2 (uses cached results). Close window with 'q' or Ctrl+C. Example: -R 1 or -R 1 2 3"
|
|
198
|
+
)
|
|
199
|
+
display_group.add_argument(
|
|
200
|
+
"--stat", action="store_true",
|
|
201
|
+
help="Show comprehensive statistics dashboard with visualizations (publication trends, top journals, top authors, source distribution)"
|
|
202
|
+
)
|
|
203
|
+
display_group.add_argument(
|
|
204
|
+
"--stat-top", type=int, default=10, metavar="N",
|
|
205
|
+
help="Number of top items to show in statistics rankings (default: 10)"
|
|
206
|
+
)
|
|
207
|
+
display_group.add_argument(
|
|
208
|
+
"-p", "--page", type=int, default=1, metavar="N",
|
|
209
|
+
help="Page number to display when results exceed page size (default: 1). Example: -p 2"
|
|
210
|
+
)
|
|
211
|
+
display_group.add_argument(
|
|
212
|
+
"--page-size", type=int, default=20, metavar="N",
|
|
213
|
+
help="Number of results to display per page (default: 20). Example: --page-size 50"
|
|
214
|
+
)
|
|
215
|
+
display_group.add_argument(
|
|
216
|
+
"--show-pdf-links", action="store_true",
|
|
217
|
+
help="Display PDF links for open access articles (from PMC, arXiv, Unpaywall). Links are clickable in modern terminals. Example: -P -q 'cancer' --show-pdf-links"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
# ===== EXPORT & OUTPUT =====
|
|
221
|
+
export_group = parser.add_argument_group(
|
|
222
|
+
'[EXPORT & OUTPUT]',
|
|
223
|
+
'Export results in various formats'
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
export_group.add_argument(
|
|
227
|
+
"-X", "--export", type=str,
|
|
228
|
+
metavar="FORMAT",
|
|
229
|
+
help="Export results to format(s). Single format: csv, json, bibtex, ris, endnote (XML), enw (EndNote Tagged), xlsx (Excel), xml. Multiple formats (comma-separated): csv,ris,bibtex. Files saved to exports/ folder. Example: -X csv or -X csv,ris,bibtex"
|
|
230
|
+
)
|
|
231
|
+
export_group.add_argument(
|
|
232
|
+
"-o", "--output", type=str, metavar="FILE",
|
|
233
|
+
help="Custom output filename for export (default: auto-generated with timestamp). Example: -o my_results.csv"
|
|
234
|
+
)
|
|
235
|
+
export_group.add_argument(
|
|
236
|
+
"-S", "--select", nargs="+", default=[], metavar="SELECTION",
|
|
237
|
+
help="Select article(s) to export. Supports: numbers (1 3 5), ranges (1-10), keywords (odd, even, first:N, last:N, top:N). Examples: -S 1 3 5 | -S odd | -S even | -S 1-10 | -S first:5 | -S last:3 | -S top:10. Without this flag, all results are exported."
|
|
238
|
+
)
|
|
239
|
+
export_group.add_argument(
|
|
240
|
+
"--export-fields", type=str, nargs="+", metavar="FIELD",
|
|
241
|
+
help="Select specific fields to export. Available fields: title, authors, abstract, journal, year, doi, url, source. Example: --export-fields title authors year doi"
|
|
242
|
+
)
|
|
243
|
+
export_group.add_argument(
|
|
244
|
+
"--zip", action="store_true",
|
|
245
|
+
help="Compress exported file(s) to ZIP format. Example: -X csv --zip"
|
|
246
|
+
)
|
|
247
|
+
export_group.add_argument(
|
|
248
|
+
"-c", "--citations", type=str,
|
|
249
|
+
choices=["apa", "mla", "chicago", "ieee"],
|
|
250
|
+
metavar="STYLE",
|
|
251
|
+
help="Export as formatted citations. Styles: apa, mla, chicago, ieee. Example: -c apa or --citations apa"
|
|
252
|
+
)
|
|
253
|
+
export_group.add_argument(
|
|
254
|
+
"--save-profile", type=str, metavar="NAME",
|
|
255
|
+
help="Save current export settings as a reusable profile. Example: --save-profile my_nature_export"
|
|
256
|
+
)
|
|
257
|
+
export_group.add_argument(
|
|
258
|
+
"--load-profile", type=str, metavar="NAME",
|
|
259
|
+
help="Load previously saved export profile. Example: --load-profile my_nature_export"
|
|
260
|
+
)
|
|
261
|
+
export_group.add_argument(
|
|
262
|
+
"--template", type=str, metavar="NAME",
|
|
263
|
+
help="Use predefined export template. Available: nature, science, ieee. Example: --template nature. Use --list-templates to see all"
|
|
264
|
+
)
|
|
265
|
+
export_group.add_argument(
|
|
266
|
+
"--download-pdf", action="store_true",
|
|
267
|
+
help="Download full-text PDFs for search results. Tries PMC, arXiv, DOI resolution, and optionally SciHub. PDFs saved to ~/Lixplore_PDFs/"
|
|
268
|
+
)
|
|
269
|
+
export_group.add_argument(
|
|
270
|
+
"--pdf-numbers", type=int, nargs="+", metavar="N",
|
|
271
|
+
help="Download PDFs only for specific article numbers. Example: --pdf-numbers 1 3 5"
|
|
272
|
+
)
|
|
273
|
+
export_group.add_argument(
|
|
274
|
+
"--use-scihub", action="store_true",
|
|
275
|
+
help="Use SciHub as fallback for PDF download (requires --set-scihub-mirror configuration). Use at your own discretion."
|
|
276
|
+
)
|
|
277
|
+
export_group.add_argument(
|
|
278
|
+
"--add-to-zotero", action="store_true",
|
|
279
|
+
help="Add search results to Zotero library (requires Zotero API configuration)"
|
|
280
|
+
)
|
|
281
|
+
export_group.add_argument(
|
|
282
|
+
"--zotero-collection", type=str, metavar="KEY",
|
|
283
|
+
help="Zotero collection key to add items to (use with --add-to-zotero)"
|
|
284
|
+
)
|
|
285
|
+
export_group.add_argument(
|
|
286
|
+
"--export-for-mendeley", action="store_true",
|
|
287
|
+
help="Export results as RIS file for Mendeley Desktop import"
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
# ===== UTILITY =====
|
|
291
|
+
utility_group = parser.add_argument_group(
|
|
292
|
+
'[UTILITY]',
|
|
293
|
+
'Additional utility options'
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
utility_group.add_argument(
|
|
297
|
+
"-H", "--history", action="store_true",
|
|
298
|
+
help="Show search history"
|
|
299
|
+
)
|
|
300
|
+
utility_group.add_argument(
|
|
301
|
+
"--refresh", action="store_true",
|
|
302
|
+
help="Bypass cache and fetch fresh results (ignore cached data)"
|
|
303
|
+
)
|
|
304
|
+
utility_group.add_argument(
|
|
305
|
+
"--examples", action="store_true",
|
|
306
|
+
help="Show quick examples (tldr-style) and exit"
|
|
307
|
+
)
|
|
308
|
+
utility_group.add_argument(
|
|
309
|
+
"--list-profiles", action="store_true",
|
|
310
|
+
help="List all saved export profiles"
|
|
311
|
+
)
|
|
312
|
+
utility_group.add_argument(
|
|
313
|
+
"--delete-profile", type=str, metavar="NAME",
|
|
314
|
+
help="Delete saved export profile"
|
|
315
|
+
)
|
|
316
|
+
utility_group.add_argument(
|
|
317
|
+
"--list-templates", action="store_true",
|
|
318
|
+
help="List all available export templates"
|
|
319
|
+
)
|
|
320
|
+
utility_group.add_argument(
|
|
321
|
+
"--list-custom-apis", action="store_true",
|
|
322
|
+
help="List all configured custom API sources"
|
|
323
|
+
)
|
|
324
|
+
utility_group.add_argument(
|
|
325
|
+
"--create-api-examples", action="store_true",
|
|
326
|
+
help="Create example custom API configurations (Springer, BASE, etc.)"
|
|
327
|
+
)
|
|
328
|
+
utility_group.add_argument(
|
|
329
|
+
"--set-scihub-mirror", type=str, metavar="URL",
|
|
330
|
+
help="Configure SciHub mirror URL for PDF downloads. Example: --set-scihub-mirror https://sci-hub.se"
|
|
331
|
+
)
|
|
332
|
+
utility_group.add_argument(
|
|
333
|
+
"--show-pdf-dir", action="store_true",
|
|
334
|
+
help="Show PDF download directory location"
|
|
335
|
+
)
|
|
336
|
+
utility_group.add_argument(
|
|
337
|
+
"--configure-zotero", nargs=2, metavar=("API_KEY", "USER_ID"),
|
|
338
|
+
help="Configure Zotero API access. Get API key from https://www.zotero.org/settings/keys"
|
|
339
|
+
)
|
|
340
|
+
utility_group.add_argument(
|
|
341
|
+
"--show-zotero-collections", action="store_true",
|
|
342
|
+
help="List Zotero collections with their keys"
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
# ===== INTERACTIVE MODES =====
|
|
346
|
+
mode_group = parser.add_argument_group(
|
|
347
|
+
'[INTERACTIVE MODES]',
|
|
348
|
+
'Enhanced interactive experiences for easier usage'
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
mode_group.add_argument(
|
|
352
|
+
"--tui", action="store_true",
|
|
353
|
+
help="Launch enhanced TUI (Text User Interface) mode - the primary interactive interface. Includes search, annotation, statistics, and export in a beautiful visual interface. This is the DEFAULT when no query is provided."
|
|
354
|
+
)
|
|
355
|
+
mode_group.add_argument(
|
|
356
|
+
"--shell", action="store_true",
|
|
357
|
+
help="[Deprecated - use --tui instead] Launch interactive shell mode."
|
|
358
|
+
)
|
|
359
|
+
mode_group.add_argument(
|
|
360
|
+
"--wizard", action="store_true",
|
|
361
|
+
help="[Deprecated - use --tui instead] Launch wizard mode."
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
# ===== ANNOTATIONS =====
|
|
365
|
+
annotation_group = parser.add_argument_group(
|
|
366
|
+
'[ANNOTATIONS]',
|
|
367
|
+
'Annotate, rate, and organize articles'
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
annotation_group.add_argument(
|
|
371
|
+
"--annotate", type=int, metavar="N",
|
|
372
|
+
help="Annotate article #N from last search. Use with --comment, --rating, --tags, etc. Example: --annotate 5 --rating 4 --tags 'important,methodology'"
|
|
373
|
+
)
|
|
374
|
+
annotation_group.add_argument(
|
|
375
|
+
"--comment", type=str, metavar="TEXT",
|
|
376
|
+
help="Add comment/note to article (use with --annotate). Example: --annotate 3 --comment 'Excellent methodology section'"
|
|
377
|
+
)
|
|
378
|
+
annotation_group.add_argument(
|
|
379
|
+
"--rating", type=int, choices=[1, 2, 3, 4, 5], metavar="1-5",
|
|
380
|
+
help="Rate article 1-5 stars (use with --annotate). Example: --annotate 3 --rating 5"
|
|
381
|
+
)
|
|
382
|
+
annotation_group.add_argument(
|
|
383
|
+
"--tags", type=str, metavar="TAGS",
|
|
384
|
+
help="Add comma-separated tags (use with --annotate). Example: --annotate 3 --tags 'important,review-later,cite'"
|
|
385
|
+
)
|
|
386
|
+
annotation_group.add_argument(
|
|
387
|
+
"--read-status", type=str, choices=['unread', 'reading', 'read'], metavar="STATUS",
|
|
388
|
+
help="Set read status: unread, reading, read (use with --annotate). Example: --annotate 3 --read-status read"
|
|
389
|
+
)
|
|
390
|
+
annotation_group.add_argument(
|
|
391
|
+
"--priority", type=str, choices=['low', 'medium', 'high'], metavar="LEVEL",
|
|
392
|
+
help="Set priority level: low, medium, high (use with --annotate). Example: --annotate 3 --priority high"
|
|
393
|
+
)
|
|
394
|
+
annotation_group.add_argument(
|
|
395
|
+
"--show-annotation", type=int, metavar="N",
|
|
396
|
+
help="Show annotation for article #N from last search"
|
|
397
|
+
)
|
|
398
|
+
annotation_group.add_argument(
|
|
399
|
+
"--list-annotations", action="store_true",
|
|
400
|
+
help="List all annotated articles"
|
|
401
|
+
)
|
|
402
|
+
annotation_group.add_argument(
|
|
403
|
+
"--filter-annotations", type=str, metavar="FILTERS",
|
|
404
|
+
help="Filter annotations. Format: key=value,key=value. Keys: min_rating, read_status, priority, tag. Example: --filter-annotations 'min_rating=4,priority=high'"
|
|
405
|
+
)
|
|
406
|
+
annotation_group.add_argument(
|
|
407
|
+
"--search-annotations", type=str, metavar="QUERY",
|
|
408
|
+
help="Search annotations by keyword in comments, tags, or title. Example: --search-annotations 'methodology'"
|
|
409
|
+
)
|
|
410
|
+
annotation_group.add_argument(
|
|
411
|
+
"--export-annotations", type=str, choices=['markdown', 'json', 'csv'], metavar="FORMAT",
|
|
412
|
+
help="Export all annotations to file. Formats: markdown, json, csv. Example: --export-annotations markdown"
|
|
413
|
+
)
|
|
414
|
+
annotation_group.add_argument(
|
|
415
|
+
"--annotation-stats", action="store_true",
|
|
416
|
+
help="Show annotation statistics (total, ratings distribution, tags, etc.)"
|
|
417
|
+
)
|
|
418
|
+
annotation_group.add_argument(
|
|
419
|
+
"--delete-annotation", type=int, metavar="N",
|
|
420
|
+
help="Delete annotation for article #N from last search"
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
parser.set_defaults(func=run_main)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def sort_results(results, sort_order):
|
|
427
|
+
"""
|
|
428
|
+
Sort results based on specified order.
|
|
429
|
+
|
|
430
|
+
Args:
|
|
431
|
+
results: List of article dictionaries
|
|
432
|
+
sort_order: Sort order (relevant, newest, oldest, journal, author)
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
Sorted list of articles
|
|
436
|
+
"""
|
|
437
|
+
if not results or sort_order == "relevant":
|
|
438
|
+
# Keep original order (most relevant from API)
|
|
439
|
+
return results
|
|
440
|
+
|
|
441
|
+
sorted_results = results.copy()
|
|
442
|
+
|
|
443
|
+
if sort_order == "newest":
|
|
444
|
+
# Sort by year descending (newest first)
|
|
445
|
+
sorted_results.sort(key=lambda x: x.get('year', 0), reverse=True)
|
|
446
|
+
|
|
447
|
+
elif sort_order == "oldest":
|
|
448
|
+
# Sort by year ascending (oldest first)
|
|
449
|
+
sorted_results.sort(key=lambda x: x.get('year', 9999))
|
|
450
|
+
|
|
451
|
+
elif sort_order == "journal":
|
|
452
|
+
# Sort by journal name alphabetically
|
|
453
|
+
sorted_results.sort(key=lambda x: (x.get('journal', '') or '').lower())
|
|
454
|
+
|
|
455
|
+
elif sort_order == "author":
|
|
456
|
+
# Sort by first author's last name
|
|
457
|
+
def get_first_author_last_name(article):
|
|
458
|
+
authors = article.get('authors', [])
|
|
459
|
+
if authors and len(authors) > 0:
|
|
460
|
+
# Get first author, try to extract last name
|
|
461
|
+
first_author = authors[0]
|
|
462
|
+
# Assume last name is the last word
|
|
463
|
+
parts = first_author.split()
|
|
464
|
+
return parts[-1].lower() if parts else ''
|
|
465
|
+
return ''
|
|
466
|
+
|
|
467
|
+
sorted_results.sort(key=get_first_author_last_name)
|
|
468
|
+
|
|
469
|
+
return sorted_results
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def parse_selection(selection_args, total_results):
|
|
473
|
+
"""
|
|
474
|
+
Parse selection arguments and return list of article indices.
|
|
475
|
+
|
|
476
|
+
Supports:
|
|
477
|
+
- Numbers: 1 3 5
|
|
478
|
+
- Ranges: 1-10, 5-15
|
|
479
|
+
- Keywords: odd, even
|
|
480
|
+
- First N: first:5, top:5
|
|
481
|
+
- Last N: last:3
|
|
482
|
+
|
|
483
|
+
Args:
|
|
484
|
+
selection_args: List of selection arguments
|
|
485
|
+
total_results: Total number of results available
|
|
486
|
+
|
|
487
|
+
Returns:
|
|
488
|
+
List of article numbers (1-based)
|
|
489
|
+
"""
|
|
490
|
+
selected = set()
|
|
491
|
+
|
|
492
|
+
for arg in selection_args:
|
|
493
|
+
arg = str(arg).lower()
|
|
494
|
+
|
|
495
|
+
# Keyword: odd
|
|
496
|
+
if arg == 'odd':
|
|
497
|
+
selected.update(range(1, total_results + 1, 2))
|
|
498
|
+
|
|
499
|
+
# Keyword: even
|
|
500
|
+
elif arg == 'even':
|
|
501
|
+
selected.update(range(2, total_results + 1, 2))
|
|
502
|
+
|
|
503
|
+
# Keyword: first:N or top:N
|
|
504
|
+
elif arg.startswith('first:') or arg.startswith('top:'):
|
|
505
|
+
try:
|
|
506
|
+
n = int(arg.split(':')[1])
|
|
507
|
+
selected.update(range(1, min(n + 1, total_results + 1)))
|
|
508
|
+
except (ValueError, IndexError):
|
|
509
|
+
print(f"Warning: Invalid format '{arg}'. Use 'first:N' or 'top:N'")
|
|
510
|
+
|
|
511
|
+
# Keyword: last:N
|
|
512
|
+
elif arg.startswith('last:'):
|
|
513
|
+
try:
|
|
514
|
+
n = int(arg.split(':')[1])
|
|
515
|
+
start = max(1, total_results - n + 1)
|
|
516
|
+
selected.update(range(start, total_results + 1))
|
|
517
|
+
except (ValueError, IndexError):
|
|
518
|
+
print(f"Warning: Invalid format '{arg}'. Use 'last:N'")
|
|
519
|
+
|
|
520
|
+
# Range: 1-10
|
|
521
|
+
elif '-' in arg and not arg.startswith('-'):
|
|
522
|
+
try:
|
|
523
|
+
start, end = arg.split('-')
|
|
524
|
+
start, end = int(start), int(end)
|
|
525
|
+
if start > end:
|
|
526
|
+
start, end = end, start
|
|
527
|
+
selected.update(range(start, min(end + 1, total_results + 1)))
|
|
528
|
+
except ValueError:
|
|
529
|
+
print(f"Warning: Invalid range '{arg}'. Use format: 1-10")
|
|
530
|
+
|
|
531
|
+
# Single number
|
|
532
|
+
else:
|
|
533
|
+
try:
|
|
534
|
+
num = int(arg)
|
|
535
|
+
if 1 <= num <= total_results:
|
|
536
|
+
selected.add(num)
|
|
537
|
+
else:
|
|
538
|
+
print(f"Warning: Article #{num} is out of range (1-{total_results})")
|
|
539
|
+
except ValueError:
|
|
540
|
+
print(f"Warning: Unrecognized selection '{arg}'")
|
|
541
|
+
|
|
542
|
+
return sorted(list(selected))
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def _supports_unicode_output() -> bool:
|
|
546
|
+
"""Return True if stdout can encode common Unicode characters (emojis/box)."""
|
|
547
|
+
import sys
|
|
548
|
+
test_string = "═│"
|
|
549
|
+
try:
|
|
550
|
+
test_string.encode(sys.stdout.encoding or "utf-8")
|
|
551
|
+
return True
|
|
552
|
+
except Exception:
|
|
553
|
+
return False
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def _examples_text(unicode_ok: bool) -> str:
|
|
557
|
+
if unicode_ok:
|
|
558
|
+
return """
|
|
559
|
+
╔═══════════════════════════════════════════════════════════════════════╗
|
|
560
|
+
║ LIXPLORE - Quick Examples ║
|
|
561
|
+
╚═══════════════════════════════════════════════════════════════════════╝
|
|
562
|
+
|
|
563
|
+
BASIC SEARCH
|
|
564
|
+
Search PubMed for a query:
|
|
565
|
+
$ lixplore -P -q "cancer treatment" -m 10
|
|
566
|
+
|
|
567
|
+
Search with abstracts:
|
|
568
|
+
$ lixplore -P -q "diabetes" -m 5 -a
|
|
569
|
+
|
|
570
|
+
MULTI-SOURCE SEARCH
|
|
571
|
+
Search PubMed + arXiv:
|
|
572
|
+
$ lixplore -s PX -q "machine learning" -m 20
|
|
573
|
+
|
|
574
|
+
Search all sources with deduplication:
|
|
575
|
+
$ lixplore -A -q "COVID-19" -m 50 -D
|
|
576
|
+
|
|
577
|
+
EXPORT RESULTS
|
|
578
|
+
Export to Excel:
|
|
579
|
+
$ lixplore -P -q "neuroscience" -m 15 -X xlsx -o brain_research.xlsx
|
|
580
|
+
|
|
581
|
+
Export to EndNote Tagged:
|
|
582
|
+
$ lixplore -P -q "quantum physics" -m 20 -X enw -o physics.enw
|
|
583
|
+
|
|
584
|
+
Export to CSV:
|
|
585
|
+
$ lixplore -P -q "climate change" -m 30 -X csv
|
|
586
|
+
|
|
587
|
+
Export to BibTeX:
|
|
588
|
+
$ lixplore -P -q "artificial intelligence" -m 25 -X bibtex
|
|
589
|
+
|
|
590
|
+
ADVANCED SEARCH
|
|
591
|
+
Search with date filter:
|
|
592
|
+
$ lixplore -P -q "vaccine development" -d 2020-01-01 2024-12-31 -m 15
|
|
593
|
+
|
|
594
|
+
Search by author:
|
|
595
|
+
$ lixplore -P -au "Smith J" -m 10 -a
|
|
596
|
+
|
|
597
|
+
Search by DOI:
|
|
598
|
+
$ lixplore -DOI "10.1038/nature12345"
|
|
599
|
+
|
|
600
|
+
🔤 BOOLEAN OPERATORS (Advanced Query Syntax)
|
|
601
|
+
AND operator (both terms required):
|
|
602
|
+
$ lixplore -P -q "cancer AND treatment" -m 10
|
|
603
|
+
|
|
604
|
+
OR operator (either term):
|
|
605
|
+
$ lixplore -P -q "cancer OR tumor" -m 10
|
|
606
|
+
|
|
607
|
+
NOT operator (exclude term):
|
|
608
|
+
$ lixplore -P -q "diabetes NOT type1" -m 10
|
|
609
|
+
|
|
610
|
+
Complex queries with parentheses:
|
|
611
|
+
$ lixplore -P -q "(cancer OR tumor) AND (treatment OR therapy)" -m 20
|
|
612
|
+
|
|
613
|
+
Combine with other features:
|
|
614
|
+
$ lixplore -A -q "COVID-19 AND vaccine" -m 50 -D --sort newest -X xlsx
|
|
615
|
+
|
|
616
|
+
🔄 COMBINED FEATURES
|
|
617
|
+
Multi-source search with export and deduplication:
|
|
618
|
+
$ lixplore -s PCE -q "gene therapy" -m 30 -D -X xlsx -o genes.xlsx
|
|
619
|
+
|
|
620
|
+
Search with date filter, abstracts, and export:
|
|
621
|
+
$ lixplore -P -q "cancer immunotherapy" -d 2023-01-01 2024-12-31 -a -m 20 -X json
|
|
622
|
+
|
|
623
|
+
📖 REVIEW ARTICLES (Two-Step Workflow)
|
|
624
|
+
Step 1 - Search and cache results:
|
|
625
|
+
$ lixplore -P -q "diabetes" -m 10
|
|
626
|
+
|
|
627
|
+
Step 2 - Review specific articles:
|
|
628
|
+
$ lixplore -R 2 # Review article #2
|
|
629
|
+
$ lixplore -R 1 5 9 # Review multiple articles
|
|
630
|
+
|
|
631
|
+
One-step (search + review):
|
|
632
|
+
$ lixplore -P -q "aspirin" -m 10 -R 1 3 5
|
|
633
|
+
|
|
634
|
+
In review window:
|
|
635
|
+
Press 'q' or Ctrl+C to close (won't close with other keys)
|
|
636
|
+
|
|
637
|
+
🔢 SMART SELECTION (Export Specific Articles)
|
|
638
|
+
Export odd-numbered articles:
|
|
639
|
+
$ lixplore -P -q "research" -m 50 -S odd -X csv
|
|
640
|
+
|
|
641
|
+
Export even-numbered articles:
|
|
642
|
+
$ lixplore -P -q "study" -m 50 -S even -X xlsx
|
|
643
|
+
|
|
644
|
+
Export first 10 articles:
|
|
645
|
+
$ lixplore -P -q "cancer" -m 50 -S first:10 -X enw
|
|
646
|
+
|
|
647
|
+
Export last 5 articles:
|
|
648
|
+
$ lixplore -P -q "science" -m 30 -S last:5 -X csv
|
|
649
|
+
|
|
650
|
+
Export specific range:
|
|
651
|
+
$ lixplore -P -q "biology" -m 50 -S 10-20 -X xlsx
|
|
652
|
+
|
|
653
|
+
Mixed selection (combine patterns):
|
|
654
|
+
$ lixplore -P -q "chemistry" -m 50 -S 1 3 5-10 odd -X csv
|
|
655
|
+
|
|
656
|
+
SORT RESULTS
|
|
657
|
+
Sort by newest (latest first):
|
|
658
|
+
$ lixplore -P -q "COVID-19" -m 50 --sort newest
|
|
659
|
+
|
|
660
|
+
Sort by oldest (historical research):
|
|
661
|
+
$ lixplore -P -q "diabetes" -m 50 --sort oldest
|
|
662
|
+
|
|
663
|
+
Sort by journal (alphabetical):
|
|
664
|
+
$ lixplore -A -q "AI" -m 50 -D --sort journal
|
|
665
|
+
|
|
666
|
+
Sort by author (alphabetical):
|
|
667
|
+
$ lixplore -P -q "physics" -m 50 --sort author
|
|
668
|
+
|
|
669
|
+
Combine sort + selection + export:
|
|
670
|
+
$ lixplore -P -q "cancer" -m 50 --sort newest -S first:10 -X xlsx
|
|
671
|
+
|
|
672
|
+
PDF LINKS (Display without downloading)
|
|
673
|
+
Show clickable PDF links for open access articles:
|
|
674
|
+
$ lixplore -x -q "neural networks" -m 10 --show-pdf-links
|
|
675
|
+
|
|
676
|
+
Combine with abstracts:
|
|
677
|
+
$ lixplore -P -q "cancer" -m 20 -a --show-pdf-links
|
|
678
|
+
|
|
679
|
+
Multi-source with PDF links:
|
|
680
|
+
$ lixplore -A -q "COVID-19" -m 50 -D --show-pdf-links
|
|
681
|
+
|
|
682
|
+
Links work in modern terminals (iTerm2, GNOME Terminal, Windows Terminal)
|
|
683
|
+
Click to open PDF in browser - no download required!
|
|
684
|
+
|
|
685
|
+
SOURCE CODES (for -s flag)
|
|
686
|
+
P = PubMed C = Crossref J = DOAJ
|
|
687
|
+
E = EuropePMC X = arXiv A = All sources
|
|
688
|
+
|
|
689
|
+
EXPORT FORMATS
|
|
690
|
+
csv - CSV (Excel, Google Sheets)
|
|
691
|
+
xlsx - Excel with formatting
|
|
692
|
+
json - JSON structured data
|
|
693
|
+
bibtex - BibTeX for LaTeX
|
|
694
|
+
ris - RIS (Zotero, Mendeley)
|
|
695
|
+
enw - EndNote Tagged (recommended)
|
|
696
|
+
endnote - EndNote XML
|
|
697
|
+
xml - Generic XML
|
|
698
|
+
|
|
699
|
+
Export locations: All files saved to exports/ folder
|
|
700
|
+
(organized by format: exports/csv/, exports/excel/, etc.)
|
|
701
|
+
|
|
702
|
+
TIP: Use -D flag when searching multiple sources to remove duplicates
|
|
703
|
+
TIP: Use -a flag to see abstracts in results
|
|
704
|
+
TIP: Use -R for detailed review in separate windows (press 'q' to close)
|
|
705
|
+
TIP: Use -S with keywords (odd, even, first:N, last:N) for smart selection
|
|
706
|
+
TIP: Use --sort newest to get latest research first
|
|
707
|
+
TIP: Use --show-pdf-links to see clickable PDF links (works in modern terminals!)
|
|
708
|
+
TIP: Results are cached - review later with: lixplore -R 1 2 3
|
|
709
|
+
TIP: Combine features: --sort newest -S first:10 -X xlsx
|
|
710
|
+
TIP: Use --help for complete documentation
|
|
711
|
+
TIP: Use 'man lixplore' for detailed manual page
|
|
712
|
+
|
|
713
|
+
For more information: lixplore --help
|
|
714
|
+
"""
|
|
715
|
+
else:
|
|
716
|
+
return """
|
|
717
|
+
=======================================================================
|
|
718
|
+
LIXPLORE - Quick Examples
|
|
719
|
+
=======================================================================
|
|
720
|
+
|
|
721
|
+
BASIC SEARCH
|
|
722
|
+
Search PubMed for a query:
|
|
723
|
+
$ lixplore -P -q "cancer treatment" -m 10
|
|
724
|
+
|
|
725
|
+
Search with abstracts:
|
|
726
|
+
$ lixplore -P -q "diabetes" -m 5 -a
|
|
727
|
+
|
|
728
|
+
MULTI-SOURCE SEARCH
|
|
729
|
+
Search PubMed + arXiv:
|
|
730
|
+
$ lixplore -s PX -q "machine learning" -m 20
|
|
731
|
+
|
|
732
|
+
Search all sources with deduplication:
|
|
733
|
+
$ lixplore -A -q "COVID-19" -m 50 -D
|
|
734
|
+
|
|
735
|
+
EXPORT RESULTS
|
|
736
|
+
Export to Excel:
|
|
737
|
+
$ lixplore -P -q "neuroscience" -m 15 -X xlsx -o brain_research.xlsx
|
|
738
|
+
|
|
739
|
+
Export to EndNote Tagged:
|
|
740
|
+
$ lixplore -P -q "quantum physics" -m 20 -X enw -o physics.enw
|
|
741
|
+
|
|
742
|
+
Export to CSV:
|
|
743
|
+
$ lixplore -P -q "climate change" -m 30 -X csv
|
|
744
|
+
|
|
745
|
+
Export to BibTeX:
|
|
746
|
+
$ lixplore -P -q "artificial intelligence" -m 25 -X bibtex
|
|
747
|
+
|
|
748
|
+
ADVANCED SEARCH
|
|
749
|
+
Search with date filter:
|
|
750
|
+
$ lixplore -P -q "vaccine development" -d 2020-01-01 2024-12-31 -m 15
|
|
751
|
+
|
|
752
|
+
Search by author:
|
|
753
|
+
$ lixplore -P -au "Smith J" -m 10 -a
|
|
754
|
+
|
|
755
|
+
Search by DOI:
|
|
756
|
+
$ lixplore -DOI "10.1038/nature12345"
|
|
757
|
+
|
|
758
|
+
BOOLEAN OPERATORS (Advanced Query Syntax)
|
|
759
|
+
AND operator (both terms required):
|
|
760
|
+
$ lixplore -P -q "cancer AND treatment" -m 10
|
|
761
|
+
|
|
762
|
+
OR operator (either term):
|
|
763
|
+
$ lixplore -P -q "cancer OR tumor" -m 10
|
|
764
|
+
|
|
765
|
+
NOT operator (exclude term):
|
|
766
|
+
$ lixplore -P -q "diabetes NOT type1" -m 10
|
|
767
|
+
|
|
768
|
+
Complex queries with parentheses:
|
|
769
|
+
$ lixplore -P -q "(cancer OR tumor) AND (treatment OR therapy)" -m 20
|
|
770
|
+
|
|
771
|
+
Combine with other features:
|
|
772
|
+
$ lixplore -A -q "COVID-19 AND vaccine" -m 50 -D --sort newest -X xlsx
|
|
773
|
+
|
|
774
|
+
COMBINED FEATURES
|
|
775
|
+
Multi-source search with export and deduplication:
|
|
776
|
+
$ lixplore -s PCE -q "gene therapy" -m 30 -D -X xlsx -o genes.xlsx
|
|
777
|
+
|
|
778
|
+
Search with date filter, abstracts, and export:
|
|
779
|
+
$ lixplore -P -q "cancer immunotherapy" -d 2023-01-01 2024-12-31 -a -m 20 -X json
|
|
780
|
+
|
|
781
|
+
REVIEW ARTICLES (Two-Step Workflow)
|
|
782
|
+
Step 1 - Search and cache results:
|
|
783
|
+
$ lixplore -P -q "diabetes" -m 10
|
|
784
|
+
|
|
785
|
+
Step 2 - Review specific articles:
|
|
786
|
+
$ lixplore -R 2 # Review article #2
|
|
787
|
+
$ lixplore -R 1 5 9 # Review multiple articles
|
|
788
|
+
|
|
789
|
+
One-step (search + review):
|
|
790
|
+
$ lixplore -P -q "aspirin" -m 10 -R 1 3 5
|
|
791
|
+
|
|
792
|
+
In review window:
|
|
793
|
+
Press 'q' or Ctrl+C to close (won't close with other keys)
|
|
794
|
+
|
|
795
|
+
SMART SELECTION (Export Specific Articles)
|
|
796
|
+
Export odd-numbered articles:
|
|
797
|
+
$ lixplore -P -q "research" -m 50 -S odd -X csv
|
|
798
|
+
|
|
799
|
+
Export even-numbered articles:
|
|
800
|
+
$ lixplore -P -q "study" -m 50 -S even -X xlsx
|
|
801
|
+
|
|
802
|
+
Export first 10 articles:
|
|
803
|
+
$ lixplore -P -q "cancer" -m 50 -S first:10 -X enw
|
|
804
|
+
|
|
805
|
+
Export last 5 articles:
|
|
806
|
+
$ lixplore -P -q "science" -m 30 -S last:5 -X csv
|
|
807
|
+
|
|
808
|
+
Export specific range:
|
|
809
|
+
$ lixplore -P -q "biology" -m 50 -S 10-20 -X xlsx
|
|
810
|
+
|
|
811
|
+
Mixed selection (combine patterns):
|
|
812
|
+
$ lixplore -P -q "chemistry" -m 50 -S 1 3 5-10 odd -X csv
|
|
813
|
+
|
|
814
|
+
SORT RESULTS
|
|
815
|
+
Sort by newest (latest first):
|
|
816
|
+
$ lixplore -P -q "COVID-19" -m 50 --sort newest
|
|
817
|
+
|
|
818
|
+
Sort by oldest (historical research):
|
|
819
|
+
$ lixplore -P -q "diabetes" -m 50 --sort oldest
|
|
820
|
+
|
|
821
|
+
Sort by journal (alphabetical):
|
|
822
|
+
$ lixplore -A -q "AI" -m 50 -D --sort journal
|
|
823
|
+
|
|
824
|
+
Sort by author (alphabetical):
|
|
825
|
+
$ lixplore -P -q "physics" -m 50 --sort author
|
|
826
|
+
|
|
827
|
+
Combine sort + selection + export:
|
|
828
|
+
$ lixplore -P -q "cancer" -m 50 --sort newest -S first:10 -X xlsx
|
|
829
|
+
|
|
830
|
+
SOURCE CODES (for -s flag)
|
|
831
|
+
P = PubMed C = Crossref J = DOAJ
|
|
832
|
+
E = EuropePMC X = arXiv A = All sources
|
|
833
|
+
|
|
834
|
+
EXPORT FORMATS
|
|
835
|
+
csv - CSV (Excel, Google Sheets)
|
|
836
|
+
xlsx - Excel with formatting
|
|
837
|
+
json - JSON structured data
|
|
838
|
+
bibtex - BibTeX for LaTeX
|
|
839
|
+
ris - RIS (Zotero, Mendeley)
|
|
840
|
+
enw - EndNote Tagged (recommended)
|
|
841
|
+
endnote - EndNote XML
|
|
842
|
+
xml - Generic XML
|
|
843
|
+
|
|
844
|
+
Export locations: All files saved to exports/ folder
|
|
845
|
+
(organized by format: exports/csv/, exports/excel/, etc.)
|
|
846
|
+
|
|
847
|
+
TIP: Use -D flag when searching multiple sources to remove duplicates
|
|
848
|
+
TIP: Use -a flag to see abstracts in results
|
|
849
|
+
TIP: Use -R for detailed review in separate windows (press 'q' to close)
|
|
850
|
+
TIP: Use -S with keywords (odd, even, first:N, last:N) for smart selection
|
|
851
|
+
TIP: Use --sort newest to get latest research first
|
|
852
|
+
TIP: Results are cached - review later with: lixplore -R 1 2 3
|
|
853
|
+
TIP: Combine features: --sort newest -S first:10 -X xlsx
|
|
854
|
+
TIP: Use --help for complete documentation
|
|
855
|
+
TIP: Use 'man lixplore' for detailed manual page
|
|
856
|
+
|
|
857
|
+
For more information: lixplore --help
|
|
858
|
+
"""
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
def show_examples():
|
|
862
|
+
"""Display tldr-style quick examples."""
|
|
863
|
+
unicode_ok = _supports_unicode_output()
|
|
864
|
+
print(_examples_text(unicode_ok))
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
def run_main(args):
|
|
868
|
+
"""Main handler for CLI options."""
|
|
869
|
+
|
|
870
|
+
# Handle interactive modes first (only if used standalone, without search)
|
|
871
|
+
# If -i is used WITH search flags, it will be handled later after the search
|
|
872
|
+
has_search_params = (args.query or args.author or args.doi or
|
|
873
|
+
any([args.pubmed, args.crossref, args.doaj,
|
|
874
|
+
args.europepmc, args.arxiv, getattr(args, 'all', False),
|
|
875
|
+
args.sources]))
|
|
876
|
+
|
|
877
|
+
if args.interactive and not has_search_params:
|
|
878
|
+
# Launch simple interactive TUI standalone
|
|
879
|
+
from lixplore.utils.interactive_tui import launch_interactive_mode
|
|
880
|
+
launch_interactive_mode([])
|
|
881
|
+
return
|
|
882
|
+
|
|
883
|
+
if args.tui:
|
|
884
|
+
from lixplore.utils.enhanced_tui import launch_enhanced_tui
|
|
885
|
+
launch_enhanced_tui()
|
|
886
|
+
return
|
|
887
|
+
|
|
888
|
+
# Deprecated modes (kept for backwards compatibility)
|
|
889
|
+
if args.shell:
|
|
890
|
+
print("Note: --shell is deprecated.")
|
|
891
|
+
from lixplore.utils.shell_mode import launch_shell
|
|
892
|
+
launch_shell()
|
|
893
|
+
return
|
|
894
|
+
|
|
895
|
+
if args.wizard:
|
|
896
|
+
print("Note: --wizard is deprecated.")
|
|
897
|
+
from lixplore.utils.wizard_mode import launch_wizard
|
|
898
|
+
launch_wizard()
|
|
899
|
+
return
|
|
900
|
+
|
|
901
|
+
# If user only wants examples
|
|
902
|
+
if args.examples:
|
|
903
|
+
show_examples()
|
|
904
|
+
return
|
|
905
|
+
|
|
906
|
+
# If user only wants history
|
|
907
|
+
# Handle annotation commands
|
|
908
|
+
from lixplore.utils.annotations import AnnotationManager, display_annotation
|
|
909
|
+
|
|
910
|
+
# Annotation-only commands (don't require search)
|
|
911
|
+
if args.list_annotations:
|
|
912
|
+
manager = AnnotationManager()
|
|
913
|
+
filter_params = None
|
|
914
|
+
|
|
915
|
+
if hasattr(args, 'filter_annotations') and args.filter_annotations:
|
|
916
|
+
# Parse filter string: "min_rating=4,priority=high"
|
|
917
|
+
filter_params = {}
|
|
918
|
+
for item in args.filter_annotations.split(','):
|
|
919
|
+
if '=' in item:
|
|
920
|
+
key, value = item.split('=', 1)
|
|
921
|
+
key = key.strip()
|
|
922
|
+
value = value.strip()
|
|
923
|
+
|
|
924
|
+
if key == 'min_rating':
|
|
925
|
+
filter_params['min_rating'] = int(value)
|
|
926
|
+
elif key == 'max_rating':
|
|
927
|
+
filter_params['max_rating'] = int(value)
|
|
928
|
+
elif key == 'read_status':
|
|
929
|
+
filter_params['read_status'] = value
|
|
930
|
+
elif key == 'priority':
|
|
931
|
+
filter_params['priority'] = value
|
|
932
|
+
elif key == 'tag':
|
|
933
|
+
filter_params['tags'] = [value]
|
|
934
|
+
|
|
935
|
+
annotations = manager.list_all(filter_params)
|
|
936
|
+
|
|
937
|
+
if not annotations:
|
|
938
|
+
print("No annotated articles found.")
|
|
939
|
+
if filter_params:
|
|
940
|
+
print("Try removing filters or annotate some articles first.")
|
|
941
|
+
else:
|
|
942
|
+
print(f"\n{'='*80}")
|
|
943
|
+
print(f"ANNOTATED ARTICLES ({len(annotations)})")
|
|
944
|
+
print(f"{'='*80}\n")
|
|
945
|
+
|
|
946
|
+
for i, item in enumerate(annotations, 1):
|
|
947
|
+
annotation = item['annotation']
|
|
948
|
+
info = annotation.get('article_info', {})
|
|
949
|
+
|
|
950
|
+
print(f"[{i}] {info.get('title', 'No title')}")
|
|
951
|
+
|
|
952
|
+
# Show rating
|
|
953
|
+
if annotation.get('rating'):
|
|
954
|
+
stars = '' * annotation['rating']
|
|
955
|
+
print(f" Rating: {stars} ({annotation['rating']}/5)")
|
|
956
|
+
|
|
957
|
+
# Show tags
|
|
958
|
+
if annotation.get('tags'):
|
|
959
|
+
print(f" Tags: {', '.join(annotation['tags'])}")
|
|
960
|
+
|
|
961
|
+
# Show status and priority
|
|
962
|
+
print(f" Status: {annotation.get('read_status', 'unread').title()} | Priority: {annotation.get('priority', 'medium').title()}")
|
|
963
|
+
|
|
964
|
+
# Show comment count
|
|
965
|
+
comments_count = len(annotation.get('comments', []))
|
|
966
|
+
if comments_count > 0:
|
|
967
|
+
print(f" Comments: {comments_count}")
|
|
968
|
+
|
|
969
|
+
print()
|
|
970
|
+
|
|
971
|
+
print(f"{'='*80}\n")
|
|
972
|
+
return
|
|
973
|
+
|
|
974
|
+
if args.search_annotations:
|
|
975
|
+
manager = AnnotationManager()
|
|
976
|
+
results = manager.search_annotations(args.search_annotations)
|
|
977
|
+
|
|
978
|
+
if not results:
|
|
979
|
+
print(f"No annotations found matching: '{args.search_annotations}'")
|
|
980
|
+
else:
|
|
981
|
+
print(f"\n{'='*80}")
|
|
982
|
+
print(f"SEARCH RESULTS ({len(results)} matches)")
|
|
983
|
+
print(f"{'='*80}\n")
|
|
984
|
+
|
|
985
|
+
for i, item in enumerate(results, 1):
|
|
986
|
+
annotation = item['annotation']
|
|
987
|
+
info = annotation.get('article_info', {})
|
|
988
|
+
|
|
989
|
+
print(f"[{i}] {info.get('title', 'No title')}")
|
|
990
|
+
print(f" Match type: {item['match_type']}")
|
|
991
|
+
print(f" Match: {item['match_text'][:100]}...")
|
|
992
|
+
print()
|
|
993
|
+
|
|
994
|
+
print(f"{'='*80}\n")
|
|
995
|
+
return
|
|
996
|
+
|
|
997
|
+
if args.export_annotations:
|
|
998
|
+
manager = AnnotationManager()
|
|
999
|
+
output_file = manager.export_annotations(format=args.export_annotations)
|
|
1000
|
+
print(f"Annotations exported to: {output_file}")
|
|
1001
|
+
return
|
|
1002
|
+
|
|
1003
|
+
if args.annotation_stats:
|
|
1004
|
+
manager = AnnotationManager()
|
|
1005
|
+
stats = manager.get_statistics()
|
|
1006
|
+
|
|
1007
|
+
print(f"\n{'='*80}")
|
|
1008
|
+
print("ANNOTATION STATISTICS")
|
|
1009
|
+
print(f"{'='*80}\n")
|
|
1010
|
+
|
|
1011
|
+
print(f"Total Annotated Articles: {stats['total']}")
|
|
1012
|
+
|
|
1013
|
+
if stats['total'] == 0:
|
|
1014
|
+
print("\nNo annotations yet. Start annotating articles with:")
|
|
1015
|
+
print(" lixplore -P -q 'query' -m 10")
|
|
1016
|
+
print(" lixplore --annotate 1 --rating 5 --tags 'important'")
|
|
1017
|
+
print(f"{'='*80}\n")
|
|
1018
|
+
return
|
|
1019
|
+
|
|
1020
|
+
# Rating distribution
|
|
1021
|
+
if stats['by_rating']:
|
|
1022
|
+
print(f"\nRating Distribution:")
|
|
1023
|
+
for rating in sorted(stats['by_rating'].keys(), reverse=True):
|
|
1024
|
+
count = stats['by_rating'][rating]
|
|
1025
|
+
stars = '' * rating
|
|
1026
|
+
bar = '█' * count
|
|
1027
|
+
print(f" {stars} ({rating}): {bar} {count}")
|
|
1028
|
+
|
|
1029
|
+
# Read status
|
|
1030
|
+
if stats['by_status']:
|
|
1031
|
+
print(f"\nRead Status:")
|
|
1032
|
+
for status, count in stats['by_status'].items():
|
|
1033
|
+
print(f" {status.title()}: {count}")
|
|
1034
|
+
|
|
1035
|
+
# Priority
|
|
1036
|
+
if stats['by_priority']:
|
|
1037
|
+
print(f"\nPriority:")
|
|
1038
|
+
for priority, count in stats['by_priority'].items():
|
|
1039
|
+
print(f" {priority.title()}: {count}")
|
|
1040
|
+
|
|
1041
|
+
# Comments
|
|
1042
|
+
print(f"\nComments:")
|
|
1043
|
+
print(f" Articles with comments: {stats['with_comments']}")
|
|
1044
|
+
print(f" Total comments: {stats['total_comments']}")
|
|
1045
|
+
|
|
1046
|
+
# Tags
|
|
1047
|
+
print(f"\nTags:")
|
|
1048
|
+
print(f" Unique tags: {stats['total_tags']}")
|
|
1049
|
+
if stats['unique_tags']:
|
|
1050
|
+
print(f" Tags: {', '.join(stats['unique_tags'][:20])}")
|
|
1051
|
+
if len(stats['unique_tags']) > 20:
|
|
1052
|
+
print(f" ... and {len(stats['unique_tags']) - 20} more")
|
|
1053
|
+
|
|
1054
|
+
print(f"\n{'='*80}\n")
|
|
1055
|
+
return
|
|
1056
|
+
|
|
1057
|
+
# Handle template and profile management commands
|
|
1058
|
+
from lixplore.utils import profiles
|
|
1059
|
+
from lixplore.utils import template_engine
|
|
1060
|
+
from lixplore.utils import custom_apis
|
|
1061
|
+
|
|
1062
|
+
# Handle PDF downloader configuration commands
|
|
1063
|
+
from lixplore.utils import pdf_downloader
|
|
1064
|
+
|
|
1065
|
+
if args.set_scihub_mirror:
|
|
1066
|
+
pdf_downloader.set_scihub_mirror(args.set_scihub_mirror)
|
|
1067
|
+
return
|
|
1068
|
+
|
|
1069
|
+
if args.show_pdf_dir:
|
|
1070
|
+
print(f"PDF download directory: {pdf_downloader.PDF_DIR}")
|
|
1071
|
+
if os.path.exists(pdf_downloader.PDF_DIR):
|
|
1072
|
+
# Count PDFs
|
|
1073
|
+
pdf_count = sum(1 for root, dirs, files in os.walk(pdf_downloader.PDF_DIR)
|
|
1074
|
+
for f in files if f.endswith('.pdf'))
|
|
1075
|
+
print(f"Total PDFs downloaded: {pdf_count}")
|
|
1076
|
+
else:
|
|
1077
|
+
print("(Directory does not exist yet - will be created on first download)")
|
|
1078
|
+
return
|
|
1079
|
+
|
|
1080
|
+
# Handle reference manager configuration commands
|
|
1081
|
+
from lixplore.utils import reference_managers
|
|
1082
|
+
|
|
1083
|
+
if args.configure_zotero:
|
|
1084
|
+
api_key, user_id = args.configure_zotero
|
|
1085
|
+
reference_managers.configure_zotero(api_key, user_id)
|
|
1086
|
+
return
|
|
1087
|
+
|
|
1088
|
+
if args.show_zotero_collections:
|
|
1089
|
+
reference_managers.show_zotero_collections()
|
|
1090
|
+
return
|
|
1091
|
+
|
|
1092
|
+
# Handle custom API management commands
|
|
1093
|
+
if args.list_custom_apis:
|
|
1094
|
+
api_list = custom_apis.list_custom_apis()
|
|
1095
|
+
if api_list:
|
|
1096
|
+
print("Configured custom API sources:")
|
|
1097
|
+
for api in api_list:
|
|
1098
|
+
config = custom_apis.load_custom_api_config(api)
|
|
1099
|
+
if config:
|
|
1100
|
+
print(f" • {api}")
|
|
1101
|
+
if 'description' in config:
|
|
1102
|
+
print(f" {config['description']}")
|
|
1103
|
+
if 'requires_auth' in config and config['requires_auth']:
|
|
1104
|
+
print(f" Requires authentication")
|
|
1105
|
+
else:
|
|
1106
|
+
print("No custom APIs configured.")
|
|
1107
|
+
print("Run: lixplore --create-api-examples to create example configurations")
|
|
1108
|
+
return
|
|
1109
|
+
|
|
1110
|
+
if args.create_api_examples:
|
|
1111
|
+
custom_apis.create_example_configs()
|
|
1112
|
+
return
|
|
1113
|
+
|
|
1114
|
+
if args.list_templates:
|
|
1115
|
+
template_names = template_engine.list_templates()
|
|
1116
|
+
if template_names:
|
|
1117
|
+
print("Available export templates:")
|
|
1118
|
+
for name in template_names:
|
|
1119
|
+
template = template_engine.load_template(name.replace(' (user)', ''))
|
|
1120
|
+
if template:
|
|
1121
|
+
print(f" • {name}")
|
|
1122
|
+
if 'description' in template:
|
|
1123
|
+
print(f" {template['description']}")
|
|
1124
|
+
if 'format' in template:
|
|
1125
|
+
print(f" Format: {template['format']}")
|
|
1126
|
+
else:
|
|
1127
|
+
print("No templates found.")
|
|
1128
|
+
return
|
|
1129
|
+
|
|
1130
|
+
if args.list_profiles:
|
|
1131
|
+
profile_names = profiles.list_profiles()
|
|
1132
|
+
if profile_names:
|
|
1133
|
+
print("Saved export profiles:")
|
|
1134
|
+
for name in profile_names:
|
|
1135
|
+
profile = profiles.load_profile(name)
|
|
1136
|
+
print(f" • {name}")
|
|
1137
|
+
if 'export_format' in profile:
|
|
1138
|
+
print(f" Format: {profile['export_format']}")
|
|
1139
|
+
if 'citation_style' in profile:
|
|
1140
|
+
print(f" Citation: {profile['citation_style']}")
|
|
1141
|
+
else:
|
|
1142
|
+
print("No saved profiles found.")
|
|
1143
|
+
print("Save a profile with: --save-profile <name>")
|
|
1144
|
+
return
|
|
1145
|
+
|
|
1146
|
+
if args.delete_profile:
|
|
1147
|
+
if profiles.delete_profile(args.delete_profile):
|
|
1148
|
+
print(f"Profile '{args.delete_profile}' deleted successfully")
|
|
1149
|
+
return
|
|
1150
|
+
|
|
1151
|
+
# Load template if specified
|
|
1152
|
+
if args.template:
|
|
1153
|
+
template = template_engine.load_template(args.template)
|
|
1154
|
+
if template:
|
|
1155
|
+
print(f"Loading template: {args.template}")
|
|
1156
|
+
args = template_engine.apply_template_to_args(args, template)
|
|
1157
|
+
else:
|
|
1158
|
+
print(f"Error: Template '{args.template}' not found")
|
|
1159
|
+
print("Use --list-templates to see available templates")
|
|
1160
|
+
return
|
|
1161
|
+
|
|
1162
|
+
# Load profile if specified (profiles override templates)
|
|
1163
|
+
if args.load_profile:
|
|
1164
|
+
profile = profiles.load_profile(args.load_profile)
|
|
1165
|
+
if profile:
|
|
1166
|
+
print(f"Loading profile: {args.load_profile}")
|
|
1167
|
+
args = profiles.apply_profile_to_args(args, profile)
|
|
1168
|
+
else:
|
|
1169
|
+
print(f"Error: Profile '{args.load_profile}' not found")
|
|
1170
|
+
print("Use --list-profiles to see available profiles")
|
|
1171
|
+
return
|
|
1172
|
+
|
|
1173
|
+
if args.history:
|
|
1174
|
+
dispatcher.show_history()
|
|
1175
|
+
return
|
|
1176
|
+
|
|
1177
|
+
# If user only wants to review cached results (no new search)
|
|
1178
|
+
if args.review and not any([args.pubmed, args.crossref, args.doaj, args.europepmc, args.arxiv, args.all, args.sources, args.query]):
|
|
1179
|
+
# Load cached results and review (ignore --refresh flag for standalone review)
|
|
1180
|
+
cached_results = dispatcher.load_cached_results(check_expiry=True, force_refresh=False)
|
|
1181
|
+
if cached_results:
|
|
1182
|
+
print(f"Loading cached results ({len(cached_results)} articles)...")
|
|
1183
|
+
print("\nCached results:")
|
|
1184
|
+
for i, result in enumerate(cached_results, 1):
|
|
1185
|
+
print(f"[{i}] {result.get('title', 'No title')}")
|
|
1186
|
+
print("")
|
|
1187
|
+
dispatcher.review_articles(cached_results, args.review)
|
|
1188
|
+
else:
|
|
1189
|
+
print("No cached results found. Please run a search first.")
|
|
1190
|
+
print("Example: lixplore -P -q \"paracetamol\" -m 5")
|
|
1191
|
+
return
|
|
1192
|
+
|
|
1193
|
+
# Determine which sources to search
|
|
1194
|
+
sources_to_search = []
|
|
1195
|
+
use_custom_api = False
|
|
1196
|
+
custom_api_name = None
|
|
1197
|
+
|
|
1198
|
+
# Check for custom API first (takes precedence)
|
|
1199
|
+
if hasattr(args, 'custom_api') and args.custom_api:
|
|
1200
|
+
use_custom_api = True
|
|
1201
|
+
custom_api_name = args.custom_api
|
|
1202
|
+
# Custom API is handled separately, not added to sources_to_search
|
|
1203
|
+
|
|
1204
|
+
# Source letter mapping
|
|
1205
|
+
source_map = {
|
|
1206
|
+
'P': 'pubmed',
|
|
1207
|
+
'C': 'crossref',
|
|
1208
|
+
'J': 'doaj',
|
|
1209
|
+
'E': 'europepmc',
|
|
1210
|
+
'X': 'arxiv',
|
|
1211
|
+
'A': 'all'
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
# Check for combined sources flag (-s/--sources)
|
|
1215
|
+
if hasattr(args, 'sources') and args.sources:
|
|
1216
|
+
sources_str = args.sources.upper()
|
|
1217
|
+
|
|
1218
|
+
# If 'A' is in the string, search all sources
|
|
1219
|
+
if 'A' in sources_str:
|
|
1220
|
+
sources_to_search = ["pubmed", "crossref", "doaj", "europepmc", "arxiv"]
|
|
1221
|
+
else:
|
|
1222
|
+
# Parse each character
|
|
1223
|
+
for char in sources_str:
|
|
1224
|
+
if char in source_map and source_map[char] != 'all':
|
|
1225
|
+
source = source_map[char]
|
|
1226
|
+
if source not in sources_to_search:
|
|
1227
|
+
sources_to_search.append(source)
|
|
1228
|
+
elif char not in [' ', ',']: # Ignore spaces and commas
|
|
1229
|
+
print(f"Warning: Unknown source code '{char}' - ignoring")
|
|
1230
|
+
|
|
1231
|
+
# Check for -A/--all flag
|
|
1232
|
+
elif args.all:
|
|
1233
|
+
sources_to_search = ["pubmed", "crossref", "doaj", "europepmc", "arxiv"]
|
|
1234
|
+
|
|
1235
|
+
# Fall back to individual source flags
|
|
1236
|
+
else:
|
|
1237
|
+
if args.pubmed:
|
|
1238
|
+
sources_to_search.append("pubmed")
|
|
1239
|
+
if args.crossref:
|
|
1240
|
+
sources_to_search.append("crossref")
|
|
1241
|
+
if args.doaj:
|
|
1242
|
+
sources_to_search.append("doaj")
|
|
1243
|
+
if args.europepmc:
|
|
1244
|
+
sources_to_search.append("europepmc")
|
|
1245
|
+
if args.arxiv:
|
|
1246
|
+
sources_to_search.append("arxiv")
|
|
1247
|
+
|
|
1248
|
+
# Check if at least one source is selected (either standard or custom)
|
|
1249
|
+
if not sources_to_search and not use_custom_api:
|
|
1250
|
+
print("Error: Please specify at least one source to search:")
|
|
1251
|
+
print(" -s PX Combined sources (P=PubMed, C=Crossref, J=DOAJ, E=EuropePMC, X=arXiv, A=All)")
|
|
1252
|
+
print(" -P or --pubmed Search PubMed")
|
|
1253
|
+
print(" -C or --crossref Search Crossref")
|
|
1254
|
+
print(" -J or --doaj Search DOAJ")
|
|
1255
|
+
print(" -E or --europepmc Search EuropePMC")
|
|
1256
|
+
print(" -x or --arxiv Search arXiv")
|
|
1257
|
+
print(" -A or --all Search all sources")
|
|
1258
|
+
print(" --custom-api NAME Search custom API (Springer, BASE, etc.)")
|
|
1259
|
+
print("\nFor interactive mode, use: lixplore -i")
|
|
1260
|
+
print("\nExamples:")
|
|
1261
|
+
print(" lixplore -P -q 'search term' -m 20 # PubMed search")
|
|
1262
|
+
print(" lixplore -s PX -q 'search term' # PubMed + arXiv")
|
|
1263
|
+
print(" lixplore -s PCE -q 'search term' # PubMed + Crossref + EuropePMC")
|
|
1264
|
+
print(" lixplore -i # Interactive TUI mode")
|
|
1265
|
+
print(" lixplore --custom-api springer -q 'term' # Custom API (requires configuration)")
|
|
1266
|
+
return
|
|
1267
|
+
|
|
1268
|
+
results = []
|
|
1269
|
+
query = None
|
|
1270
|
+
|
|
1271
|
+
# Build query based on search type
|
|
1272
|
+
if args.query:
|
|
1273
|
+
query = args.query
|
|
1274
|
+
print(f"Searching for query: {query}")
|
|
1275
|
+
elif args.author:
|
|
1276
|
+
# Note: Author search syntax is PubMed-specific
|
|
1277
|
+
query = f"{args.author}[Author]" if "pubmed" in sources_to_search else args.author
|
|
1278
|
+
print(f"Searching articles by author: {args.author}")
|
|
1279
|
+
elif args.doi:
|
|
1280
|
+
query = args.doi
|
|
1281
|
+
print(f"Fetching article with DOI: {args.doi}")
|
|
1282
|
+
else:
|
|
1283
|
+
print("Error: Please provide a query, author, or DOI to search:")
|
|
1284
|
+
print(" -q, --query QUERY Search query")
|
|
1285
|
+
print(" -au, --author AUTHOR Search by author")
|
|
1286
|
+
print(" -DOI DOI Fetch article by DOI")
|
|
1287
|
+
print("\nFor interactive mode, use: lixplore -i")
|
|
1288
|
+
print("\nExample: lixplore -P -q \"cancer treatment\" -m 20")
|
|
1289
|
+
return
|
|
1290
|
+
|
|
1291
|
+
# Display selected sources
|
|
1292
|
+
source_names = {
|
|
1293
|
+
"pubmed": "PubMed",
|
|
1294
|
+
"crossref": "Crossref",
|
|
1295
|
+
"doaj": "DOAJ",
|
|
1296
|
+
"europepmc": "EuropePMC",
|
|
1297
|
+
"arxiv": "arXiv"
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
# Show standard sources if any
|
|
1301
|
+
if sources_to_search:
|
|
1302
|
+
selected_names = [source_names[src] for src in sources_to_search]
|
|
1303
|
+
print(f"Sources: {', '.join(selected_names)}")
|
|
1304
|
+
|
|
1305
|
+
# Show custom API if selected
|
|
1306
|
+
if use_custom_api:
|
|
1307
|
+
print(f"Custom API: {custom_api_name}")
|
|
1308
|
+
|
|
1309
|
+
# Execute search on selected sources
|
|
1310
|
+
for src in sources_to_search:
|
|
1311
|
+
print(f" Searching {source_names[src]}...")
|
|
1312
|
+
src_results = dispatcher.search(
|
|
1313
|
+
source=src,
|
|
1314
|
+
query=query,
|
|
1315
|
+
limit=args.max_results,
|
|
1316
|
+
)
|
|
1317
|
+
results.extend(src_results)
|
|
1318
|
+
|
|
1319
|
+
# Execute search on custom API if selected
|
|
1320
|
+
if use_custom_api:
|
|
1321
|
+
print(f" Searching {custom_api_name} (custom API)...")
|
|
1322
|
+
custom_results = custom_apis.call_custom_api(custom_api_name, query, args.max_results)
|
|
1323
|
+
results.extend(custom_results)
|
|
1324
|
+
|
|
1325
|
+
if (len(sources_to_search) > 1) or (len(sources_to_search) > 0 and use_custom_api):
|
|
1326
|
+
print(f"Total results before deduplication: {len(results)}")
|
|
1327
|
+
|
|
1328
|
+
# Post-processing
|
|
1329
|
+
if args.deduplicate and results:
|
|
1330
|
+
print("Removing duplicates")
|
|
1331
|
+
results = dispatcher.deduplicate_advanced(
|
|
1332
|
+
results,
|
|
1333
|
+
strategy=args.deduplicate,
|
|
1334
|
+
title_threshold=args.dedup_threshold,
|
|
1335
|
+
keep_preference=args.dedup_keep,
|
|
1336
|
+
merge_metadata=args.dedup_merge
|
|
1337
|
+
)
|
|
1338
|
+
|
|
1339
|
+
# Enrich metadata if requested
|
|
1340
|
+
if args.enrich is not None and results:
|
|
1341
|
+
from lixplore.utils.enrichment import enrich_results
|
|
1342
|
+
# If --enrich used without arguments, use all APIs
|
|
1343
|
+
apis = args.enrich if args.enrich else ['all']
|
|
1344
|
+
results = enrich_results(results, apis, show_progress=True)
|
|
1345
|
+
|
|
1346
|
+
# Sort results if requested
|
|
1347
|
+
if results and args.sort and args.sort != "relevant":
|
|
1348
|
+
results = sort_results(results, args.sort)
|
|
1349
|
+
print(f"Results sorted by: {args.sort}")
|
|
1350
|
+
|
|
1351
|
+
# Export to file format if requested
|
|
1352
|
+
if args.export and results:
|
|
1353
|
+
# Check if multiple formats specified (comma-separated)
|
|
1354
|
+
formats = [f.strip() for f in args.export.split(',')]
|
|
1355
|
+
|
|
1356
|
+
# Validate formats
|
|
1357
|
+
valid_formats = ["csv", "json", "bibtex", "ris", "endnote", "enw", "xlsx", "xml"]
|
|
1358
|
+
invalid_formats = [f for f in formats if f not in valid_formats]
|
|
1359
|
+
if invalid_formats:
|
|
1360
|
+
print(f"Error: Invalid export format(s): {', '.join(invalid_formats)}")
|
|
1361
|
+
print(f"Valid formats: {', '.join(valid_formats)}")
|
|
1362
|
+
return
|
|
1363
|
+
|
|
1364
|
+
# Filter results if specific articles selected
|
|
1365
|
+
if args.select:
|
|
1366
|
+
# Parse selection arguments (supports: numbers, ranges, keywords)
|
|
1367
|
+
selected_numbers = parse_selection(args.select, len(results))
|
|
1368
|
+
|
|
1369
|
+
if selected_numbers:
|
|
1370
|
+
selected_results = [results[num - 1] for num in selected_numbers]
|
|
1371
|
+
print(f"Selected articles: {', '.join(f'#{n}' for n in selected_numbers)}")
|
|
1372
|
+
print(f"Exporting {len(selected_results)} selected article(s)...")
|
|
1373
|
+
|
|
1374
|
+
# Use batch export if multiple formats, otherwise single export
|
|
1375
|
+
if len(formats) > 1:
|
|
1376
|
+
# Extract base filename from output (remove extension)
|
|
1377
|
+
output_base = args.output.rsplit('.', 1)[0] if args.output else None
|
|
1378
|
+
dispatcher.batch_export(selected_results, formats, output_base, args.export_fields, args.zip)
|
|
1379
|
+
else:
|
|
1380
|
+
dispatcher.export_to_format(selected_results, formats[0], args.output, args.export_fields, args.zip)
|
|
1381
|
+
else:
|
|
1382
|
+
print("No valid articles selected for export.")
|
|
1383
|
+
else:
|
|
1384
|
+
# Export all results
|
|
1385
|
+
if len(formats) > 1:
|
|
1386
|
+
# Batch export: Extract base filename from output (remove extension)
|
|
1387
|
+
output_base = args.output.rsplit('.', 1)[0] if args.output else None
|
|
1388
|
+
dispatcher.batch_export(results, formats, output_base, args.export_fields, args.zip)
|
|
1389
|
+
else:
|
|
1390
|
+
# Single format export
|
|
1391
|
+
dispatcher.export_to_format(results, formats[0], args.output, args.export_fields, args.zip)
|
|
1392
|
+
|
|
1393
|
+
# Export as formatted citations if requested
|
|
1394
|
+
if args.citations and results:
|
|
1395
|
+
from lixplore.utils.export import export_to_citations, compress_export
|
|
1396
|
+
print(f"Generating {args.citations.upper()} style citations...")
|
|
1397
|
+
exported_path = export_to_citations(results, args.citations, args.output, args.export_fields)
|
|
1398
|
+
if args.zip and exported_path:
|
|
1399
|
+
compress_export(exported_path, remove_original=False)
|
|
1400
|
+
|
|
1401
|
+
# Save profile if requested (after export completes)
|
|
1402
|
+
if args.save_profile:
|
|
1403
|
+
config = profiles.create_profile_from_args(args)
|
|
1404
|
+
if config: # Only save if there are settings to save
|
|
1405
|
+
if profiles.save_profile(args.save_profile, config):
|
|
1406
|
+
print(f"Profile '{args.save_profile}' saved successfully")
|
|
1407
|
+
print(f" Settings saved: {', '.join(config.keys())}")
|
|
1408
|
+
else:
|
|
1409
|
+
print(f"Error: Could not save profile '{args.save_profile}'")
|
|
1410
|
+
else:
|
|
1411
|
+
print("Warning: No export settings to save in profile")
|
|
1412
|
+
|
|
1413
|
+
# Show results (titles + optional inline abstracts)
|
|
1414
|
+
# Display results BEFORE review so users don't see them again after closing review windows
|
|
1415
|
+
if results:
|
|
1416
|
+
print(f"\nFound {len(results)} results:")
|
|
1417
|
+
dispatcher.show_results(results, args)
|
|
1418
|
+
|
|
1419
|
+
# Save results to cache for later review
|
|
1420
|
+
all_sources = sources_to_search.copy()
|
|
1421
|
+
if use_custom_api:
|
|
1422
|
+
all_sources.append(f"custom:{custom_api_name}")
|
|
1423
|
+
dispatcher.save_results(results, query=query, sources=all_sources)
|
|
1424
|
+
|
|
1425
|
+
# Save to search history
|
|
1426
|
+
dispatcher.save_to_history(query=query, sources=all_sources, result_count=len(results))
|
|
1427
|
+
|
|
1428
|
+
# If user requested detailed view(s) via -N, print them inline
|
|
1429
|
+
if args.number:
|
|
1430
|
+
for n in args.number:
|
|
1431
|
+
if not isinstance(n, int):
|
|
1432
|
+
print(f"Invalid selection (not an integer): {n}")
|
|
1433
|
+
continue
|
|
1434
|
+
if 1 <= n <= len(results):
|
|
1435
|
+
idx = n - 1
|
|
1436
|
+
print("\n=== Detailed View ===")
|
|
1437
|
+
# print dict as readable JSON
|
|
1438
|
+
print(json.dumps(results[idx], indent=2, ensure_ascii=False))
|
|
1439
|
+
else:
|
|
1440
|
+
print(f"Selection out of range: {n} (valid 1..{len(results)})")
|
|
1441
|
+
|
|
1442
|
+
# Download PDFs if requested
|
|
1443
|
+
if args.download_pdf:
|
|
1444
|
+
pdf_numbers = args.pdf_numbers if args.pdf_numbers else None
|
|
1445
|
+
pdf_downloader.download_multiple_pdfs(
|
|
1446
|
+
results,
|
|
1447
|
+
article_numbers=pdf_numbers,
|
|
1448
|
+
use_scihub=args.use_scihub
|
|
1449
|
+
)
|
|
1450
|
+
|
|
1451
|
+
# Add to Zotero if requested
|
|
1452
|
+
if args.add_to_zotero:
|
|
1453
|
+
collection_key = args.zotero_collection if hasattr(args, 'zotero_collection') and args.zotero_collection else None
|
|
1454
|
+
reference_managers.add_to_zotero(results, collection_key=collection_key)
|
|
1455
|
+
|
|
1456
|
+
# Export for Mendeley if requested
|
|
1457
|
+
if args.export_for_mendeley:
|
|
1458
|
+
reference_managers.export_for_mendeley(results)
|
|
1459
|
+
|
|
1460
|
+
# Show statistics dashboard if requested
|
|
1461
|
+
if args.stat:
|
|
1462
|
+
from lixplore.utils.statistics import generate_statistics_report
|
|
1463
|
+
stats_report = generate_statistics_report(results, top_n=args.stat_top)
|
|
1464
|
+
print(stats_report)
|
|
1465
|
+
|
|
1466
|
+
# Launch interactive mode if requested
|
|
1467
|
+
if args.interactive:
|
|
1468
|
+
from lixplore.utils.interactive_tui import launch_interactive_mode
|
|
1469
|
+
launch_interactive_mode(results)
|
|
1470
|
+
return # Interactive mode handles everything
|
|
1471
|
+
|
|
1472
|
+
# Review articles in separate terminal if requested
|
|
1473
|
+
# This comes AFTER showing results so user sees the list, then reviews, then returns without duplication
|
|
1474
|
+
if args.review:
|
|
1475
|
+
dispatcher.review_articles(results, args.review)
|
|
1476
|
+
|
|
1477
|
+
# Handle annotations for articles from search results
|
|
1478
|
+
if args.annotate or args.show_annotation or args.delete_annotation:
|
|
1479
|
+
manager = AnnotationManager()
|
|
1480
|
+
|
|
1481
|
+
# Annotate article
|
|
1482
|
+
if args.annotate:
|
|
1483
|
+
article_num = args.annotate
|
|
1484
|
+
if 1 <= article_num <= len(results):
|
|
1485
|
+
article = results[article_num - 1]
|
|
1486
|
+
|
|
1487
|
+
# Collect annotation data
|
|
1488
|
+
tags_list = None
|
|
1489
|
+
if args.tags:
|
|
1490
|
+
tags_list = [t.strip() for t in args.tags.split(',')]
|
|
1491
|
+
|
|
1492
|
+
# Add annotation
|
|
1493
|
+
article_id = manager.annotate(
|
|
1494
|
+
article=article,
|
|
1495
|
+
comment=args.comment if hasattr(args, 'comment') and args.comment else None,
|
|
1496
|
+
rating=args.rating if hasattr(args, 'rating') and args.rating else None,
|
|
1497
|
+
tags=tags_list,
|
|
1498
|
+
read_status=args.read_status if hasattr(args, 'read_status') and args.read_status else None,
|
|
1499
|
+
priority=args.priority if hasattr(args, 'priority') and args.priority else None
|
|
1500
|
+
)
|
|
1501
|
+
|
|
1502
|
+
print(f"\nAnnotation saved for article #{article_num}: {article.get('title', 'No title')[:60]}...")
|
|
1503
|
+
|
|
1504
|
+
# Show the annotation
|
|
1505
|
+
annotation = manager.get_annotation(article_id)
|
|
1506
|
+
if annotation:
|
|
1507
|
+
display_annotation(annotation, article_id)
|
|
1508
|
+
else:
|
|
1509
|
+
print(f"Error: Article #{article_num} is out of range (1-{len(results)})")
|
|
1510
|
+
|
|
1511
|
+
# Show annotation
|
|
1512
|
+
if args.show_annotation:
|
|
1513
|
+
article_num = args.show_annotation
|
|
1514
|
+
if 1 <= article_num <= len(results):
|
|
1515
|
+
article = results[article_num - 1]
|
|
1516
|
+
annotation = manager.get_annotation_for_article(article)
|
|
1517
|
+
|
|
1518
|
+
if annotation:
|
|
1519
|
+
article_id = manager._get_article_id(article)
|
|
1520
|
+
display_annotation(annotation, article_id)
|
|
1521
|
+
else:
|
|
1522
|
+
print(f"\nNo annotation found for article #{article_num}")
|
|
1523
|
+
print(f"Add annotation with: lixplore --annotate {article_num} --rating 5 --tags 'important'")
|
|
1524
|
+
else:
|
|
1525
|
+
print(f"Error: Article #{article_num} is out of range (1-{len(results)})")
|
|
1526
|
+
|
|
1527
|
+
# Delete annotation
|
|
1528
|
+
if args.delete_annotation:
|
|
1529
|
+
article_num = args.delete_annotation
|
|
1530
|
+
if 1 <= article_num <= len(results):
|
|
1531
|
+
article = results[article_num - 1]
|
|
1532
|
+
article_id = manager._get_article_id(article)
|
|
1533
|
+
|
|
1534
|
+
if manager.remove_annotation(article_id):
|
|
1535
|
+
print(f"Annotation deleted for article #{article_num}")
|
|
1536
|
+
else:
|
|
1537
|
+
print(f"No annotation found for article #{article_num}")
|
|
1538
|
+
else:
|
|
1539
|
+
print(f"Error: Article #{article_num} is out of range (1-{len(results)})")
|
|
1540
|
+
|
|
1541
|
+
else:
|
|
1542
|
+
print("No results found.")
|
|
1543
|
+
|