python-arch-wiki 0.1.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.
- python_arch_wiki/__init__.py +0 -0
- python_arch_wiki/__main__.py +46 -0
- python_arch_wiki/menu.py +341 -0
- python_arch_wiki/toc.py +468 -0
- python_arch_wiki-0.1.0.dist-info/METADATA +109 -0
- python_arch_wiki-0.1.0.dist-info/RECORD +9 -0
- python_arch_wiki-0.1.0.dist-info/WHEEL +4 -0
- python_arch_wiki-0.1.0.dist-info/entry_points.txt +3 -0
- python_arch_wiki-0.1.0.dist-info/licenses/LICENSE +674 -0
python_arch_wiki/toc.py
ADDED
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
from collections.abc import Iterator
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
from bs4 import BeautifulSoup
|
|
10
|
+
from bs4.element import Tag
|
|
11
|
+
from rich import box
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.markup import escape
|
|
14
|
+
from rich.table import Table
|
|
15
|
+
|
|
16
|
+
# use Console() for paging, set default text color
|
|
17
|
+
console = Console(style="color(248)")
|
|
18
|
+
# set less as the pager
|
|
19
|
+
os.environ["MANPAGER"] = "less --raw-control-chars --mouse"
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Toc:
|
|
25
|
+
"""Arch wiki table of contents."""
|
|
26
|
+
|
|
27
|
+
root_url = "https://wiki.archlinux.org"
|
|
28
|
+
toc_session = requests.Session()
|
|
29
|
+
link_url: bool | None = None
|
|
30
|
+
|
|
31
|
+
Section = tuple[int, ...] | None
|
|
32
|
+
TocItem = tuple[Section, str, str, int]
|
|
33
|
+
MenuItem = tuple[Section, str]
|
|
34
|
+
Article = tuple[str, str]
|
|
35
|
+
|
|
36
|
+
section: Section
|
|
37
|
+
title: str
|
|
38
|
+
href: str
|
|
39
|
+
articles: list[Article]
|
|
40
|
+
num_articles: int
|
|
41
|
+
subsections: list["Toc"]
|
|
42
|
+
folded: bool
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
section: Tag | TocItem | None = None,
|
|
47
|
+
folded=True,
|
|
48
|
+
link_url=False,
|
|
49
|
+
) -> None:
|
|
50
|
+
"""Initialize Toc section."""
|
|
51
|
+
if type(self).link_url is None:
|
|
52
|
+
type(self).link_url = link_url
|
|
53
|
+
|
|
54
|
+
self.subsections = []
|
|
55
|
+
self.articles = []
|
|
56
|
+
|
|
57
|
+
if section is None:
|
|
58
|
+
for i in range(1, 4):
|
|
59
|
+
try:
|
|
60
|
+
r = self.toc_session.get(
|
|
61
|
+
self._url_from_parts(
|
|
62
|
+
[self.root_url, "/title/Table_of_contents"]
|
|
63
|
+
),
|
|
64
|
+
timeout=2,
|
|
65
|
+
)
|
|
66
|
+
r.raise_for_status()
|
|
67
|
+
|
|
68
|
+
soup = BeautifulSoup(r.text, "lxml")
|
|
69
|
+
rows = soup.select("#wiki-scripts-toc-table tr")
|
|
70
|
+
section = rows[0]
|
|
71
|
+
for row in rows[1:]:
|
|
72
|
+
self.add_subsection(row, folded=folded)
|
|
73
|
+
|
|
74
|
+
break
|
|
75
|
+
|
|
76
|
+
except requests.exceptions.ConnectionError as e:
|
|
77
|
+
time.sleep(0.5 * i)
|
|
78
|
+
logger.warning(f"Connection reset: {i=}: {e}")
|
|
79
|
+
continue
|
|
80
|
+
|
|
81
|
+
except requests.exceptions.RequestException as e:
|
|
82
|
+
logger.error(f"{e}")
|
|
83
|
+
sys.exit(f"{e}")
|
|
84
|
+
|
|
85
|
+
if isinstance(section, tuple):
|
|
86
|
+
self.section, self.title, self.href, self.num_articles = section
|
|
87
|
+
elif isinstance(section, Tag):
|
|
88
|
+
self.section, self.title, self.href, self.num_articles = (
|
|
89
|
+
self.parse_tag(section)
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
self.folded = False if self.section is None else folded
|
|
93
|
+
|
|
94
|
+
def close(self) -> None:
|
|
95
|
+
"""Close session."""
|
|
96
|
+
self.toc_session.close()
|
|
97
|
+
|
|
98
|
+
def add_subsection(self, tag: Tag, folded: bool = False) -> None:
|
|
99
|
+
"""Add a subsection to table of contents."""
|
|
100
|
+
subsection = self.parse_tag(tag)
|
|
101
|
+
assert subsection[0], "Can not add subsection without section"
|
|
102
|
+
|
|
103
|
+
# create a nested structure using section as a tuple of indices
|
|
104
|
+
parent = self
|
|
105
|
+
for i in subsection[0][:-1]:
|
|
106
|
+
parent = parent.subsections[i - 1]
|
|
107
|
+
|
|
108
|
+
parent.subsections.append(Toc(subsection, folded=folded))
|
|
109
|
+
|
|
110
|
+
def parse_tag(self, trtag: Tag) -> TocItem:
|
|
111
|
+
"""Extract section info from tr tag into a tuple."""
|
|
112
|
+
a = trtag.find("a") if trtag else None
|
|
113
|
+
if a is None:
|
|
114
|
+
raise ValueError("'a' tag not found")
|
|
115
|
+
|
|
116
|
+
title = a.get_text()
|
|
117
|
+
href = str(a["href"])
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
section = tuple(
|
|
121
|
+
int(x)
|
|
122
|
+
for x in a.find_previous_sibling("small")
|
|
123
|
+
.get_text() # type: ignore
|
|
124
|
+
.strip(".")
|
|
125
|
+
.split(".")
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
except (AttributeError, ValueError):
|
|
129
|
+
section = None
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
num_articles = int(
|
|
133
|
+
a.find_next_sibling("small")
|
|
134
|
+
.get_text() # type: ignore
|
|
135
|
+
.strip("()")
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
except (AttributeError, ValueError):
|
|
139
|
+
num_articles = 0
|
|
140
|
+
|
|
141
|
+
if not title or not href:
|
|
142
|
+
raise ValueError(f"Failed to parse tag: {a}")
|
|
143
|
+
else:
|
|
144
|
+
return section, title, href, num_articles
|
|
145
|
+
|
|
146
|
+
def get_entries(
|
|
147
|
+
self,
|
|
148
|
+
section: "Toc | None" = None,
|
|
149
|
+
toc_list: list[MenuItem] | None = None,
|
|
150
|
+
) -> list[MenuItem]:
|
|
151
|
+
"""Return a list of section/title tuples."""
|
|
152
|
+
if section is None:
|
|
153
|
+
section = self
|
|
154
|
+
if toc_list is None:
|
|
155
|
+
toc_list = []
|
|
156
|
+
|
|
157
|
+
if section.section:
|
|
158
|
+
section_str = (
|
|
159
|
+
f"{' ' * 4 * (len(section.section) - 1)}"
|
|
160
|
+
f"{section.section[-1]:>2}. "
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
if section.num_articles:
|
|
164
|
+
num_art_str = f" ({section.num_articles})"
|
|
165
|
+
else:
|
|
166
|
+
num_art_str = ""
|
|
167
|
+
|
|
168
|
+
if section.folded and section.subsections:
|
|
169
|
+
subsect_str = f" *{len(section.subsections)}"
|
|
170
|
+
else:
|
|
171
|
+
subsect_str = ""
|
|
172
|
+
|
|
173
|
+
toc_list.append(
|
|
174
|
+
(
|
|
175
|
+
section.section,
|
|
176
|
+
f"{section_str}{section.title}{num_art_str}{subsect_str}",
|
|
177
|
+
)
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
if section.subsections and not section.folded:
|
|
181
|
+
for subsection in section.subsections:
|
|
182
|
+
self.get_entries(subsection, toc_list)
|
|
183
|
+
|
|
184
|
+
return toc_list
|
|
185
|
+
|
|
186
|
+
def __iter__(self) -> Iterator[MenuItem]:
|
|
187
|
+
"""Return Toc iterator."""
|
|
188
|
+
return iter(self.get_entries())
|
|
189
|
+
|
|
190
|
+
def fold(self, section: Section) -> None:
|
|
191
|
+
"""Toggle displaying subsections."""
|
|
192
|
+
|
|
193
|
+
if isinstance(section, tuple):
|
|
194
|
+
subsection = self
|
|
195
|
+
for i in section:
|
|
196
|
+
subsection = subsection.subsections[i - 1]
|
|
197
|
+
subsection.folded = not subsection.folded
|
|
198
|
+
|
|
199
|
+
@staticmethod
|
|
200
|
+
def _url_from_parts(parts: list) -> str:
|
|
201
|
+
return "/".join(part.strip("/") for part in parts)
|
|
202
|
+
|
|
203
|
+
def get_submenu(self, section: Section) -> list[Article]:
|
|
204
|
+
"""Return list of articles."""
|
|
205
|
+
assert section, "Valid section required"
|
|
206
|
+
subsection = self
|
|
207
|
+
for i in section:
|
|
208
|
+
subsection = subsection.subsections[i - 1]
|
|
209
|
+
|
|
210
|
+
if not subsection.articles:
|
|
211
|
+
try:
|
|
212
|
+
with self.toc_session.get(
|
|
213
|
+
self._url_from_parts([self.root_url, subsection.href]),
|
|
214
|
+
timeout=2,
|
|
215
|
+
) as r:
|
|
216
|
+
r.raise_for_status()
|
|
217
|
+
|
|
218
|
+
soup = BeautifulSoup(r.text, "lxml")
|
|
219
|
+
categories = soup.select_one(".mw-category")
|
|
220
|
+
if categories:
|
|
221
|
+
for a in categories.select("a"):
|
|
222
|
+
subsection.articles.append(
|
|
223
|
+
(str(a["href"]), str(a["title"]))
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
except requests.exceptions.RequestException as e:
|
|
227
|
+
self.close()
|
|
228
|
+
logger.error(f"{e}")
|
|
229
|
+
sys.exit(f"{e}")
|
|
230
|
+
|
|
231
|
+
return subsection.articles
|
|
232
|
+
|
|
233
|
+
@staticmethod
|
|
234
|
+
def _console_print(text: str) -> None:
|
|
235
|
+
"""Add escape sequences and print."""
|
|
236
|
+
text = re.sub( # Command highlight
|
|
237
|
+
r"""
|
|
238
|
+
(^|:\s) # 1) start of the line or colon space
|
|
239
|
+
([#\$]) # 2) hash or dollar
|
|
240
|
+
(\s[a-z]\S*) # 3) space, letter, one or more of not a space
|
|
241
|
+
(.*) # 4) rest of the line
|
|
242
|
+
""",
|
|
243
|
+
r"\1[bold][green]\2[/][bright_white]\3[/]\4[/]",
|
|
244
|
+
text,
|
|
245
|
+
flags=re.MULTILINE | re.VERBOSE,
|
|
246
|
+
)
|
|
247
|
+
text = re.sub( # Option highlight
|
|
248
|
+
r"(\s-[\w-]+\b)",
|
|
249
|
+
r"[bold color(103)]\1[/]",
|
|
250
|
+
text,
|
|
251
|
+
)
|
|
252
|
+
console.print(text)
|
|
253
|
+
|
|
254
|
+
@staticmethod
|
|
255
|
+
def get_table_rows(tr_list: list[Tag]) -> list[list[str]] | None:
|
|
256
|
+
"""Return a list of lists (rows) of strings (cols)."""
|
|
257
|
+
nrows = len(tr_list)
|
|
258
|
+
if nrows == 0:
|
|
259
|
+
return None
|
|
260
|
+
|
|
261
|
+
rows = [[] for _ in range(nrows)]
|
|
262
|
+
rgx = re.compile("t[dh]")
|
|
263
|
+
|
|
264
|
+
for col in tr_list[0](rgx):
|
|
265
|
+
colspan = col.get("colspan")
|
|
266
|
+
rowspan = col.get("rowspan")
|
|
267
|
+
coltext = col.get_text()
|
|
268
|
+
rowspan = rowspan if rowspan is None else int(str(rowspan)) - 1
|
|
269
|
+
|
|
270
|
+
if colspan is None:
|
|
271
|
+
rows[0].append(coltext)
|
|
272
|
+
for row in rows[1:]:
|
|
273
|
+
if rowspan:
|
|
274
|
+
row.append("")
|
|
275
|
+
rowspan -= 1
|
|
276
|
+
else:
|
|
277
|
+
row.append(None)
|
|
278
|
+
else:
|
|
279
|
+
for _ in range(int(str(colspan))):
|
|
280
|
+
rows[0].append(f"[underline]{coltext}[/]")
|
|
281
|
+
for row in rows[1:]:
|
|
282
|
+
if rowspan:
|
|
283
|
+
row.append("")
|
|
284
|
+
rowspan -= 1
|
|
285
|
+
else:
|
|
286
|
+
row.append(None)
|
|
287
|
+
|
|
288
|
+
for i, row in enumerate(tr_list[1:]):
|
|
289
|
+
for j, col in enumerate(row(rgx)):
|
|
290
|
+
colspan = col.get("colspan")
|
|
291
|
+
rowspan = col.get("rowspan")
|
|
292
|
+
coltext = col.get_text()
|
|
293
|
+
rowspan = 1 if rowspan is None else int(str(rowspan))
|
|
294
|
+
|
|
295
|
+
while rows[i + 1][j] is not None:
|
|
296
|
+
j += 1
|
|
297
|
+
|
|
298
|
+
if colspan is None:
|
|
299
|
+
for n in range(rowspan):
|
|
300
|
+
rows[i + 1 + n][j] = coltext if n == 0 else ""
|
|
301
|
+
else:
|
|
302
|
+
j -= 1
|
|
303
|
+
for _ in range(int(str(colspan))):
|
|
304
|
+
j += 1
|
|
305
|
+
for n in range(rowspan):
|
|
306
|
+
rows[i + 1 + n][j] = (
|
|
307
|
+
f"[underline]{coltext}[/]" if n == 0 else ""
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
return rows
|
|
311
|
+
|
|
312
|
+
@classmethod
|
|
313
|
+
def _parse_section(cls, section: Tag | None) -> None:
|
|
314
|
+
"""Extract text from tags and apply formatting."""
|
|
315
|
+
if section is None:
|
|
316
|
+
return
|
|
317
|
+
|
|
318
|
+
for tag in section.children: # type: ignore
|
|
319
|
+
tag: Tag
|
|
320
|
+
if tag.name is None:
|
|
321
|
+
continue
|
|
322
|
+
|
|
323
|
+
for code in tag("code"):
|
|
324
|
+
code.name = "p"
|
|
325
|
+
code.string = f"[bold white]{code.get_text()}[/]"
|
|
326
|
+
|
|
327
|
+
for a in tag("a"):
|
|
328
|
+
href = str(a.get("href"))
|
|
329
|
+
text = a.string
|
|
330
|
+
|
|
331
|
+
if text and href != text:
|
|
332
|
+
a.name = "p"
|
|
333
|
+
if href.startswith("/title"):
|
|
334
|
+
href = cls._url_from_parts([cls.root_url, href])
|
|
335
|
+
|
|
336
|
+
# do we follow link with url?
|
|
337
|
+
text_len = len(text)
|
|
338
|
+
href_len = len(href)
|
|
339
|
+
if cls.link_url and href.startswith("http"):
|
|
340
|
+
text += rf" \[[#515478]{href}[/]]"
|
|
341
|
+
if text_len + href_len < console.width:
|
|
342
|
+
text = text.replace(" ", "[#515478]_[/]")
|
|
343
|
+
|
|
344
|
+
a.string = f"[bold color(251)]{text}[/]"
|
|
345
|
+
|
|
346
|
+
if "mw-heading" in tag.get_attribute_list("class"):
|
|
347
|
+
console.rule(
|
|
348
|
+
f"[white]{tag.get_text()}[/]", style="bright_black"
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
elif "archwiki-template-box" in tag.get_attribute_list("class"):
|
|
352
|
+
strong: Tag | str | None = tag.find("strong")
|
|
353
|
+
if strong:
|
|
354
|
+
strong = strong.extract().get_text()
|
|
355
|
+
color = "bold"
|
|
356
|
+
|
|
357
|
+
match strong.lower():
|
|
358
|
+
case "tip":
|
|
359
|
+
color += " green"
|
|
360
|
+
case "note":
|
|
361
|
+
color += " blue"
|
|
362
|
+
case "warning":
|
|
363
|
+
color += " yellow"
|
|
364
|
+
|
|
365
|
+
cls._console_print(
|
|
366
|
+
f"[{color}]{strong}:[/]{tag.get_text()}\n"
|
|
367
|
+
)
|
|
368
|
+
else:
|
|
369
|
+
cls._console_print(tag.get_text())
|
|
370
|
+
|
|
371
|
+
elif "archwiki-template-message" in tag.get_attribute_list(
|
|
372
|
+
"class"
|
|
373
|
+
):
|
|
374
|
+
cls._console_print(tag.get_text())
|
|
375
|
+
|
|
376
|
+
elif "mw-hidden-catlinks" in tag.get_attribute_list("class"):
|
|
377
|
+
continue
|
|
378
|
+
|
|
379
|
+
elif tag.name == "div":
|
|
380
|
+
cls._parse_section(tag)
|
|
381
|
+
|
|
382
|
+
elif tag.name == "table":
|
|
383
|
+
caption = tag.find("caption")
|
|
384
|
+
title = caption.get_text() if caption else None
|
|
385
|
+
table = Table(
|
|
386
|
+
title=title,
|
|
387
|
+
box=box.ROUNDED,
|
|
388
|
+
highlight=True,
|
|
389
|
+
show_lines=True,
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
rows = cls.get_table_rows(tag("tr"))
|
|
393
|
+
if not rows:
|
|
394
|
+
logger.warning(f"Empty table: {tag}")
|
|
395
|
+
continue
|
|
396
|
+
|
|
397
|
+
for col in rows[0]:
|
|
398
|
+
table.add_column(col)
|
|
399
|
+
for row in rows[1:]:
|
|
400
|
+
table.add_row(*row)
|
|
401
|
+
|
|
402
|
+
console.print(table)
|
|
403
|
+
|
|
404
|
+
else:
|
|
405
|
+
cls._console_print(tag.get_text())
|
|
406
|
+
|
|
407
|
+
@classmethod
|
|
408
|
+
def display_contents(cls, href: str | tuple[str]) -> None:
|
|
409
|
+
"""Display article."""
|
|
410
|
+
if isinstance(href, tuple):
|
|
411
|
+
href = "/title/" + "_".join(href)
|
|
412
|
+
try:
|
|
413
|
+
with cls.toc_session.get(
|
|
414
|
+
cls._url_from_parts([cls.root_url, href]),
|
|
415
|
+
timeout=1,
|
|
416
|
+
) as r:
|
|
417
|
+
r.raise_for_status()
|
|
418
|
+
soup = BeautifulSoup(escape(r.text), "lxml")
|
|
419
|
+
|
|
420
|
+
except requests.exceptions.RequestException as e:
|
|
421
|
+
cls.toc_session.close()
|
|
422
|
+
logger.error(f"{e}")
|
|
423
|
+
sys.exit(f"{e}")
|
|
424
|
+
|
|
425
|
+
with console.pager(styles=True):
|
|
426
|
+
cls._parse_section(soup.select_one("#bodyContent"))
|
|
427
|
+
|
|
428
|
+
def search(self, text: str) -> list[Article]:
|
|
429
|
+
"""Search arch wiki."""
|
|
430
|
+
search_text = text.strip().replace(" ", "+")
|
|
431
|
+
payload = {
|
|
432
|
+
"search": search_text,
|
|
433
|
+
"title": "Special%3ASearch",
|
|
434
|
+
"profile": "default",
|
|
435
|
+
"fulltext": "1",
|
|
436
|
+
}
|
|
437
|
+
results = []
|
|
438
|
+
|
|
439
|
+
try:
|
|
440
|
+
with self.toc_session.get(
|
|
441
|
+
self._url_from_parts([self.root_url, "index.php"]),
|
|
442
|
+
params=payload,
|
|
443
|
+
timeout=1,
|
|
444
|
+
) as r:
|
|
445
|
+
r.raise_for_status()
|
|
446
|
+
soup = BeautifulSoup(r.text, "lxml")
|
|
447
|
+
|
|
448
|
+
except requests.exceptions.RequestException as e:
|
|
449
|
+
logger.error(f"{e}")
|
|
450
|
+
|
|
451
|
+
else:
|
|
452
|
+
for tag in soup.select(".mw-search-result"):
|
|
453
|
+
heading = tag.find(class_="mw-search-result-heading")
|
|
454
|
+
if heading and (a := heading.find("a")):
|
|
455
|
+
results.append((str(a["href"]), str(a["title"])))
|
|
456
|
+
|
|
457
|
+
return results
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def main() -> None:
|
|
461
|
+
toc = Toc(folded=False)
|
|
462
|
+
|
|
463
|
+
print(toc.get_submenu((1, 1)))
|
|
464
|
+
toc.close()
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
if __name__ == "__main__":
|
|
468
|
+
main()
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-arch-wiki
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A terminal interface for browsing the Arch Linux Wiki
|
|
5
|
+
Author: Anton Kaplan
|
|
6
|
+
Author-email: Anton Kaplan <anton.kaplan@live.ca>
|
|
7
|
+
License-Expression: GPL-3.0-or-later
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Dist: autocommand>=2.2.2
|
|
12
|
+
Requires-Dist: bs4>=0.0.2
|
|
13
|
+
Requires-Dist: lxml>=6.0.2
|
|
14
|
+
Requires-Dist: platformdirs>=4.11.0
|
|
15
|
+
Requires-Dist: requests>=2.32.5
|
|
16
|
+
Requires-Dist: rich>=14.2.0
|
|
17
|
+
Requires-Python: >=3.13
|
|
18
|
+
Project-URL: Homepage, https://github.com/termctrlseq/python-arch-wiki
|
|
19
|
+
Project-URL: Issues, https://github.com/termctrlseq/python-arch-wiki/issues
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# python-arch-wiki
|
|
23
|
+
|
|
24
|
+
A terminal interface for browsing the [Arch Linux Wiki](https://wiki.archlinux.org/), with an interactive table of contents and article search.
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
To install in the virtual environment in the directory `python_arch_wiki` and create a link with a name *wiki* (assuming `~/.local/bin` is in the **PATH**):
|
|
29
|
+
```bash
|
|
30
|
+
mkdir python_arch_wiki
|
|
31
|
+
cd python_arch_wiki
|
|
32
|
+
python3 -m venv venv
|
|
33
|
+
venv/bin/python -m pip install \
|
|
34
|
+
-e 'python-arch-wiki @ git+https://github.com/termctrlseq/python-arch-wiki.git'
|
|
35
|
+
ln -s "$PWD/venv/bin/python-arch-wiki" "$HOME/.local/bin/wiki"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
To add `~/.local/bin` to **PATH** put this in your `~/.bashrc`
|
|
39
|
+
```bash
|
|
40
|
+
# add ~/.local/bin to PATH if not in it
|
|
41
|
+
[[ ":${PATH}:" != *:"${HOME}/.local/bin":* ]] \
|
|
42
|
+
&& export PATH="${HOME}/.local/bin:${PATH}"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Then the source code can be found in `~/python_arch_wiki/venv/src/`.
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
wiki [-l] [-v] [article name]
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Options
|
|
54
|
+
|
|
55
|
+
```text
|
|
56
|
+
-l, --link-url Show URLs in links
|
|
57
|
+
-v, --verbose Enable debug logging
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Examples
|
|
61
|
+
|
|
62
|
+
Open the table of contents:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
wiki
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
From there, press `/` to search the Arch Wiki. The search uses the Arch Wiki's own search engine, and the results are presented in a navigable menu. For convenience, this can be preferable to specifying a longer query on the command line:
|
|
69
|
+
|
|
70
|
+
```text
|
|
71
|
+
/install
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
For short, specific searches, giving the article name on the command line is often more convenient:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
wiki zram
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Controls
|
|
81
|
+
|
|
82
|
+
| Key | Action |
|
|
83
|
+
| ---------------------------------- | ------------------- |
|
|
84
|
+
| `↑` / `k` / `Ctrl-P` / `Shift-Tab` | Previous |
|
|
85
|
+
| `↓` / `j` / `Ctrl-N` / `Tab` | Next |
|
|
86
|
+
| `H` | First visible item |
|
|
87
|
+
| `M` | Middle visible item |
|
|
88
|
+
| `L` | Last visible item |
|
|
89
|
+
| `Enter` | Open |
|
|
90
|
+
| `Space` / `l` | Fold/unfold |
|
|
91
|
+
| `h` / `u` / `Esc` | Go up |
|
|
92
|
+
| `/` / `?` | Search |
|
|
93
|
+
| `q` / `Ctrl-D` | Quit |
|
|
94
|
+
|
|
95
|
+
Mouse input is also supported:
|
|
96
|
+
| Button | Action |
|
|
97
|
+
| ------------------- | ----------- |
|
|
98
|
+
| *Left click* | select |
|
|
99
|
+
| *Double left click* | open |
|
|
100
|
+
| *Right click* | fold/unfold |
|
|
101
|
+
| *Wheel* | navigate |
|
|
102
|
+
|
|
103
|
+
Articles are rendered in the terminal and paged with `less`.
|
|
104
|
+
|
|
105
|
+
## Requirements
|
|
106
|
+
|
|
107
|
+
* Python 3.13+
|
|
108
|
+
* A terminal with `curses` support
|
|
109
|
+
* Internet connection
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
python_arch_wiki/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
python_arch_wiki/__main__.py,sha256=Koj2bOv0fnNW9BFQ6dhs1kXhr5ZY9bLt1-aggJv1VX0,1114
|
|
3
|
+
python_arch_wiki/menu.py,sha256=sjtfLww7OcFPpdniGyoWmof_Eu94NRKJiRCf5hWhUIM,10075
|
|
4
|
+
python_arch_wiki/toc.py,sha256=LL7O0k9Cw3f3w5bKS572w9drhAzQd-xtpBwZt9LjhWE,14999
|
|
5
|
+
python_arch_wiki-0.1.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
6
|
+
python_arch_wiki-0.1.0.dist-info/WHEEL,sha256=Kot-FOXwz2no6rGIg4et79Z7m6vcKPAXr2Y6fTFpZuI,81
|
|
7
|
+
python_arch_wiki-0.1.0.dist-info/entry_points.txt,sha256=JgtU2VxggJtVjpkcj8GxmpTNzPqD0a4wcjxqqWfMCXs,69
|
|
8
|
+
python_arch_wiki-0.1.0.dist-info/METADATA,sha256=U9ew55rU3XjNjUhd4lxl5IcMbkuS63kVuZ0x3iQoeG0,3378
|
|
9
|
+
python_arch_wiki-0.1.0.dist-info/RECORD,,
|