diff --git a/fuzzforge-cli/src/fuzzforge_cli/tui/app.py b/fuzzforge-cli/src/fuzzforge_cli/tui/app.py index 611e0a6..7999459 100644 --- a/fuzzforge-cli/src/fuzzforge_cli/tui/app.py +++ b/fuzzforge-cli/src/fuzzforge_cli/tui/app.py @@ -46,13 +46,14 @@ class SingleClickDataTable(DataTable): class RowClicked(Message): """Fired on every single mouse click on a data row.""" - def __init__(self, data_table: "SingleClickDataTable", cursor_row: int) -> None: + def __init__(self, data_table: SingleClickDataTable, cursor_row: int) -> None: self.data_table = data_table self.cursor_row = cursor_row super().__init__() @property - def control(self) -> "SingleClickDataTable": + def control(self) -> SingleClickDataTable: + """Return the data table that fired this event.""" return self.data_table async def _on_click(self, event: events.Click) -> None: # type: ignore[override] @@ -471,7 +472,6 @@ class FuzzForgeApp(App[None]): @work(thread=True) def _run_build(self, server_name: str, image: str, hub_name: str) -> None: """Build a Docker/Podman image in a background thread.""" - import subprocess from fuzzforge_cli.tui.helpers import build_image, find_dockerfile_for_server logs = self._build_logs.setdefault(image, []) @@ -481,7 +481,7 @@ class FuzzForgeApp(App[None]): logs.append(f"ERROR: Dockerfile not found for '{server_name}' in hub '{hub_name}'") self._build_results[image] = False self._active_builds.pop(image, None) - self.call_from_thread(self._on_build_done, image, False) + self.call_from_thread(self._on_build_done, image, success=False) return logs.append(f"Building {image} from {dockerfile.parent}") @@ -493,13 +493,14 @@ class FuzzForgeApp(App[None]): logs.append(f"ERROR: {exc}") self._build_results[image] = False self._active_builds.pop(image, None) - self.call_from_thread(self._on_build_done, image, False) + self.call_from_thread(self._on_build_done, image, success=False) return self._active_builds[image] = proc # replace pending marker with actual process self.call_from_thread(self._refresh_hub) # show ⏳ in table - assert proc.stdout is not None + if proc.stdout is None: + return for line in proc.stdout: logs.append(line.rstrip()) @@ -507,10 +508,10 @@ class FuzzForgeApp(App[None]): self._active_builds.pop(image, None) success = proc.returncode == 0 self._build_results[image] = success - self.call_from_thread(self._on_build_done, image, success) + self.call_from_thread(self._on_build_done, image, success=success) - def _on_build_done(self, image: str, success: bool) -> None: - """Called on the main thread when a background build finishes.""" + def _on_build_done(self, image: str, *, success: bool) -> None: + """Handle completion of a background build on the main thread.""" self._refresh_hub() if success: self.notify(f"✓ {image} built successfully", severity="information") diff --git a/fuzzforge-cli/src/fuzzforge_cli/tui/helpers.py b/fuzzforge-cli/src/fuzzforge_cli/tui/helpers.py index cad106d..3efce39 100644 --- a/fuzzforge-cli/src/fuzzforge_cli/tui/helpers.py +++ b/fuzzforge-cli/src/fuzzforge_cli/tui/helpers.py @@ -8,6 +8,7 @@ and managing linked MCP hub repositories. from __future__ import annotations +import contextlib import json import os import subprocess @@ -301,9 +302,10 @@ def _discover_hub_dirs() -> list[Path]: candidates: list[Path] = [] for base in (get_fuzzforge_user_dir() / "hubs", get_fuzzforge_dir() / "hubs"): if base.is_dir(): - for entry in base.iterdir(): - if entry.is_dir() and (entry / ".git").is_dir(): - candidates.append(entry) + candidates.extend( + entry for entry in base.iterdir() + if entry.is_dir() and (entry / ".git").is_dir() + ) return candidates @@ -356,10 +358,8 @@ def load_hubs_registry() -> dict[str, Any]: registry: dict[str, Any] = {"hubs": hubs} # Persist so we don't re-scan on every load - try: + with contextlib.suppress(OSError): save_hubs_registry(registry) - except OSError: - pass return registry diff --git a/fuzzforge-cli/src/fuzzforge_cli/tui/screens/build_image.py b/fuzzforge-cli/src/fuzzforge_cli/tui/screens/build_image.py index 30c6a2c..f556f67 100644 --- a/fuzzforge-cli/src/fuzzforge_cli/tui/screens/build_image.py +++ b/fuzzforge-cli/src/fuzzforge_cli/tui/screens/build_image.py @@ -30,6 +30,7 @@ class BuildImageScreen(ModalScreen[bool]): self._hub_name = hub_name def compose(self) -> ComposeResult: + """Build the confirmation dialog UI.""" with Vertical(id="build-dialog"): yield Label(f"Build {self._image}", classes="dialog-title") yield Label( @@ -38,7 +39,7 @@ class BuildImageScreen(ModalScreen[bool]): ) yield Label( "The image will be built in the background.\n" - "You\'ll receive a notification when it\'s done.", + "You'll receive a notification when it's done.", id="confirm-text", ) with Horizontal(classes="dialog-buttons"): @@ -46,10 +47,12 @@ class BuildImageScreen(ModalScreen[bool]): yield _NoFocusButton("Cancel", variant="default", id="btn-cancel") def on_button_pressed(self, event: Button.Pressed) -> None: + """Handle Build or Cancel button clicks.""" if event.button.id == "btn-build": - self.dismiss(True) + self.dismiss(result=True) elif event.button.id == "btn-cancel": - self.dismiss(False) + self.dismiss(result=False) def action_cancel(self) -> None: - self.dismiss(False) + """Dismiss the dialog when Escape is pressed.""" + self.dismiss(result=False) diff --git a/fuzzforge-cli/src/fuzzforge_cli/tui/screens/build_log.py b/fuzzforge-cli/src/fuzzforge_cli/tui/screens/build_log.py index ba96311..cf9357e 100644 --- a/fuzzforge-cli/src/fuzzforge_cli/tui/screens/build_log.py +++ b/fuzzforge-cli/src/fuzzforge_cli/tui/screens/build_log.py @@ -29,6 +29,7 @@ class BuildLogScreen(ModalScreen[None]): self._last_line: int = 0 def compose(self) -> ComposeResult: + """Build the log viewer UI.""" with Vertical(id="build-dialog"): yield Label(f"Build log — {self._image}", classes="dialog-title") yield Label("", id="build-status") @@ -37,6 +38,7 @@ class BuildLogScreen(ModalScreen[None]): yield _NoFocusButton("Close", variant="default", id="btn-close") def on_mount(self) -> None: + """Initialize log polling when the screen is mounted.""" self._flush_log() self.set_interval(0.5, self._poll_log) @@ -63,12 +65,14 @@ class BuildLogScreen(ModalScreen[None]): status.update(f"[red]✗ {self._image} build failed[/red]") def _poll_log(self) -> None: - """Called every 500 ms by set_interval.""" + """Poll for new log lines periodically.""" self._flush_log() def on_button_pressed(self, event: Button.Pressed) -> None: + """Handle Close button click.""" if event.button.id == "btn-close": self.dismiss(None) def action_close(self) -> None: + """Dismiss the dialog when Escape is pressed.""" self.dismiss(None)