237 lines
5.9 KiB
Python
237 lines
5.9 KiB
Python
"""Modal dialog screens for oAI TUI."""
|
|
|
|
from typing import Callable, Optional
|
|
|
|
from textual.app import ComposeResult
|
|
from textual.containers import Container, Horizontal, Vertical
|
|
from textual.screen import ModalScreen
|
|
from textual.widgets import Button, Input, Label, Static
|
|
|
|
|
|
class ConfirmDialog(ModalScreen[bool]):
|
|
"""A confirmation dialog with Yes/No buttons."""
|
|
|
|
DEFAULT_CSS = """
|
|
ConfirmDialog {
|
|
align: center middle;
|
|
}
|
|
|
|
ConfirmDialog > Container {
|
|
width: 60;
|
|
height: auto;
|
|
background: #2d2d2d;
|
|
border: solid #555555;
|
|
padding: 2;
|
|
}
|
|
|
|
ConfirmDialog Label {
|
|
width: 100%;
|
|
content-align: center middle;
|
|
margin-bottom: 2;
|
|
color: #cccccc;
|
|
}
|
|
|
|
ConfirmDialog Horizontal {
|
|
width: 100%;
|
|
height: auto;
|
|
align: center middle;
|
|
}
|
|
|
|
ConfirmDialog Button {
|
|
margin: 0 1;
|
|
}
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
message: str,
|
|
title: str = "Confirm",
|
|
yes_label: str = "Yes",
|
|
no_label: str = "No",
|
|
):
|
|
super().__init__()
|
|
self.message = message
|
|
self.title = title
|
|
self.yes_label = yes_label
|
|
self.no_label = no_label
|
|
|
|
def compose(self) -> ComposeResult:
|
|
"""Compose the dialog."""
|
|
with Container():
|
|
yield Static(f"[bold]{self.title}[/]", classes="dialog-title")
|
|
yield Label(self.message)
|
|
with Horizontal():
|
|
yield Button(self.yes_label, id="yes", variant="success")
|
|
yield Button(self.no_label, id="no", variant="error")
|
|
|
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
"""Handle button press."""
|
|
if event.button.id == "yes":
|
|
self.dismiss(True)
|
|
else:
|
|
self.dismiss(False)
|
|
|
|
def on_key(self, event) -> None:
|
|
"""Handle keyboard shortcuts."""
|
|
if event.key == "escape":
|
|
self.dismiss(False)
|
|
elif event.key == "enter":
|
|
self.dismiss(True)
|
|
|
|
|
|
class InputDialog(ModalScreen[Optional[str]]):
|
|
"""An input dialog for text entry."""
|
|
|
|
DEFAULT_CSS = """
|
|
InputDialog {
|
|
align: center middle;
|
|
}
|
|
|
|
InputDialog > Container {
|
|
width: 70;
|
|
height: auto;
|
|
background: #2d2d2d;
|
|
border: solid #555555;
|
|
padding: 2;
|
|
}
|
|
|
|
InputDialog Label {
|
|
width: 100%;
|
|
margin-bottom: 1;
|
|
color: #cccccc;
|
|
}
|
|
|
|
InputDialog Input {
|
|
width: 100%;
|
|
margin-bottom: 2;
|
|
background: #3a3a3a;
|
|
border: solid #555555;
|
|
}
|
|
|
|
InputDialog Input:focus {
|
|
border: solid #888888;
|
|
}
|
|
|
|
InputDialog Horizontal {
|
|
width: 100%;
|
|
height: auto;
|
|
align: center middle;
|
|
}
|
|
|
|
InputDialog Button {
|
|
margin: 0 1;
|
|
}
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
message: str,
|
|
title: str = "Input",
|
|
default: str = "",
|
|
placeholder: str = "",
|
|
):
|
|
super().__init__()
|
|
self.message = message
|
|
self.title = title
|
|
self.default = default
|
|
self.placeholder = placeholder
|
|
|
|
def compose(self) -> ComposeResult:
|
|
"""Compose the dialog."""
|
|
with Container():
|
|
yield Static(f"[bold]{self.title}[/]", classes="dialog-title")
|
|
yield Label(self.message)
|
|
yield Input(
|
|
value=self.default,
|
|
placeholder=self.placeholder,
|
|
id="input-field"
|
|
)
|
|
with Horizontal():
|
|
yield Button("OK", id="ok", variant="primary")
|
|
yield Button("Cancel", id="cancel")
|
|
|
|
def on_mount(self) -> None:
|
|
"""Focus the input field when mounted."""
|
|
input_field = self.query_one("#input-field", Input)
|
|
input_field.focus()
|
|
|
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
"""Handle button press."""
|
|
if event.button.id == "ok":
|
|
input_field = self.query_one("#input-field", Input)
|
|
self.dismiss(input_field.value)
|
|
else:
|
|
self.dismiss(None)
|
|
|
|
def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
"""Handle Enter key in input field."""
|
|
self.dismiss(event.value)
|
|
|
|
def on_key(self, event) -> None:
|
|
"""Handle keyboard shortcuts."""
|
|
if event.key == "escape":
|
|
self.dismiss(None)
|
|
|
|
|
|
class AlertDialog(ModalScreen[None]):
|
|
"""A simple alert/message dialog."""
|
|
|
|
DEFAULT_CSS = """
|
|
AlertDialog {
|
|
align: center middle;
|
|
}
|
|
|
|
AlertDialog > Container {
|
|
width: 60;
|
|
height: auto;
|
|
background: #2d2d2d;
|
|
border: solid #555555;
|
|
padding: 2;
|
|
}
|
|
|
|
AlertDialog Label {
|
|
width: 100%;
|
|
content-align: center middle;
|
|
margin-bottom: 2;
|
|
color: #cccccc;
|
|
}
|
|
|
|
AlertDialog Horizontal {
|
|
width: 100%;
|
|
height: auto;
|
|
align: center middle;
|
|
}
|
|
"""
|
|
|
|
def __init__(self, message: str, title: str = "Alert", variant: str = "default"):
|
|
super().__init__()
|
|
self.message = message
|
|
self.title = title
|
|
self.variant = variant
|
|
|
|
def compose(self) -> ComposeResult:
|
|
"""Compose the dialog."""
|
|
# Choose color based on variant (using design system)
|
|
color = "$primary"
|
|
if self.variant == "error":
|
|
color = "$error"
|
|
elif self.variant == "success":
|
|
color = "$success"
|
|
elif self.variant == "warning":
|
|
color = "$warning"
|
|
|
|
with Container():
|
|
yield Static(f"[bold {color}]{self.title}[/]", classes="dialog-title")
|
|
yield Label(self.message)
|
|
with Horizontal():
|
|
yield Button("OK", id="ok", variant="primary")
|
|
|
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
"""Handle button press."""
|
|
self.dismiss()
|
|
|
|
def on_key(self, event) -> None:
|
|
"""Handle keyboard shortcuts."""
|
|
if event.key in ("escape", "enter"):
|
|
self.dismiss()
|