termtools-tui 0.1.0__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,49 @@
1
+ Metadata-Version: 2.4
2
+ Name: termtools-tui
3
+ Version: 0.1.0
4
+ Summary: A beautiful TUI to browse the CLI tools you have installed.
5
+ Author: Max Tillinger
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/mtt2016/termtools
8
+ Keywords: tui,cli,terminal,tools,textual
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: System :: Shells
15
+ Classifier: Topic :: Utilities
16
+ Requires-Python: >=3.9
17
+ Description-Content-Type: text/markdown
18
+ Requires-Dist: textual>=0.50
19
+
20
+ # termtools ✨
21
+
22
+ A beautiful Textual TUI to browse the CLI tools you have installed on your system.
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ pipx install termtools-tui
28
+ ```
29
+
30
+ Then run:
31
+
32
+ ```bash
33
+ termtools
34
+ ```
35
+
36
+ ## Features
37
+
38
+ - Scans your `$PATH` against a curated catalog of ~100 CLI tools across 13 categories
39
+ - Tabs to filter: Editors, Search, File & Disk, Git, System, Network, Languages, Package Managers, Containers, Data, Shells, AI, Fun, plus a "Cool Picks ★" tab
40
+ - Live search bar
41
+ - Per-tool detail pane: description, path, size
42
+ - Buttons to **Run**, **Inspect**, and view **Install info** with copy-paste install commands for brew, apt, dnf, pacman, apk, winget, scoop, and chocolatey
43
+
44
+ ## Keys
45
+
46
+ - `/` focus search
47
+ - `r` refresh
48
+ - `enter` inspect selected tool
49
+ - `q` quit
@@ -0,0 +1,30 @@
1
+ # termtools ✨
2
+
3
+ A beautiful Textual TUI to browse the CLI tools you have installed on your system.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pipx install termtools-tui
9
+ ```
10
+
11
+ Then run:
12
+
13
+ ```bash
14
+ termtools
15
+ ```
16
+
17
+ ## Features
18
+
19
+ - Scans your `$PATH` against a curated catalog of ~100 CLI tools across 13 categories
20
+ - Tabs to filter: Editors, Search, File & Disk, Git, System, Network, Languages, Package Managers, Containers, Data, Shells, AI, Fun, plus a "Cool Picks ★" tab
21
+ - Live search bar
22
+ - Per-tool detail pane: description, path, size
23
+ - Buttons to **Run**, **Inspect**, and view **Install info** with copy-paste install commands for brew, apt, dnf, pacman, apk, winget, scoop, and chocolatey
24
+
25
+ ## Keys
26
+
27
+ - `/` focus search
28
+ - `r` refresh
29
+ - `enter` inspect selected tool
30
+ - `q` quit
@@ -0,0 +1,32 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "termtools-tui"
7
+ version = "0.1.0"
8
+ description = "A beautiful TUI to browse the CLI tools you have installed."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Max Tillinger" }]
13
+ keywords = ["tui", "cli", "terminal", "tools", "textual"]
14
+ classifiers = [
15
+ "Environment :: Console",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3",
20
+ "Topic :: System :: Shells",
21
+ "Topic :: Utilities",
22
+ ]
23
+ dependencies = ["textual>=0.50"]
24
+
25
+ [project.scripts]
26
+ termtools = "termtools:main"
27
+
28
+ [project.urls]
29
+ Homepage = "https://github.com/mtt2016/termtools"
30
+
31
+ [tool.setuptools.packages.find]
32
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,543 @@
1
+ #!/usr/bin/env python3
2
+ """termtools — a beautiful TUI to browse the CLI tools installed on your system."""
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import shutil
7
+ import subprocess
8
+ import platform
9
+ from dataclasses import dataclass, field
10
+ from pathlib import Path
11
+
12
+ from textual.app import App, ComposeResult
13
+ from textual.containers import Horizontal, Vertical, VerticalScroll, Container
14
+ from textual.screen import ModalScreen
15
+ from textual.widgets import (
16
+ Header, Footer, Input, Button, Static, DataTable, Label, Tabs, Tab, TextArea
17
+ )
18
+ from textual.binding import Binding
19
+ from textual.reactive import reactive
20
+
21
+
22
+ # ---------- curated metadata ----------
23
+ # Category -> list of (tool, description)
24
+ CATALOG: dict[str, list[tuple[str, str]]] = {
25
+ "Editors": [
26
+ ("vim", "Highly configurable modal text editor"),
27
+ ("nvim", "Hyperextensible Vim-based text editor"),
28
+ ("nano", "Tiny friendly terminal editor"),
29
+ ("emacs", "Extensible self-documenting editor"),
30
+ ("micro", "Modern intuitive terminal editor"),
31
+ ("helix", "Post-modern modal editor"),
32
+ ("code", "VS Code launcher"),
33
+ ],
34
+ "Search & Find": [
35
+ ("rg", "ripgrep — blazing fast recursive search"),
36
+ ("ag", "the_silver_searcher — fast code search"),
37
+ ("ack", "grep-like tool optimized for code"),
38
+ ("fd", "Simple, fast alternative to find"),
39
+ ("find", "POSIX file search"),
40
+ ("grep", "POSIX pattern search"),
41
+ ("fzf", "Fuzzy finder for the command line"),
42
+ ("locate", "Find files by name from a database"),
43
+ ],
44
+ "File & Disk": [
45
+ ("ls", "List directory contents"),
46
+ ("eza", "Modern replacement for ls"),
47
+ ("exa", "Modern replacement for ls (legacy)"),
48
+ ("tree", "Recursive directory listing"),
49
+ ("bat", "cat clone with syntax highlighting"),
50
+ ("less", "Terminal pager"),
51
+ ("dust", "More intuitive du"),
52
+ ("duf", "Disk usage / free utility"),
53
+ ("ncdu", "Disk usage analyzer with TUI"),
54
+ ("rsync", "Fast incremental file transfer"),
55
+ ("trash", "Send files to the trash"),
56
+ ],
57
+ "Git & VCS": [
58
+ ("git", "Distributed version control"),
59
+ ("gh", "GitHub CLI"),
60
+ ("glab", "GitLab CLI"),
61
+ ("lazygit", "Simple terminal UI for git"),
62
+ ("tig", "Text-mode interface for git"),
63
+ ("hub", "Git wrapper for GitHub"),
64
+ ("delta", "Syntax-highlighted git diffs"),
65
+ ],
66
+ "System & Process": [
67
+ ("htop", "Interactive process viewer"),
68
+ ("btop", "Beautiful resource monitor"),
69
+ ("top", "Process monitor"),
70
+ ("ps", "Process status"),
71
+ ("procs", "Modern replacement for ps"),
72
+ ("bottom", "Cross-platform graphical process monitor"),
73
+ ("glances", "Cross-platform monitoring tool"),
74
+ ("neofetch", "System info ASCII art"),
75
+ ("fastfetch", "Fast system info tool"),
76
+ ],
77
+ "Network": [
78
+ ("curl", "Transfer data from/to URLs"),
79
+ ("wget", "Network downloader"),
80
+ ("httpie", "User-friendly HTTP client"),
81
+ ("xh", "Friendly and fast HTTP client"),
82
+ ("dog", "Modern command-line DNS client"),
83
+ ("dig", "DNS lookup utility"),
84
+ ("nslookup", "Query DNS records"),
85
+ ("ping", "Send ICMP echo requests"),
86
+ ("traceroute", "Trace network path"),
87
+ ("mtr", "Network diagnostic tool"),
88
+ ("nmap", "Network exploration & security"),
89
+ ("netcat", "TCP/UDP swiss army knife"),
90
+ ("nc", "TCP/UDP swiss army knife"),
91
+ ("ssh", "OpenSSH client"),
92
+ ],
93
+ "Languages & Runtimes": [
94
+ ("python", "Python interpreter"),
95
+ ("python3", "Python 3 interpreter"),
96
+ ("node", "Node.js JavaScript runtime"),
97
+ ("deno", "Modern JS/TS runtime"),
98
+ ("bun", "Fast JS runtime & toolkit"),
99
+ ("ruby", "Ruby interpreter"),
100
+ ("go", "Go toolchain"),
101
+ ("rustc", "Rust compiler"),
102
+ ("cargo", "Rust package manager"),
103
+ ("java", "Java runtime"),
104
+ ("php", "PHP interpreter"),
105
+ ],
106
+ "Package Managers": [
107
+ ("brew", "Homebrew package manager"),
108
+ ("npm", "Node package manager"),
109
+ ("pnpm", "Fast disk-efficient package manager"),
110
+ ("yarn", "JS package manager"),
111
+ ("pip", "Python package installer"),
112
+ ("pipx", "Install Python apps in isolation"),
113
+ ("uv", "Extremely fast Python package manager"),
114
+ ("apt", "Debian package manager"),
115
+ ("dnf", "Fedora package manager"),
116
+ ("pacman", "Arch package manager"),
117
+ ],
118
+ "Containers & Cloud": [
119
+ ("docker", "Container platform"),
120
+ ("podman", "Daemonless container engine"),
121
+ ("kubectl", "Kubernetes CLI"),
122
+ ("helm", "Kubernetes package manager"),
123
+ ("k9s", "Kubernetes TUI"),
124
+ ("aws", "AWS CLI"),
125
+ ("gcloud", "Google Cloud CLI"),
126
+ ("az", "Azure CLI"),
127
+ ("terraform", "Infrastructure as code"),
128
+ ],
129
+ "Data & Text": [
130
+ ("jq", "Command-line JSON processor"),
131
+ ("yq", "YAML/JSON/XML processor"),
132
+ ("awk", "Pattern scanning language"),
133
+ ("sed", "Stream editor"),
134
+ ("sqlite3", "SQLite shell"),
135
+ ("psql", "PostgreSQL client"),
136
+ ("mysql", "MySQL client"),
137
+ ("xsv", "Fast CSV toolkit"),
138
+ ("miller", "Like awk/sed for CSV/JSON"),
139
+ ],
140
+ "Shells & Multiplex": [
141
+ ("bash", "Bourne Again Shell"),
142
+ ("zsh", "Z shell"),
143
+ ("fish", "Friendly interactive shell"),
144
+ ("nu", "Nushell — structured data shell"),
145
+ ("tmux", "Terminal multiplexer"),
146
+ ("screen", "Terminal multiplexer (classic)"),
147
+ ("zellij", "Modern terminal workspace"),
148
+ ],
149
+ "AI & Dev Tools": [
150
+ ("claude", "Claude Code CLI"),
151
+ ("ollama", "Run LLMs locally"),
152
+ ("gemini", "Gemini CLI"),
153
+ ("aider", "AI pair programming"),
154
+ ("make", "Build automation"),
155
+ ("cmake", "Cross-platform build system"),
156
+ ("ninja", "Small fast build system"),
157
+ ],
158
+ "Fun & Misc": [
159
+ ("cowsay", "ASCII cow speaks your message"),
160
+ ("figlet", "Big ASCII text banners"),
161
+ ("lolcat", "Rainbow text"),
162
+ ("sl", "Steam locomotive (typo of ls)"),
163
+ ("fortune", "Random quotes"),
164
+ ("toilet", "More ASCII text banners"),
165
+ ("tldr", "Simplified man pages"),
166
+ ],
167
+ }
168
+
169
+ COOL_PICKS = {
170
+ "rg", "fd", "fzf", "bat", "eza", "delta", "lazygit", "btop",
171
+ "jq", "httpie", "xh", "zellij", "helix", "nvim", "tldr",
172
+ "dust", "duf", "ncdu", "k9s", "uv", "bun", "nu", "claude",
173
+ }
174
+
175
+ # Build reverse lookup
176
+ TOOL_META: dict[str, tuple[str, str]] = {} # tool -> (category, description)
177
+ for cat, items in CATALOG.items():
178
+ for name, desc in items:
179
+ TOOL_META[name] = (cat, desc)
180
+
181
+ ALL_CATEGORIES = ["All", "Cool Picks ★"] + list(CATALOG.keys())
182
+
183
+
184
+ # ---------- detection ----------
185
+ @dataclass
186
+ class Tool:
187
+ name: str
188
+ path: str
189
+ category: str
190
+ description: str
191
+ installed: bool = True
192
+ is_cool: bool = False
193
+ size: int | None = None
194
+ version: str | None = None
195
+
196
+
197
+ def scan_installed() -> dict[str, Tool]:
198
+ """Scan PATH for installed executables that we know about."""
199
+ found: dict[str, Tool] = {}
200
+ path_dirs = os.environ.get("PATH", "").split(os.pathsep)
201
+ seen_in_path: set[str] = set()
202
+ for d in path_dirs:
203
+ try:
204
+ for entry in os.scandir(d):
205
+ if entry.is_file() and os.access(entry.path, os.X_OK):
206
+ seen_in_path.add(entry.name)
207
+ except (FileNotFoundError, PermissionError, NotADirectoryError):
208
+ continue
209
+
210
+ # Known tools that exist
211
+ for name, (cat, desc) in TOOL_META.items():
212
+ if name in seen_in_path:
213
+ p = shutil.which(name) or ""
214
+ found[name] = Tool(
215
+ name=name, path=p, category=cat, description=desc,
216
+ installed=True, is_cool=name in COOL_PICKS,
217
+ )
218
+
219
+ # Also include unknown tools as "Other" — but only ones that look like real CLIs
220
+ # Skip to avoid noise; user wants what they have downloaded that we know about.
221
+ return found
222
+
223
+
224
+ # ---------- install instructions ----------
225
+ def install_instructions(tool: str) -> str:
226
+ return (
227
+ f"[b cyan]Install instructions for[/] [b yellow]{tool}[/]\n\n"
228
+ f"[b]macOS (Homebrew)[/]\n brew install {tool}\n\n"
229
+ f"[b]macOS (MacPorts)[/]\n sudo port install {tool}\n\n"
230
+ f"[b]Debian / Ubuntu[/]\n sudo apt install {tool}\n\n"
231
+ f"[b]Fedora[/]\n sudo dnf install {tool}\n\n"
232
+ f"[b]Arch[/]\n sudo pacman -S {tool}\n\n"
233
+ f"[b]Alpine[/]\n sudo apk add {tool}\n\n"
234
+ f"[b]Windows (winget)[/]\n winget install {tool}\n\n"
235
+ f"[b]Windows (Scoop)[/]\n scoop install {tool}\n\n"
236
+ f"[b]Windows (Chocolatey)[/]\n choco install {tool}\n\n"
237
+ f"[dim]Note: package names occasionally differ across distros.[/]"
238
+ )
239
+
240
+
241
+ def detect_install_command(tool: str) -> tuple[str, list[str]] | None:
242
+ sysname = platform.system()
243
+ if sysname == "Darwin" and shutil.which("brew"):
244
+ return ("Homebrew", ["brew", "install", tool])
245
+ if shutil.which("apt"):
246
+ return ("apt", ["sudo", "apt", "install", "-y", tool])
247
+ if shutil.which("dnf"):
248
+ return ("dnf", ["sudo", "dnf", "install", "-y", tool])
249
+ if shutil.which("pacman"):
250
+ return ("pacman", ["sudo", "pacman", "-S", "--noconfirm", tool])
251
+ if shutil.which("apk"):
252
+ return ("apk", ["sudo", "apk", "add", tool])
253
+ if shutil.which("winget"):
254
+ return ("winget", ["winget", "install", tool])
255
+ return None
256
+
257
+
258
+ def file_size(path: str) -> int | None:
259
+ try:
260
+ return os.path.getsize(os.path.realpath(path))
261
+ except OSError:
262
+ return None
263
+
264
+
265
+ def human_size(n: int | None) -> str:
266
+ if n is None:
267
+ return "?"
268
+ for unit in ("B", "KB", "MB", "GB"):
269
+ if n < 1024:
270
+ return f"{n:.1f} {unit}" if unit != "B" else f"{n} {unit}"
271
+ n /= 1024
272
+ return f"{n:.1f} TB"
273
+
274
+
275
+ def try_version(tool: str) -> str:
276
+ for flag in ("--version", "-V", "-v", "version"):
277
+ try:
278
+ r = subprocess.run(
279
+ [tool, flag], capture_output=True, text=True, timeout=2
280
+ )
281
+ out = (r.stdout or r.stderr).strip().splitlines()
282
+ if out:
283
+ return out[0][:80]
284
+ except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
285
+ continue
286
+ return "(unknown)"
287
+
288
+
289
+ # ---------- modals ----------
290
+ class InspectModal(ModalScreen):
291
+ def __init__(self, tool: Tool):
292
+ super().__init__()
293
+ self.tool = tool
294
+
295
+ def compose(self) -> ComposeResult:
296
+ size = file_size(self.tool.path)
297
+ version = try_version(self.tool.name)
298
+ real = os.path.realpath(self.tool.path) if self.tool.path else "?"
299
+ body = (
300
+ f"[b cyan]🔍 Inspect:[/] [b yellow]{self.tool.name}[/]\n\n"
301
+ f"[b]Category:[/] {self.tool.category}\n"
302
+ f"[b]Description:[/] {self.tool.description}\n"
303
+ f"[b]Path:[/] {self.tool.path}\n"
304
+ f"[b]Real path:[/] {real}\n"
305
+ f"[b]Size:[/] {human_size(size)}\n"
306
+ f"[b]Version:[/] {version}\n"
307
+ f"[b]Cool pick:[/] {'★ yes' if self.tool.is_cool else 'no'}\n"
308
+ )
309
+ with Container(id="modal-box"):
310
+ yield Static(body, id="modal-body")
311
+ with Horizontal(id="modal-buttons"):
312
+ yield Button("Close", variant="primary", id="close")
313
+
314
+ def on_button_pressed(self, event: Button.Pressed) -> None:
315
+ self.dismiss()
316
+
317
+
318
+ class InstallModal(ModalScreen):
319
+ def __init__(self, tool_name: str):
320
+ super().__init__()
321
+ self.tool_name = tool_name
322
+
323
+ def compose(self) -> ComposeResult:
324
+ with Container(id="modal-box"):
325
+ yield Static(install_instructions(self.tool_name), id="modal-body")
326
+ with Horizontal(id="modal-buttons"):
327
+ cmd = detect_install_command(self.tool_name)
328
+ if cmd:
329
+ yield Button(f"Install via {cmd[0]}", variant="success", id="install")
330
+ yield Button("Close", variant="primary", id="close")
331
+
332
+ def on_button_pressed(self, event: Button.Pressed) -> None:
333
+ if event.button.id == "install":
334
+ cmd = detect_install_command(self.tool_name)
335
+ if cmd:
336
+ self.app.exit(result=("install", cmd[1]))
337
+ return
338
+ self.dismiss()
339
+
340
+
341
+ class ConfirmRunModal(ModalScreen):
342
+ def __init__(self, tool_name: str):
343
+ super().__init__()
344
+ self.tool_name = tool_name
345
+
346
+ def compose(self) -> ComposeResult:
347
+ with Container(id="modal-box"):
348
+ yield Static(
349
+ f"[b cyan]▶ Run[/] [b yellow]{self.tool_name}[/]?\n\n"
350
+ f"This will exit termtools and launch [b]{self.tool_name}[/] "
351
+ f"in your current shell.\n",
352
+ id="modal-body",
353
+ )
354
+ with Horizontal(id="modal-buttons"):
355
+ yield Button("Run", variant="success", id="run")
356
+ yield Button("Cancel", variant="error", id="cancel")
357
+
358
+ def on_button_pressed(self, event: Button.Pressed) -> None:
359
+ if event.button.id == "run":
360
+ self.app.exit(result=("run", [self.tool_name]))
361
+ else:
362
+ self.dismiss()
363
+
364
+
365
+ # ---------- main app ----------
366
+ class TermTools(App):
367
+ CSS = """
368
+ Screen { background: #0b0f1a; }
369
+ Header { background: #1a1f3a; color: #ffd866; }
370
+ Footer { background: #1a1f3a; color: #a9b1d6; }
371
+
372
+ #top { height: 3; padding: 0 1; }
373
+ #search { width: 1fr; }
374
+
375
+ Tabs { background: #0b0f1a; }
376
+ Tab { color: #a9b1d6; }
377
+ Tab.-active { color: #ffd866; text-style: bold; }
378
+
379
+ #main { height: 1fr; }
380
+ #left { width: 2fr; border: round #6272a4; padding: 0 1; }
381
+ #right { width: 1fr; border: round #ff79c6; padding: 1 1; }
382
+
383
+ DataTable { background: #0b0f1a; }
384
+ DataTable > .datatable--header { background: #1a1f3a; color: #ffd866; text-style: bold; }
385
+ DataTable > .datatable--cursor { background: #44475a; color: #f8f8f2; }
386
+ DataTable > .datatable--hover { background: #21263a; }
387
+
388
+ #detail-title { color: #50fa7b; text-style: bold; }
389
+ #detail-body { color: #f8f8f2; padding: 1 0; }
390
+
391
+ #buttons { height: auto; padding: 1 0; }
392
+ Button { margin: 0 1; }
393
+
394
+ #modal-box {
395
+ align: center middle;
396
+ background: #1a1f3a;
397
+ border: thick #ff79c6;
398
+ padding: 2 4;
399
+ width: 80;
400
+ height: auto;
401
+ }
402
+ #modal-body { color: #f8f8f2; padding: 0 0 1 0; }
403
+ #modal-buttons { align: center middle; height: auto; padding-top: 1; }
404
+
405
+ .stat { color: #8be9fd; }
406
+ """
407
+
408
+ BINDINGS = [
409
+ Binding("q", "quit", "Quit"),
410
+ Binding("/", "focus_search", "Search"),
411
+ Binding("r", "refresh", "Refresh"),
412
+ Binding("enter", "inspect", "Inspect"),
413
+ ]
414
+
415
+ current_category: reactive[str] = reactive("All")
416
+ search_text: reactive[str] = reactive("")
417
+
418
+ def __init__(self):
419
+ super().__init__()
420
+ self.tools: dict[str, Tool] = {}
421
+ self.selected_tool: Tool | None = None
422
+
423
+ def compose(self) -> ComposeResult:
424
+ yield Header(show_clock=True)
425
+ with Vertical():
426
+ with Horizontal(id="top"):
427
+ yield Input(placeholder="🔎 Search tools…", id="search")
428
+ yield Tabs(*(Tab(c, id=f"tab-{i}") for i, c in enumerate(ALL_CATEGORIES)))
429
+ with Horizontal(id="main"):
430
+ with Vertical(id="left"):
431
+ yield DataTable(id="table", cursor_type="row", zebra_stripes=True)
432
+ with Vertical(id="right"):
433
+ yield Static("Select a tool to see details", id="detail-title")
434
+ yield Static("", id="detail-body")
435
+ with Horizontal(id="buttons"):
436
+ yield Button("▶ Run", variant="success", id="btn-run")
437
+ yield Button("🔍 Inspect", variant="primary", id="btn-inspect")
438
+ yield Button("📦 Install info", variant="warning", id="btn-install")
439
+ yield Footer()
440
+
441
+ def on_mount(self) -> None:
442
+ self.title = "termtools ✨"
443
+ self.sub_title = "Browse the CLI tools you have downloaded"
444
+ self.tools = scan_installed()
445
+ table = self.query_one("#table", DataTable)
446
+ table.add_columns("★", "Tool", "Category", "Description")
447
+ self.refresh_table()
448
+
449
+ def refresh_table(self) -> None:
450
+ table = self.query_one("#table", DataTable)
451
+ table.clear()
452
+ q = self.search_text.lower().strip()
453
+ cat = self.current_category
454
+ installed_count = 0
455
+ for name in sorted(self.tools.keys()):
456
+ t = self.tools[name]
457
+ if cat == "Cool Picks ★":
458
+ if not t.is_cool:
459
+ continue
460
+ elif cat != "All" and t.category != cat:
461
+ continue
462
+ if q and q not in t.name.lower() and q not in t.description.lower():
463
+ continue
464
+ star = "★" if t.is_cool else " "
465
+ table.add_row(star, t.name, t.category, t.description, key=t.name)
466
+ installed_count += 1
467
+ self.sub_title = (
468
+ f"{len(self.tools)} known tools detected · "
469
+ f"{installed_count} shown · category: {cat}"
470
+ )
471
+
472
+ def on_input_changed(self, event: Input.Changed) -> None:
473
+ if event.input.id == "search":
474
+ self.search_text = event.value
475
+ self.refresh_table()
476
+
477
+ def on_tabs_tab_activated(self, event: Tabs.TabActivated) -> None:
478
+ idx = int(event.tab.id.split("-")[1])
479
+ self.current_category = ALL_CATEGORIES[idx]
480
+ self.refresh_table()
481
+
482
+ def on_data_table_row_highlighted(self, event: DataTable.RowHighlighted) -> None:
483
+ key = event.row_key.value if event.row_key else None
484
+ if key and key in self.tools:
485
+ self.selected_tool = self.tools[key]
486
+ self.update_detail()
487
+
488
+ def update_detail(self) -> None:
489
+ t = self.selected_tool
490
+ if not t:
491
+ return
492
+ size = file_size(t.path)
493
+ title = f"{'★ ' if t.is_cool else ''}{t.name}"
494
+ self.query_one("#detail-title", Static).update(f"[b green]{title}[/]")
495
+ body = (
496
+ f"[b]Category:[/] [cyan]{t.category}[/]\n"
497
+ f"[b]About:[/] {t.description}\n\n"
498
+ f"[b]Path:[/] [dim]{t.path}[/]\n"
499
+ f"[b]Size:[/] [magenta]{human_size(size)}[/]\n"
500
+ )
501
+ self.query_one("#detail-body", Static).update(body)
502
+
503
+ def on_button_pressed(self, event: Button.Pressed) -> None:
504
+ if not self.selected_tool:
505
+ return
506
+ bid = event.button.id
507
+ if bid == "btn-run":
508
+ self.push_screen(ConfirmRunModal(self.selected_tool.name))
509
+ elif bid == "btn-inspect":
510
+ self.push_screen(InspectModal(self.selected_tool))
511
+ elif bid == "btn-install":
512
+ self.push_screen(InstallModal(self.selected_tool.name))
513
+
514
+ def action_focus_search(self) -> None:
515
+ self.query_one("#search", Input).focus()
516
+
517
+ def action_refresh(self) -> None:
518
+ self.tools = scan_installed()
519
+ self.refresh_table()
520
+
521
+ def action_inspect(self) -> None:
522
+ if self.selected_tool:
523
+ self.push_screen(InspectModal(self.selected_tool))
524
+
525
+
526
+ def main() -> None:
527
+ app = TermTools()
528
+ result = app.run()
529
+ if isinstance(result, tuple):
530
+ action, cmd = result
531
+ if action == "run":
532
+ print(f"\n\033[1;32m▶ Launching {cmd[0]}…\033[0m\n")
533
+ try:
534
+ subprocess.call(cmd)
535
+ except FileNotFoundError:
536
+ print(f"\033[1;31mCould not launch {cmd[0]}\033[0m")
537
+ elif action == "install":
538
+ print(f"\n\033[1;33m📦 Installing: {' '.join(cmd)}\033[0m\n")
539
+ subprocess.call(cmd)
540
+
541
+
542
+ if __name__ == "__main__":
543
+ main()
@@ -0,0 +1,2 @@
1
+ from . import main
2
+ main()
@@ -0,0 +1,49 @@
1
+ Metadata-Version: 2.4
2
+ Name: termtools-tui
3
+ Version: 0.1.0
4
+ Summary: A beautiful TUI to browse the CLI tools you have installed.
5
+ Author: Max Tillinger
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/mtt2016/termtools
8
+ Keywords: tui,cli,terminal,tools,textual
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: System :: Shells
15
+ Classifier: Topic :: Utilities
16
+ Requires-Python: >=3.9
17
+ Description-Content-Type: text/markdown
18
+ Requires-Dist: textual>=0.50
19
+
20
+ # termtools ✨
21
+
22
+ A beautiful Textual TUI to browse the CLI tools you have installed on your system.
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ pipx install termtools-tui
28
+ ```
29
+
30
+ Then run:
31
+
32
+ ```bash
33
+ termtools
34
+ ```
35
+
36
+ ## Features
37
+
38
+ - Scans your `$PATH` against a curated catalog of ~100 CLI tools across 13 categories
39
+ - Tabs to filter: Editors, Search, File & Disk, Git, System, Network, Languages, Package Managers, Containers, Data, Shells, AI, Fun, plus a "Cool Picks ★" tab
40
+ - Live search bar
41
+ - Per-tool detail pane: description, path, size
42
+ - Buttons to **Run**, **Inspect**, and view **Install info** with copy-paste install commands for brew, apt, dnf, pacman, apk, winget, scoop, and chocolatey
43
+
44
+ ## Keys
45
+
46
+ - `/` focus search
47
+ - `r` refresh
48
+ - `enter` inspect selected tool
49
+ - `q` quit
@@ -0,0 +1,10 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/termtools/__init__.py
4
+ src/termtools/__main__.py
5
+ src/termtools_tui.egg-info/PKG-INFO
6
+ src/termtools_tui.egg-info/SOURCES.txt
7
+ src/termtools_tui.egg-info/dependency_links.txt
8
+ src/termtools_tui.egg-info/entry_points.txt
9
+ src/termtools_tui.egg-info/requires.txt
10
+ src/termtools_tui.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ termtools = termtools:main
@@ -0,0 +1 @@
1
+ textual>=0.50