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.
- flex_uv-0.1.0/.gitignore +8 -0
- flex_uv-0.1.0/LICENSE +21 -0
- flex_uv-0.1.0/PKG-INFO +300 -0
- flex_uv-0.1.0/README.md +254 -0
- flex_uv-0.1.0/pyproject.toml +41 -0
- flex_uv-0.1.0/src/flex_uv/__init__.py +6 -0
- flex_uv-0.1.0/src/flex_uv/app.py +620 -0
flex_uv-0.1.0/.gitignore
ADDED
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
|
+
[](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
|
+
|
flex_uv-0.1.0/README.md
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# ⚡ FlexUV
|
|
2
|
+
|
|
3
|
+
[](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,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()
|