Coverage for src / lilbee / cli / tui / widgets / confirm_dialog.py: 100%

35 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-05-15 20:55 +0000

1"""Reusable confirmation modal dialog.""" 

2 

3from __future__ import annotations 

4 

5from typing import ClassVar 

6 

7from textual import events 

8from textual.app import ComposeResult 

9from textual.binding import Binding, BindingType 

10from textual.containers import Center, Horizontal, Vertical 

11from textual.screen import ModalScreen 

12from textual.widgets import Label, Static 

13 

14 

15class _ConfirmPill(Static, can_focus=True): 

16 """Pill-styled clickable label used as Yes/No in :class:`ConfirmDialog`.""" 

17 

18 def __init__(self, label: str, *, dialog_id: str) -> None: 

19 super().__init__(label, id=dialog_id, classes="confirm-pill") 

20 self._dialog_id = dialog_id 

21 

22 def on_click(self, event: events.Click) -> None: 

23 event.stop() 

24 screen = self.screen 

25 if isinstance(screen, ConfirmDialog): 

26 screen.dismiss(self._dialog_id == "confirm-yes") 

27 

28 

29class ConfirmDialog(ModalScreen[bool]): 

30 """Modal yes/no dialog that returns True (confirmed) or False (cancelled).""" 

31 

32 CSS_PATH = "confirm_dialog.tcss" 

33 

34 BINDINGS: ClassVar[list[BindingType]] = [ 

35 Binding("y", "confirm", "Yes", show=True), 

36 Binding("enter", "confirm", "Confirm", show=False), 

37 Binding("n", "cancel", "No", show=True), 

38 Binding("escape", "cancel", "Cancel", show=False), 

39 ] 

40 

41 def __init__(self, title: str, message: str) -> None: 

42 super().__init__() 

43 self._title = title 

44 self._message = message 

45 

46 def compose(self) -> ComposeResult: 

47 with Vertical(): 

48 yield Static(self._title, id="confirm-title") 

49 yield Label(self._message, id="confirm-message") 

50 with Center(), Horizontal(id="confirm-buttons"): 

51 yield _ConfirmPill("Yes (y)", dialog_id="confirm-yes") 

52 yield _ConfirmPill("No (n)", dialog_id="confirm-no") 

53 

54 def action_confirm(self) -> None: 

55 self.dismiss(True) 

56 

57 def action_cancel(self) -> None: 

58 self.dismiss(False)