pas-tui 2026.1.20__tar.gz
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.
- pas_tui-2026.1.20/.gitignore +10 -0
- pas_tui-2026.1.20/PKG-INFO +30 -0
- pas_tui-2026.1.20/README.md +17 -0
- pas_tui-2026.1.20/pyproject.toml +25 -0
- pas_tui-2026.1.20/src/pas_tui/__init__.py +15 -0
- pas_tui-2026.1.20/src/pas_tui/core.py +184 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pas-tui
|
|
3
|
+
Version: 2026.1.20
|
|
4
|
+
Summary: A professional, hotkey-driven TUI system for Python CLIs
|
|
5
|
+
Project-URL: Homepage, https://github.com/nextoken/pas
|
|
6
|
+
Author-email: Roger <roger@example.com>
|
|
7
|
+
License: MIT
|
|
8
|
+
Requires-Python: >=3.8
|
|
9
|
+
Requires-Dist: prompt-toolkit>=3.0.0
|
|
10
|
+
Requires-Dist: questionary>=2.0.0
|
|
11
|
+
Requires-Dist: rich>=13.0.0
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# pas-tui
|
|
15
|
+
|
|
16
|
+
A professional, hotkey-driven TUI system for Python CLIs.
|
|
17
|
+
|
|
18
|
+
This is part of the [PAS Toolkit](https://github.com/nextoken/pas) which will be open-sourced in the future, but can be used independently.
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install pas-tui
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Features
|
|
27
|
+
- Numerical index padding (01, 02, etc.)
|
|
28
|
+
- Automatic hotkey mapping (1-9, q for Quit, b for Back)
|
|
29
|
+
- Rich-formatted output
|
|
30
|
+
- Cross-platform clipboard support
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# pas-tui
|
|
2
|
+
|
|
3
|
+
A professional, hotkey-driven TUI system for Python CLIs.
|
|
4
|
+
|
|
5
|
+
This is part of the [PAS Toolkit](https://github.com/nextoken/pas) which will be open-sourced in the future, but can be used independently.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install pas-tui
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
- Numerical index padding (01, 02, etc.)
|
|
15
|
+
- Automatic hotkey mapping (1-9, q for Quit, b for Back)
|
|
16
|
+
- Rich-formatted output
|
|
17
|
+
- Cross-platform clipboard support
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pas-tui"
|
|
7
|
+
version = "2026.01.20"
|
|
8
|
+
description = "A professional, hotkey-driven TUI system for Python CLIs"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Roger", email = "roger@example.com" }
|
|
14
|
+
]
|
|
15
|
+
dependencies = [
|
|
16
|
+
"rich>=13.0.0",
|
|
17
|
+
"prompt-toolkit>=3.0.0",
|
|
18
|
+
"questionary>=2.0.0"
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.urls]
|
|
22
|
+
Homepage = "https://github.com/nextoken/pas"
|
|
23
|
+
|
|
24
|
+
[tool.hatch.build.targets.wheel]
|
|
25
|
+
packages = ["src/pas_tui"]
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
import subprocess
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Optional, Dict, Any, List
|
|
5
|
+
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.panel import Panel
|
|
8
|
+
|
|
9
|
+
# Shared console instance for all Rich output
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
def prompt_yes_no(message: str, default: bool = False) -> bool:
|
|
13
|
+
"""Standardize yes/no confirmation prompts."""
|
|
14
|
+
suffix = "[Y/n]" if default else "[y/N]"
|
|
15
|
+
while True:
|
|
16
|
+
choice = input(f"{message} {suffix}: ").strip().lower()
|
|
17
|
+
if choice == "":
|
|
18
|
+
return default
|
|
19
|
+
if choice in {"y", "yes"}:
|
|
20
|
+
return True
|
|
21
|
+
if choice in {"n", "no"}:
|
|
22
|
+
return False
|
|
23
|
+
console.print("[yellow]Please enter y or n.[/yellow]")
|
|
24
|
+
|
|
25
|
+
def copy_to_clipboard(text: str) -> bool:
|
|
26
|
+
"""Copy text to the system clipboard across different platforms."""
|
|
27
|
+
try:
|
|
28
|
+
if shutil.which("pbcopy"):
|
|
29
|
+
subprocess.run(["pbcopy"], input=text, text=True, check=True)
|
|
30
|
+
return True
|
|
31
|
+
elif shutil.which("xclip"):
|
|
32
|
+
subprocess.run(["xclip", "-selection", "clipboard"], input=text, text=True, check=True)
|
|
33
|
+
return True
|
|
34
|
+
elif shutil.which("xsel"):
|
|
35
|
+
subprocess.run(["xsel", "--clipboard", "--input"], input=text, text=True, check=True)
|
|
36
|
+
return True
|
|
37
|
+
except Exception:
|
|
38
|
+
pass
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
def prompt_toolkit_menu(choices, style=None, hotkeys=None, default_idx=0):
|
|
42
|
+
"""Interactive selection menu supporting arrow keys and immediate hotkeys."""
|
|
43
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
44
|
+
from prompt_toolkit.application import Application
|
|
45
|
+
from prompt_toolkit.layout import Layout
|
|
46
|
+
from prompt_toolkit.layout.containers import Window, HSplit
|
|
47
|
+
from prompt_toolkit.layout.controls import FormattedTextControl
|
|
48
|
+
|
|
49
|
+
idx = max(0, min(default_idx, len(choices) - 1)) if choices else 0
|
|
50
|
+
kb = KeyBindings()
|
|
51
|
+
|
|
52
|
+
@kb.add('up')
|
|
53
|
+
def _(event):
|
|
54
|
+
nonlocal idx
|
|
55
|
+
idx = (idx - 1) % len(choices)
|
|
56
|
+
|
|
57
|
+
@kb.add('down')
|
|
58
|
+
def _(event):
|
|
59
|
+
nonlocal idx
|
|
60
|
+
idx = (idx + 1) % len(choices)
|
|
61
|
+
|
|
62
|
+
@kb.add('enter')
|
|
63
|
+
def _(event):
|
|
64
|
+
event.app.exit(result=choices[idx].value)
|
|
65
|
+
|
|
66
|
+
@kb.add('escape')
|
|
67
|
+
@kb.add('c-c')
|
|
68
|
+
def _(event):
|
|
69
|
+
event.app.exit(result=None)
|
|
70
|
+
|
|
71
|
+
def make_hotkey_handler(h_val):
|
|
72
|
+
keys = list(str(h_val))
|
|
73
|
+
@kb.add(*keys)
|
|
74
|
+
def _(event):
|
|
75
|
+
for choice in choices:
|
|
76
|
+
clean_title = choice.title.strip().lower()
|
|
77
|
+
title_prefix = clean_title.split('.')[0].strip().lstrip('0')
|
|
78
|
+
h_prefix = str(h_val).lstrip('0')
|
|
79
|
+
|
|
80
|
+
if clean_title.startswith(f"{h_val}."):
|
|
81
|
+
event.app.exit(result=choice.value)
|
|
82
|
+
return
|
|
83
|
+
if title_prefix == h_prefix and title_prefix != "":
|
|
84
|
+
event.app.exit(result=choice.value)
|
|
85
|
+
return
|
|
86
|
+
if str(choice.value) == str(h_val):
|
|
87
|
+
event.app.exit(result=choice.value)
|
|
88
|
+
return
|
|
89
|
+
return _
|
|
90
|
+
|
|
91
|
+
if not hotkeys:
|
|
92
|
+
hotkeys = []
|
|
93
|
+
for choice in choices:
|
|
94
|
+
title = choice.title.strip()
|
|
95
|
+
if '.' in title:
|
|
96
|
+
prefix = title.split('.')[0].strip().lower()
|
|
97
|
+
if prefix:
|
|
98
|
+
hotkeys.append(prefix)
|
|
99
|
+
if prefix.isdigit() and prefix.startswith('0') and prefix != '0':
|
|
100
|
+
hotkeys.append(prefix.lstrip('0'))
|
|
101
|
+
if not hotkeys:
|
|
102
|
+
hotkeys = [str(i) for i in range(1, 10)] + ['q', 'b']
|
|
103
|
+
|
|
104
|
+
seen = set()
|
|
105
|
+
unique_hotkeys = []
|
|
106
|
+
for h in hotkeys:
|
|
107
|
+
if h not in seen:
|
|
108
|
+
unique_hotkeys.append(h)
|
|
109
|
+
seen.add(h)
|
|
110
|
+
|
|
111
|
+
for h in unique_hotkeys:
|
|
112
|
+
make_hotkey_handler(h)
|
|
113
|
+
|
|
114
|
+
def get_text():
|
|
115
|
+
result = []
|
|
116
|
+
for i, choice in enumerate(choices):
|
|
117
|
+
if i == idx:
|
|
118
|
+
result.append(('class:selected', f" » {choice.title}\n"))
|
|
119
|
+
else:
|
|
120
|
+
result.append(('', f" {choice.title}\n"))
|
|
121
|
+
return result
|
|
122
|
+
|
|
123
|
+
layout = Layout(HSplit([
|
|
124
|
+
Window(content=FormattedTextControl(get_text)),
|
|
125
|
+
]))
|
|
126
|
+
|
|
127
|
+
from prompt_toolkit.styles import Style
|
|
128
|
+
if not style:
|
|
129
|
+
style = Style([('selected', 'fg:#cc9900')])
|
|
130
|
+
|
|
131
|
+
app = Application(layout=layout, key_bindings=kb, style=style, full_screen=False)
|
|
132
|
+
return app.run()
|
|
133
|
+
|
|
134
|
+
def format_menu_choices(items: List[Any], title_field: Optional[str] = None, value_field: Optional[str] = None) -> List[Any]:
|
|
135
|
+
"""Prepare a list of items for `prompt_toolkit_menu` by adding index numbers and hotkeys."""
|
|
136
|
+
import questionary
|
|
137
|
+
|
|
138
|
+
special_keywords = {
|
|
139
|
+
"quit": "q", "[quit]": "q", "(q) [quit]": "q", "q": "q",
|
|
140
|
+
"back": "b", "[back]": "b", "(b) [back]": "b", "b": "b",
|
|
141
|
+
"menu": "m", "[menu]": "m", "return to menu": "m"
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
regular_items = []
|
|
145
|
+
special_items = []
|
|
146
|
+
|
|
147
|
+
for item in items:
|
|
148
|
+
title = ""
|
|
149
|
+
if isinstance(item, dict):
|
|
150
|
+
if title_field: title = str(item.get(title_field))
|
|
151
|
+
elif "title" in item: title = str(item["title"])
|
|
152
|
+
else: title = str(item)
|
|
153
|
+
else:
|
|
154
|
+
title = str(item)
|
|
155
|
+
|
|
156
|
+
lower_title = title.strip().lower()
|
|
157
|
+
if lower_title in special_keywords:
|
|
158
|
+
special_items.append((item, special_keywords[lower_title]))
|
|
159
|
+
else:
|
|
160
|
+
regular_items.append(item)
|
|
161
|
+
|
|
162
|
+
pad = len(str(len(regular_items)))
|
|
163
|
+
choices = []
|
|
164
|
+
|
|
165
|
+
for i, item in enumerate(regular_items, 1):
|
|
166
|
+
idx_str = str(i).zfill(pad)
|
|
167
|
+
if isinstance(item, dict):
|
|
168
|
+
title = item.get(title_field) if title_field else item.get("title", str(item))
|
|
169
|
+
value = item.get(value_field) if value_field else item.get("value", item)
|
|
170
|
+
else:
|
|
171
|
+
title = str(item)
|
|
172
|
+
value = item
|
|
173
|
+
choices.append(questionary.Choice(f"{idx_str}. {title}", value=value))
|
|
174
|
+
|
|
175
|
+
for item, key in special_items:
|
|
176
|
+
if isinstance(item, dict):
|
|
177
|
+
title = item.get(title_field) if title_field else item.get("title", str(item))
|
|
178
|
+
value = item.get(value_field) if value_field else item.get("value", item)
|
|
179
|
+
else:
|
|
180
|
+
title = str(item)
|
|
181
|
+
value = item
|
|
182
|
+
choices.append(questionary.Choice(f"{' ' * (pad - 1)}{key}. {title}", value=value))
|
|
183
|
+
|
|
184
|
+
return choices
|