Coverage for src/lilbee/cli/tui/widgets/confirm_dialog.py: 100%
46 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-09-17 10:02 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-09-17 10:02 +0000
1"""Reusable confirmation modal dialog."""
3from __future__ import annotations
5from typing import ClassVar
7from textual import events, on
8from textual.app import ComposeResult
9from textual.binding import Binding, BindingType
10from textual.containers import Center, Horizontal, Vertical
11from textual.message import Message
12from textual.screen import ModalScreen
13from textual.widgets import Label, Static
15from lilbee.cli.tui import messages as msg
18class ConfirmPill(Static, can_focus=True):
19 """Focusable yes/no pill; Enter, Space or a click picks it."""
21 BINDINGS: ClassVar[list[BindingType]] = [
22 Binding("enter", "select", "Pick", show=False),
23 Binding("space", "select", "Pick", show=False),
24 ]
26 class Picked(Message):
27 """A pill was picked, carrying the answer it stands for."""
29 def __init__(self, answer: bool) -> None:
30 super().__init__()
31 self.answer = answer
33 def __init__(self, label: str, *, pill_id: str, answer: bool) -> None:
34 super().__init__(label, id=pill_id)
35 self._answer = answer
37 def action_select(self) -> None:
38 self.post_message(self.Picked(self._answer))
40 def on_click(self, event: events.Click) -> None:
41 event.stop()
42 self.action_select()
45class ConfirmDialog(ModalScreen[bool]):
46 """Modal yes/no dialog that returns True (confirmed) or False (cancelled)."""
48 CSS_PATH = "confirm_dialog.tcss"
50 BINDINGS: ClassVar[list[BindingType]] = [
51 Binding("y", "confirm", "Yes", show=True),
52 # Fallback for when no pill holds focus; a focused pill takes enter itself.
53 Binding("enter", "confirm", "Confirm", show=False),
54 Binding("n", "cancel", "No", show=True),
55 Binding("escape", "cancel", "Cancel", show=False),
56 # `app.` because the focus actions live on the App, as Screen's own
57 # tab binding has them.
58 Binding("left", "app.focus_previous", "Previous", show=False),
59 Binding("right", "app.focus_next", "Next", show=False),
60 ]
62 def __init__(self, title: str, message: str) -> None:
63 super().__init__()
64 self._title = title
65 self._message = message
67 def compose(self) -> ComposeResult:
68 with Vertical():
69 yield Static(self._title, id="confirm-title")
70 yield Label(self._message, id="confirm-message")
71 with Center(), Horizontal(id="confirm-buttons"):
72 yield ConfirmPill(msg.CONFIRM_YES_LABEL, pill_id="confirm-yes", answer=True)
73 yield ConfirmPill(msg.CONFIRM_NO_LABEL, pill_id="confirm-no", answer=False)
75 @on(ConfirmPill.Picked)
76 def _on_pill_picked(self, event: ConfirmPill.Picked) -> None:
77 event.stop()
78 self.dismiss(event.answer)
80 def action_confirm(self) -> None:
81 self.dismiss(True)
83 def action_cancel(self) -> None:
84 self.dismiss(False)