-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Basic vertical menu module, with ptvertmenu-man for tests
- Loading branch information
Showing
6 changed files
with
383 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
.gitignore export-ignore | ||
.gitattributes export-ignore | ||
.github export-ignore |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,4 +6,4 @@ build-backend = "setuptools.build_meta" | |
|
||
[tool.mypy] | ||
strict = true | ||
files = "src" | ||
files = "src, src/bin/*" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
#!/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 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[str, tuple[int, str]] | ||
|
||
|
||
def generator() -> 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 | ||
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]}..." | ||
width = 80 | ||
if contents.window.render_info: | ||
width = contents.window.render_info.window_width - 1 | ||
man = await asyncio.create_subprocess_shell( | ||
f"MANWIDTH={width} man --encoding=utf-8 {str(item[0])} {item[1]} | col -bh", | ||
stdout=asyncio.subprocess.PIPE, | ||
stderr=asyncio.subprocess.STDOUT, | ||
) | ||
(manpage, _) = await man.communicate() | ||
contents.text = manpage.decode("utf-8", errors="ignore").replace("\t", " ") | ||
queue.task_done() | ||
|
||
|
||
async def manmenu() -> None: | ||
items: List[ManItem] = list(generator()) | ||
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 current_handler(item: Optional[ManItem]) -> 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.VertMenu( | ||
items=cast(Sequence[Item], items), | ||
current_handler=current_handler, | ||
accept_handler=accept_handler, | ||
) | ||
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.current": "fg:black bg:white", | ||
"vertmenu.unfocused vertmenu.current": "reverse", | ||
"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( | ||
"--version", "-V", action="version", version="%(prog)s " + ptvertmenu.version() | ||
) | ||
_ = parser.parse_args() | ||
await manmenu() | ||
|
||
|
||
if __name__ == "__main__": | ||
asyncio.get_event_loop().run_until_complete(main()) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.