Replace content CLI with Textual TUI
This commit is contained in:
1 parent
0d58ddd430
commit
737700a9a8
28 files changed
+2403
-167
No files matched your search
@@ -0,0 +1,106 @@
|
||||
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):
|
||||
super().__init__()
|
||||
self.message = message
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Vertical(classes="dialog"):
|
||||
yield Label(self.message, classes="dialog-title")
|
||||
with Horizontal(classes="dialog-buttons"):
|
||||
yield Button("Delete", variant="error", 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)
|
||||
Reference in new issue
Block a user