Skip to content

Commit

Permalink
Basic vertical menu module, with ptvertmenu-man for tests
Browse files Browse the repository at this point in the history
  • Loading branch information
lpenz committed Oct 30, 2023
1 parent a3710ef commit 97e10ce
Show file tree
Hide file tree
Showing 6 changed files with 383 additions and 1 deletion.
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.gitignore export-ignore
.gitattributes export-ignore
.github export-ignore
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ build-backend = "setuptools.build_meta"

[tool.mypy]
strict = true
files = "src"
files = "src, src/bin/*"
2 changes: 2 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ classifiers =
package_dir =
ptvertmenu = src/ptvertmenu
packages = find:
scripts =
src/bin/ptvertmenu-man
python_requires = >=3.9
install_requires = file:requirements.txt

Expand Down
141 changes: 141 additions & 0 deletions src/bin/ptvertmenu-man
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())
8 changes: 8 additions & 0 deletions src/ptvertmenu/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

import importlib.metadata

from .vertmenu import VertMenu


def version() -> str:
return importlib.metadata.version("ptvertmenu")


__all__ = [
"version",
"VertMenu",
]
Loading

0 comments on commit 97e10ce

Please sign in to comment.