tasktree-manager 1.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. tasktree_manager-1.0.1/.claude/skills/textual-tui/SKILL.md +703 -0
  2. tasktree_manager-1.0.1/.claude/skills/textual-tui/assets/README.md +165 -0
  3. tasktree_manager-1.0.1/.claude/skills/textual-tui/assets/dashboard_app.py +256 -0
  4. tasktree_manager-1.0.1/.claude/skills/textual-tui/assets/data_viewer.py +304 -0
  5. tasktree_manager-1.0.1/.claude/skills/textual-tui/assets/todo_app.py +236 -0
  6. tasktree_manager-1.0.1/.claude/skills/textual-tui/assets/worker_demo.py +406 -0
  7. tasktree_manager-1.0.1/.claude/skills/textual-tui/references/layouts.md +575 -0
  8. tasktree_manager-1.0.1/.claude/skills/textual-tui/references/official-guides-index.md +284 -0
  9. tasktree_manager-1.0.1/.claude/skills/textual-tui/references/styling.md +700 -0
  10. tasktree_manager-1.0.1/.claude/skills/textual-tui/references/widgets.md +533 -0
  11. tasktree_manager-1.0.1/.github/templates/CHANGELOG.md.j2 +13 -0
  12. tasktree_manager-1.0.1/.github/workflows/ci.yml +138 -0
  13. tasktree_manager-1.0.1/.github/workflows/release.yml +294 -0
  14. tasktree_manager-1.0.1/.gitignore +57 -0
  15. tasktree_manager-1.0.1/CHANGELOG.md +131 -0
  16. tasktree_manager-1.0.1/CONTRIBUTING.md +273 -0
  17. tasktree_manager-1.0.1/LICENSE +21 -0
  18. tasktree_manager-1.0.1/OPENSPEC.md +457 -0
  19. tasktree_manager-1.0.1/PKG-INFO +15 -0
  20. tasktree_manager-1.0.1/README.md +195 -0
  21. tasktree_manager-1.0.1/ROADMAP.md +243 -0
  22. tasktree_manager-1.0.1/docs/claude-code-integration-spec.md +180 -0
  23. tasktree_manager-1.0.1/docs/configuration.md +678 -0
  24. tasktree_manager-1.0.1/docs/installation.md +419 -0
  25. tasktree_manager-1.0.1/docs/troubleshooting.md +1045 -0
  26. tasktree_manager-1.0.1/docs/user-guide.md +812 -0
  27. tasktree_manager-1.0.1/mise.toml +85 -0
  28. tasktree_manager-1.0.1/pyproject.toml +86 -0
  29. tasktree_manager-1.0.1/scripts/bump_version.py +283 -0
  30. tasktree_manager-1.0.1/tasktree/__init__.py +6 -0
  31. tasktree_manager-1.0.1/tasktree/_version.py +34 -0
  32. tasktree_manager-1.0.1/tasktree/app.py +1179 -0
  33. tasktree_manager-1.0.1/tasktree/services/__init__.py +17 -0
  34. tasktree_manager-1.0.1/tasktree/services/config.py +416 -0
  35. tasktree_manager-1.0.1/tasktree/services/git_ops.py +267 -0
  36. tasktree_manager-1.0.1/tasktree/services/models.py +119 -0
  37. tasktree_manager-1.0.1/tasktree/services/task_manager.py +477 -0
  38. tasktree_manager-1.0.1/tasktree/widgets/__init__.py +20 -0
  39. tasktree_manager-1.0.1/tasktree/widgets/create_modal.py +653 -0
  40. tasktree_manager-1.0.1/tasktree/widgets/messages_panel.py +133 -0
  41. tasktree_manager-1.0.1/tasktree/widgets/setup_modal.py +162 -0
  42. tasktree_manager-1.0.1/tasktree/widgets/status_panel.py +95 -0
  43. tasktree_manager-1.0.1/tasktree/widgets/task_list.py +191 -0
  44. tasktree_manager-1.0.1/tasktree/widgets/worktree_list.py +193 -0
  45. tasktree_manager-1.0.1/tests/__init__.py +1 -0
  46. tasktree_manager-1.0.1/tests/conftest.py +207 -0
  47. tasktree_manager-1.0.1/tests/test_app.py +509 -0
  48. tasktree_manager-1.0.1/tests/test_config.py +351 -0
  49. tasktree_manager-1.0.1/tests/test_git_ops.py +461 -0
  50. tasktree_manager-1.0.1/tests/test_integration_git.py +396 -0
  51. tasktree_manager-1.0.1/tests/test_memory.py +292 -0
  52. tasktree_manager-1.0.1/tests/test_messages_panel.py +251 -0
  53. tasktree_manager-1.0.1/tests/test_modals.py +463 -0
  54. tasktree_manager-1.0.1/tests/test_setup_modal.py +184 -0
  55. tasktree_manager-1.0.1/tests/test_status_panel.py +222 -0
  56. tasktree_manager-1.0.1/tests/test_task_manager.py +690 -0
