Skip to content
This repository was archived by the owner on Apr 22, 2026. It is now read-only.

Commit b975d28

Browse files
committed
tui: fix single-click buttons and double-modal push
1 parent 1891a43 commit b975d28

3 files changed

Lines changed: 41 additions & 20 deletions

File tree

fuzzforge-cli/src/fuzzforge_cli/tui/app.py

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,16 @@ def control(self) -> "SingleClickDataTable":
5656
return self.data_table
5757

5858
async def _on_click(self, event: events.Click) -> None: # type: ignore[override]
59-
"""Forward to parent, then post RowClicked for single-click detection."""
59+
"""Forward to parent, then post RowClicked on every mouse click.
60+
61+
The hub table is handled exclusively via RowClicked. RowSelected is
62+
intentionally NOT used for the hub table to avoid double-dispatch.
63+
"""
6064
await super()._on_click(event)
6165
meta = event.style.meta
62-
if "row" in meta and self.cursor_type == "row":
63-
row_index: int = meta["row"]
64-
if row_index >= 0: # skip header row
66+
if meta and "row" in meta and self.cursor_type == "row":
67+
row_index: int = int(meta["row"])
68+
if row_index >= 0:
6569
self.post_message(SingleClickDataTable.RowClicked(self, row_index))
6670

6771

@@ -371,11 +375,14 @@ def _refresh_hub(self) -> None:
371375
self._hub_rows.append((name, image, hub_name, is_ready))
372376

373377
def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
374-
"""Handle Enter-key row selection on the agents table."""
378+
"""Handle Enter-key row selection (agents table only).
379+
380+
Hub table uses RowClicked exclusively — wiring it to RowSelected too
381+
would cause a double push on every click since Textual 8 fires
382+
RowSelected on ALL clicks, not just second-click-on-same-row.
383+
"""
375384
if event.data_table.id == "agents-table":
376385
self._handle_agent_row(event.cursor_row)
377-
elif event.data_table.id == "hub-table":
378-
self._handle_hub_row(event.cursor_row)
379386

380387
def on_single_click_data_table_row_clicked(
381388
self, event: SingleClickDataTable.RowClicked
@@ -408,6 +415,10 @@ def _handle_agent_row(self, idx: int) -> None:
408415

409416
def _handle_hub_row(self, idx: int) -> None:
410417
"""Handle a click on a hub table row."""
418+
# Guard: never push two build dialogs at once (double-click protection)
419+
if getattr(self, "_build_dialog_open", False):
420+
return
421+
411422
if idx < 0 or idx >= len(self._hub_rows):
412423
return
413424
row_data = self._hub_rows[idx]
@@ -419,7 +430,11 @@ def _handle_hub_row(self, idx: int) -> None:
419430
# If a build is already running, open the live log viewer
420431
if image in self._active_builds:
421432
from fuzzforge_cli.tui.screens.build_log import BuildLogScreen
422-
self.push_screen(BuildLogScreen(image))
433+
self._build_dialog_open = True
434+
self.push_screen(
435+
BuildLogScreen(image),
436+
callback=lambda _: setattr(self, "_build_dialog_open", False),
437+
)
423438
return
424439

425440
if is_ready:
@@ -432,10 +447,15 @@ def _handle_hub_row(self, idx: int) -> None:
432447

433448
from fuzzforge_cli.tui.screens.build_image import BuildImageScreen
434449

450+
self._build_dialog_open = True
451+
452+
def _on_build_dialog_done(confirmed: bool, sn: str = server_name, im: str = image, hn: str = hub_name) -> None:
453+
self._build_dialog_open = False
454+
self._on_build_confirmed(confirmed, sn, im, hn)
455+
435456
self.push_screen(
436457
BuildImageScreen(server_name, image, hub_name),
437-
callback=lambda confirmed, sn=server_name, im=image, hn=hub_name:
438-
self._on_build_confirmed(confirmed, sn, im, hn),
458+
callback=_on_build_dialog_done,
439459
)
440460

441461
def _on_build_confirmed(self, confirmed: bool, server_name: str, image: str, hub_name: str) -> None:

fuzzforge-cli/src/fuzzforge_cli/tui/screens/build_image.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414
from textual.widgets import Button, Label
1515

1616

17+
class _NoFocusButton(Button):
18+
can_focus = False
19+
20+
1721
class BuildImageScreen(ModalScreen[bool]):
1822
"""Quick confirmation before starting a background Docker/Podman build."""
1923

@@ -38,16 +42,10 @@ def compose(self) -> ComposeResult:
3842
id="confirm-text",
3943
)
4044
with Horizontal(classes="dialog-buttons"):
41-
yield Button("Build", variant="success", id="btn-build")
42-
yield Button("Cancel", variant="default", id="btn-cancel")
43-
44-
def on_mount(self) -> None:
45-
# Ensure a widget is focused so both buttons respond to a single click.
46-
# Default to Cancel so Build is never pre-selected.
47-
self.query_one("#btn-cancel", Button).focus()
45+
yield _NoFocusButton("Build", variant="primary", id="btn-build")
46+
yield _NoFocusButton("Cancel", variant="default", id="btn-cancel")
4847

4948
def on_button_pressed(self, event: Button.Pressed) -> None:
50-
event.stop()
5149
if event.button.id == "btn-build":
5250
self.dismiss(True)
5351
elif event.button.id == "btn-cancel":

fuzzforge-cli/src/fuzzforge_cli/tui/screens/build_log.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414
from textual.widgets import Button, Label, Log
1515

1616

17+
class _NoFocusButton(Button):
18+
can_focus = False
19+
20+
1721
class BuildLogScreen(ModalScreen[None]):
1822
"""Live log viewer for a background build job managed by the app."""
1923

@@ -30,10 +34,9 @@ def compose(self) -> ComposeResult:
3034
yield Label("", id="build-status")
3135
yield Log(id="build-log", auto_scroll=True)
3236
with Horizontal(classes="dialog-buttons"):
33-
yield Button("Close", variant="default", id="btn-close")
37+
yield _NoFocusButton("Close", variant="default", id="btn-close")
3438

3539
def on_mount(self) -> None:
36-
self.query_one("#btn-close", Button).focus()
3740
self._flush_log()
3841
self.set_interval(0.5, self._poll_log)
3942

0 commit comments

Comments
 (0)