from dataclasses import dataclass, field from textual import on from textual.app import ComposeResult from textual.containers import Horizontal, Vertical from textual.screen import ModalScreen from textual.widgets import Button, Input, Label, Select @dataclass class FormField: name: str label: str kind: str = "text" placeholder: str = "" required: bool = False value: str = "" options: list[str] = field(default_factory=list) class FormDialog(ModalScreen[dict | None]): BINDINGS = [("escape", "cancel", "Cancel")] def __init__(self, title: str, fields: list[FormField], submit_label: str = "Save"): super().__init__() self.form_title = title self.fields = fields self.submit_label = submit_label def compose(self) -> ComposeResult: with Vertical(classes="dialog"): yield Label(self.form_title, classes="dialog-title") for form_field in self.fields: yield Label(form_field.label, classes="field-label") yield self.field_widget(form_field) yield Label("", id="dialog-error") with Horizontal(classes="dialog-buttons"): yield Button(self.submit_label, variant="primary", id="submit") yield Button("Cancel", id="cancel") def field_widget(self, form_field: FormField): widget_id = f"field-{form_field.name}" if form_field.kind == "select": options = [(option, option) for option in form_field.options] return Select(options, value=form_field.value or form_field.options[0], allow_blank=False, id=widget_id) input_type = "integer" if form_field.kind == "rating" else "text" return Input(value=form_field.value, placeholder=form_field.placeholder, type=input_type, id=widget_id) def show_error(self, message: str): self.query_one("#dialog-error", Label).update(message) def read_value(self, form_field: FormField): text = str(self.query_one(f"#field-{form_field.name}").value or "").strip() if form_field.required and not text: raise ValueError(f"{form_field.label} is required") if form_field.kind != "rating": return text or None if not text: return None rating = int(text) if not 0 <= rating <= 10: raise ValueError("Rating must be between 0 and 10") return rating def submit(self): try: values = {form_field.name: self.read_value(form_field) for form_field in self.fields} except ValueError as error: self.show_error(str(error)) return self.dismiss(values) @on(Input.Submitted) def submit_from_input(self): self.submit() @on(Button.Pressed, "#submit") def submit_from_button(self): self.submit() @on(Button.Pressed, "#cancel") def action_cancel(self): self.dismiss(None) class ConfirmDialog(ModalScreen[bool]): BINDINGS = [("y", "confirm", "Yes"), ("n", "cancel", "No"), ("escape", "cancel", "No")] def __init__(self, message: str, confirm_label: str = "Delete", confirm_variant: str = "error"): super().__init__() self.message = message self.confirm_label = confirm_label self.confirm_variant = confirm_variant def compose(self) -> ComposeResult: with Vertical(classes="dialog"): yield Label(self.message, classes="dialog-title") with Horizontal(classes="dialog-buttons"): yield Button(self.confirm_label, variant=self.confirm_variant, id="confirm") yield Button("Cancel", id="cancel") @on(Button.Pressed, "#confirm") def action_confirm(self): self.dismiss(True) @on(Button.Pressed, "#cancel") def action_cancel(self): self.dismiss(False)