@@ -0,0 +1,703 @@
1
+ ---
2
+ name: textual-tui
3
+ description: Build modern, interactive terminal user interfaces with Textual. Use when creating command-line applications, dashboard tools, monitoring interfaces, data viewers, or any terminal-based UI. Covers architecture, widgets, layouts, styling, event handling, reactive programming, workers for background tasks, and testing patterns.
4
+ ---
5
+
6
+ # Textual TUI Development
7
+
8
+ Build production-quality terminal user interfaces using Textual, a modern Python framework for creating interactive TUI applications.
9
+
10
+ ## Quick Start
11
+
12
+ Install Textual:
13
+ ```bash
14
+ pip install textual textual-dev
15
+ ```
16
+
17
+ Basic app structure:
18
+ ```python
19
+ from textual.app import App, ComposeResult
20
+ from textual.widgets import Header, Footer, Button
21
+
22
+ class MyApp(App):
23
+ """A simple Textual app."""
24
+
25
+ def compose(self) -> ComposeResult:
26
+ """Create child widgets."""
27
+ yield Header()
28
+ yield Button("Click me!", id="click")
29
+ yield Footer()
30
+
31
+ def on_button_pressed(self, event: Button.Pressed) -> None:
32
+ """Handle button press."""
33
+ self.exit()
34
+
35
+ if __name__ == "__main__":
36
+ app = MyApp()
37
+ app.run()
38
+ ```
39
+
40
+ Run with hot reload during development:
41
+ ```bash
42
+ textual run --dev your_app.py
43
+ ```
44
+
45
+ Use the Textual console for debugging:
46
+ ```bash
47
+ textual console
48
+ ```
49
+
50
+ ## Core Architecture
51
+
52
+ ### App Lifecycle
53
+
54
+ 1. **Initialization**: Create App instance with config
55
+ 2. **Composition**: Build widget tree via `compose()` method
56
+ 3. **Mounting**: Widgets mounted to DOM
57
+ 4. **Running**: Event loop processes messages and renders UI
58
+ 5. **Shutdown**: Cleanup and exit
59
+
60
+ ### Message Passing System
61
+
62
+ Textual uses an async message queue for all interactions:
63
+
64
+ ```python
65
+ from textual.message import Message
66
+
67
+ class CustomMessage(Message):
68
+ """Custom message with data."""
69
+ def __init__(self, value: int) -> None:
70
+ self.value = value
71
+ super().__init__()
72
+
73
+ class MyWidget(Widget):
74
+ def on_click(self) -> None:
75
+ # Post message to parent
76
+ self.post_message(CustomMessage(42))
77
+
78
+ class MyApp(App):
79
+ def on_custom_message(self, message: CustomMessage) -> None:
80
+ # Handle message with naming convention: on_{message_name}
81
+ self.log(f"Received: {message.value}")
82
+ ```
83
+
84
+ ### Reactive Programming
85
+
86
+ Use reactive attributes for automatic UI updates:
87
+
88
+ ```python
89
+ from textual.reactive import reactive
90
+
91
+ class Counter(Widget):
92
+ count = reactive(0) # Reactive attribute
93
+
94
+ def watch_count(self, new_value: int) -> None:
95
+ """Called automatically when count changes."""
96
+ self.refresh()
97
+
98
+ def increment(self) -> None:
99
+ self.count += 1 # Triggers watch_count
100
+ ```
101
+
102
+ ## Layout System
103
+
104
+ ### Container Layouts
105
+
106
+ Textual provides flexible layout options:
107
+
108
+ **Vertical Layout (default)**:
109
+ ```python
110
+ def compose(self) -> ComposeResult:
111
+ yield Label("Top")
112
+ yield Label("Bottom")
113
+ ```
114
+
115
+ **Horizontal Layout**:
116
+ ```python
117
+ class MyApp(App):
118
+ CSS = """
119
+ Screen {
120
+ layout: horizontal;
121
+ }
122
+ """
123
+ ```
124
+
125
+ **Grid Layout**:
126
+ ```python
127
+ class MyApp(App):
128
+ CSS = """
129
+ Screen {
130
+ layout: grid;
131
+ grid-size: 3 2; /* 3 columns, 2 rows */
132
+ }
133
+ """
134
+ ```
135
+
136
+ ### Sizing and Positioning
137
+
138
+ Control widget dimensions:
139
+ ```python
140
+ class MyApp(App):
141
+ CSS = """
142
+ #sidebar {
143
+ width: 30; /* Fixed width */
144
+ height: 100%; /* Full height */
145
+ }
146
+
147
+ #content {
148
+ width: 1fr; /* Remaining space */
149
+ }
150
+
151
+ .compact {
152
+ height: auto; /* Size to content */
153
+ }
154
+ """
155
+ ```
156
+
157
+ ## Styling with CSS
158
+
159
+ Textual uses CSS-like syntax for styling.
160
+
161
+ ### Inline Styles
162
+
163
+ ```python
164
+ class StyledWidget(Widget):
165
+ DEFAULT_CSS = """
166
+ StyledWidget {
167
+ background: $primary;
168
+ color: $text;
169
+ border: solid $accent;
170
+ padding: 1 2;
171
+ margin: 1;
172
+ }
173
+ """
174
+ ```
175
+
176
+ ### External CSS Files
177
+
178
+ ```python
179
+ class MyApp(App):
180
+ CSS_PATH = "app.tcss" # Load from file
181
+ ```
182
+
183
+ ### Color System
184
+
185
+ Use Textual's semantic colors:
186
+ ```css
187
+ .error { background: $error; }
188
+ .success { background: $success; }
189
+ .warning { background: $warning; }
190
+ .primary { background: $primary; }
191
+ ```
192
+
193
+ Or define custom colors:
194
+ ```css
195
+ .custom {
196
+ background: #1e3a8a;
197
+ color: rgb(255, 255, 255);
198
+ }
199
+ ```
200
+
201
+ ## Common Widgets
202
+
203
+ ### Input and Forms
204
+
205
+ ```python
206
+ from textual.widgets import Input, Button, Select
207
+ from textual.containers import Container
208
+
209
+ def compose(self) -> ComposeResult:
210
+ with Container(id="form"):
211
+ yield Input(placeholder="Enter name", id="name")
212
+ yield Select(options=[("A", 1), ("B", 2)], id="choice")
213
+ yield Button("Submit", variant="primary")
214
+
215
+ def on_button_pressed(self, event: Button.Pressed) -> None:
216
+ name = self.query_one("#name", Input).value
217
+ choice = self.query_one("#choice", Select).value
218
+ ```
219
+
220
+ ### Data Display
221
+
222
+ ```python
223
+ from textual.widgets import DataTable, Tree, Log
224
+
225
+ # DataTable for tabular data
226
+ table = DataTable()
227
+ table.add_columns("Name", "Age", "City")
228
+ table.add_row("Alice", 30, "NYC")
229
+
230
+ # Tree for hierarchical data
231
+ tree = Tree("Root")
232
+ tree.root.add("Child 1")
233
+ tree.root.add("Child 2")
234
+
235
+ # Log for streaming output
236
+ log = Log(auto_scroll=True)
237
+ log.write_line("Log entry")
238
+ ```
239
+
240
+ ### Containers and Layout
241
+
242
+ ```python
243
+ from textual.containers import (
244
+ Container, Horizontal, Vertical,
245
+ Grid, ScrollableContainer
246
+ )
247
+
248
+ def compose(self) -> ComposeResult:
249
+ with Vertical():
250
+ yield Header()
251
+ with Horizontal():
252
+ with Container(id="sidebar"):
253
+ yield Label("Menu")
254
+ with ScrollableContainer(id="content"):
255
+ yield Label("Content...")
256
+ yield Footer()
257
+ ```
258
+
259
+ ## Event Handling
260
+
261
+ ### Built-in Events
262
+
263
+ ```python
264
+ from textual.events import Key, Click, Mount
265
+
266
+ def on_mount(self) -> None:
267
+ """Called when widget is mounted."""
268
+ self.log("Widget mounted!")
269
+
270
+ def on_key(self, event: Key) -> None:
271
+ """Handle all key presses."""
272
+ if event.key == "q":
273
+ self.app.exit()
274
+
275
+ def on_click(self, event: Click) -> None:
276
+ """Handle mouse clicks."""
277
+ self.log(f"Clicked at {event.x}, {event.y}")
278
+ ```
279
+
280
+ ### Widget-Specific Handlers
281
+
282
+ ```python
283
+ def on_input_submitted(self, event: Input.Submitted) -> None:
284
+ """Handle input submission."""
285
+ self.query_one(Log).write(event.value)
286
+
287
+ def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
288
+ """Handle table row selection."""
289
+ row_key = event.row_key
290
+ ```
291
+
292
+ ### Keyboard Bindings
293
+
294
+ ```python
295
+ class MyApp(App):
296
+ BINDINGS = [
297
+ ("q", "quit", "Quit"),
298
+ ("d", "toggle_dark", "Toggle dark mode"),
299
+ ("ctrl+s", "save", "Save"),
300
+ ]
301
+
302
+ def action_quit(self) -> None:
303
+ self.exit()
304
+
305
+ def action_toggle_dark(self) -> None:
306
+ self.dark = not self.dark
307
+ ```
308
+
309
+ ## Advanced Patterns
310
+
311
+ ### Custom Widgets
312
+
313
+ Create reusable components:
314
+ ```python
315
+ from textual.widget import Widget
316
+ from textual.widgets import Label, Button
317
+
318
+ class StatusCard(Widget):
319
+ """A card showing status info."""
320
+
321
+ def __init__(self, title: str, status: str) -> None:
322
+ super().__init__()
323
+ self.title = title
324
+ self.status = status
325
+
326
+ def compose(self) -> ComposeResult:
327
+ yield Label(self.title, classes="title")
328
+ yield Label(self.status, classes="status")
329
+ ```
330
+
331
+ ### Workers and Background Tasks
332
+
333
+ CRITICAL: Use workers for any long-running operations to prevent blocking the UI. The event loop must remain responsive.
334
+
335
+ #### Basic Worker Usage
336
+
337
+ Run tasks in background threads:
338
+ ```python
339
+ from textual.worker import Worker, WorkerState
340
+
341
+ class MyApp(App):
342
+ def on_button_pressed(self, event: Button.Pressed) -> None:
343
+ # Start background task
344
+ self.run_worker(self.process_data(), exclusive=True)
345
+
346
+ async def process_data(self) -> str:
347
+ """Long-running task."""
348
+ # Simulate work
349
+ await asyncio.sleep(5)
350
+ return "Processing complete"
351
+ ```
352
+
353
+ #### Worker with Progress Updates
354
+
355
+ Update UI during processing:
356
+ ```python
357
+ from textual.widgets import ProgressBar
358
+
359
+ class MyApp(App):
360
+ def compose(self) -> ComposeResult:
361
+ yield ProgressBar(total=100, id="progress")
362
+
363
+ def on_mount(self) -> None:
364
+ self.run_worker(self.long_task())
365
+
366
+ async def long_task(self) -> None:
367
+ """Task with progress updates."""
368
+ progress = self.query_one(ProgressBar)
369
+
370
+ for i in range(100):
371
+ await asyncio.sleep(0.1)
372
+ progress.update(progress=i + 1)
373
+ # Use call_from_thread for thread safety
374
+ self.call_from_thread(progress.update, progress=i + 1)
375
+ ```
376
+
377
+ #### Worker Communication Patterns
378
+
379
+ Use `call_from_thread` for thread-safe UI updates:
380
+ ```python
381
+ import time
382
+ from threading import Thread
383
+
384
+ class MyApp(App):
385
+ def on_mount(self) -> None:
386
+ self.run_worker(self.fetch_data(), thread=True)
387
+
388
+ def fetch_data(self) -> None:
389
+ """CPU-bound task in thread."""
390
+ # Blocking operation
391
+ result = expensive_computation()
392
+
393
+ # Update UI safely from thread
394
+ self.call_from_thread(self.display_result, result)
395
+
396
+ def display_result(self, result: str) -> None:
397
+ """Called on main thread."""
398
+ self.query_one("#output").update(result)
399
+ ```
400
+
401
+ #### Worker Cancellation
402
+
403
+ Cancel workers when no longer needed:
404
+ ```python
405
+ class MyApp(App):
406
+ worker: Worker | None = None
407
+
408
+ def start_task(self) -> None:
409
+ # Store worker reference
410
+ self.worker = self.run_worker(self.long_task())
411
+
412
+ def cancel_task(self) -> None:
413
+ # Cancel running worker
414
+ if self.worker and not self.worker.is_finished:
415
+ self.worker.cancel()
416
+ self.notify("Task cancelled")
417
+
418
+ async def long_task(self) -> None:
419
+ for i in range(1000):
420
+ await asyncio.sleep(0.1)
421
+ # Check if cancelled
422
+ if self.worker.is_cancelled:
423
+ return
424
+ ```
425
+
426
+ #### Worker Error Handling
427
+
428
+ Handle worker failures gracefully:
429
+ ```python
430
+ class MyApp(App):
431
+ def on_mount(self) -> None:
432
+ worker = self.run_worker(self.risky_task())
433
+ worker.name = "data_processor" # Name for debugging
434
+
435
+ async def risky_task(self) -> str:
436
+ """Task that might fail."""
437
+ try:
438
+ result = await fetch_from_api()
439
+ return result
440
+ except Exception as e:
441
+ self.notify(f"Error: {e}", severity="error")
442
+ raise
443
+
444
+ def on_worker_state_changed(self, event: Worker.StateChanged) -> None:
445
+ """Handle worker state changes."""
446
+ if event.state == WorkerState.ERROR:
447
+ self.log.error(f"Worker failed: {event.worker.name}")
448
+ elif event.state == WorkerState.SUCCESS:
449
+ self.log.info(f"Worker completed: {event.worker.name}")
450
+ ```
451
+
452
+ #### Multiple Workers
453
+
454
+ Manage concurrent workers:
455
+ ```python
456
+ class MyApp(App):
457
+ def on_mount(self) -> None:
458
+ # Run multiple workers concurrently
459
+ self.run_worker(self.task_one(), name="task1", group="processing")
460
+ self.run_worker(self.task_two(), name="task2", group="processing")
461
+ self.run_worker(self.task_three(), name="task3", group="processing")
462
+
463
+ async def task_one(self) -> None:
464
+ await asyncio.sleep(2)
465
+ self.notify("Task 1 complete")
466
+
467
+ async def task_two(self) -> None:
468
+ await asyncio.sleep(3)
469
+ self.notify("Task 2 complete")
470
+
471
+ async def task_three(self) -> None:
472
+ await asyncio.sleep(1)
473
+ self.notify("Task 3 complete")
474
+
475
+ def cancel_all_tasks(self) -> None:
476
+ """Cancel all workers in a group."""
477
+ for worker in self.workers:
478
+ if worker.group == "processing":
479
+ worker.cancel()
480
+ ```
481
+
482
+ #### Thread vs Process Workers
483
+
484
+ Choose the right worker type:
485
+ ```python
486
+ class MyApp(App):
487
+ def on_mount(self) -> None:
488
+ # Async task (default) - for I/O bound operations
489
+ self.run_worker(self.fetch_data())
490
+
491
+ # Thread worker - for CPU-bound tasks
492
+ self.run_worker(self.process_data(), thread=True)
493
+
494
+ async def fetch_data(self) -> str:
495
+ """I/O bound: use async."""
496
+ async with httpx.AsyncClient() as client:
497
+ response = await client.get("https://api.example.com")
498
+ return response.text
499
+
500
+ def process_data(self) -> str:
501
+ """CPU bound: use thread."""
502
+ # Heavy computation
503
+ result = [i**2 for i in range(1000000)]
504
+ return str(sum(result))
505
+ ```
506
+
507
+ #### Worker Best Practices
508
+
509
+ 1. **Always use workers for**:
510
+ - Network requests
511
+ - File I/O
512
+ - Database queries
513
+ - CPU-intensive computations
514
+ - Anything taking > 100ms
515
+
516
+ 2. **Worker patterns**:
517
+ - Use `exclusive=True` to prevent duplicate workers
518
+ - Name workers for easier debugging
519
+ - Group related workers for batch cancellation
520
+ - Always handle worker errors
521
+
522
+ 3. **Thread safety**:
523
+ - Use `call_from_thread()` for UI updates from threads
524
+ - Never modify widgets directly from threads
525
+ - Use locks for shared mutable state
526
+
527
+ 4. **Cancellation**:
528
+ - Store worker references if you need to cancel
529
+ - Check `worker.is_cancelled` in long loops
530
+ - Clean up resources in finally blocks
531
+
532
+ ### Modal Dialogs
533
+
534
+ ```python
535
+ from textual.screen import ModalScreen
536
+
537
+ class ConfirmDialog(ModalScreen[bool]):
538
+ """Modal confirmation dialog."""
539
+
540
+ def compose(self) -> ComposeResult:
541
+ with Container(id="dialog"):
542
+ yield Label("Are you sure?")
543
+ with Horizontal():
544
+ yield Button("Yes", variant="primary", id="yes")
545
+ yield Button("No", variant="error", id="no")
546
+
547
+ def on_button_pressed(self, event: Button.Pressed) -> None:
548
+ self.dismiss(event.button.id == "yes")
549
+
550
+ # Use in app
551
+ async def confirm_action(self) -> None:
552
+ result = await self.push_screen_wait(ConfirmDialog())
553
+ if result:
554
+ self.log("Confirmed!")
555
+ ```
556
+
557
+ ### Screens and Navigation
558
+
559
+ ```python
560
+ from textual.screen import Screen
561
+
562
+ class MainScreen(Screen):
563
+ def compose(self) -> ComposeResult:
564
+ yield Header()
565
+ yield Button("Go to Settings")
566
+ yield Footer()
567
+
568
+ def on_button_pressed(self) -> None:
569
+ self.app.push_screen("settings")
570
+
571
+ class SettingsScreen(Screen):
572
+ def compose(self) -> ComposeResult:
573
+ yield Label("Settings")
574
+ yield Button("Back")
575
+
576
+ def on_button_pressed(self) -> None:
577
+ self.app.pop_screen()
578
+
579
+ class MyApp(App):
580
+ SCREENS = {
581
+ "main": MainScreen(),
582
+ "settings": SettingsScreen(),
583
+ }
584
+ ```
585
+
586
+ ## Testing
587
+
588
+ Test Textual apps with pytest and the Pilot API:
589
+
590
+ ```python
591
+ import pytest
592
+ from textual.pilot import Pilot
593
+ from my_app import MyApp
594
+
595
+ @pytest.mark.asyncio
596
+ async def test_app_starts():
597
+ app = MyApp()
598
+ async with app.run_test() as pilot:
599
+ assert app.screen is not None
600
+
601
+ @pytest.mark.asyncio
602
+ async def test_button_click():
603
+ app = MyApp()
604
+ async with app.run_test() as pilot:
605
+ await pilot.click("#my-button")
606
+ # Assert expected state changes
607
+
608
+ @pytest.mark.asyncio
609
+ async def test_keyboard_input():
610
+ app = MyApp()
611
+ async with app.run_test() as pilot:
612
+ await pilot.press("q")
613
+ # Verify app exited or state changed
614
+ ```
615
+
616
+ ## Best Practices
617
+
618
+ ### Performance
619
+
620
+ - Use `Lazy` for expensive widgets loaded on demand
621
+ - Implement efficient `render()` methods, avoid unnecessary work
622
+ - Use reactive attributes sparingly for truly dynamic values
623
+ - Batch UI updates when processing multiple changes
624
+
625
+ ### State Management
626
+
627
+ - Keep app state in the App instance for global access
628
+ - Use reactive attributes for UI-bound state
629
+ - Store complex state in dedicated data models
630
+ - Avoid deeply nested widget communication
631
+
632
+ ### Error Handling
633
+
634
+ ```python
635
+ from textual.widgets import RichLog
636
+
637
+ def compose(self) -> ComposeResult:
638
+ yield RichLog(id="log")
639
+
640
+ async def action_risky_operation(self) -> None:
641
+ try:
642
+ result = await some_async_operation()
643
+ self.notify("Success!", severity="information")
644
+ except Exception as e:
645
+ self.notify(f"Error: {e}", severity="error")
646
+ self.query_one(RichLog).write(f"[red]Error:[/] {e}")
647
+ ```
648
+
649
+ ### Accessibility
650
+
651
+ - Always provide keyboard navigation
652
+ - Use semantic widget names and IDs
653
+ - Include ARIA-like descriptions where appropriate
654
+ - Test with screen reader compatibility in mind
655
+
656
+ ## Development Tools
657
+
658
+ ### Textual Console
659
+
660
+ Debug running apps:
661
+ ```bash
662
+ # Terminal 1: Run console
663
+ textual console
664
+
665
+ # Terminal 2: Run app with console enabled
666
+ textual run --dev app.py
667
+ ```
668
+
669
+ App code to enable console:
670
+ ```python
671
+ self.log("Debug message") # Appears in console
672
+ self.log.info("Info level")
673
+ self.log.error("Error level")
674
+ ```
675
+
676
+ ### Textual Devtools
677
+
678
+ Use the devtools for live inspection:
679
+ ```bash
680
+ pip install textual-dev
681
+ textual run --dev app.py # Enables hot reload
682
+ ```
683
+
684
+ ## References
685
+
686
+ - **Widget Gallery**: See references/widgets.md for comprehensive widget examples
687
+ - **Layout Patterns**: See references/layouts.md for common layout recipes
688
+ - **Styling Guide**: See references/styling.md for CSS patterns and themes
689
+ - **Official Guides Index**: See references/official-guides-index.md for URLs to all official Textual documentation guides (use web_fetch for detailed information on-demand)
690
+ - **Example Apps**: See assets/ for complete example applications
691
+
692
+ ## Common Pitfalls
693
+
694
+ 1. **Forgetting async/await**: Many Textual methods are async, always await them
695
+ 2. **Blocking the event loop**: CRITICAL - Use `run_worker()` for long-running tasks (network, I/O, heavy computation). Never use `time.sleep()` or blocking operations in the main thread
696
+ 3. **Incorrect message handling**: Method names must match `on_{message_name}` pattern
697
+ 4. **CSS specificity issues**: Use IDs and classes appropriately for targeted styling
698
+ 5. **Not using query methods**: Use `query_one()` and `query()` instead of manual traversal
699
+ 6. **Thread safety violations**: Never modify widgets directly from worker threads - use `call_from_thread()`
700
+ 7. **Not cancelling workers**: Workers continue running even when screens close - always cancel or store references
701
+ 8. **Using time.sleep in async**: Use `await asyncio.sleep()` instead of `time.sleep()` in async functions
702
+ 9. **Not handling worker errors**: Workers can fail silently - always implement error handling
703
+ 10. **Wrong worker type**: Use async workers for I/O, thread workers for CPU-bound tasks