| 1 | """Confirmation modal for destructive tool operations.""" |
| 2 | |
| 3 | from rich.text import Text |
| 4 | from textual.app import ComposeResult |
| 5 | from textual.binding import Binding |
| 6 | from textual.containers import Horizontal, Vertical |
| 7 | from textual.screen import ModalScreen |
| 8 | from textual.widgets import Button, Static |
| 9 | |
| 10 | |
| 11 | class ConfirmationModal(ModalScreen[bool]): |
| 12 | """Modal screen for confirming destructive operations.""" |
| 13 | |
| 14 | BINDINGS = [ |
| 15 | Binding("y", "confirm", "Yes", show=True), |
| 16 | Binding("n", "deny", "No", show=True), |
| 17 | Binding("escape", "deny", "Cancel", show=False), |
| 18 | ] |
| 19 | |
| 20 | CSS = """ |
| 21 | ConfirmationModal { |
| 22 | align: center middle; |
| 23 | } |
| 24 | |
| 25 | #confirmation-dialog { |
| 26 | width: 60; |
| 27 | height: auto; |
| 28 | border: heavy $primary; |
| 29 | background: $surface; |
| 30 | padding: 1 2; |
| 31 | } |
| 32 | |
| 33 | #confirmation-title { |
| 34 | text-style: bold; |
| 35 | color: $warning; |
| 36 | margin-bottom: 1; |
| 37 | } |
| 38 | |
| 39 | #confirmation-message { |
| 40 | margin-bottom: 1; |
| 41 | } |
| 42 | |
| 43 | #confirmation-details { |
| 44 | color: $text-muted; |
| 45 | margin-bottom: 1; |
| 46 | padding: 0 1; |
| 47 | border-left: solid $primary-lighten-2; |
| 48 | } |
| 49 | |
| 50 | #confirmation-buttons { |
| 51 | align: center middle; |
| 52 | height: auto; |
| 53 | margin-top: 1; |
| 54 | } |
| 55 | |
| 56 | #confirmation-buttons Button { |
| 57 | margin: 0 1; |
| 58 | } |
| 59 | |
| 60 | #btn-yes { |
| 61 | background: $success; |
| 62 | } |
| 63 | |
| 64 | #btn-no { |
| 65 | background: $error; |
| 66 | } |
| 67 | """ |
| 68 | |
| 69 | def __init__( |
| 70 | self, |
| 71 | tool_name: str, |
| 72 | message: str, |
| 73 | details: str = "", |
| 74 | **kwargs, |
| 75 | ) -> None: |
| 76 | super().__init__(**kwargs) |
| 77 | self.tool_name = tool_name |
| 78 | self.message = message |
| 79 | self.details = details |
| 80 | |
| 81 | def compose(self) -> ComposeResult: |
| 82 | with Vertical(id="confirmation-dialog"): |
| 83 | yield Static( |
| 84 | Text(f"⚠ Confirm {self.tool_name}", style="bold yellow"), |
| 85 | id="confirmation-title", |
| 86 | markup=False, |
| 87 | ) |
| 88 | yield Static(Text(self.message), id="confirmation-message", markup=False) |
| 89 | if self.details: |
| 90 | yield Static( |
| 91 | Text(self.details, style="dim"), |
| 92 | id="confirmation-details", |
| 93 | markup=False, |
| 94 | ) |
| 95 | with Horizontal(id="confirmation-buttons"): |
| 96 | yield Button("Yes (y)", id="btn-yes", variant="success") |
| 97 | yield Button("No (n)", id="btn-no", variant="error") |
| 98 | |
| 99 | def on_button_pressed(self, event: Button.Pressed) -> None: |
| 100 | """Handle button press.""" |
| 101 | if event.button.id == "btn-yes": |
| 102 | self.dismiss(True) |
| 103 | else: |
| 104 | self.dismiss(False) |
| 105 | |
| 106 | def action_confirm(self) -> None: |
| 107 | """Handle 'y' key.""" |
| 108 | self.dismiss(True) |
| 109 | |
| 110 | def action_deny(self) -> None: |
| 111 | """Handle 'n' or escape.""" |
| 112 | self.dismiss(False) |