flex-uv 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,8 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv/
7
+ .python-version
8
+ uv.lock
flex_uv-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Chris Hirschauer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
flex_uv-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,300 @@
1
+ Metadata-Version: 2.4
2
+ Name: flex-uv
3
+ Version: 0.1.0
4
+ Summary: The Interactive UV Command Center — a Textual TUI for uv
5
+ Project-URL: Homepage, https://github.com/chirschauer/flex_uv
6
+ Project-URL: Repository, https://github.com/chirschauer/flex_uv
7
+ Project-URL: Issues, https://github.com/chirschauer/flex_uv/issues
8
+ Author: Chris Hirschauer
9
+ License: MIT License
10
+
11
+ Copyright (c) 2026 Chris Hirschauer
12
+
13
+ Permission is hereby granted, free of charge, to any person obtaining a copy
14
+ of this software and associated documentation files (the "Software"), to deal
15
+ in the Software without restriction, including without limitation the rights
16
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
+ copies of the Software, and to permit persons to whom the Software is
18
+ furnished to do so, subject to the following conditions:
19
+
20
+ The above copyright notice and this permission notice shall be included in all
21
+ copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
+ SOFTWARE.
30
+ License-File: LICENSE
31
+ Keywords: cli,package-manager,python,textual,tui,uv
32
+ Classifier: Development Status :: 3 - Alpha
33
+ Classifier: Environment :: Console
34
+ Classifier: Intended Audience :: Developers
35
+ Classifier: License :: OSI Approved :: MIT License
36
+ Classifier: Programming Language :: Python :: 3
37
+ Classifier: Programming Language :: Python :: 3.10
38
+ Classifier: Programming Language :: Python :: 3.11
39
+ Classifier: Programming Language :: Python :: 3.12
40
+ Classifier: Programming Language :: Python :: 3.13
41
+ Classifier: Topic :: Software Development :: Build Tools
42
+ Classifier: Topic :: Utilities
43
+ Requires-Python: >=3.10
44
+ Requires-Dist: textual>=0.80.0
45
+ Description-Content-Type: text/markdown
46
+
47
+ # ⚡ FlexUV
48
+
49
+ [![Chat-GPT-Image-Mar-15-2026-02-20-47-AM-removebg-preview.png](https://i.postimg.cc/pTy8Nf2Y/Chat-GPT-Image-Mar-15-2026-02-20-47-AM-removebg-preview.png)](https://postimg.cc/cgy6w8tr)
50
+
51
+
52
+ ### The Interactive Command Center for `uv`
53
+
54
+ > Stop memorizing `uv` commands.
55
+ > Manage Python environments visually — **without leaving your terminal.**
56
+
57
+ FlexUV is a **terminal UI (TUI)** built with **Textual** that turns the `uv` Python ecosystem into a **visual command center**.
58
+
59
+ Instead of typing dozens of commands, you get a **guided interface for managing projects, environments, dependencies, and tools**.
60
+
61
+ Think:
62
+
63
+ > **LazyGit — but for Python environments.**
64
+
65
+ ---
66
+
67
+ # 🚀 Demo
68
+
69
+ ```
70
+ ███████╗██╗ ███████╗██╗ ██╗██╗ ██╗██╗ ██╗
71
+ ██╔════╝██║ ██╔════╝╚██╗██╔╝██║ ██║██║ ██║
72
+ █████╗ ██║ █████╗ ╚███╔╝ ██║ ██║██║ ██║
73
+ ██╔══╝ ██║ ██╔══╝ ██╔██╗ ██║ ██║╚██╗ ██╔╝
74
+ ██║ ███████╗███████╗██╔╝ ██╗╚██████╔╝ ╚████╔╝
75
+ ╚═╝ ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═══╝
76
+
77
+ FlexUV — The Interactive UV Command Center
78
+ ```
79
+
80
+ ---
81
+
82
+ # ✨ Features
83
+
84
+ ### 🧭 Dashboard
85
+
86
+ See the state of your environment instantly.
87
+
88
+ * OS + Python detection
89
+ * `uv` installation check
90
+ * project detection
91
+ * environment markers
92
+
93
+ ---
94
+
95
+ ### 📦 Project Management
96
+
97
+ Run core `uv` workflows from a guided interface.
98
+
99
+ ```
100
+ uv init
101
+ uv sync
102
+ uv lock
103
+ uv tree
104
+ ```
105
+
106
+ Create new projects and configure:
107
+
108
+ * package name
109
+ * description
110
+ * Python version
111
+
112
+ ---
113
+
114
+ ### 📚 Dependency Management
115
+
116
+ Add or remove dependencies quickly.
117
+
118
+ ```
119
+ uv add fastapi
120
+ uv remove requests
121
+ ```
122
+
123
+ Run commands directly inside your project environment.
124
+
125
+ ---
126
+
127
+ ### 🐍 Python Version Manager
128
+
129
+ Manage Python versions with `uv`.
130
+
131
+ ```
132
+ uv python install 3.12
133
+ uv python list
134
+ uv python find
135
+ uv python pin
136
+ ```
137
+
138
+ ---
139
+
140
+ ### 🧰 Tool Manager
141
+
142
+ Install global developer tools.
143
+
144
+ ```
145
+ uv tool install ruff
146
+ uv tool uninstall black
147
+ uv tool list
148
+ ```
149
+
150
+ Run tools with `uv tool run`.
151
+
152
+ ---
153
+
154
+ ### 🔁 Pip Compatibility Mode
155
+
156
+ Still need pip workflows?
157
+
158
+ FlexUV exposes:
159
+
160
+ ```
161
+ uv pip install
162
+ uv pip uninstall
163
+ uv pip list
164
+ uv pip freeze
165
+ uv pip tree
166
+ ```
167
+
168
+ ---
169
+
170
+ ### ⚡ Command Center
171
+
172
+ Quick-access presets:
173
+
174
+ ```
175
+ uv version
176
+ uv self update
177
+ uv cache dir
178
+ uv cache clean
179
+ uv tool list
180
+ uv python list
181
+ ```
182
+
183
+ Or run **custom uv commands**.
184
+
185
+ ---
186
+
187
+ ### 📜 Command Logging
188
+
189
+ Every command executed is logged.
190
+
191
+ ```
192
+ $ uv add textual
193
+
194
+ Installed successfully
195
+
196
+ (exit code: 0)
197
+ ```
198
+
199
+ No hidden magic — you always see what happens.
200
+
201
+ ---
202
+
203
+ # 🖥 Interface
204
+
205
+ FlexUV organizes everything into tabs:
206
+
207
+ ```
208
+ Dashboard
209
+ Project
210
+ Python
211
+ Tools
212
+ Pip Mode
213
+ Command Center
214
+ Logs
215
+ ```
216
+
217
+ It’s designed to feel like a **modern terminal application**.
218
+
219
+ ---
220
+
221
+ # 📦 Installation
222
+
223
+ First install **uv**:
224
+
225
+ ```
226
+ curl -LsSf https://astral.sh/uv/install.sh | sh
227
+ ```
228
+
229
+ Then run FlexUV:
230
+
231
+ ```
232
+ python app.py
233
+ ```
234
+
235
+ ---
236
+
237
+ # 🧠 Why FlexUV Exists
238
+
239
+ `uv` is incredibly powerful.
240
+
241
+ But command-heavy tools have a discoverability problem.
242
+
243
+ FlexUV solves this by providing:
244
+
245
+ * visual workflows
246
+ * command guidance
247
+ * environment awareness
248
+ * command logs
249
+ * safer actions
250
+
251
+ All **without leaving the terminal**.
252
+
253
+ ---
254
+
255
+ # 🛠 Built With
256
+
257
+ * Python
258
+ * Textual
259
+ * Rich
260
+ * uv
261
+
262
+ ---
263
+
264
+ # 🗺 Roadmap
265
+
266
+ Planned improvements:
267
+
268
+ * dependency graph visualization
269
+ * environment health checks
270
+ * plugin system
271
+ * task runner integration
272
+ * project templates
273
+ * package security scanning
274
+
275
+ ---
276
+
277
+ # ⭐ Contributing
278
+
279
+ Contributions welcome!
280
+
281
+ If you have ideas, open an issue or PR.
282
+
283
+ ---
284
+
285
+ # 🔥 If You Like This Project
286
+
287
+ Give it a ⭐ on GitHub.
288
+
289
+ It helps the project grow and reach more developers.
290
+
291
+ ---
292
+
293
+ If you want, I can also give you **3 things that massively increase GitHub stars**:
294
+
295
+ 1️⃣ **A README banner that looks like a dev tool homepage**
296
+ 2️⃣ **A screenshot section that makes the project look polished**
297
+ 3️⃣ **A Hacker News launch post that drives traffic to the repo**
298
+
299
+ Those three together can take a repo from **0 → 500 stars very quickly**.
300
+
@@ -0,0 +1,254 @@
1
+ # ⚡ FlexUV
2
+
3
+ [![Chat-GPT-Image-Mar-15-2026-02-20-47-AM-removebg-preview.png](https://i.postimg.cc/pTy8Nf2Y/Chat-GPT-Image-Mar-15-2026-02-20-47-AM-removebg-preview.png)](https://postimg.cc/cgy6w8tr)
4
+
5
+
6
+ ### The Interactive Command Center for `uv`
7
+
8
+ > Stop memorizing `uv` commands.
9
+ > Manage Python environments visually — **without leaving your terminal.**
10
+
11
+ FlexUV is a **terminal UI (TUI)** built with **Textual** that turns the `uv` Python ecosystem into a **visual command center**.
12
+
13
+ Instead of typing dozens of commands, you get a **guided interface for managing projects, environments, dependencies, and tools**.
14
+
15
+ Think:
16
+
17
+ > **LazyGit — but for Python environments.**
18
+
19
+ ---
20
+
21
+ # 🚀 Demo
22
+
23
+ ```
24
+ ███████╗██╗ ███████╗██╗ ██╗██╗ ██╗██╗ ██╗
25
+ ██╔════╝██║ ██╔════╝╚██╗██╔╝██║ ██║██║ ██║
26
+ █████╗ ██║ █████╗ ╚███╔╝ ██║ ██║██║ ██║
27
+ ██╔══╝ ██║ ██╔══╝ ██╔██╗ ██║ ██║╚██╗ ██╔╝
28
+ ██║ ███████╗███████╗██╔╝ ██╗╚██████╔╝ ╚████╔╝
29
+ ╚═╝ ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═══╝
30
+
31
+ FlexUV — The Interactive UV Command Center
32
+ ```
33
+
34
+ ---
35
+
36
+ # ✨ Features
37
+
38
+ ### 🧭 Dashboard
39
+
40
+ See the state of your environment instantly.
41
+
42
+ * OS + Python detection
43
+ * `uv` installation check
44
+ * project detection
45
+ * environment markers
46
+
47
+ ---
48
+
49
+ ### 📦 Project Management
50
+
51
+ Run core `uv` workflows from a guided interface.
52
+
53
+ ```
54
+ uv init
55
+ uv sync
56
+ uv lock
57
+ uv tree
58
+ ```
59
+
60
+ Create new projects and configure:
61
+
62
+ * package name
63
+ * description
64
+ * Python version
65
+
66
+ ---
67
+
68
+ ### 📚 Dependency Management
69
+
70
+ Add or remove dependencies quickly.
71
+
72
+ ```
73
+ uv add fastapi
74
+ uv remove requests
75
+ ```
76
+
77
+ Run commands directly inside your project environment.
78
+
79
+ ---
80
+
81
+ ### 🐍 Python Version Manager
82
+
83
+ Manage Python versions with `uv`.
84
+
85
+ ```
86
+ uv python install 3.12
87
+ uv python list
88
+ uv python find
89
+ uv python pin
90
+ ```
91
+
92
+ ---
93
+
94
+ ### 🧰 Tool Manager
95
+
96
+ Install global developer tools.
97
+
98
+ ```
99
+ uv tool install ruff
100
+ uv tool uninstall black
101
+ uv tool list
102
+ ```
103
+
104
+ Run tools with `uv tool run`.
105
+
106
+ ---
107
+
108
+ ### 🔁 Pip Compatibility Mode
109
+
110
+ Still need pip workflows?
111
+
112
+ FlexUV exposes:
113
+
114
+ ```
115
+ uv pip install
116
+ uv pip uninstall
117
+ uv pip list
118
+ uv pip freeze
119
+ uv pip tree
120
+ ```
121
+
122
+ ---
123
+
124
+ ### ⚡ Command Center
125
+
126
+ Quick-access presets:
127
+
128
+ ```
129
+ uv version
130
+ uv self update
131
+ uv cache dir
132
+ uv cache clean
133
+ uv tool list
134
+ uv python list
135
+ ```
136
+
137
+ Or run **custom uv commands**.
138
+
139
+ ---
140
+
141
+ ### 📜 Command Logging
142
+
143
+ Every command executed is logged.
144
+
145
+ ```
146
+ $ uv add textual
147
+
148
+ Installed successfully
149
+
150
+ (exit code: 0)
151
+ ```
152
+
153
+ No hidden magic — you always see what happens.
154
+
155
+ ---
156
+
157
+ # 🖥 Interface
158
+
159
+ FlexUV organizes everything into tabs:
160
+
161
+ ```
162
+ Dashboard
163
+ Project
164
+ Python
165
+ Tools
166
+ Pip Mode
167
+ Command Center
168
+ Logs
169
+ ```
170
+
171
+ It’s designed to feel like a **modern terminal application**.
172
+
173
+ ---
174
+
175
+ # 📦 Installation
176
+
177
+ First install **uv**:
178
+
179
+ ```
180
+ curl -LsSf https://astral.sh/uv/install.sh | sh
181
+ ```
182
+
183
+ Then run FlexUV:
184
+
185
+ ```
186
+ python app.py
187
+ ```
188
+
189
+ ---
190
+
191
+ # 🧠 Why FlexUV Exists
192
+
193
+ `uv` is incredibly powerful.
194
+
195
+ But command-heavy tools have a discoverability problem.
196
+
197
+ FlexUV solves this by providing:
198
+
199
+ * visual workflows
200
+ * command guidance
201
+ * environment awareness
202
+ * command logs
203
+ * safer actions
204
+
205
+ All **without leaving the terminal**.
206
+
207
+ ---
208
+
209
+ # 🛠 Built With
210
+
211
+ * Python
212
+ * Textual
213
+ * Rich
214
+ * uv
215
+
216
+ ---
217
+
218
+ # 🗺 Roadmap
219
+
220
+ Planned improvements:
221
+
222
+ * dependency graph visualization
223
+ * environment health checks
224
+ * plugin system
225
+ * task runner integration
226
+ * project templates
227
+ * package security scanning
228
+
229
+ ---
230
+
231
+ # ⭐ Contributing
232
+
233
+ Contributions welcome!
234
+
235
+ If you have ideas, open an issue or PR.
236
+
237
+ ---
238
+
239
+ # 🔥 If You Like This Project
240
+
241
+ Give it a ⭐ on GitHub.
242
+
243
+ It helps the project grow and reach more developers.
244
+
245
+ ---
246
+
247
+ If you want, I can also give you **3 things that massively increase GitHub stars**:
248
+
249
+ 1️⃣ **A README banner that looks like a dev tool homepage**
250
+ 2️⃣ **A screenshot section that makes the project look polished**
251
+ 3️⃣ **A Hacker News launch post that drives traffic to the repo**
252
+
253
+ Those three together can take a repo from **0 → 500 stars very quickly**.
254
+
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "flex-uv"
7
+ version = "0.1.0"
8
+ description = "The Interactive UV Command Center — a Textual TUI for uv"
9
+ readme = "README.md"
10
+ license = { file = "LICENSE" }
11
+ requires-python = ">=3.10"
12
+ authors = [{ name = "Chris Hirschauer" }]
13
+ keywords = ["uv", "python", "tui", "textual", "package-manager", "cli"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Environment :: Console",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Topic :: Software Development :: Build Tools",
25
+ "Topic :: Utilities",
26
+ ]
27
+ dependencies = ["textual>=0.80.0"]
28
+
29
+ [project.scripts]
30
+ flex-uv = "flex_uv.app:main"
31
+
32
+ [project.urls]
33
+ Homepage = "https://github.com/chirschauer/flex_uv"
34
+ Repository = "https://github.com/chirschauer/flex_uv"
35
+ Issues = "https://github.com/chirschauer/flex_uv/issues"
36
+
37
+ [tool.hatch.build.targets.wheel]
38
+ packages = ["src/flex_uv"]
39
+
40
+ [tool.hatch.build.targets.sdist]
41
+ include = ["src/", "README.md", "LICENSE", "pyproject.toml"]
@@ -0,0 +1,6 @@
1
+ """FlexUV — The Interactive UV Command Center."""
2
+
3
+ from flex_uv.app import UVManager, main
4
+
5
+ __version__ = "0.1.0"
6
+ __all__ = ["UVManager", "main"]
@@ -0,0 +1,620 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import asyncio
5
+ import os
6
+ import platform
7
+ import shutil
8
+ import subprocess
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+
12
+ from rich.text import Text
13
+ from textual import on, work
14
+ from textual.app import App, ComposeResult
15
+ from textual.containers import Center, Container, Horizontal, Vertical, VerticalScroll
16
+ from textual.reactive import reactive
17
+ from textual.screen import ModalScreen, Screen
18
+ from textual.widgets import (
19
+ Button,
20
+ DataTable,
21
+ Footer,
22
+ Header,
23
+ Input,
24
+ Label,
25
+ ListItem,
26
+ ListView,
27
+ Markdown,
28
+ Pretty,
29
+ Select,
30
+ Static,
31
+ TabbedContent,
32
+ TabPane,
33
+ TextArea,
34
+ )
35
+
36
+ # ── colours ────────────────────────────────────────────────────────────────────
37
+ _UNC_BLUE = (0x7B, 0xAF, 0xD4) # Carolina / UNC blue
38
+ _HOYAS_DARK = (0x04, 0x1E, 0x42) # Georgetown dark navy
39
+
40
+
41
+ def _gradient(text: str, start: tuple[int, int, int], end: tuple[int, int, int]) -> Text:
42
+ """Return a Rich Text object with per-line colour gradient from *start* to *end*."""
43
+ lines = text.split("\n")
44
+ non_blank = [l for l in lines if l.strip()]
45
+ steps = max(len(non_blank) - 1, 1)
46
+ rich = Text()
47
+ seen = 0
48
+ for line in lines:
49
+ if line.strip():
50
+ t = seen / steps
51
+ r = int(start[0] + (end[0] - start[0]) * t)
52
+ g = int(start[1] + (end[1] - start[1]) * t)
53
+ b = int(start[2] + (end[2] - start[2]) * t)
54
+ rich.append(line + "\n", style=f"rgb({r},{g},{b})")
55
+ seen += 1
56
+ else:
57
+ rich.append(line + "\n")
58
+ return rich
59
+
60
+
61
+ # ── splash ─────────────────────────────────────────────────────────────────────
62
+ SPLASH_CSS = """
63
+ SplashScreen {
64
+ align: center middle;
65
+ background: $background;
66
+ }
67
+
68
+ #splash-banner {
69
+ width: auto;
70
+ text-align: center;
71
+ text-style: bold;
72
+ padding: 2 4;
73
+ }
74
+ """
75
+
76
+ _BANNER_TEXT = """\
77
+ ███████╗██╗ ███████╗██╗ ██╗██╗ ██╗██╗ ██╗
78
+ ██╔════╝██║ ██╔════╝╚██╗██╔╝██║ ██║██║ ██║
79
+ █████╗ ██║ █████╗ ╚███╔╝ ██║ ██║██║ ██║
80
+ ██╔══╝ ██║ ██╔══╝ ██╔██╗ ██║ ██║╚██╗ ██╔╝
81
+ ██║ ███████╗███████╗██╔╝ ██╗╚██████╔╝ ╚████╔╝
82
+ ╚═╝ ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═══╝
83
+
84
+ FlexUV — The Interactive UV Command Center
85
+
86
+ Copyright (c) 2026 Chris Hirschauer
87
+ All Rights Reserved"""
88
+
89
+
90
+ class SplashScreen(Screen):
91
+ def compose(self) -> ComposeResult:
92
+ yield Center(
93
+ Static(_gradient(_BANNER_TEXT, _UNC_BLUE, _HOYAS_DARK), id="splash-banner")
94
+ )
95
+
96
+ async def on_mount(self) -> None:
97
+ await asyncio.sleep(3)
98
+ self.app.pop_screen()
99
+
100
+
101
+ # ── main app CSS ───────────────────────────────────────────────────────────────
102
+ APP_CSS = """
103
+ Screen {
104
+ layout: vertical;
105
+ }
106
+
107
+ #body {
108
+ height: 1fr;
109
+ }
110
+
111
+ .panel {
112
+ border: round $accent;
113
+ padding: 1;
114
+ margin: 0 1 1 1;
115
+ }
116
+
117
+ .title {
118
+ text-style: bold;
119
+ margin-bottom: 1;
120
+ }
121
+
122
+ .row {
123
+ height: auto;
124
+ margin-bottom: 1;
125
+ }
126
+
127
+ Input, Select, TextArea {
128
+ margin-bottom: 1;
129
+ }
130
+
131
+ Button {
132
+ margin-right: 1;
133
+ margin-bottom: 1;
134
+ }
135
+
136
+ #log {
137
+ height: 1fr;
138
+ border: round $primary;
139
+ }
140
+
141
+ #command_output {
142
+ height: 1fr;
143
+ border: round $success;
144
+ }
145
+
146
+ .status-ok {
147
+ color: $success;
148
+ text-style: bold;
149
+ }
150
+
151
+ .status-bad {
152
+ color: $error;
153
+ text-style: bold;
154
+ }
155
+ """
156
+
157
+
158
+ # ── helpers ────────────────────────────────────────────────────────────────────
159
+ @dataclass
160
+ class CommandResult:
161
+ cmd: list[str]
162
+ returncode: int
163
+ stdout: str
164
+ stderr: str
165
+
166
+ @property
167
+ def ok(self) -> bool:
168
+ return self.returncode == 0
169
+
170
+ @property
171
+ def combined(self) -> str:
172
+ parts = [f"$ {' '.join(self.cmd)}"]
173
+ if self.stdout.strip():
174
+ parts.append(self.stdout.rstrip())
175
+ if self.stderr.strip():
176
+ parts.append("[stderr]\n" + self.stderr.rstrip())
177
+ parts.append(f"\n(exit code: {self.returncode})")
178
+ return "\n\n".join(parts)
179
+
180
+
181
+ class ConfirmScreen(ModalScreen[bool]):
182
+ def __init__(self, title: str, body: str) -> None:
183
+ super().__init__()
184
+ self.title = title
185
+ self.body = body
186
+
187
+ def compose(self) -> ComposeResult:
188
+ with Container(classes="panel"):
189
+ yield Static(self.title, classes="title")
190
+ yield Markdown(self.body)
191
+ with Horizontal(classes="row"):
192
+ yield Button("Cancel", id="cancel")
193
+ yield Button("Confirm", id="confirm", variant="success")
194
+
195
+ @on(Button.Pressed, "#cancel")
196
+ def cancel(self) -> None:
197
+ self.dismiss(False)
198
+
199
+ @on(Button.Pressed, "#confirm")
200
+ def confirm(self) -> None:
201
+ self.dismiss(True)
202
+
203
+
204
+ # ── app ────────────────────────────────────────────────────────────────────────
205
+ class UVManager(App):
206
+ TITLE = "UV Manager"
207
+ SUB_TITLE = "Safe, guided terminal management for uv"
208
+ CSS = APP_CSS + SPLASH_CSS
209
+
210
+ cwd = reactive(Path.cwd())
211
+ uv_path = reactive("")
212
+
213
+ def compose(self) -> ComposeResult:
214
+ yield Header()
215
+ with TabbedContent(id="body"):
216
+ with TabPane("Dashboard", id="dashboard"):
217
+ with Horizontal():
218
+ with Vertical(classes="panel"):
219
+ yield Static("Environment", classes="title")
220
+ yield Static(id="env_summary")
221
+ yield Button("Refresh", id="refresh_dashboard")
222
+ yield Button("Install uv", id="install_uv", variant="primary")
223
+ yield Button("Open Current Folder", id="open_folder")
224
+ with Vertical(classes="panel"):
225
+ yield Static("Current Project", classes="title")
226
+ yield Static(id="project_summary")
227
+ yield Button("Detect Project", id="detect_project")
228
+ yield Button("Initialize New Project", id="wizard_init", variant="success")
229
+ with TabPane("Project", id="project"):
230
+ with Horizontal():
231
+ with Vertical(classes="panel"):
232
+ yield Static("Project Setup", classes="title")
233
+ yield Input(str(Path.cwd()), placeholder="Project root", id="project_root")
234
+ yield Input("my_app", placeholder="Package name", id="project_name")
235
+ yield Input("A uv-managed Python project", placeholder="Description", id="project_desc")
236
+ yield Input("3.12", placeholder="Python version", id="project_python")
237
+ with Horizontal(classes="row"):
238
+ yield Button("uv init", id="run_init", variant="success")
239
+ yield Button("uv sync", id="run_sync")
240
+ yield Button("uv lock", id="run_lock")
241
+ yield Button("uv tree", id="run_tree")
242
+ with Vertical(classes="panel"):
243
+ yield Static("Dependencies", classes="title")
244
+ yield Input(placeholder="Add dependency, e.g. textual", id="dependency_name")
245
+ with Horizontal(classes="row"):
246
+ yield Button("Add", id="dep_add", variant="success")
247
+ yield Button("Remove", id="dep_remove", variant="warning")
248
+ yield Input(placeholder="Command to run, e.g. python -m main", id="run_command")
249
+ yield Button("Run In Project", id="project_run")
250
+ with TabPane("Python", id="python"):
251
+ with Horizontal():
252
+ with Vertical(classes="panel"):
253
+ yield Static("Managed Python", classes="title")
254
+ yield Input("3.12", placeholder="Version like 3.12 or pypy@3.10", id="python_version")
255
+ with Horizontal(classes="row"):
256
+ yield Button("Install", id="python_install", variant="success")
257
+ yield Button("List", id="python_list")
258
+ yield Button("Find", id="python_find")
259
+ yield Button("Pin In Project", id="python_pin")
260
+ with Vertical(classes="panel"):
261
+ yield Static("Virtual Environments", classes="title")
262
+ yield Input(".venv", placeholder="Environment path", id="venv_path")
263
+ with Horizontal(classes="row"):
264
+ yield Button("Create venv", id="venv_create", variant="success")
265
+ yield Button("Show activation help", id="venv_help")
266
+ yield Static(id="venv_help_text")
267
+ with TabPane("Tools", id="tools"):
268
+ with Horizontal():
269
+ with Vertical(classes="panel"):
270
+ yield Static("Install Tools", classes="title")
271
+ yield Input(placeholder="Tool package, e.g. ruff", id="tool_name")
272
+ with Horizontal(classes="row"):
273
+ yield Button("Install Tool", id="tool_install", variant="success")
274
+ yield Button("Uninstall Tool", id="tool_uninstall", variant="warning")
275
+ yield Button("List Tools", id="tool_list")
276
+ with Vertical(classes="panel"):
277
+ yield Static("Run One-Off Tool", classes="title")
278
+ yield Input(placeholder="Tool command, e.g. ruff check .", id="tool_run_cmd")
279
+ yield Button("Run via uvx", id="tool_run", variant="primary")
280
+ with TabPane("Pip Mode", id="pip"):
281
+ with Horizontal():
282
+ with Vertical(classes="panel"):
283
+ yield Static("Legacy Pip Commands", classes="title")
284
+ yield Input(placeholder="Package, e.g. requests", id="pip_package")
285
+ with Horizontal(classes="row"):
286
+ yield Button("uv pip install", id="pip_install", variant="success")
287
+ yield Button("uv pip uninstall", id="pip_uninstall", variant="warning")
288
+ with Horizontal(classes="row"):
289
+ yield Button("uv pip list", id="pip_list")
290
+ yield Button("uv pip freeze", id="pip_freeze")
291
+ yield Button("uv pip tree", id="pip_tree")
292
+ with TabPane("Command Center", id="commands"):
293
+ with Horizontal():
294
+ with Vertical(classes="panel"):
295
+ yield Static("Preset Actions", classes="title")
296
+ yield ListView(
297
+ ListItem(Label("uv version")),
298
+ ListItem(Label("uv self update")),
299
+ ListItem(Label("uv cache dir")),
300
+ ListItem(Label("uv cache clean")),
301
+ ListItem(Label("uv tool list")),
302
+ ListItem(Label("uv python list")),
303
+ id="preset_list",
304
+ )
305
+ yield Button("Run Selected Preset", id="run_preset", variant="primary")
306
+ with Vertical(classes="panel"):
307
+ yield Static("Custom Command", classes="title")
308
+ yield Input(placeholder="Example: uv add rich", id="custom_command")
309
+ yield Button("Run Custom uv Command", id="run_custom", variant="error")
310
+ yield Markdown(
311
+ "Use this only when the guided buttons do not cover your task. "
312
+ "This app intentionally funnels common workflows into safer actions."
313
+ )
314
+ with TabPane("Logs", id="logs"):
315
+ with Vertical(classes="panel"):
316
+ yield Static("Command Output", classes="title")
317
+ yield TextArea("", id="command_output", read_only=True)
318
+ yield Button("Clear Output", id="clear_output")
319
+ yield Footer()
320
+
321
+ async def on_mount(self) -> None:
322
+ await self.push_screen(SplashScreen())
323
+ self._refresh_everything()
324
+
325
+ def _refresh_everything(self) -> None:
326
+ self.uv_path = shutil.which("uv") or ""
327
+ self._update_env_summary()
328
+ self._update_project_summary()
329
+ self._update_venv_help()
330
+
331
+ def _project_root(self) -> Path:
332
+ raw = self.query_one("#project_root", Input).value.strip() or str(Path.cwd())
333
+ return Path(raw).expanduser().resolve()
334
+
335
+ def _update_env_summary(self) -> None:
336
+ python_path = shutil.which("python") or "not found"
337
+ uv_state = self.uv_path if self.uv_path else "not installed or not on PATH"
338
+ summary = (
339
+ f"OS: {platform.system()} {platform.release()}\n"
340
+ f"Current folder: {Path.cwd()}\n"
341
+ f"Python: {python_path}\n"
342
+ f"uv: {uv_state}\n"
343
+ f"Project marker files here: {', '.join(self._markers_in(Path.cwd())) or 'none'}"
344
+ )
345
+ self.query_one("#env_summary", Static).update(summary)
346
+
347
+ def _markers_in(self, root: Path) -> list[str]:
348
+ names = ["pyproject.toml", "uv.lock", ".venv", ".python-version"]
349
+ return [name for name in names if (root / name).exists()]
350
+
351
+ def _update_project_summary(self) -> None:
352
+ root = self._project_root() if self.query("#project_root").first() else Path.cwd()
353
+ markers = self._markers_in(root)
354
+ status = "Looks like a uv project" if (root / "pyproject.toml").exists() else "Not initialized yet"
355
+ summary = f"Root: {root}\nStatus: {status}\nMarkers: {', '.join(markers) or 'none'}"
356
+ self.query_one("#project_summary", Static).update(summary)
357
+
358
+ def _update_venv_help(self) -> None:
359
+ if os.name == "nt":
360
+ text = "Activate with: .venv\\Scripts\\activate"
361
+ else:
362
+ text = "Activate with: source .venv/bin/activate"
363
+ self.query_one("#venv_help_text", Static).update(text)
364
+
365
+ def _append_output(self, text: str) -> None:
366
+ output = self.query_one("#command_output", TextArea)
367
+ current = output.text
368
+ output.load_text((current + "\n\n" + text).strip())
369
+
370
+ def _require_uv(self) -> bool:
371
+ if self.uv_path:
372
+ return True
373
+ self._append_output("uv was not found on PATH. Install it first from the Dashboard tab.")
374
+ self.notify("uv not found", severity="error")
375
+ return False
376
+
377
+ @work(thread=True)
378
+ def run_command(self, cmd: list[str], cwd: Path | None = None) -> None:
379
+ result = self._execute(cmd, cwd=cwd)
380
+ self.call_from_thread(self._append_output, result.combined)
381
+ if result.ok:
382
+ self.call_from_thread(self.notify, "Command completed", severity="information")
383
+ else:
384
+ self.call_from_thread(self.notify, "Command failed", severity="error")
385
+ self.call_from_thread(self._refresh_everything)
386
+
387
+ def _execute(self, cmd: list[str], cwd: Path | None = None) -> CommandResult:
388
+ proc = subprocess.run(
389
+ cmd,
390
+ cwd=cwd,
391
+ text=True,
392
+ capture_output=True,
393
+ env=os.environ.copy(),
394
+ )
395
+ return CommandResult(cmd=cmd, returncode=proc.returncode, stdout=proc.stdout, stderr=proc.stderr)
396
+
397
+ def _safe_uv(self, *parts: str) -> list[str]:
398
+ return [self.uv_path or "uv", *parts]
399
+
400
+ def _run_uv(self, *parts: str, cwd: Path | None = None) -> None:
401
+ if not self._require_uv():
402
+ return
403
+ self.run_command(self._safe_uv(*parts), cwd=cwd)
404
+
405
+ @on(Button.Pressed, "#refresh_dashboard")
406
+ def refresh_dashboard(self) -> None:
407
+ self._refresh_everything()
408
+ self.notify("Refreshed")
409
+
410
+ @on(Button.Pressed, "#detect_project")
411
+ def detect_project(self) -> None:
412
+ self._update_project_summary()
413
+ self.notify("Project status updated")
414
+
415
+ @on(Button.Pressed, "#open_folder")
416
+ def open_folder(self) -> None:
417
+ path = str(self._project_root())
418
+ if platform.system() == "Darwin":
419
+ self.run_command(["open", path])
420
+ elif os.name == "nt":
421
+ self.run_command(["explorer", path])
422
+ else:
423
+ self.run_command(["xdg-open", path])
424
+
425
+ @on(Button.Pressed, "#install_uv")
426
+ def install_uv(self) -> None:
427
+ if self.uv_path:
428
+ self.notify("uv already appears to be installed")
429
+ return
430
+ system = platform.system()
431
+ if system in {"Linux", "Darwin"}:
432
+ self._append_output("Install uv with: curl -LsSf https://astral.sh/uv/install.sh | sh")
433
+ elif os.name == "nt":
434
+ self._append_output(
435
+ 'Install uv with: powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"'
436
+ )
437
+ else:
438
+ self._append_output("Unknown platform. See the uv installation docs.")
439
+
440
+ @on(Button.Pressed, "#wizard_init")
441
+ def wizard_init(self) -> None:
442
+ self.query_one("#project_name", Input).focus()
443
+ self.notify("Fill in the fields on the Project tab, then click uv init")
444
+
445
+ @on(Button.Pressed, "#run_init")
446
+ def run_init(self) -> None:
447
+ root = self._project_root()
448
+ name = self.query_one("#project_name", Input).value.strip() or "my_app"
449
+ desc = self.query_one("#project_desc", Input).value.strip()
450
+ py = self.query_one("#project_python", Input).value.strip()
451
+ root.mkdir(parents=True, exist_ok=True)
452
+ cmd = self._safe_uv("init", "--package", "--name", name)
453
+ if desc:
454
+ cmd.extend(["--description", desc])
455
+ if py:
456
+ cmd.extend(["--python", py])
457
+ cmd.append(str(root))
458
+ self.run_command(cmd)
459
+
460
+ @on(Button.Pressed, "#run_sync")
461
+ def run_sync(self) -> None:
462
+ self._run_uv("sync", cwd=self._project_root())
463
+
464
+ @on(Button.Pressed, "#run_lock")
465
+ def run_lock(self) -> None:
466
+ self._run_uv("lock", cwd=self._project_root())
467
+
468
+ @on(Button.Pressed, "#run_tree")
469
+ def run_tree(self) -> None:
470
+ self._run_uv("tree", cwd=self._project_root())
471
+
472
+ @on(Button.Pressed, "#dep_add")
473
+ def dep_add(self) -> None:
474
+ pkg = self.query_one("#dependency_name", Input).value.strip()
475
+ if not pkg:
476
+ self.notify("Enter a dependency name", severity="warning")
477
+ return
478
+ self._run_uv("add", pkg, cwd=self._project_root())
479
+
480
+ @on(Button.Pressed, "#dep_remove")
481
+ def dep_remove(self) -> None:
482
+ pkg = self.query_one("#dependency_name", Input).value.strip()
483
+ if not pkg:
484
+ self.notify("Enter a dependency name", severity="warning")
485
+ return
486
+ self._run_uv("remove", pkg, cwd=self._project_root())
487
+
488
+ @on(Button.Pressed, "#project_run")
489
+ def project_run(self) -> None:
490
+ raw = self.query_one("#run_command", Input).value.strip()
491
+ if not raw:
492
+ self.notify("Enter a command to run", severity="warning")
493
+ return
494
+ self._run_uv("run", *raw.split(), cwd=self._project_root())
495
+
496
+ @on(Button.Pressed, "#python_install")
497
+ def python_install(self) -> None:
498
+ version = self.query_one("#python_version", Input).value.strip() or "3.12"
499
+ self._run_uv("python", "install", version)
500
+
501
+ @on(Button.Pressed, "#python_list")
502
+ def python_list(self) -> None:
503
+ self._run_uv("python", "list")
504
+
505
+ @on(Button.Pressed, "#python_find")
506
+ def python_find(self) -> None:
507
+ version = self.query_one("#python_version", Input).value.strip()
508
+ parts = ["python", "find"] + ([version] if version else [])
509
+ self._run_uv(*parts)
510
+
511
+ @on(Button.Pressed, "#python_pin")
512
+ def python_pin(self) -> None:
513
+ version = self.query_one("#python_version", Input).value.strip() or "3.12"
514
+ self._run_uv("python", "pin", version, cwd=self._project_root())
515
+
516
+ @on(Button.Pressed, "#venv_create")
517
+ def venv_create(self) -> None:
518
+ path = self.query_one("#venv_path", Input).value.strip() or ".venv"
519
+ self._run_uv("venv", path, cwd=self._project_root())
520
+
521
+ @on(Button.Pressed, "#venv_help")
522
+ def venv_help(self) -> None:
523
+ self._update_venv_help()
524
+ self.notify("Activation help updated")
525
+
526
+ @on(Button.Pressed, "#tool_install")
527
+ def tool_install(self) -> None:
528
+ tool = self.query_one("#tool_name", Input).value.strip()
529
+ if not tool:
530
+ self.notify("Enter a tool name", severity="warning")
531
+ return
532
+ self._run_uv("tool", "install", tool)
533
+
534
+ @on(Button.Pressed, "#tool_uninstall")
535
+ def tool_uninstall(self) -> None:
536
+ tool = self.query_one("#tool_name", Input).value.strip()
537
+ if not tool:
538
+ self.notify("Enter a tool name", severity="warning")
539
+ return
540
+ self._run_uv("tool", "uninstall", tool)
541
+
542
+ @on(Button.Pressed, "#tool_list")
543
+ def tool_list(self) -> None:
544
+ self._run_uv("tool", "list")
545
+
546
+ @on(Button.Pressed, "#tool_run")
547
+ def tool_run(self) -> None:
548
+ raw = self.query_one("#tool_run_cmd", Input).value.strip()
549
+ if not raw:
550
+ self.notify("Enter a tool command", severity="warning")
551
+ return
552
+ first, *rest = raw.split()
553
+ self._run_uv("tool", "run", first, *rest)
554
+
555
+ @on(Button.Pressed, "#pip_install")
556
+ def pip_install(self) -> None:
557
+ pkg = self.query_one("#pip_package", Input).value.strip()
558
+ if not pkg:
559
+ self.notify("Enter a package name", severity="warning")
560
+ return
561
+ self._run_uv("pip", "install", pkg, cwd=self._project_root())
562
+
563
+ @on(Button.Pressed, "#pip_uninstall")
564
+ def pip_uninstall(self) -> None:
565
+ pkg = self.query_one("#pip_package", Input).value.strip()
566
+ if not pkg:
567
+ self.notify("Enter a package name", severity="warning")
568
+ return
569
+ self._run_uv("pip", "uninstall", pkg, cwd=self._project_root())
570
+
571
+ @on(Button.Pressed, "#pip_list")
572
+ def pip_list(self) -> None:
573
+ self._run_uv("pip", "list", cwd=self._project_root())
574
+
575
+ @on(Button.Pressed, "#pip_freeze")
576
+ def pip_freeze(self) -> None:
577
+ self._run_uv("pip", "freeze", cwd=self._project_root())
578
+
579
+ @on(Button.Pressed, "#pip_tree")
580
+ def pip_tree(self) -> None:
581
+ self._run_uv("pip", "tree", cwd=self._project_root())
582
+
583
+ @on(Button.Pressed, "#run_preset")
584
+ def run_preset(self) -> None:
585
+ list_view = self.query_one("#preset_list", ListView)
586
+ if list_view.index is None:
587
+ self.notify("Choose a preset first", severity="warning")
588
+ return
589
+ presets = [
590
+ ["version"],
591
+ ["self", "update"],
592
+ ["cache", "dir"],
593
+ ["cache", "clean"],
594
+ ["tool", "list"],
595
+ ["python", "list"],
596
+ ]
597
+ self._run_uv(*presets[list_view.index])
598
+
599
+ @on(Button.Pressed, "#run_custom")
600
+ def run_custom(self) -> None:
601
+ raw = self.query_one("#custom_command", Input).value.strip()
602
+ if not raw:
603
+ self.notify("Enter a uv command", severity="warning")
604
+ return
605
+ parts = raw.split()
606
+ if parts[0] == "uv":
607
+ parts = parts[1:]
608
+ self._run_uv(*parts, cwd=self._project_root())
609
+
610
+ @on(Button.Pressed, "#clear_output")
611
+ def clear_output(self) -> None:
612
+ self.query_one("#command_output", TextArea).load_text("")
613
+
614
+
615
+ def main() -> None:
616
+ UVManager().run()
617
+
618
+
619
+ if __name__ == "__main__":
620
+ main()