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.
@@ -0,0 +1,10 @@
1
+ .clj-kondo/
2
+ .lsp/
3
+ .portal/
4
+ .git.bak/
5
+ .env.local
6
+ __pycache__/
7
+ .eggs/
8
+ dist/
9
+ build/
10
+ .editable.*
@@ -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,15 @@
1
+ from .core import (
2
+ console,
3
+ prompt_yes_no,
4
+ copy_to_clipboard,
5
+ prompt_toolkit_menu,
6
+ format_menu_choices
7
+ )
8
+
9
+ __all__ = [
10
+ "console",
11
+ "prompt_yes_no",
12
+ "copy_to_clipboard",
13
+ "prompt_toolkit_menu",
14
+ "format_menu_choices"
15
+ ]
@@ -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