Initial commit
This commit is contained in:
236
skill/assets/todo_app.py
Normal file
236
skill/assets/todo_app.py
Normal file
@@ -0,0 +1,236 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user