Initial commit
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
"""Reusable modal prompt screen."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from textual.app import ComposeResult
|
||||
from textual.containers import Horizontal, Vertical
|
||||
from textual.screen import ModalScreen
|
||||
from textual.widgets import Button, Input, Label, Static, TextArea
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PromptField:
|
||||
key: str
|
||||
label: str
|
||||
value: str = ""
|
||||
placeholder: str = ""
|
||||
password: bool = False
|
||||
multiline: bool = False
|
||||
|
||||
|
||||
class PromptScreen(ModalScreen[dict[str, str] | None]):
|
||||
"""Simple form dialog used by the CLI board."""
|
||||
|
||||
DEFAULT_CSS = """
|
||||
PromptScreen {
|
||||
align: center middle;
|
||||
}
|
||||
|
||||
.prompt-dialog {
|
||||
width: 88;
|
||||
max-width: 90%;
|
||||
height: auto;
|
||||
border: solid $primary;
|
||||
background: $surface;
|
||||
padding: 1 2;
|
||||
}
|
||||
|
||||
.prompt-actions {
|
||||
align-horizontal: right;
|
||||
height: auto;
|
||||
margin-top: 1;
|
||||
}
|
||||
|
||||
.prompt-field {
|
||||
margin-top: 1;
|
||||
}
|
||||
|
||||
.prompt-title {
|
||||
text-style: bold;
|
||||
}
|
||||
|
||||
.prompt-help {
|
||||
color: $text-muted;
|
||||
margin-top: 1;
|
||||
}
|
||||
|
||||
.prompt-textarea {
|
||||
height: 6;
|
||||
margin-top: 1;
|
||||
}
|
||||
"""
|
||||
|
||||
BINDINGS = [("escape", "cancel", "Cancel")]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
title: str,
|
||||
fields: list[PromptField],
|
||||
help_text: str = "",
|
||||
confirm_label: str = "Confirm",
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.title_text = title
|
||||
self.fields = fields
|
||||
self.help_text = help_text
|
||||
self.confirm_label = confirm_label
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Vertical(classes="prompt-dialog"):
|
||||
yield Static(self.title_text, id="prompt-title", classes="prompt-title")
|
||||
for field in self.fields:
|
||||
yield Label(field.label, classes="prompt-field")
|
||||
if field.multiline:
|
||||
yield TextArea(
|
||||
field.value,
|
||||
id=f"field-{field.key}",
|
||||
classes="prompt-textarea",
|
||||
tab_behavior="indent",
|
||||
)
|
||||
else:
|
||||
yield Input(
|
||||
value=field.value,
|
||||
placeholder=field.placeholder,
|
||||
password=field.password,
|
||||
id=f"field-{field.key}",
|
||||
)
|
||||
if self.help_text:
|
||||
yield Static(self.help_text, classes="prompt-help")
|
||||
with Horizontal(classes="prompt-actions"):
|
||||
yield Button("Cancel", id="cancel")
|
||||
yield Button(self.confirm_label, id="confirm", variant="primary")
|
||||
|
||||
def on_mount(self) -> None:
|
||||
if self.fields:
|
||||
first_id = f"field-{self.fields[0].key}"
|
||||
try:
|
||||
widget = self.query_one(f"#{first_id}")
|
||||
self.set_focus(widget)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
if event.button.id == "cancel":
|
||||
self.dismiss(None)
|
||||
return
|
||||
if event.button.id == "confirm":
|
||||
self.dismiss(self._collect_values())
|
||||
|
||||
def on_input_submitted(self, event: Input.Submitted) -> None:
|
||||
input_field_ids = [f"field-{f.key}" for f in self.fields if not f.multiline]
|
||||
if event.input.id not in input_field_ids:
|
||||
return
|
||||
# Find position among ALL fields (not just Input fields)
|
||||
all_field_ids = [f"field-{f.key}" for f in self.fields]
|
||||
idx = all_field_ids.index(event.input.id)
|
||||
if idx == len(all_field_ids) - 1:
|
||||
self.dismiss(self._collect_values())
|
||||
return
|
||||
next_id = all_field_ids[idx + 1]
|
||||
try:
|
||||
next_widget = self.query_one(f"#{next_id}")
|
||||
self.set_focus(next_widget)
|
||||
except Exception:
|
||||
self.dismiss(self._collect_values())
|
||||
|
||||
def action_cancel(self) -> None:
|
||||
self.dismiss(None)
|
||||
|
||||
def _collect_values(self) -> dict[str, str]:
|
||||
result: dict[str, str] = {}
|
||||
for field in self.fields:
|
||||
widget_id = f"field-{field.key}"
|
||||
if field.multiline:
|
||||
widget = self.query_one(f"#{widget_id}", TextArea)
|
||||
result[field.key] = widget.text
|
||||
else:
|
||||
widget = self.query_one(f"#{widget_id}", Input)
|
||||
result[field.key] = widget.value
|
||||
return result
|
||||
Reference in New Issue
Block a user