odoo-wt 1.0.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,7 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ .venv/
5
+ .env
6
+ .pytest_cache/
7
+ .vscode/
odoo_wt-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,85 @@
1
+ Metadata-Version: 2.4
2
+ Name: odoo-wt
3
+ Version: 1.0.0
4
+ Summary: Modern TUI-driven Odoo Worktree Manager
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: pyspellchecker>=0.8.1
7
+ Requires-Dist: rich>=13.7.1
8
+ Requires-Dist: textual>=0.52.1
9
+ Description-Content-Type: text/markdown
10
+
11
+ # Odoo Worktree Assistant (`odoo-wt`)
12
+
13
+ A professional, modern TUI-driven Git worktree manager explicitly designed for Odoo developers. Built with Python, [Textual](https://textual.textualize.io/), and [uv](https://github.com/astral-sh/uv).
14
+
15
+ ![Python](https://img.shields.io/badge/Language-Python-blue) ![TUI](https://img.shields.io/badge/UI-Textual-green) ![Package Manager](https://img.shields.io/badge/Manager-uv-orange)
16
+
17
+ ## 🚀 Overview
18
+
19
+ `odoo-wt` streamlines the process of spinning up new Odoo development environments. It handles the complex "handshake" between Git worktrees and Python virtual environments so you can focus on code.
20
+
21
+ ### Key Features
22
+
23
+ - **Modern TUI:** A sleek, reactive "All-in-One" form built with Textual.
24
+ - **Smart Branching:** Follows the Odoo standard: `[VERSION]-[description]-[SUFFIX]`.
25
+ - **Dual Repo Support:** Simultaneously creates worktrees for both `odoo` (Community) and `enterprise`.
26
+ - **Automated UV Environments:**
27
+ - Centralizes environments in `~/.envs/[VERSION]`.
28
+ - Automatically runs `uv venv` and `uv pip install` if the environment is missing.
29
+ - Instantly symlinks `.venv` into your new worktree folder for automatic VS Code detection.
30
+ - **Proactive Status Checks:** Checks `odoo-dev` and local remotes to detect if branches already exist before creation.
31
+
32
+ ## 🛠️ Installation
33
+
34
+ The recommended way to install `odoo-wt` is using `uv tool`:
35
+
36
+ ```bash
37
+ # From the project directory
38
+ uv tool install . --force
39
+ ```
40
+
41
+ This creates an isolated environment for the tool and places an `odoo-wt` executable in your `~/.local/bin/`.
42
+
43
+ ## ⚙️ Configuration
44
+
45
+ The tool saves its configuration to `~/.config/odoo-wt.json`. You can edit paths directly in the **Settings** tab within the application.
46
+
47
+ - **`wt_root`**: Where your worktree folders live (e.g., `~/repos/Odoo/wt`).
48
+ - **`env_root`**: Where your centralized global Python environments are stored (e.g., `~/.envs`).
49
+ - **`suffix`**: Your default developer quadrigram (e.g., `pian`).
50
+
51
+ ## 📖 Usage
52
+
53
+ ### Interactive Mode (The Guide)
54
+ Simply run the command with no arguments:
55
+ ```bash
56
+ odoo-wt
57
+ ```
58
+ - Use **`Tab`** to switch between Version, Description, and Suffix.
59
+ - Type **`custom...`** in any dropdown to reveal a custom input field.
60
+ - Press **`Ctrl+S`** to instantly submit and deploy.
61
+ - Press **`Esc`** to cancel and exit.
62
+
63
+ ### Fast Mode
64
+ If you already have a full branch name ready:
65
+ ```bash
66
+ odoo-wt 17.0-fix-account-bug-pian
67
+ ```
68
+
69
+ ## ⚠️ Known Limitations
70
+
71
+ - **Clipboard Interactions (Copy/Paste):** Because `odoo-wt` uses mouse-capture to enable clickable buttons and scrollbars in the terminal, your terminal emulator's native click-to-highlight features are disabled by default.
72
+ - **To copy text manually:** Hold the `Shift` key while clicking and dragging over the text to bypass the app and use your terminal's native selection, then use `Ctrl+Shift+C`.
73
+ - **To paste text:** Sometimes native terminal paste (`Ctrl+Shift+V` or middle-click) events are intercepted unreliably by the underlying framework. If pasting into an input field fails, you may need to type the branch name manually.
74
+
75
+ ## 📝 Development
76
+
77
+ The source code is located at `/home/odoo/repos/Scripts/odoo-wt/`.
78
+
79
+ To apply changes made to the source:
80
+ ```bash
81
+ uv tool install . --force
82
+ ```
83
+
84
+ ## 📜 License
85
+ MIT
@@ -0,0 +1,75 @@
1
+ # Odoo Worktree Assistant (`odoo-wt`)
2
+
3
+ A professional, modern TUI-driven Git worktree manager explicitly designed for Odoo developers. Built with Python, [Textual](https://textual.textualize.io/), and [uv](https://github.com/astral-sh/uv).
4
+
5
+ ![Python](https://img.shields.io/badge/Language-Python-blue) ![TUI](https://img.shields.io/badge/UI-Textual-green) ![Package Manager](https://img.shields.io/badge/Manager-uv-orange)
6
+
7
+ ## 🚀 Overview
8
+
9
+ `odoo-wt` streamlines the process of spinning up new Odoo development environments. It handles the complex "handshake" between Git worktrees and Python virtual environments so you can focus on code.
10
+
11
+ ### Key Features
12
+
13
+ - **Modern TUI:** A sleek, reactive "All-in-One" form built with Textual.
14
+ - **Smart Branching:** Follows the Odoo standard: `[VERSION]-[description]-[SUFFIX]`.
15
+ - **Dual Repo Support:** Simultaneously creates worktrees for both `odoo` (Community) and `enterprise`.
16
+ - **Automated UV Environments:**
17
+ - Centralizes environments in `~/.envs/[VERSION]`.
18
+ - Automatically runs `uv venv` and `uv pip install` if the environment is missing.
19
+ - Instantly symlinks `.venv` into your new worktree folder for automatic VS Code detection.
20
+ - **Proactive Status Checks:** Checks `odoo-dev` and local remotes to detect if branches already exist before creation.
21
+
22
+ ## 🛠️ Installation
23
+
24
+ The recommended way to install `odoo-wt` is using `uv tool`:
25
+
26
+ ```bash
27
+ # From the project directory
28
+ uv tool install . --force
29
+ ```
30
+
31
+ This creates an isolated environment for the tool and places an `odoo-wt` executable in your `~/.local/bin/`.
32
+
33
+ ## ⚙️ Configuration
34
+
35
+ The tool saves its configuration to `~/.config/odoo-wt.json`. You can edit paths directly in the **Settings** tab within the application.
36
+
37
+ - **`wt_root`**: Where your worktree folders live (e.g., `~/repos/Odoo/wt`).
38
+ - **`env_root`**: Where your centralized global Python environments are stored (e.g., `~/.envs`).
39
+ - **`suffix`**: Your default developer quadrigram (e.g., `pian`).
40
+
41
+ ## 📖 Usage
42
+
43
+ ### Interactive Mode (The Guide)
44
+ Simply run the command with no arguments:
45
+ ```bash
46
+ odoo-wt
47
+ ```
48
+ - Use **`Tab`** to switch between Version, Description, and Suffix.
49
+ - Type **`custom...`** in any dropdown to reveal a custom input field.
50
+ - Press **`Ctrl+S`** to instantly submit and deploy.
51
+ - Press **`Esc`** to cancel and exit.
52
+
53
+ ### Fast Mode
54
+ If you already have a full branch name ready:
55
+ ```bash
56
+ odoo-wt 17.0-fix-account-bug-pian
57
+ ```
58
+
59
+ ## ⚠️ Known Limitations
60
+
61
+ - **Clipboard Interactions (Copy/Paste):** Because `odoo-wt` uses mouse-capture to enable clickable buttons and scrollbars in the terminal, your terminal emulator's native click-to-highlight features are disabled by default.
62
+ - **To copy text manually:** Hold the `Shift` key while clicking and dragging over the text to bypass the app and use your terminal's native selection, then use `Ctrl+Shift+C`.
63
+ - **To paste text:** Sometimes native terminal paste (`Ctrl+Shift+V` or middle-click) events are intercepted unreliably by the underlying framework. If pasting into an input field fails, you may need to type the branch name manually.
64
+
65
+ ## 📝 Development
66
+
67
+ The source code is located at `/home/odoo/repos/Scripts/odoo-wt/`.
68
+
69
+ To apply changes made to the source:
70
+ ```bash
71
+ uv tool install . --force
72
+ ```
73
+
74
+ ## 📜 License
75
+ MIT
@@ -0,0 +1,24 @@
1
+ [project]
2
+ name = "odoo-wt"
3
+ version = "1.0.0"
4
+ description = "Modern TUI-driven Odoo Worktree Manager"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ dependencies = ["textual>=0.52.1", "rich>=13.7.1", "pyspellchecker>=0.8.1"]
8
+
9
+ [project.scripts]
10
+ odoo-wt = "odoo_wt.cli_main:main"
11
+
12
+ [build-system]
13
+ requires = ["hatchling"]
14
+ build-backend = "hatchling.build"
15
+
16
+ [tool.hatch.build.targets.wheel]
17
+ packages = ["src/odoo_wt"]
18
+
19
+ [dependency-groups]
20
+ dev = [
21
+ "pytest>=9.0.2",
22
+ "pytest-asyncio>=1.3.0",
23
+ "rich>=14.3.3",
24
+ ]
@@ -0,0 +1,41 @@
1
+ import json
2
+ import datetime
3
+ import os
4
+ from pathlib import Path
5
+
6
+ # --- CONFIGURATION ---
7
+ CONFIG_FILE = Path.home() / ".config" / "odoo-wt.json"
8
+ LOG_FILE = Path.home() / ".config" / "odoo-wt-logs.jsonl"
9
+
10
+ def append_log(action: str, details: dict = None):
11
+ if details is None: details = {}
12
+ entry = {"timestamp": datetime.datetime.now().isoformat(), "action": action, "details": details}
13
+ try:
14
+ with open(LOG_FILE, "a") as f:
15
+ f.write(json.dumps(entry) + "\n")
16
+ except: pass
17
+
18
+ def load_config():
19
+ default_config = {
20
+ "wt_root": str(Path.home() / "repos" / "Odoo" / "wt"),
21
+ "env_root": str(Path.home() / ".envs"),
22
+ "suffix": "pian",
23
+ "remote_name": "odoo-dev",
24
+ "community_dir": "odoo",
25
+ "enterprise_dir": "enterprise",
26
+ "default_tab": "tab-create",
27
+ "ignored_versions": [],
28
+ "ignored_suffixes": []
29
+ }
30
+ if CONFIG_FILE.exists():
31
+ try:
32
+ with open(CONFIG_FILE, "r") as f:
33
+ default_config.update(json.load(f))
34
+ except:
35
+ pass
36
+ return default_config
37
+
38
+ def save_config(config):
39
+ CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
40
+ with open(CONFIG_FILE, "w") as f:
41
+ json.dump(config, f, indent=4)
@@ -0,0 +1,50 @@
1
+ import sys
2
+ import os
3
+ from .app_config import load_config, CONFIG_FILE
4
+ from .system_discovery import discover_system_data
5
+ from .setup_wizard import WizardApp
6
+ from .main_tui import OdooWtApp
7
+
8
+ def main():
9
+ if not CONFIG_FILE.exists():
10
+ app = WizardApp()
11
+ config = app.run()
12
+ if not config:
13
+ return
14
+ else:
15
+ config = load_config()
16
+
17
+ # CLI direct creation mode
18
+ if len(sys.argv) > 1:
19
+ branch = sys.argv[1]
20
+ v = branch.split("-")[0] if "-" in branch else "master"
21
+ # We manually build the data for DeployScreen
22
+ data = {"action": "create", "version": v, "desc": branch, "suffix": ""}
23
+ # In CLI mode, we still need to boot OdooWtApp to push the DeployScreen
24
+ # or we just boot a minimal app to host the DeployScreen.
25
+ # For now, let's just launch the full app and it will handle it if we modify init.
26
+ # But easier: just boot the app normally.
27
+ v_list, s_list, worktrees = discover_system_data(config["wt_root"], config["suffix"])
28
+ app = OdooWtApp(config, v_list, s_list, worktrees)
29
+ # We don't have an easy way to auto-start deployment from CLI yet,
30
+ # let's just open the app for now.
31
+ app.run()
32
+ else:
33
+ v_list, s_list, worktrees = discover_system_data(config["wt_root"], config["suffix"])
34
+ app = OdooWtApp(config, v_list, s_list, worktrees)
35
+ data = app.run()
36
+
37
+ # Handle deployment completion actions
38
+ if data and isinstance(data, dict):
39
+ action = data.get("action")
40
+ if action == "terminal":
41
+ target = data["path"]
42
+ os.chdir(target)
43
+ shell = os.environ.get("SHELL", "/bin/bash")
44
+ os.execv(shell, [shell])
45
+ elif action == "vscode":
46
+ target = data["path"]
47
+ os.system(f"code {target}")
48
+
49
+ if __name__ == "__main__":
50
+ main()
@@ -0,0 +1,290 @@
1
+ import asyncio
2
+ import os
3
+ import json
4
+ from pathlib import Path
5
+ from textual import on, work
6
+ from textual.binding import Binding
7
+ from textual.screen import ModalScreen, Screen
8
+ from textual.widgets import Label, Button, ProgressBar, RichLog
9
+ from textual.containers import Vertical, Horizontal, VerticalScroll
10
+
11
+ from .app_config import append_log
12
+ from .system_discovery import get_remote, check_local
13
+
14
+ async def run_cmd_stream(cmd, cwd, log_widget, prefix=""):
15
+ process = await asyncio.create_subprocess_exec(
16
+ *cmd,
17
+ cwd=cwd,
18
+ stdout=asyncio.subprocess.PIPE,
19
+ stderr=asyncio.subprocess.STDOUT
20
+ )
21
+ while True:
22
+ line = await process.stdout.readline()
23
+ if not line:
24
+ break
25
+ text = line.decode('utf-8', errors='replace').rstrip()
26
+ log_widget.write(f"{prefix}{text}")
27
+ await process.wait()
28
+ return process.returncode == 0
29
+
30
+ class DeleteConfirmScreen(ModalScreen[bool]):
31
+ def __init__(self, wt_name: str):
32
+ super().__init__()
33
+ self.wt_name = wt_name
34
+ self.step = 1
35
+
36
+ def on_key(self, event) -> None:
37
+ if event.key == "left":
38
+ self.focus_previous()
39
+ elif event.key == "right":
40
+ self.focus_next()
41
+
42
+ def compose(self):
43
+ with Vertical(id="delete-dialog"):
44
+ yield Label(f"Delete worktree '{self.wt_name}'? (1/3)", id="del-msg")
45
+ with Horizontal(classes="del-btn-row", id="btn-container"):
46
+ yield Button("Yes, delete", variant="error", id="btn-yes")
47
+ yield Button("Cancel", variant="primary", id="btn-cancel")
48
+
49
+ @on(Button.Pressed, "#btn-yes")
50
+ async def on_yes(self):
51
+ append_log("Delete Confirm Step", {"step": self.step, "worktree": self.wt_name})
52
+ self.step += 1
53
+ if self.step > 3:
54
+ self.dismiss(True)
55
+ return
56
+ msg = self.query_one("#del-msg", Label)
57
+ container = self.query_one("#btn-container", Horizontal)
58
+ await container.query_children().remove()
59
+ if self.step == 2:
60
+ msg.update(f"Are you SURE? (2/3)")
61
+ await container.mount(Button("Cancel", variant="primary", id="btn-cancel"))
62
+ await container.mount(Button("Yes, delete", variant="error", id="btn-yes"))
63
+ elif self.step == 3:
64
+ msg.update(f"Final warning: NUKE '{self.wt_name}'? (3/3)")
65
+ await container.mount(Button("Yes, delete", variant="error", id="btn-yes"))
66
+ await container.mount(Button("Cancel", variant="primary", id="btn-cancel"))
67
+ self.query_one("#btn-cancel").focus()
68
+
69
+ @on(Button.Pressed, "#btn-cancel")
70
+ def on_cancel(self):
71
+ append_log("Delete Cancelled", {"worktree": self.wt_name})
72
+ self.dismiss(False)
73
+
74
+
75
+
76
+ from textual.binding import Binding
77
+ class DeployScreen(Screen):
78
+ BINDINGS = [
79
+ Binding("t", "terminal", "Terminal", show=False),
80
+ Binding("v", "vscode", "VS Code", show=False),
81
+ Binding("b", "back", "Back", show=False),
82
+ Binding("x", "exit", "Exit", show=False),
83
+ ]
84
+
85
+ def action_terminal(self):
86
+ if not self.query_one("#success-footer").has_class("hidden"):
87
+ self.on_terminal()
88
+
89
+ def action_vscode(self):
90
+ if not self.query_one("#success-footer").has_class("hidden"):
91
+ self.on_vscode()
92
+
93
+ def action_back(self):
94
+ if not self.query_one("#success-footer").has_class("hidden"):
95
+ self.on_back()
96
+
97
+ def action_exit(self):
98
+ if not self.query_one("#success-footer").has_class("hidden"):
99
+ self.on_exit()
100
+
101
+ def __init__(self, data, config):
102
+ super().__init__()
103
+ self.data = data
104
+ self.config = config
105
+
106
+ def compose(self):
107
+ with VerticalScroll(id="deploy-logs-scroll"):
108
+ with Vertical(classes="log-box"):
109
+ with Horizontal(classes="log-header"):
110
+ yield Label("Community", classes="log-title")
111
+ yield ProgressBar(id="prog-odoo", show_eta=False)
112
+ yield RichLog(id="log-odoo", markup=False, highlight=False)
113
+
114
+ with Vertical(classes="log-box"):
115
+ with Horizontal(classes="log-header"):
116
+ yield Label("Enterprise", classes="log-title")
117
+ yield ProgressBar(id="prog-ent", show_eta=False)
118
+ yield RichLog(id="log-ent", markup=False, highlight=False)
119
+
120
+ with Vertical(classes="log-uv-box", id="uv-box"):
121
+ with Horizontal(classes="log-header"):
122
+ yield Label("UV Env", classes="log-title")
123
+ yield ProgressBar(id="prog-uv", show_eta=False)
124
+ yield RichLog(id="log-uv", markup=False, highlight=False)
125
+
126
+ with Vertical(id="success-footer", classes="hidden"):
127
+ yield Label("", id="success-message", classes="success-msg")
128
+ with Horizontal(classes="success-btn-row"):
129
+ yield Button("(T)erminal", variant="success", id="btn-terminal")
130
+ yield Button("(V)S Code", variant="primary", id="btn-vscode")
131
+ yield Button("(B)ack to Tool", variant="warning", id="btn-back")
132
+ yield Button("E(x)it", variant="error", id="btn-exit")
133
+
134
+ def on_mount(self):
135
+ self.run_deployment()
136
+
137
+ @work(exclusive=True)
138
+ async def run_deployment(self):
139
+ wt_root = Path(self.config["wt_root"])
140
+ dev_remote = self.config.get("remote_name", "odoo-dev")
141
+ comm_dir = self.config.get("community_dir", "odoo")
142
+ ent_dir = self.config.get("enterprise_dir", "enterprise")
143
+
144
+ clean_desc = self.data["desc"].strip().replace(" ", "_")
145
+ parts = [p for p in [self.data["version"], clean_desc, self.data["suffix"]] if p]
146
+ branch_name = "-".join(parts)
147
+ append_log("Deployment Started", {"branch": branch_name, "version": self.data["version"]})
148
+ target_dir = wt_root / branch_name
149
+ base_odoo = wt_root / "master" / comm_dir
150
+ base_ent = wt_root / "master" / ent_dir
151
+ base_v = self.data["version"] or "master"
152
+
153
+ target_dir.mkdir(parents=True, exist_ok=True)
154
+
155
+ async def deploy_repo(repo, dest, log_id, prog_id):
156
+ log = self.query_one(f"#{log_id}", RichLog)
157
+ prog = self.query_one(f"#{prog_id}", ProgressBar)
158
+ prog.update(total=3)
159
+
160
+ remote = await asyncio.to_thread(get_remote, repo)
161
+ log.write(f"Detected base remote: {remote}")
162
+ prog.advance(1)
163
+
164
+ log.write(f"Fetching '{branch_name}' from '{dev_remote}'...")
165
+ success = await run_cmd_stream(["git", "fetch", dev_remote, f"{branch_name}:{branch_name}", "--force"], repo, log)
166
+ prog.advance(1)
167
+
168
+ if success:
169
+ log.write("Fetch successful. Creating worktree...")
170
+ append_log("Branch Strategy Selected", {"repo": dest, "strategy": "remote_pull", "remote": dev_remote, "branch": branch_name})
171
+ await run_cmd_stream(["git", "worktree", "add", str(target_dir / dest), branch_name], repo, log)
172
+ else:
173
+ log.write(f"Branch not found on '{dev_remote}'. Fetching '{base_v}' from '{remote}'...")
174
+ await run_cmd_stream(["git", "fetch", remote, base_v], repo, log)
175
+
176
+ is_local = await asyncio.to_thread(check_local, repo, branch_name)
177
+ if is_local:
178
+ log.write("Branch exists locally. Creating worktree...")
179
+ append_log("Branch Strategy Selected", {"repo": dest, "strategy": "local_checkout", "branch": branch_name})
180
+ await run_cmd_stream(["git", "worktree", "add", str(target_dir / dest), branch_name], repo, log)
181
+ else:
182
+ log.write(f"Creating new branch from {remote}/{base_v}...")
183
+ append_log("Branch Strategy Selected", {"repo": dest, "strategy": "new_branch", "base": f"{remote}/{base_v}", "branch": branch_name})
184
+ await run_cmd_stream(["git", "worktree", "add", "-b", branch_name, str(target_dir / dest), f"{remote}/{base_v}"], repo, log)
185
+ await run_cmd_stream(["git", "branch", "--set-upstream-to", f"{remote}/{base_v}", branch_name], repo, log)
186
+
187
+ prog.advance(1)
188
+ log.write("✅ Done.")
189
+
190
+ async def setup_uv():
191
+ log_uv = self.query_one("#log-uv", RichLog)
192
+ prog_uv = self.query_one("#prog-uv", ProgressBar)
193
+ prog_uv.update(total=4)
194
+
195
+ env_root = Path(self.config["env_root"])
196
+ env_root.mkdir(parents=True, exist_ok=True)
197
+ target_env = env_root / base_v
198
+
199
+ if not target_env.exists():
200
+ log_uv.write(f"Initializing UV environment for {base_v}...")
201
+ await run_cmd_stream(["uv", "venv", str(target_env), "--python", "3.12"], env_root, log_uv)
202
+ prog_uv.advance(1)
203
+
204
+ # Use base folder requirements to avoid waiting for worktree creation
205
+ base_req = wt_root / "master" / comm_dir / "requirements.txt"
206
+ if base_req.exists():
207
+ log_uv.write(f"Installing requirements from base '{comm_dir}'...")
208
+ await run_cmd_stream([
209
+ "uv", "pip", "install", "-r", str(base_req),
210
+ "--python", str(target_env / "bin" / "python")
211
+ ], env_root, log_uv)
212
+ prog_uv.advance(1)
213
+ else:
214
+ log_uv.write(f"UV environment '{base_v}' already exists.")
215
+ prog_uv.advance(2)
216
+
217
+ # Wait for target_dir to definitely exist (though we create it at start)
218
+ # but more importantly, we need to wait for odoo/enterprise worktrees
219
+ # to finish before we can reliably symlink .venv into the target_dir
220
+ # actually target_dir is just a folder, we can symlink now!
221
+ venv_symlink = target_dir / ".venv"
222
+ if not venv_symlink.exists():
223
+ try:
224
+ os.symlink(target_env, venv_symlink)
225
+ log_uv.write("Created .venv symlink.")
226
+ except Exception as e:
227
+ log_uv.write(f"Failed to create symlink: {e}")
228
+ prog_uv.advance(1)
229
+ log_uv.write("✅ Done.")
230
+ return True
231
+
232
+ await asyncio.gather(
233
+ deploy_repo(base_odoo, comm_dir, "log-odoo", "prog-odoo"),
234
+ deploy_repo(base_ent, ent_dir, "log-ent", "prog-ent"),
235
+ setup_uv()
236
+ )
237
+
238
+ append_log("Deployment Success", {"branch": branch_name, "path": str(target_dir)})
239
+ self.target_dir = target_dir
240
+ self.show_success_footer()
241
+
242
+ def show_success_footer(self):
243
+ msg = self.query_one("#success-message", Label)
244
+ msg.update(f"SUCCESS! Worktree ready at: {self.target_dir}\nWhat would you like to do next?")
245
+ self.query_one("#success-footer").remove_class("hidden")
246
+ self.query_one("#btn-terminal").focus()
247
+
248
+ @on(Button.Pressed, "#btn-terminal")
249
+ def on_terminal(self):
250
+ append_log("Deployment Complete", {"choice": "terminal"})
251
+ self.app.exit({"action": "terminal", "path": str(self.target_dir)})
252
+
253
+ @on(Button.Pressed, "#btn-vscode")
254
+ def on_vscode(self):
255
+ append_log("Deployment Complete", {"choice": "vscode"})
256
+ self.app.exit({"action": "vscode", "path": str(self.target_dir)})
257
+
258
+ @on(Button.Pressed, "#btn-back")
259
+ def on_back(self):
260
+ append_log("Deployment Complete", {"choice": "back"})
261
+ self.dismiss()
262
+
263
+ @on(Button.Pressed, "#btn-exit")
264
+ def on_exit(self):
265
+ append_log("Deployment Complete", {"choice": "exit"})
266
+ self.app.exit()
267
+
268
+ class LogDetailScreen(ModalScreen[None]):
269
+ def __init__(self, ts, action, details):
270
+ super().__init__()
271
+ self.ts = ts
272
+ self.action = action
273
+ self.details = details
274
+
275
+ def compose(self):
276
+ with Vertical(id="log-detail-dialog"):
277
+ yield Label(f"[{self.ts}] {self.action}", classes="log-detail-title")
278
+ try:
279
+ parsed = json.loads(self.details)
280
+ pretty_details = json.dumps(parsed, indent=4)
281
+ except:
282
+ pretty_details = str(self.details)
283
+ with VerticalScroll():
284
+ yield Label(pretty_details, classes="log-detail-text")
285
+ with Horizontal(classes="log-detail-btn-row"):
286
+ yield Button("Close", variant="primary", id="btn-close-log")
287
+
288
+ @on(Button.Pressed, "#btn-close-log")
289
+ def on_close(self):
290
+ self.dismiss()