-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathptvertmenu-man
executable file
·159 lines (138 loc) · 4.86 KB
/
ptvertmenu-man
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
#!/usr/bin/env python3
"""
Choose a man page to read interactively from a menu
"""
import argparse
import asyncio
import os
import re
from typing import Any, Generator, List, Optional, Sequence, cast
import ptvertmenu
from prompt_toolkit import Application
from prompt_toolkit.application import get_app
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.key_binding.bindings.focus import focus_next
from prompt_toolkit.key_binding.key_processor import KeyPressEvent
from prompt_toolkit.layout.containers import VSplit
from prompt_toolkit.layout.layout import Layout
from prompt_toolkit.styles import Style
from prompt_toolkit.widgets import Frame, TextArea
from ptvertmenu.vertmenu import Item
E = KeyPressEvent
PATH = "/usr/share/man"
ManItem = tuple[Any, tuple[int, str]]
def generator(section: Optional[int] = None) -> Generator[ManItem, None, None]:
labelre = re.compile(
re.escape(PATH)
+ r"/(?P<label>man(?P<section>[0-9]+)/(?P<base>.*))\.[0-9]\S*\.gz"
)
for root, _, files in os.walk(PATH):
for filename in files:
path = os.path.join(root, filename)
m = labelre.match(path)
if not m:
continue
if section is not None and int(m.group("section")) != section:
continue
yield (m.group("label"), (int(m.group("section")), m.group("base")))
async def man_loader(
contents: TextArea, queue: asyncio.Queue[Optional[tuple[int, str]]]
) -> None:
while True:
item = None
item = await queue.get()
while not queue.empty():
item = await queue.get()
if item is None:
contents.text = ""
queue.task_done()
continue
contents.text = f"Loading {item[1]}..."
manwidth = ""
if contents.window.render_info:
manwidth = f"MANWIDTH={contents.window.render_info.window_width - 1} "
man = await asyncio.create_subprocess_shell(
f"{manwidth}man --encoding=utf-8 {str(item[0])} {item[1]}",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.STDOUT,
)
(manpage, _) = await man.communicate()
manpage = re.sub(rb"\x1b\[[0-9;]*m?", b"", manpage)
contents.text = manpage.decode("utf-8", errors="ignore").replace("\t", " ")
queue.task_done()
async def manmenu(
section: Optional[int] = None, menu_max_width: Optional[int] = None
) -> None:
items: List[ManItem] = list(generator(section=section))
items.sort()
contents = TextArea(text="", multiline=True, wrap_lines=True, read_only=True)
manloader_queue: asyncio.Queue[Optional[tuple[int, str]]] = asyncio.Queue()
manloader_task = asyncio.create_task(man_loader(contents, manloader_queue))
def selected_handler(item: Optional[ManItem], index: Optional[int]) -> None:
if item is not None:
manloader_queue.put_nowait(item[1])
else:
manloader_queue.put_nowait(None)
def accept_handler(item: ManItem) -> None:
get_app().layout.focus(contents)
menu = ptvertmenu.FuzzFilterVertMenu(
items=cast(Sequence[Item], items),
selected_handler=selected_handler,
accept_handler=accept_handler,
menu_max_width=menu_max_width,
)
root_container = VSplit(
[
Frame(title="Man pages", body=menu),
Frame(title="Contents", body=contents),
]
)
layout = Layout(root_container)
layout.focus(menu)
# Use a basic style
style = Style.from_dict(
{
"vertmenu.focused vertmenu.selected": "bold fg:white bg:red",
"vertmenu.unfocused vertmenu.selected": "fg:white bg:darkred",
"vertmenu.unfocused vertmenu.item": "fg:grey bg:black",
}
)
kb = KeyBindings()
app: Application[None] = Application(
layout=layout,
key_bindings=kb,
full_screen=True,
style=style,
mouse_support=True,
)
@kb.add("tab")
def tab(event: E) -> None:
focus_next(event)
@kb.add("c-c")
@kb.add("c-d")
@kb.add("escape", "q")
def close(event: E) -> None:
manloader_task.cancel()
app.exit()
await app.run_async()
async def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--section",
type=int,
default=None,
help="Show only the specified man section",
)
parser.add_argument(
"--menu-max-width",
type=int,
default=None,
help="Max width of the menu on the left",
)
parser.add_argument(
"--version", "-V", action="version", version="%(prog)s " + ptvertmenu.version()
)
args = parser.parse_args()
await manmenu(section=args.section, menu_max_width=args.menu_max_width)
if __name__ == "__main__":
asyncio.get_event_loop().run_until_complete(main())