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.
- termtools_tui-0.1.0/PKG-INFO +49 -0
- termtools_tui-0.1.0/README.md +30 -0
- termtools_tui-0.1.0/pyproject.toml +32 -0
- termtools_tui-0.1.0/setup.cfg +4 -0
- termtools_tui-0.1.0/src/termtools/__init__.py +543 -0
- termtools_tui-0.1.0/src/termtools/__main__.py +2 -0
- termtools_tui-0.1.0/src/termtools_tui.egg-info/PKG-INFO +49 -0
- termtools_tui-0.1.0/src/termtools_tui.egg-info/SOURCES.txt +10 -0
- termtools_tui-0.1.0/src/termtools_tui.egg-info/dependency_links.txt +1 -0
- termtools_tui-0.1.0/src/termtools_tui.egg-info/entry_points.txt +2 -0
- termtools_tui-0.1.0/src/termtools_tui.egg-info/requires.txt +1 -0
- termtools_tui-0.1.0/src/termtools_tui.egg-info/top_level.txt +1 -0
|
@@ -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,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,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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
textual>=0.50
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
termtools
|