Files
gh-aperepel-textual-tui-skill/skill/assets/todo_app.py
2025-11-29 17:58:08 +08:00

237 lines
6.7 KiB
Python

"""
Todo List Application - Complete Textual TUI Example
A fully functional todo list app demonstrating:
- Input handling and forms
- List view and selection
- State management with reactive attributes
- Custom styling
- Keyboard shortcuts
"""
from textual.app import App, ComposeResult
from textual.containers import Container, Horizontal, Vertical
from textual.widgets import Header, Footer, Input, Button, ListView, ListItem, Label, Static
from textual.binding import Binding
from textual.reactive import reactive
class TodoItem(ListItem):
"""A todo list item with completion status."""
def __init__(self, text: str, completed: bool = False) -> None:
super().__init__()
self.text = text
self.completed = completed
self.update_display()
def update_display(self) -> None:
"""Update the display based on completion status."""
if self.completed:
self.add_class("completed")
prefix = ""
else:
self.remove_class("completed")
prefix = ""
# Clear and re-render
self._nodes.clear()
self._nodes.append(Label(f"{prefix} {self.text}"))
def toggle(self) -> None:
"""Toggle completion status."""
self.completed = not self.completed
self.update_display()
class TodoApp(App):
"""A todo list application."""
CSS = """
Screen {
background: $background;
layout: vertical;
}
Header {
background: $primary;
}
#main-container {
height: 1fr;
padding: 1 2;
}
#input-container {
height: auto;
margin-bottom: 1;
}
#todo-input {
width: 1fr;
margin-right: 1;
border: solid $accent;
}
#todo-input:focus {
border: solid $success;
}
#add-button {
min-width: 12;
}
#todo-list {
border: solid $primary;
height: 1fr;
background: $surface;
}
#stats {
height: 3;
margin-top: 1;
border: solid $accent;
padding: 0 2;
background: $panel;
}
TodoItem {
padding: 0 1;
}
TodoItem:hover {
background: $boost;
}
TodoItem.completed {
color: $text-muted;
text-style: strike;
}
.stat {
margin: 0 2;
}
"""
BINDINGS = [
Binding("ctrl+n", "new_todo", "New Todo"),
Binding("ctrl+d", "delete_todo", "Delete"),
Binding("space", "toggle_todo", "Toggle"),
Binding("ctrl+c", "clear_completed", "Clear Completed"),
("q", "quit", "Quit"),
]
total_count = reactive(0)
completed_count = reactive(0)
def compose(self) -> ComposeResult:
"""Compose the UI."""
yield Header(show_clock=True)
with Container(id="main-container"):
with Horizontal(id="input-container"):
yield Input(
placeholder="What needs to be done?",
id="todo-input"
)
yield Button("Add", variant="primary", id="add-button")
yield ListView(id="todo-list")
with Horizontal(id="stats"):
yield Static("", id="total-stat", classes="stat")
yield Static("", id="completed-stat", classes="stat")
yield Static("", id="remaining-stat", classes="stat")
yield Footer()
def on_mount(self) -> None:
"""Initialize the app."""
self.query_one("#todo-input").focus()
self.update_stats()
# Add some example todos
self.add_todo_item("Learn Textual", False)
self.add_todo_item("Build amazing TUI apps", False)
self.add_todo_item("Share with the world", False)
def on_button_pressed(self, event: Button.Pressed) -> None:
"""Handle button press."""
if event.button.id == "add-button":
self.add_todo_from_input()
def on_input_submitted(self, event: Input.Submitted) -> None:
"""Handle input submission."""
if event.input.id == "todo-input":
self.add_todo_from_input()
def on_list_view_selected(self, event: ListView.Selected) -> None:
"""Handle todo item selection."""
if isinstance(event.item, TodoItem):
event.item.toggle()
self.update_stats()
def add_todo_from_input(self) -> None:
"""Add a todo from the input field."""
todo_input = self.query_one("#todo-input", Input)
text = todo_input.value.strip()
if text:
self.add_todo_item(text)
todo_input.value = ""
todo_input.focus()
def add_todo_item(self, text: str, completed: bool = False) -> None:
"""Add a todo item to the list."""
list_view = self.query_one("#todo-list", ListView)
todo = TodoItem(text, completed)
list_view.append(todo)
self.update_stats()
def action_new_todo(self) -> None:
"""Focus the input for a new todo."""
self.query_one("#todo-input").focus()
def action_delete_todo(self) -> None:
"""Delete the selected todo."""
list_view = self.query_one("#todo-list", ListView)
if list_view.highlighted_child:
list_view.highlighted_child.remove()
self.update_stats()
def action_toggle_todo(self) -> None:
"""Toggle the selected todo."""
list_view = self.query_one("#todo-list", ListView)
if isinstance(list_view.highlighted_child, TodoItem):
list_view.highlighted_child.toggle()
self.update_stats()
def action_clear_completed(self) -> None:
"""Clear all completed todos."""
list_view = self.query_one("#todo-list", ListView)
for item in list_view.query(TodoItem):
if item.completed:
item.remove()
self.update_stats()
def update_stats(self) -> None:
"""Update the statistics display."""
list_view = self.query_one("#todo-list", ListView)
todos = list(list_view.query(TodoItem))
total = len(todos)
completed = sum(1 for t in todos if t.completed)
remaining = total - completed
self.total_count = total
self.completed_count = completed
self.query_one("#total-stat", Static).update(f"Total: {total}")
self.query_one("#completed-stat", Static).update(f"Completed: {completed}")
self.query_one("#remaining-stat", Static).update(f"Remaining: {remaining}")
if __name__ == "__main__":
app = TodoApp()
app.run()