Initial commit
This commit is contained in:
165
skill/assets/README.md
Normal file
165
skill/assets/README.md
Normal file
@@ -0,0 +1,165 @@
|
||||
# Example Textual Applications
|
||||
|
||||
Complete, working example applications demonstrating various Textual patterns and features.
|
||||
|
||||
## Running the Examples
|
||||
|
||||
Each example is a standalone Python file. To run:
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
pip install textual textual-dev psutil
|
||||
|
||||
# Run any example
|
||||
python todo_app.py
|
||||
python dashboard_app.py
|
||||
python data_viewer.py
|
||||
python worker_demo.py
|
||||
|
||||
# Or with hot reload during development
|
||||
textual run --dev todo_app.py
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### todo_app.py - Todo List Application
|
||||
|
||||
A fully functional todo list demonstrating:
|
||||
- Input handling and form validation
|
||||
- List view with custom list items
|
||||
- State management with reactive attributes
|
||||
- Keyboard shortcuts and bindings
|
||||
- Custom styling and theming
|
||||
- Toggle states and visual feedback
|
||||
|
||||
**Key Features:**
|
||||
- Add/delete/toggle todos
|
||||
- Mark items as complete
|
||||
- Statistics tracking
|
||||
- Keyboard shortcuts (Ctrl+N, Space, Ctrl+D, etc.)
|
||||
|
||||
**Patterns Demonstrated:**
|
||||
- Custom widget creation (TodoItem)
|
||||
- Reactive state updates
|
||||
- Event handling (button press, input submit)
|
||||
- Keyboard bindings
|
||||
- CSS styling with pseudo-classes
|
||||
|
||||
### dashboard_app.py - System Monitor Dashboard
|
||||
|
||||
A real-time system monitoring dashboard demonstrating:
|
||||
- Grid layouts for dashboard design
|
||||
- Reactive data updates
|
||||
- Custom composite widgets
|
||||
- Data visualization with Sparkline and ProgressBar
|
||||
- Real-time monitoring with intervals
|
||||
- Metric cards and charts
|
||||
|
||||
**Key Features:**
|
||||
- Live CPU and memory monitoring
|
||||
- Historical CPU usage chart
|
||||
- System metrics (cores, frequency, processes, uptime)
|
||||
- Auto-refresh every second
|
||||
- Manual refresh with 'R' key
|
||||
|
||||
**Patterns Demonstrated:**
|
||||
- Grid-based layouts
|
||||
- Custom widget composition (MetricCard, CPUChart, MemoryChart)
|
||||
- Timed updates with `set_interval()`
|
||||
- Reactive attributes for live updates
|
||||
- Data visualization components
|
||||
|
||||
### data_viewer.py - JSON/CSV Data Viewer
|
||||
|
||||
A file browser and data viewer demonstrating:
|
||||
- File system navigation with DirectoryTree
|
||||
- Multiple view modes (Table, Tree, Info) with tabs
|
||||
- DataTable for tabular data
|
||||
- Tree widget for hierarchical data
|
||||
- Modal dialogs for errors
|
||||
- Search and filter functionality
|
||||
|
||||
**Key Features:**
|
||||
- Browse and select files from file system
|
||||
- Load and display JSON and CSV files
|
||||
- View data as table or tree structure
|
||||
- File information panel
|
||||
- Search functionality
|
||||
- Error handling with modal dialogs
|
||||
|
||||
**Patterns Demonstrated:**
|
||||
- Horizontal split layout (sidebar + content)
|
||||
- Tabbed content interface
|
||||
- Modal screen dialogs
|
||||
- File I/O and data parsing
|
||||
- Dynamic data loading
|
||||
- Event handling across multiple widgets
|
||||
|
||||
### worker_demo.py - Background Task Processing
|
||||
|
||||
A comprehensive worker pattern demonstration with two apps:
|
||||
|
||||
**FileProcessor App:**
|
||||
- Single long-running worker with progress updates
|
||||
- Worker cancellation
|
||||
- Real-time statistics (speed, time elapsed)
|
||||
- Progress bar updates from worker
|
||||
- Error handling and state management
|
||||
|
||||
**MultiWorkerDemo App:**
|
||||
- Multiple concurrent workers
|
||||
- Group-based worker management
|
||||
- Batch cancellation
|
||||
- Independent progress tracking per worker
|
||||
|
||||
**Key Features:**
|
||||
- Simulated file processing with 100 files
|
||||
- Real-time progress updates
|
||||
- Worker state change monitoring
|
||||
- Cancellation support
|
||||
- Statistics tracking (files/sec, elapsed time)
|
||||
|
||||
**Patterns Demonstrated:**
|
||||
- `run_worker()` for background tasks
|
||||
- Worker lifecycle management
|
||||
- Thread-safe UI updates
|
||||
- Progress reporting from workers
|
||||
- Worker cancellation and cleanup
|
||||
- Multiple concurrent workers
|
||||
- Worker groups and batch operations
|
||||
- Error handling in workers
|
||||
|
||||
## Common Patterns
|
||||
|
||||
All examples demonstrate these fundamental patterns:
|
||||
- Proper app structure with `compose()` method
|
||||
- Header and Footer usage
|
||||
- Keyboard bindings
|
||||
- CSS styling with Textual's CSS system
|
||||
- Event handling with `on_*` methods
|
||||
- Reactive state management
|
||||
- Widget querying with `query_one()` and `query()`
|
||||
|
||||
## Learning Path
|
||||
|
||||
1. **Start with todo_app.py** - Learn basic input, lists, and state management
|
||||
2. **Move to dashboard_app.py** - Understand layouts, custom widgets, and real-time updates
|
||||
3. **Try worker_demo.py** - Master background tasks and worker patterns (IMPORTANT!)
|
||||
4. **Explore data_viewer.py** - Master complex layouts, tabs, and data handling
|
||||
|
||||
## Extending the Examples
|
||||
|
||||
Feel free to modify these examples:
|
||||
- Add persistence to todo_app (save/load from file)
|
||||
- Add network monitoring to dashboard_app
|
||||
- Add export functionality to data_viewer
|
||||
- Add real file processing to worker_demo (use actual files)
|
||||
- Customize the styling and themes
|
||||
- Add new features and widgets
|
||||
|
||||
## Resources
|
||||
|
||||
- Textual Documentation: https://textual.textualize.io/
|
||||
- Widget Gallery: See references/widgets.md
|
||||
- Layout Patterns: See references/layouts.md
|
||||
- Styling Guide: See references/styling.md
|
||||
256
skill/assets/dashboard_app.py
Normal file
256
skill/assets/dashboard_app.py
Normal file
@@ -0,0 +1,256 @@
|
||||
"""
|
||||
System Monitor Dashboard - Textual TUI Example
|
||||
|
||||
Demonstrates:
|
||||
- Grid layouts for dashboard design
|
||||
- Reactive data updates
|
||||
- Custom widgets
|
||||
- Data visualization with Sparkline
|
||||
- Real-time monitoring
|
||||
- Worker threads for background tasks
|
||||
"""
|
||||
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.containers import Container, Grid
|
||||
from textual.widgets import Header, Footer, Static, Sparkline, ProgressBar
|
||||
from textual.reactive import reactive
|
||||
from textual.worker import Worker
|
||||
import psutil
|
||||
import time
|
||||
from collections import deque
|
||||
|
||||
|
||||
class MetricCard(Static):
|
||||
"""A card displaying a metric with a label and value."""
|
||||
|
||||
DEFAULT_CSS = """
|
||||
MetricCard {
|
||||
border: solid $primary;
|
||||
padding: 1;
|
||||
height: 7;
|
||||
background: $surface;
|
||||
}
|
||||
|
||||
MetricCard .label {
|
||||
text-style: bold;
|
||||
color: $accent;
|
||||
}
|
||||
|
||||
MetricCard .value {
|
||||
text-align: center;
|
||||
text-style: bold;
|
||||
color: $success;
|
||||
margin-top: 1;
|
||||
}
|
||||
"""
|
||||
|
||||
value = reactive("")
|
||||
label = reactive("")
|
||||
|
||||
def __init__(self, label: str, initial_value: str = "0") -> None:
|
||||
super().__init__()
|
||||
self.label = label
|
||||
self.value = initial_value
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Static(self.label, classes="label")
|
||||
yield Static(self.value, classes="value", id=f"value-{id(self)}")
|
||||
|
||||
def watch_value(self, new_value: str) -> None:
|
||||
"""Update the value display when it changes."""
|
||||
value_widget = self.query_one(f"#value-{id(self)}", Static)
|
||||
value_widget.update(new_value)
|
||||
|
||||
|
||||
class CPUChart(Static):
|
||||
"""A widget showing CPU usage over time."""
|
||||
|
||||
DEFAULT_CSS = """
|
||||
CPUChart {
|
||||
border: solid $primary;
|
||||
padding: 1;
|
||||
height: 12;
|
||||
background: $surface;
|
||||
}
|
||||
|
||||
CPUChart .title {
|
||||
text-style: bold;
|
||||
color: $accent;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.history = deque([0] * 50, maxlen=50)
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Static("CPU Usage History", classes="title")
|
||||
yield Sparkline(list(self.history), id="cpu-sparkline")
|
||||
yield ProgressBar(total=100, show_percentage=True, id="cpu-progress")
|
||||
|
||||
def update_cpu(self, value: float) -> None:
|
||||
"""Update CPU usage display."""
|
||||
self.history.append(value)
|
||||
|
||||
sparkline = self.query_one("#cpu-sparkline", Sparkline)
|
||||
sparkline.data = list(self.history)
|
||||
|
||||
progress = self.query_one("#cpu-progress", ProgressBar)
|
||||
progress.update(progress=value)
|
||||
|
||||
|
||||
class MemoryChart(Static):
|
||||
"""A widget showing memory usage."""
|
||||
|
||||
DEFAULT_CSS = """
|
||||
MemoryChart {
|
||||
border: solid $primary;
|
||||
padding: 1;
|
||||
height: 12;
|
||||
background: $surface;
|
||||
}
|
||||
|
||||
MemoryChart .title {
|
||||
text-style: bold;
|
||||
color: $accent;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
|
||||
MemoryChart .detail {
|
||||
margin-top: 1;
|
||||
color: $text-muted;
|
||||
}
|
||||
"""
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Static("Memory Usage", classes="title")
|
||||
yield ProgressBar(total=100, show_percentage=True, id="mem-progress")
|
||||
yield Static("", classes="detail", id="mem-detail")
|
||||
|
||||
def update_memory(self, percent: float, used: float, total: float) -> None:
|
||||
"""Update memory usage display."""
|
||||
progress = self.query_one("#mem-progress", ProgressBar)
|
||||
progress.update(progress=percent)
|
||||
|
||||
detail = self.query_one("#mem-detail", Static)
|
||||
detail.update(f"Used: {used:.1f} GB / Total: {total:.1f} GB")
|
||||
|
||||
|
||||
class SystemMonitor(App):
|
||||
"""A system monitoring dashboard."""
|
||||
|
||||
CSS = """
|
||||
Screen {
|
||||
background: $background;
|
||||
}
|
||||
|
||||
Header {
|
||||
background: $primary;
|
||||
}
|
||||
|
||||
#dashboard {
|
||||
padding: 1 2;
|
||||
height: 1fr;
|
||||
}
|
||||
|
||||
#metrics-grid {
|
||||
grid-size: 4;
|
||||
grid-gutter: 1 2;
|
||||
height: auto;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
|
||||
#charts-grid {
|
||||
grid-size: 2;
|
||||
grid-gutter: 1 2;
|
||||
height: 1fr;
|
||||
}
|
||||
|
||||
#status {
|
||||
dock: bottom;
|
||||
height: 1;
|
||||
background: $panel;
|
||||
padding: 0 2;
|
||||
color: $text-muted;
|
||||
}
|
||||
"""
|
||||
|
||||
BINDINGS = [
|
||||
("r", "refresh", "Refresh"),
|
||||
("q", "quit", "Quit"),
|
||||
]
|
||||
|
||||
update_count = reactive(0)
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
"""Compose the dashboard UI."""
|
||||
yield Header(show_clock=True)
|
||||
|
||||
with Container(id="dashboard"):
|
||||
with Grid(id="metrics-grid"):
|
||||
yield MetricCard("CPU Cores", f"{psutil.cpu_count()}")
|
||||
yield MetricCard("CPU Freq", "0 MHz")
|
||||
yield MetricCard("Processes", "0")
|
||||
yield MetricCard("Uptime", "0h 0m")
|
||||
|
||||
with Grid(id="charts-grid"):
|
||||
yield CPUChart()
|
||||
yield MemoryChart()
|
||||
|
||||
yield Static("", id="status")
|
||||
yield Footer()
|
||||
|
||||
def on_mount(self) -> None:
|
||||
"""Start monitoring when app starts."""
|
||||
self.update_system_info()
|
||||
self.set_interval(1.0, self.update_system_info)
|
||||
|
||||
def update_system_info(self) -> None:
|
||||
"""Update all system information."""
|
||||
self.update_count += 1
|
||||
|
||||
# Update metrics
|
||||
cpu_freq = psutil.cpu_freq()
|
||||
if cpu_freq:
|
||||
freq_card = self.query("MetricCard")[1]
|
||||
freq_card.value = f"{cpu_freq.current:.0f} MHz"
|
||||
|
||||
process_count = len(psutil.pids())
|
||||
proc_card = self.query("MetricCard")[2]
|
||||
proc_card.value = f"{process_count}"
|
||||
|
||||
boot_time = psutil.boot_time()
|
||||
uptime_seconds = time.time() - boot_time
|
||||
hours = int(uptime_seconds // 3600)
|
||||
minutes = int((uptime_seconds % 3600) // 60)
|
||||
uptime_card = self.query("MetricCard")[3]
|
||||
uptime_card.value = f"{hours}h {minutes}m"
|
||||
|
||||
# Update CPU chart
|
||||
cpu_percent = psutil.cpu_percent(interval=0.1)
|
||||
cpu_chart = self.query_one(CPUChart)
|
||||
cpu_chart.update_cpu(cpu_percent)
|
||||
|
||||
# Update memory chart
|
||||
mem = psutil.virtual_memory()
|
||||
mem_chart = self.query_one(MemoryChart)
|
||||
mem_chart.update_memory(
|
||||
mem.percent,
|
||||
mem.used / (1024**3),
|
||||
mem.total / (1024**3)
|
||||
)
|
||||
|
||||
# Update status
|
||||
status = self.query_one("#status", Static)
|
||||
status.update(f"Last updated: {time.strftime('%H:%M:%S')} | Updates: {self.update_count}")
|
||||
|
||||
def action_refresh(self) -> None:
|
||||
"""Manually refresh the data."""
|
||||
self.update_system_info()
|
||||
self.notify("Data refreshed!", severity="information")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = SystemMonitor()
|
||||
app.run()
|
||||
304
skill/assets/data_viewer.py
Normal file
304
skill/assets/data_viewer.py
Normal file
@@ -0,0 +1,304 @@
|
||||
"""
|
||||
Data Viewer Application - Textual TUI Example
|
||||
|
||||
Demonstrates:
|
||||
- File browser and selection
|
||||
- DataTable for displaying tabular data
|
||||
- Tree for displaying hierarchical data
|
||||
- Modal screens for dialogs
|
||||
- File loading and parsing
|
||||
- Search and filter functionality
|
||||
"""
|
||||
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.containers import Container, Horizontal, Vertical
|
||||
from textual.widgets import (
|
||||
Header, Footer, DirectoryTree, DataTable, Tree, Static,
|
||||
Button, Input, TabbedContent, TabPane
|
||||
)
|
||||
from textual.screen import ModalScreen
|
||||
from textual.binding import Binding
|
||||
import json
|
||||
import csv
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class ErrorDialog(ModalScreen[bool]):
|
||||
"""Modal dialog for displaying errors."""
|
||||
|
||||
DEFAULT_CSS = """
|
||||
ErrorDialog {
|
||||
align: center middle;
|
||||
}
|
||||
|
||||
#error-dialog {
|
||||
width: 60;
|
||||
height: 11;
|
||||
border: thick $error;
|
||||
background: $surface;
|
||||
padding: 1 2;
|
||||
}
|
||||
|
||||
#error-title {
|
||||
text-style: bold;
|
||||
color: $error;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
|
||||
#error-message {
|
||||
margin-bottom: 1;
|
||||
}
|
||||
"""
|
||||
|
||||
def __init__(self, title: str, message: str) -> None:
|
||||
super().__init__()
|
||||
self.title = title
|
||||
self.message = message
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Container(id="error-dialog"):
|
||||
yield Static(self.title, id="error-title")
|
||||
yield Static(self.message, id="error-message")
|
||||
yield Button("OK", variant="error", id="ok")
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
self.dismiss(True)
|
||||
|
||||
|
||||
class DataViewer(App):
|
||||
"""A data viewer for JSON and CSV files."""
|
||||
|
||||
CSS = """
|
||||
Screen {
|
||||
background: $background;
|
||||
layout: horizontal;
|
||||
}
|
||||
|
||||
Header {
|
||||
background: $primary;
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
width: 35;
|
||||
border-right: solid $accent;
|
||||
background: $panel;
|
||||
}
|
||||
|
||||
#file-tree {
|
||||
height: 1fr;
|
||||
}
|
||||
|
||||
#main-content {
|
||||
width: 1fr;
|
||||
padding: 1 2;
|
||||
}
|
||||
|
||||
#toolbar {
|
||||
height: auto;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
|
||||
#search-input {
|
||||
width: 1fr;
|
||||
margin-right: 1;
|
||||
}
|
||||
|
||||
#content-tabs {
|
||||
height: 1fr;
|
||||
}
|
||||
|
||||
DataTable {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
Tree {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#info-panel {
|
||||
border: solid $primary;
|
||||
padding: 1;
|
||||
background: $surface;
|
||||
}
|
||||
"""
|
||||
|
||||
BINDINGS = [
|
||||
Binding("ctrl+o", "open_file", "Open File"),
|
||||
Binding("ctrl+f", "focus_search", "Search"),
|
||||
("q", "quit", "Quit"),
|
||||
]
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
"""Compose the UI."""
|
||||
yield Header(show_clock=True)
|
||||
|
||||
with Container(id="sidebar"):
|
||||
yield Static("📁 File Browser", id="sidebar-title")
|
||||
yield DirectoryTree(str(Path.home()), id="file-tree")
|
||||
|
||||
with Vertical(id="main-content"):
|
||||
with Horizontal(id="toolbar"):
|
||||
yield Input(placeholder="Search...", id="search-input")
|
||||
yield Button("Clear", id="clear-button")
|
||||
|
||||
with TabbedContent(id="content-tabs"):
|
||||
with TabPane("Table View", id="table-tab"):
|
||||
yield DataTable(id="data-table", cursor_type="row")
|
||||
|
||||
with TabPane("Tree View", id="tree-tab"):
|
||||
yield Tree("Data", id="data-tree")
|
||||
|
||||
with TabPane("Info", id="info-tab"):
|
||||
yield Static("No file loaded", id="info-panel")
|
||||
|
||||
yield Footer()
|
||||
|
||||
def on_mount(self) -> None:
|
||||
"""Initialize the app."""
|
||||
table = self.query_one("#data-table", DataTable)
|
||||
table.show_header = True
|
||||
table.zebra_stripes = True
|
||||
|
||||
def on_directory_tree_file_selected(self, event: DirectoryTree.FileSelected) -> None:
|
||||
"""Handle file selection from the tree."""
|
||||
file_path = event.path
|
||||
if file_path.suffix.lower() in ['.json', '.csv']:
|
||||
self.load_file(file_path)
|
||||
else:
|
||||
self.notify("Only JSON and CSV files are supported", severity="warning")
|
||||
|
||||
def load_file(self, file_path: Path) -> None:
|
||||
"""Load and display a data file."""
|
||||
try:
|
||||
if file_path.suffix.lower() == '.json':
|
||||
self.load_json(file_path)
|
||||
elif file_path.suffix.lower() == '.csv':
|
||||
self.load_csv(file_path)
|
||||
|
||||
self.notify(f"Loaded: {file_path.name}", severity="information")
|
||||
|
||||
except Exception as e:
|
||||
self.push_screen(
|
||||
ErrorDialog("Error Loading File", f"Failed to load {file_path.name}:\n{str(e)}")
|
||||
)
|
||||
|
||||
def load_json(self, file_path: Path) -> None:
|
||||
"""Load a JSON file."""
|
||||
with open(file_path, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
# Update info panel
|
||||
info = self.query_one("#info-panel", Static)
|
||||
info.update(f"File: {file_path.name}\nType: JSON\nSize: {file_path.stat().st_size} bytes")
|
||||
|
||||
# Update tree view
|
||||
tree = self.query_one("#data-tree", Tree)
|
||||
tree.clear()
|
||||
tree.root.label = file_path.name
|
||||
self._build_json_tree(tree.root, data)
|
||||
|
||||
# Update table view (if data is a list of dicts)
|
||||
if isinstance(data, list) and data and isinstance(data[0], dict):
|
||||
self._populate_table_from_list(data)
|
||||
else:
|
||||
table = self.query_one("#data-table", DataTable)
|
||||
table.clear(columns=True)
|
||||
self.notify("JSON structure not suitable for table view", severity="warning")
|
||||
|
||||
def load_csv(self, file_path: Path) -> None:
|
||||
"""Load a CSV file."""
|
||||
with open(file_path, 'r') as f:
|
||||
reader = csv.DictReader(f)
|
||||
data = list(reader)
|
||||
|
||||
# Update info panel
|
||||
info = self.query_one("#info-panel", Static)
|
||||
info.update(
|
||||
f"File: {file_path.name}\n"
|
||||
f"Type: CSV\n"
|
||||
f"Rows: {len(data)}\n"
|
||||
f"Columns: {len(data[0]) if data else 0}"
|
||||
)
|
||||
|
||||
# Update table view
|
||||
self._populate_table_from_list(data)
|
||||
|
||||
# Update tree view
|
||||
tree = self.query_one("#data-tree", Tree)
|
||||
tree.clear()
|
||||
tree.root.label = file_path.name
|
||||
for i, row in enumerate(data[:100]): # Limit to 100 for performance
|
||||
row_node = tree.root.add(f"Row {i+1}")
|
||||
for key, value in row.items():
|
||||
row_node.add_leaf(f"{key}: {value}")
|
||||
|
||||
def _populate_table_from_list(self, data: list) -> None:
|
||||
"""Populate the DataTable from a list of dicts."""
|
||||
if not data:
|
||||
return
|
||||
|
||||
table = self.query_one("#data-table", DataTable)
|
||||
table.clear(columns=True)
|
||||
|
||||
# Add columns
|
||||
columns = list(data[0].keys())
|
||||
table.add_columns(*columns)
|
||||
|
||||
# Add rows
|
||||
for row in data:
|
||||
table.add_row(*[str(row.get(col, "")) for col in columns])
|
||||
|
||||
def _build_json_tree(self, node, data, max_depth: int = 10, depth: int = 0) -> None:
|
||||
"""Recursively build a tree from JSON data."""
|
||||
if depth >= max_depth:
|
||||
node.add_leaf("... (max depth reached)")
|
||||
return
|
||||
|
||||
if isinstance(data, dict):
|
||||
for key, value in data.items():
|
||||
if isinstance(value, (dict, list)):
|
||||
child = node.add(f"📂 {key}")
|
||||
self._build_json_tree(child, value, max_depth, depth + 1)
|
||||
else:
|
||||
node.add_leaf(f"{key}: {value}")
|
||||
|
||||
elif isinstance(data, list):
|
||||
for i, item in enumerate(data[:50]): # Limit to 50 items for performance
|
||||
if isinstance(item, (dict, list)):
|
||||
child = node.add(f"[{i}]")
|
||||
self._build_json_tree(child, item, max_depth, depth + 1)
|
||||
else:
|
||||
node.add_leaf(f"[{i}]: {item}")
|
||||
|
||||
def on_input_changed(self, event: Input.Changed) -> None:
|
||||
"""Handle search input changes."""
|
||||
if event.input.id == "search-input":
|
||||
search_term = event.value.lower()
|
||||
if search_term:
|
||||
self.filter_table(search_term)
|
||||
|
||||
def filter_table(self, search_term: str) -> None:
|
||||
"""Filter table rows based on search term."""
|
||||
table = self.query_one("#data-table", DataTable)
|
||||
# Note: This is a simple example. Real implementation would need
|
||||
# to store original data and rebuild filtered rows
|
||||
pass
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
"""Handle button presses."""
|
||||
if event.button.id == "clear-button":
|
||||
search_input = self.query_one("#search-input", Input)
|
||||
search_input.value = ""
|
||||
|
||||
def action_focus_search(self) -> None:
|
||||
"""Focus the search input."""
|
||||
self.query_one("#search-input", Input).focus()
|
||||
|
||||
def action_open_file(self) -> None:
|
||||
"""Focus the file tree."""
|
||||
self.query_one("#file-tree", DirectoryTree).focus()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = DataViewer()
|
||||
app.run()
|
||||
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()
|
||||
406
skill/assets/worker_demo.py
Normal file
406
skill/assets/worker_demo.py
Normal file
@@ -0,0 +1,406 @@
|
||||
"""
|
||||
File Processor - Textual TUI Worker Example
|
||||
|
||||
Demonstrates:
|
||||
- Worker patterns for background tasks
|
||||
- Progress updates from workers
|
||||
- Worker cancellation
|
||||
- Error handling in workers
|
||||
- Multiple concurrent workers
|
||||
- Thread-safe UI updates
|
||||
"""
|
||||
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.containers import Container, Vertical, Horizontal
|
||||
from textual.widgets import Header, Footer, Button, ProgressBar, Log, Static, Label
|
||||
from textual.worker import Worker, WorkerState
|
||||
from pathlib import Path
|
||||
import asyncio
|
||||
import time
|
||||
import hashlib
|
||||
|
||||
|
||||
class FileProcessor(App):
|
||||
"""File processing app demonstrating worker patterns."""
|
||||
|
||||
CSS = """
|
||||
Screen {
|
||||
background: $background;
|
||||
}
|
||||
|
||||
Header {
|
||||
background: $primary;
|
||||
}
|
||||
|
||||
#main {
|
||||
padding: 1 2;
|
||||
height: 1fr;
|
||||
}
|
||||
|
||||
#controls {
|
||||
height: auto;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
|
||||
Button {
|
||||
margin: 0 1;
|
||||
}
|
||||
|
||||
#progress-container {
|
||||
border: solid $primary;
|
||||
padding: 1;
|
||||
margin-bottom: 1;
|
||||
height: auto;
|
||||
background: $surface;
|
||||
}
|
||||
|
||||
#progress-label {
|
||||
margin-bottom: 1;
|
||||
text-style: bold;
|
||||
color: $accent;
|
||||
}
|
||||
|
||||
ProgressBar {
|
||||
margin-bottom: 1;
|
||||
}
|
||||
|
||||
#stats {
|
||||
height: 5;
|
||||
border: solid $accent;
|
||||
padding: 1;
|
||||
margin-bottom: 1;
|
||||
background: $panel;
|
||||
}
|
||||
|
||||
.stat-line {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#log {
|
||||
border: solid $primary;
|
||||
height: 1fr;
|
||||
background: $surface;
|
||||
}
|
||||
"""
|
||||
|
||||
BINDINGS = [
|
||||
("s", "start", "Start"),
|
||||
("c", "cancel", "Cancel"),
|
||||
("r", "reset", "Reset"),
|
||||
("q", "quit", "Quit"),
|
||||
]
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.main_worker: Worker | None = None
|
||||
self.processed_count = 0
|
||||
self.total_files = 0
|
||||
self.start_time = 0.0
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
"""Compose the UI."""
|
||||
yield Header(show_clock=True)
|
||||
|
||||
with Container(id="main"):
|
||||
with Horizontal(id="controls"):
|
||||
yield Button("Start Processing", variant="success", id="start")
|
||||
yield Button("Cancel", variant="error", id="cancel", disabled=True)
|
||||
yield Button("Reset", id="reset")
|
||||
|
||||
with Container(id="progress-container"):
|
||||
yield Label("Progress", id="progress-label")
|
||||
yield ProgressBar(total=100, show_percentage=True, id="progress")
|
||||
|
||||
with Container(id="stats"):
|
||||
yield Static("Status: Idle", classes="stat-line", id="status")
|
||||
yield Static("Files Processed: 0 / 0", classes="stat-line", id="files")
|
||||
yield Static("Time Elapsed: 0s", classes="stat-line", id="time")
|
||||
yield Static("Speed: 0 files/sec", classes="stat-line", id="speed")
|
||||
|
||||
yield Log(id="log", auto_scroll=True)
|
||||
|
||||
yield Footer()
|
||||
|
||||
def on_mount(self) -> None:
|
||||
"""Initialize on mount."""
|
||||
log = self.query_one("#log", Log)
|
||||
log.write_line("[bold cyan]File Processor Started[/]")
|
||||
log.write_line("Click 'Start Processing' or press 'S' to begin")
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
"""Handle button presses."""
|
||||
if event.button.id == "start":
|
||||
self.action_start()
|
||||
elif event.button.id == "cancel":
|
||||
self.action_cancel()
|
||||
elif event.button.id == "reset":
|
||||
self.action_reset()
|
||||
|
||||
def action_start(self) -> None:
|
||||
"""Start file processing worker."""
|
||||
if self.main_worker and not self.main_worker.is_finished:
|
||||
self.log_message("[yellow]Processing already in progress[/]")
|
||||
return
|
||||
|
||||
# Disable start button, enable cancel
|
||||
self.query_one("#start", Button).disabled = True
|
||||
self.query_one("#cancel", Button).disabled = False
|
||||
|
||||
# Reset counters
|
||||
self.processed_count = 0
|
||||
self.total_files = 100 # Simulated file count
|
||||
self.start_time = time.time()
|
||||
|
||||
# Start worker
|
||||
self.log_message("[bold green]Starting file processing...[/]")
|
||||
self.main_worker = self.run_worker(
|
||||
self.process_files(),
|
||||
name="file_processor",
|
||||
group="processing"
|
||||
)
|
||||
|
||||
def action_cancel(self) -> None:
|
||||
"""Cancel the running worker."""
|
||||
if self.main_worker and not self.main_worker.is_finished:
|
||||
self.main_worker.cancel()
|
||||
self.log_message("[bold red]Cancelling worker...[/]")
|
||||
|
||||
def action_reset(self) -> None:
|
||||
"""Reset the UI."""
|
||||
if self.main_worker and not self.main_worker.is_finished:
|
||||
self.log_message("[yellow]Cannot reset while processing[/]")
|
||||
return
|
||||
|
||||
self.processed_count = 0
|
||||
self.total_files = 0
|
||||
|
||||
progress = self.query_one("#progress", ProgressBar)
|
||||
progress.update(progress=0)
|
||||
|
||||
self.update_stats()
|
||||
self.log_message("[cyan]Reset complete[/]")
|
||||
|
||||
async def process_files(self) -> None:
|
||||
"""
|
||||
Worker task: Process files in background.
|
||||
|
||||
Demonstrates:
|
||||
- Long-running async operations
|
||||
- Progress updates via call_from_thread
|
||||
- Cancellation checking
|
||||
- Error handling
|
||||
"""
|
||||
try:
|
||||
for i in range(self.total_files):
|
||||
# Check if cancelled
|
||||
if self.main_worker and self.main_worker.is_cancelled:
|
||||
self.log_message("[red]Processing cancelled![/]")
|
||||
self.finalize_processing(cancelled=True)
|
||||
return
|
||||
|
||||
# Simulate file processing (async I/O)
|
||||
await asyncio.sleep(0.05)
|
||||
|
||||
# Simulate some work
|
||||
filename = f"file_{i:04d}.txt"
|
||||
hash_val = hashlib.md5(filename.encode()).hexdigest()[:8]
|
||||
|
||||
# Update progress (thread-safe)
|
||||
self.processed_count = i + 1
|
||||
progress = (self.processed_count / self.total_files) * 100
|
||||
|
||||
# Update UI from worker
|
||||
self.update_progress(progress, filename, hash_val)
|
||||
|
||||
# Log every 10th file
|
||||
if (i + 1) % 10 == 0:
|
||||
self.log_message(
|
||||
f"[green]Processed {self.processed_count}/{self.total_files} files[/]"
|
||||
)
|
||||
|
||||
# Completed successfully
|
||||
self.log_message("[bold green]✓ All files processed successfully![/]")
|
||||
self.finalize_processing(cancelled=False)
|
||||
|
||||
except Exception as e:
|
||||
self.log_message(f"[bold red]Error: {e}[/]")
|
||||
self.finalize_processing(cancelled=False)
|
||||
raise
|
||||
|
||||
def update_progress(self, progress: float, filename: str, hash_val: str) -> None:
|
||||
"""Update progress bar and stats (called from worker)."""
|
||||
progress_bar = self.query_one("#progress", ProgressBar)
|
||||
progress_bar.update(progress=progress)
|
||||
|
||||
# Update stats
|
||||
self.update_stats()
|
||||
|
||||
# Update status
|
||||
status = self.query_one("#status", Static)
|
||||
status.update(f"Status: Processing {filename} [{hash_val}]")
|
||||
|
||||
def update_stats(self) -> None:
|
||||
"""Update statistics display."""
|
||||
files_stat = self.query_one("#files", Static)
|
||||
files_stat.update(f"Files Processed: {self.processed_count} / {self.total_files}")
|
||||
|
||||
elapsed = time.time() - self.start_time if self.start_time > 0 else 0
|
||||
time_stat = self.query_one("#time", Static)
|
||||
time_stat.update(f"Time Elapsed: {elapsed:.1f}s")
|
||||
|
||||
speed = self.processed_count / elapsed if elapsed > 0 else 0
|
||||
speed_stat = self.query_one("#speed", Static)
|
||||
speed_stat.update(f"Speed: {speed:.1f} files/sec")
|
||||
|
||||
def finalize_processing(self, cancelled: bool) -> None:
|
||||
"""Finalize after processing completes or is cancelled."""
|
||||
# Re-enable buttons
|
||||
self.query_one("#start", Button).disabled = False
|
||||
self.query_one("#cancel", Button).disabled = True
|
||||
|
||||
# Update status
|
||||
status = self.query_one("#status", Static)
|
||||
if cancelled:
|
||||
status.update("Status: Cancelled")
|
||||
else:
|
||||
status.update("Status: Complete")
|
||||
|
||||
def log_message(self, message: str) -> None:
|
||||
"""Thread-safe log message."""
|
||||
log = self.query_one("#log", Log)
|
||||
log.write_line(message)
|
||||
|
||||
def on_worker_state_changed(self, event: Worker.StateChanged) -> None:
|
||||
"""Handle worker state changes."""
|
||||
if event.worker.name != "file_processor":
|
||||
return
|
||||
|
||||
if event.state == WorkerState.PENDING:
|
||||
self.log_message("[cyan]Worker: Pending...[/]")
|
||||
elif event.state == WorkerState.RUNNING:
|
||||
self.log_message("[cyan]Worker: Running[/]")
|
||||
elif event.state == WorkerState.CANCELLED:
|
||||
self.log_message("[red]Worker: Cancelled[/]")
|
||||
elif event.state == WorkerState.ERROR:
|
||||
self.log_message(f"[bold red]Worker: Error - {event.worker.error}[/]")
|
||||
elif event.state == WorkerState.SUCCESS:
|
||||
self.log_message("[green]Worker: Completed successfully[/]")
|
||||
|
||||
|
||||
class MultiWorkerDemo(App):
|
||||
"""Demonstrates running multiple concurrent workers."""
|
||||
|
||||
CSS = """
|
||||
Screen {
|
||||
background: $background;
|
||||
}
|
||||
|
||||
#main {
|
||||
padding: 1 2;
|
||||
}
|
||||
|
||||
#controls {
|
||||
height: auto;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
|
||||
Button {
|
||||
margin: 0 1;
|
||||
}
|
||||
|
||||
.worker-card {
|
||||
border: solid $primary;
|
||||
padding: 1;
|
||||
margin-bottom: 1;
|
||||
background: $surface;
|
||||
}
|
||||
|
||||
.worker-card .title {
|
||||
text-style: bold;
|
||||
color: $accent;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
|
||||
#log {
|
||||
border: solid $primary;
|
||||
height: 20;
|
||||
}
|
||||
"""
|
||||
|
||||
BINDINGS = [
|
||||
("s", "start_all", "Start All"),
|
||||
("c", "cancel_all", "Cancel All"),
|
||||
("q", "quit", "Quit"),
|
||||
]
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header()
|
||||
|
||||
with Container(id="main"):
|
||||
with Horizontal(id="controls"):
|
||||
yield Button("Start All Workers", variant="success", id="start")
|
||||
yield Button("Cancel All", variant="error", id="cancel")
|
||||
|
||||
with Container(classes="worker-card"):
|
||||
yield Label("Worker 1: Data Fetch", classes="title")
|
||||
yield ProgressBar(total=100, id="worker1")
|
||||
|
||||
with Container(classes="worker-card"):
|
||||
yield Label("Worker 2: Data Process", classes="title")
|
||||
yield ProgressBar(total=100, id="worker2")
|
||||
|
||||
with Container(classes="worker-card"):
|
||||
yield Label("Worker 3: Data Export", classes="title")
|
||||
yield ProgressBar(total=100, id="worker3")
|
||||
|
||||
yield Log(id="log", auto_scroll=True)
|
||||
|
||||
yield Footer()
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
if event.button.id == "start":
|
||||
self.action_start_all()
|
||||
elif event.button.id == "cancel":
|
||||
self.action_cancel_all()
|
||||
|
||||
def action_start_all(self) -> None:
|
||||
"""Start all workers concurrently."""
|
||||
self.run_worker(self.worker_task(1, 3.0), name="worker1", group="tasks")
|
||||
self.run_worker(self.worker_task(2, 5.0), name="worker2", group="tasks")
|
||||
self.run_worker(self.worker_task(3, 4.0), name="worker3", group="tasks")
|
||||
|
||||
log = self.query_one("#log", Log)
|
||||
log.write_line("[bold cyan]Started all workers concurrently[/]")
|
||||
|
||||
def action_cancel_all(self) -> None:
|
||||
"""Cancel all workers in the group."""
|
||||
for worker in self.workers:
|
||||
if worker.group == "tasks":
|
||||
worker.cancel()
|
||||
|
||||
log = self.query_one("#log", Log)
|
||||
log.write_line("[bold red]Cancelled all workers[/]")
|
||||
|
||||
async def worker_task(self, worker_num: int, duration: float) -> None:
|
||||
"""Simulated worker task."""
|
||||
log = self.query_one("#log", Log)
|
||||
log.write_line(f"[cyan]Worker {worker_num} started[/]")
|
||||
|
||||
progress_bar = self.query_one(f"#worker{worker_num}", ProgressBar)
|
||||
steps = 100
|
||||
|
||||
for i in range(steps):
|
||||
await asyncio.sleep(duration / steps)
|
||||
progress_bar.update(progress=i + 1)
|
||||
|
||||
log.write_line(f"[green]Worker {worker_num} completed![/]")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Run the single worker demo
|
||||
app = FileProcessor()
|
||||
app.run()
|
||||
|
||||
# Or run the multi-worker demo
|
||||
# app = MultiWorkerDemo()
|
||||
# app.run()
|
||||
Reference in New Issue
Block a user