Initial commit
This commit is contained in:
175
skills/textual-builder/SKILL.md
Normal file
175
skills/textual-builder/SKILL.md
Normal file
@@ -0,0 +1,175 @@
|
||||
---
|
||||
name: textual-builder
|
||||
description: Build Text User Interface (TUI) applications using the Textual Python framework (v0.86.0+). Use when creating terminal-based applications, prototyping card games or interactive CLIs, or when the user mentions Textual, TUI, or terminal UI. Includes comprehensive reference documentation, card game starter template, and styling guides.
|
||||
---
|
||||
|
||||
# Textual Builder
|
||||
|
||||
## Overview
|
||||
|
||||
This skill helps you build sophisticated Text User Interfaces (TUIs) using Textual, a Python framework for creating terminal and browser-based applications with a modern web-inspired API. It includes reference documentation, a card game template, and best practices for Textual development.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Basic Textual App
|
||||
|
||||
```python
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.widgets import Header, Footer, Label
|
||||
|
||||
class MyApp(App):
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header()
|
||||
yield Label("Hello, Textual!")
|
||||
yield Footer()
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = MyApp()
|
||||
app.run()
|
||||
```
|
||||
|
||||
### Card Game Template
|
||||
|
||||
For card game prototyping, copy the template:
|
||||
|
||||
```bash
|
||||
cp -r assets/card-game-template/* ./my-game/
|
||||
cd my-game
|
||||
python app.py
|
||||
```
|
||||
|
||||
The template includes:
|
||||
- Interactive Card widget with face-up/down states
|
||||
- Hand containers for player cards
|
||||
- Play area with turn management
|
||||
- Key bindings for card selection and playing
|
||||
- Customizable styling
|
||||
|
||||
See `assets/card-game-template/README.md` for customization guide.
|
||||
|
||||
## When to Read Reference Documentation
|
||||
|
||||
This skill includes comprehensive reference files. Load them based on your task:
|
||||
|
||||
### references/basics.md
|
||||
**Read when:** Setting up app structure, using reactive attributes, handling mounting, querying widgets, or working with messages/events.
|
||||
|
||||
**Covers:**
|
||||
- App structure and compose method
|
||||
- Reactive attributes and watchers
|
||||
- Mounting and dynamic widget creation
|
||||
- Widget querying
|
||||
- Messages, events, and custom messages
|
||||
|
||||
### references/widgets.md
|
||||
**Read when:** Adding UI elements like buttons, inputs, labels, data tables, or creating custom widgets.
|
||||
|
||||
**Covers:**
|
||||
- Display widgets (Label, Static, Placeholder)
|
||||
- Input widgets (Button, Input, TextArea, Switch)
|
||||
- DataTable for tabular data
|
||||
- Layout containers (Container, Grid, Horizontal, Vertical)
|
||||
- Custom widget creation
|
||||
- Header/Footer
|
||||
|
||||
### references/layout.md
|
||||
**Read when:** Designing layouts, positioning widgets, using grid systems, or handling responsive sizing.
|
||||
|
||||
**Covers:**
|
||||
- Layout types (vertical, horizontal, grid)
|
||||
- Grid configuration (cell spanning, row/column sizing)
|
||||
- Alignment and content positioning
|
||||
- Docking widgets to screen edges
|
||||
- Sizing (fixed, relative, fractional, auto)
|
||||
- Spacing (margin, padding)
|
||||
- Scrolling
|
||||
|
||||
### references/styling.md
|
||||
**Read when:** Applying CSS styles, theming, adding borders, or customizing widget appearance.
|
||||
|
||||
**Covers:**
|
||||
- CSS files and selectors
|
||||
- Colors (named, hex, RGB, theme variables)
|
||||
- Borders and border styling
|
||||
- Text styling and alignment
|
||||
- Opacity and tinting
|
||||
- Rich markup for styled text
|
||||
- Pseudo-classes (:hover, :focus, etc.)
|
||||
|
||||
### references/interactivity.md
|
||||
**Read when:** Implementing keyboard shortcuts, handling mouse events, responding to user actions, or creating interactive behaviors.
|
||||
|
||||
**Covers:**
|
||||
- Key bindings and actions
|
||||
- Dynamic binding updates
|
||||
- Mouse events (click, hover, enter, leave)
|
||||
- Keyboard events
|
||||
- Focus management
|
||||
- Widget-specific messages
|
||||
- Custom messages
|
||||
- Notifications and timers
|
||||
|
||||
## Common Workflows
|
||||
|
||||
### Creating a New TUI App
|
||||
|
||||
1. Start with basic app structure (see Quick Start)
|
||||
2. Design layout (read `references/layout.md`)
|
||||
3. Add widgets (read `references/widgets.md`)
|
||||
4. Style with CSS (read `references/styling.md`)
|
||||
5. Add interactivity (read `references/interactivity.md`)
|
||||
|
||||
### Prototyping a Card Game
|
||||
|
||||
1. Copy the card game template
|
||||
2. Customize the Card widget for your game's card properties
|
||||
3. Modify game logic in action methods
|
||||
4. Add game-specific rules in message handlers
|
||||
5. Style cards and layout in `app.tcss`
|
||||
|
||||
### Adding Interactive Features
|
||||
|
||||
1. Define key bindings in `BINDINGS`
|
||||
2. Implement action methods (`action_*`)
|
||||
3. Handle widget messages (`on_button_pressed`, etc.)
|
||||
4. Use reactive attributes for state management
|
||||
5. Update UI in watchers
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Progressive Development**: Start simple, add complexity incrementally
|
||||
- **Reactive State**: Use `reactive()` for state that affects UI
|
||||
- **CSS Separation**: Keep styling in `.tcss` files, not inline
|
||||
- **Widget Reuse**: Create custom widgets for repeated components
|
||||
- **Message Bubbling**: Use `event.stop()` to control message propagation
|
||||
- **Type Hints**: Use proper type hints for better IDE support
|
||||
- **IDs and Classes**: Use semantic IDs/classes for querying and styling
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install textual
|
||||
# or
|
||||
uv pip install textual
|
||||
```
|
||||
|
||||
Current version: v0.86.0+ (as of November 2025, latest is v6.6.0)
|
||||
|
||||
## Resources
|
||||
|
||||
### references/
|
||||
Comprehensive documentation loaded on-demand:
|
||||
- `basics.md` - Core concepts and app structure
|
||||
- `widgets.md` - Widget catalog and usage
|
||||
- `layout.md` - Layout systems and positioning
|
||||
- `styling.md` - CSS and theming
|
||||
- `interactivity.md` - Events, bindings, and actions
|
||||
|
||||
### assets/
|
||||
- `card-game-template/` - Complete starter template for card games with interactive cards, hands, and turn management
|
||||
|
||||
## Official Documentation
|
||||
|
||||
For topics not covered in this skill, consult:
|
||||
- https://textual.textualize.io/ (official docs)
|
||||
- https://github.com/Textualize/textual (GitHub repo)
|
||||
77
skills/textual-builder/assets/card-game-template/README.md
Normal file
77
skills/textual-builder/assets/card-game-template/README.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# Card Game Template
|
||||
|
||||
A starter template for building turn-based card games with Textual.
|
||||
|
||||
## Features
|
||||
|
||||
- **Card Widget**: Customizable playing cards with suit, rank, face-up/down state
|
||||
- **Hand Container**: Display and manage player hands
|
||||
- **Play Area**: Central area for cards in play
|
||||
- **Turn System**: Basic turn management
|
||||
- **Interactivity**: Card selection and playing with keyboard shortcuts
|
||||
|
||||
## Running the Template
|
||||
|
||||
```bash
|
||||
python app.py
|
||||
```
|
||||
|
||||
## Key Bindings
|
||||
|
||||
- `d` - Draw a card
|
||||
- `space` - Select/deselect card
|
||||
- `p` - Play selected card
|
||||
- `n` - Next turn
|
||||
- `q` - Quit
|
||||
|
||||
## Customization
|
||||
|
||||
### Card Values
|
||||
|
||||
Modify the `Card` class to add game-specific properties:
|
||||
|
||||
```python
|
||||
class Card(Widget):
|
||||
def __init__(self, rank: str, suit: str, power: int = 0, special_ability: str = ""):
|
||||
self.power = power
|
||||
self.special_ability = special_ability
|
||||
# ...
|
||||
```
|
||||
|
||||
### Game Rules
|
||||
|
||||
Implement game logic in the `CardGameApp` methods:
|
||||
- `on_card_played()` - Validate and process card plays
|
||||
- `action_draw_card()` - Implement deck management
|
||||
- `action_next_turn()` - Add turn-based game logic
|
||||
|
||||
### Card Appearance
|
||||
|
||||
Edit `Card.compose()` or the CSS in `app.tcss` to change card styling.
|
||||
|
||||
### Deck Management
|
||||
|
||||
Add a `Deck` class to manage card shuffling and drawing:
|
||||
|
||||
```python
|
||||
class Deck:
|
||||
def __init__(self):
|
||||
self.cards = []
|
||||
self.shuffle()
|
||||
|
||||
def shuffle(self):
|
||||
import random
|
||||
random.shuffle(self.cards)
|
||||
|
||||
def draw(self) -> Card | None:
|
||||
return self.cards.pop() if self.cards else None
|
||||
```
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
card-game-template/
|
||||
├── app.py # Main application
|
||||
├── app.tcss # Styles
|
||||
└── README.md # This file
|
||||
```
|
||||
324
skills/textual-builder/assets/card-game-template/app.py
Normal file
324
skills/textual-builder/assets/card-game-template/app.py
Normal file
@@ -0,0 +1,324 @@
|
||||
"""
|
||||
Card Game Template for Textual
|
||||
|
||||
A starter template for building turn-based card games with Textual.
|
||||
Includes:
|
||||
- Card widget with customizable suit, rank, and face-up/down state
|
||||
- Hand container for displaying player hands
|
||||
- Play area for cards in play
|
||||
- Turn management system
|
||||
- Action system with key bindings
|
||||
|
||||
Customize this template for your specific card game rules.
|
||||
"""
|
||||
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.containers import Container, Horizontal, Vertical
|
||||
from textual.widgets import Header, Footer, Label, Static
|
||||
from textual.widget import Widget
|
||||
from textual.reactive import reactive
|
||||
from textual.message import Message
|
||||
from textual.binding import Binding
|
||||
|
||||
|
||||
class Card(Widget):
|
||||
"""A playing card widget."""
|
||||
|
||||
DEFAULT_CSS = """
|
||||
Card {
|
||||
width: 12;
|
||||
height: 10;
|
||||
border: round white;
|
||||
background: $panel;
|
||||
content-align: center middle;
|
||||
}
|
||||
|
||||
Card:hover {
|
||||
background: $boost;
|
||||
border: heavy $primary;
|
||||
}
|
||||
|
||||
Card.selected {
|
||||
border: double cyan;
|
||||
background: $accent;
|
||||
}
|
||||
|
||||
Card.face-down {
|
||||
background: $surface;
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
Card.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.card-rank {
|
||||
text-style: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card-suit {
|
||||
text-align: center;
|
||||
}
|
||||
"""
|
||||
|
||||
class Selected(Message):
|
||||
"""Posted when card is selected."""
|
||||
|
||||
def __init__(self, card: "Card") -> None:
|
||||
super().__init__()
|
||||
self.card = card
|
||||
|
||||
class Played(Message):
|
||||
"""Posted when card is played."""
|
||||
|
||||
def __init__(self, card: "Card") -> None:
|
||||
super().__init__()
|
||||
self.card = card
|
||||
|
||||
face_up = reactive(True)
|
||||
selectable = reactive(True)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
rank: str,
|
||||
suit: str,
|
||||
value: int = 0,
|
||||
face_up: bool = True,
|
||||
card_id: str | None = None,
|
||||
) -> None:
|
||||
super().__init__(id=card_id)
|
||||
self.rank = rank
|
||||
self.suit = suit
|
||||
self.value = value
|
||||
self.face_up = face_up
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
if self.face_up:
|
||||
yield Label(self.rank, classes="card-rank")
|
||||
yield Label(self.suit, classes="card-suit")
|
||||
else:
|
||||
yield Label("🂠", classes="card-back")
|
||||
|
||||
def watch_face_up(self, face_up: bool) -> None:
|
||||
"""Update display when card is flipped."""
|
||||
if face_up:
|
||||
self.remove_class("face-down")
|
||||
else:
|
||||
self.add_class("face-down")
|
||||
# Refresh the card content
|
||||
self.recompose()
|
||||
|
||||
def on_click(self) -> None:
|
||||
"""Handle card click."""
|
||||
if self.selectable:
|
||||
self.post_message(self.Selected(self))
|
||||
|
||||
def flip(self) -> None:
|
||||
"""Flip the card."""
|
||||
self.face_up = not self.face_up
|
||||
|
||||
def play(self) -> None:
|
||||
"""Play this card."""
|
||||
if self.selectable:
|
||||
self.post_message(self.Played(self))
|
||||
|
||||
|
||||
class Hand(Container):
|
||||
"""Container for a player's hand of cards."""
|
||||
|
||||
DEFAULT_CSS = """
|
||||
Hand {
|
||||
layout: horizontal;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
align: center middle;
|
||||
}
|
||||
|
||||
Hand > Card {
|
||||
margin: 0 1;
|
||||
}
|
||||
"""
|
||||
|
||||
def __init__(self, player_name: str, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.player_name = player_name
|
||||
|
||||
def add_card(self, card: Card) -> None:
|
||||
"""Add a card to this hand."""
|
||||
self.mount(card)
|
||||
|
||||
def remove_card(self, card: Card) -> None:
|
||||
"""Remove a card from this hand."""
|
||||
card.remove()
|
||||
|
||||
def get_cards(self) -> list[Card]:
|
||||
"""Get all cards in this hand."""
|
||||
return list(self.query(Card))
|
||||
|
||||
|
||||
class PlayArea(Container):
|
||||
"""Central area where cards are played."""
|
||||
|
||||
DEFAULT_CSS = """
|
||||
PlayArea {
|
||||
height: 1fr;
|
||||
border: round $primary;
|
||||
background: $surface;
|
||||
align: center middle;
|
||||
}
|
||||
|
||||
PlayArea > .play-area-label {
|
||||
color: $text-muted;
|
||||
}
|
||||
"""
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Label("Play Area", classes="play-area-label")
|
||||
|
||||
|
||||
class GameState(Static):
|
||||
"""Display current game state."""
|
||||
|
||||
DEFAULT_CSS = """
|
||||
GameState {
|
||||
dock: top;
|
||||
height: 3;
|
||||
background: $boost;
|
||||
content-align: center middle;
|
||||
text-style: bold;
|
||||
}
|
||||
"""
|
||||
|
||||
current_player = reactive("Player 1")
|
||||
turn = reactive(1)
|
||||
|
||||
def render(self) -> str:
|
||||
return f"Turn {self.turn} | Current Player: {self.current_player}"
|
||||
|
||||
|
||||
class CardGameApp(App):
|
||||
"""A card game application."""
|
||||
|
||||
CSS_PATH = "app.tcss"
|
||||
|
||||
BINDINGS = [
|
||||
("q", "quit", "Quit"),
|
||||
("n", "next_turn", "Next Turn"),
|
||||
("d", "draw_card", "Draw Card"),
|
||||
("p", "play_selected", "Play Card"),
|
||||
Binding("space", "toggle_select", "Select/Deselect", show=True),
|
||||
]
|
||||
|
||||
selected_card: Card | None = None
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
"""Create child widgets."""
|
||||
yield Header()
|
||||
yield GameState(id="game-state")
|
||||
|
||||
with Vertical(id="game-container"):
|
||||
# Opponent's hand (face down)
|
||||
with Container(id="opponent-area"):
|
||||
yield Label("Opponent", id="opponent-label")
|
||||
yield Hand("Opponent", id="opponent-hand")
|
||||
|
||||
# Play area
|
||||
yield PlayArea(id="play-area")
|
||||
|
||||
# Player's hand (face up)
|
||||
with Container(id="player-area"):
|
||||
yield Hand("Player", id="player-hand")
|
||||
yield Label("Your Hand", id="player-label")
|
||||
|
||||
yield Footer()
|
||||
|
||||
def on_mount(self) -> None:
|
||||
"""Initialize the game when app starts."""
|
||||
# Deal initial cards (example)
|
||||
player_hand = self.query_one("#player-hand", Hand)
|
||||
opponent_hand = self.query_one("#opponent-hand", Hand)
|
||||
|
||||
# Example: Deal 5 cards to each player
|
||||
suits = ["♠", "♥", "♦", "♣"]
|
||||
ranks = ["A", "2", "3", "4", "5"]
|
||||
|
||||
for i, rank in enumerate(ranks):
|
||||
# Player cards (face up)
|
||||
player_hand.add_card(Card(rank, suits[i % 4], face_up=True, card_id=f"player-{i}"))
|
||||
# Opponent cards (face down)
|
||||
opponent_hand.add_card(Card(rank, suits[i % 4], face_up=False, card_id=f"opp-{i}"))
|
||||
|
||||
def on_card_selected(self, event: Card.Selected) -> None:
|
||||
"""Handle card selection."""
|
||||
# Deselect previous
|
||||
if self.selected_card:
|
||||
self.selected_card.remove_class("selected")
|
||||
|
||||
# Select new card
|
||||
self.selected_card = event.card
|
||||
event.card.add_class("selected")
|
||||
self.notify(f"Selected {event.card.rank} of {event.card.suit}")
|
||||
|
||||
def on_card_played(self, event: Card.Played) -> None:
|
||||
"""Handle card being played."""
|
||||
play_area = self.query_one("#play-area", PlayArea)
|
||||
card = event.card
|
||||
|
||||
# Remove from hand
|
||||
hand = card.parent
|
||||
if isinstance(hand, Hand):
|
||||
hand.remove_card(card)
|
||||
|
||||
# Move to play area
|
||||
play_area.mount(card)
|
||||
self.notify(f"Played {card.rank} of {card.suit}")
|
||||
|
||||
# Deselect
|
||||
if self.selected_card == card:
|
||||
self.selected_card = None
|
||||
|
||||
def action_next_turn(self) -> None:
|
||||
"""Advance to next turn."""
|
||||
game_state = self.query_one(GameState)
|
||||
game_state.turn += 1
|
||||
|
||||
# Toggle current player
|
||||
if game_state.current_player == "Player 1":
|
||||
game_state.current_player = "Player 2"
|
||||
else:
|
||||
game_state.current_player = "Player 1"
|
||||
|
||||
self.notify(f"Turn {game_state.turn}")
|
||||
|
||||
def action_draw_card(self) -> None:
|
||||
"""Draw a card (example)."""
|
||||
player_hand = self.query_one("#player-hand", Hand)
|
||||
# Example: Draw a random card
|
||||
import random
|
||||
|
||||
suits = ["♠", "♥", "♦", "♣"]
|
||||
ranks = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"]
|
||||
|
||||
card = Card(random.choice(ranks), random.choice(suits), face_up=True)
|
||||
player_hand.add_card(card)
|
||||
self.notify("Drew a card")
|
||||
|
||||
def action_play_selected(self) -> None:
|
||||
"""Play the currently selected card."""
|
||||
if self.selected_card:
|
||||
self.selected_card.play()
|
||||
else:
|
||||
self.notify("No card selected", severity="warning")
|
||||
|
||||
def action_toggle_select(self) -> None:
|
||||
"""Select/deselect hovered card."""
|
||||
# This is a simplified version - in practice you'd track the hovered card
|
||||
if self.selected_card:
|
||||
self.selected_card.remove_class("selected")
|
||||
self.selected_card = None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = CardGameApp()
|
||||
app.run()
|
||||
51
skills/textual-builder/assets/card-game-template/app.tcss
Normal file
51
skills/textual-builder/assets/card-game-template/app.tcss
Normal file
@@ -0,0 +1,51 @@
|
||||
/* Card Game Template Styles */
|
||||
|
||||
Screen {
|
||||
background: $background;
|
||||
}
|
||||
|
||||
#game-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
layout: vertical;
|
||||
}
|
||||
|
||||
/* Opponent Area */
|
||||
#opponent-area {
|
||||
height: auto;
|
||||
padding: 1 2;
|
||||
background: $surface;
|
||||
}
|
||||
|
||||
#opponent-label {
|
||||
text-align: center;
|
||||
text-style: bold;
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
#opponent-hand {
|
||||
padding: 1 0;
|
||||
}
|
||||
|
||||
/* Play Area */
|
||||
#play-area {
|
||||
height: 1fr;
|
||||
margin: 1 2;
|
||||
}
|
||||
|
||||
/* Player Area */
|
||||
#player-area {
|
||||
height: auto;
|
||||
padding: 1 2;
|
||||
background: $boost;
|
||||
}
|
||||
|
||||
#player-label {
|
||||
text-align: center;
|
||||
text-style: bold;
|
||||
color: $text;
|
||||
}
|
||||
|
||||
#player-hand {
|
||||
padding: 1 0;
|
||||
}
|
||||
182
skills/textual-builder/references/basics.md
Normal file
182
skills/textual-builder/references/basics.md
Normal file
@@ -0,0 +1,182 @@
|
||||
# Textual Basics
|
||||
|
||||
## App Structure
|
||||
|
||||
Every Textual app follows this pattern:
|
||||
|
||||
```python
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.widgets import Widget
|
||||
|
||||
class MyApp(App):
|
||||
"""Docstring describing the app."""
|
||||
|
||||
# Optional: Link to external CSS file
|
||||
CSS_PATH = "app.tcss"
|
||||
|
||||
# Optional: Inline CSS
|
||||
CSS = """
|
||||
Screen {
|
||||
align: center middle;
|
||||
}
|
||||
"""
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
"""Create child widgets."""
|
||||
yield Widget()
|
||||
|
||||
def on_mount(self) -> None:
|
||||
"""Called when app is mounted and ready."""
|
||||
pass
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = MyApp()
|
||||
app.run()
|
||||
```
|
||||
|
||||
## Compose Method
|
||||
|
||||
The `compose()` method yields widgets to add to the app. It's called once during initialization:
|
||||
|
||||
```python
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header()
|
||||
yield ContentWidget()
|
||||
yield Footer()
|
||||
```
|
||||
|
||||
## Mounting
|
||||
|
||||
- `on_mount()`: Called when the app/widget is fully mounted and ready
|
||||
- `mount()`: Dynamically add widgets after app starts (returns a coroutine)
|
||||
|
||||
```python
|
||||
async def on_key(self) -> None:
|
||||
# Must await when modifying mounted widgets
|
||||
await self.mount(NewWidget())
|
||||
self.query_one(Button).label = "Modified!"
|
||||
```
|
||||
|
||||
## Reactive Attributes
|
||||
|
||||
Reactive attributes automatically update the UI when changed:
|
||||
|
||||
```python
|
||||
from textual.reactive import reactive
|
||||
|
||||
class Counter(Widget):
|
||||
count = reactive(0) # Initial value
|
||||
|
||||
def watch_count(self, new_value: int) -> None:
|
||||
"""Called automatically when count changes."""
|
||||
self.query_one(Label).update(f"Count: {new_value}")
|
||||
|
||||
def increment(self) -> None:
|
||||
self.count += 1 # Triggers watch_count
|
||||
```
|
||||
|
||||
### Reactive with Bindings
|
||||
|
||||
Set `bindings=True` to auto-refresh footer bindings when reactive changes:
|
||||
|
||||
```python
|
||||
class MyApp(App):
|
||||
page = reactive(0, bindings=True)
|
||||
|
||||
def check_action(self, action: str, parameters) -> bool | None:
|
||||
"""Return None to disable action."""
|
||||
if action == "next" and self.page == MAX_PAGES:
|
||||
return None # Dims the key in footer
|
||||
return True
|
||||
```
|
||||
|
||||
## Querying Widgets
|
||||
|
||||
Find widgets in the DOM:
|
||||
|
||||
```python
|
||||
# Get one widget (raises if not found)
|
||||
button = self.query_one(Button)
|
||||
button = self.query_one("#my-id")
|
||||
|
||||
# Get multiple widgets
|
||||
all_buttons = self.query(Button)
|
||||
for button in all_buttons:
|
||||
pass
|
||||
|
||||
# Get with CSS selector
|
||||
widget = self.query_one("#container .special-class")
|
||||
```
|
||||
|
||||
## Messages and Events
|
||||
|
||||
### Built-in Events
|
||||
|
||||
Handle with `on_<event>` methods:
|
||||
|
||||
```python
|
||||
def on_mount(self) -> None:
|
||||
"""When mounted."""
|
||||
pass
|
||||
|
||||
def on_key(self, event: events.Key) -> None:
|
||||
"""Key pressed."""
|
||||
if event.key == "escape":
|
||||
self.exit()
|
||||
```
|
||||
|
||||
### Widget Messages
|
||||
|
||||
Handle messages from child widgets:
|
||||
|
||||
```python
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
"""Button was clicked."""
|
||||
self.notify(f"Button {event.button.id} clicked!")
|
||||
|
||||
def on_input_changed(self, event: Input.Changed) -> None:
|
||||
"""Input text changed."""
|
||||
self.value = event.value
|
||||
```
|
||||
|
||||
### Custom Messages
|
||||
|
||||
Define custom messages in your widgets:
|
||||
|
||||
```python
|
||||
from textual.message import Message
|
||||
|
||||
class MyWidget(Widget):
|
||||
class ValueChanged(Message):
|
||||
"""Posted when value changes."""
|
||||
def __init__(self, value: int) -> None:
|
||||
super().__init__()
|
||||
self.value = value
|
||||
|
||||
def update_value(self, new_value: int) -> None:
|
||||
self.value = new_value
|
||||
self.post_message(self.ValueChanged(new_value))
|
||||
|
||||
# Handle in parent
|
||||
def on_my_widget_value_changed(self, event: MyWidget.ValueChanged) -> None:
|
||||
self.notify(f"New value: {event.value}")
|
||||
```
|
||||
|
||||
## Preventing Message Propagation
|
||||
|
||||
Stop messages from bubbling to parent:
|
||||
|
||||
```python
|
||||
def on_switch_changed(self, event: Switch.Changed) -> None:
|
||||
event.stop() # Don't propagate to parent
|
||||
# Handle here
|
||||
```
|
||||
|
||||
## Preventing Reactive Watchers
|
||||
|
||||
Temporarily prevent reactive watchers from firing:
|
||||
|
||||
```python
|
||||
with self.prevent(MyWidget.ValueChanged):
|
||||
self.value = new_value # Won't trigger watch_value or post message
|
||||
```
|
||||
346
skills/textual-builder/references/interactivity.md
Normal file
346
skills/textual-builder/references/interactivity.md
Normal file
@@ -0,0 +1,346 @@
|
||||
# Interactivity: Events, Bindings, and Actions
|
||||
|
||||
## Key Bindings
|
||||
|
||||
Define keyboard shortcuts:
|
||||
|
||||
```python
|
||||
from textual.app import App
|
||||
from textual.binding import Binding
|
||||
|
||||
class MyApp(App):
|
||||
BINDINGS = [
|
||||
("q", "quit", "Quit"), # key, action, description
|
||||
("s", "save", "Save"),
|
||||
("ctrl+c", "copy", "Copy"),
|
||||
Binding("f1", "help", "Help", show=True, priority=True),
|
||||
]
|
||||
|
||||
def action_save(self) -> None:
|
||||
"""Actions are methods prefixed with 'action_'."""
|
||||
self.notify("Saved!")
|
||||
|
||||
def action_copy(self) -> None:
|
||||
self.notify("Copied!")
|
||||
|
||||
def action_help(self) -> None:
|
||||
self.notify("Help content...")
|
||||
```
|
||||
|
||||
### Binding Options
|
||||
|
||||
```python
|
||||
Binding(
|
||||
key="f1",
|
||||
action="help",
|
||||
description="Help",
|
||||
show=True, # Show in footer (default: True)
|
||||
priority=True, # Prioritize over widget bindings
|
||||
)
|
||||
```
|
||||
|
||||
### Dynamic Bindings
|
||||
|
||||
Refresh bindings when state changes:
|
||||
|
||||
```python
|
||||
class MyApp(App):
|
||||
page = reactive(0, bindings=True) # Auto-refresh bindings
|
||||
|
||||
def check_action(self, action: str, parameters) -> bool | None:
|
||||
"""Control action availability."""
|
||||
if action == "next" and self.page >= MAX_PAGES:
|
||||
return None # Disables and dims the key
|
||||
if action == "previous" and self.page == 0:
|
||||
return None
|
||||
return True # Enabled
|
||||
```
|
||||
|
||||
Or manually refresh:
|
||||
|
||||
```python
|
||||
def update_state(self):
|
||||
self.state = "new_state"
|
||||
self.refresh_bindings() # Update footer
|
||||
```
|
||||
|
||||
## Mouse Events
|
||||
|
||||
Handle mouse interactions:
|
||||
|
||||
```python
|
||||
from textual import events
|
||||
|
||||
class MyWidget(Widget):
|
||||
def on_click(self, event: events.Click) -> None:
|
||||
"""Widget was clicked."""
|
||||
self.notify(f"Clicked at {event.x}, {event.y}")
|
||||
|
||||
def on_mouse_move(self, event: events.MouseMove) -> None:
|
||||
"""Mouse moved over widget."""
|
||||
pass
|
||||
|
||||
def on_enter(self, event: events.Enter) -> None:
|
||||
"""Mouse entered widget."""
|
||||
self.add_class("hover")
|
||||
|
||||
def on_leave(self, event: events.Leave) -> None:
|
||||
"""Mouse left widget."""
|
||||
self.remove_class("hover")
|
||||
```
|
||||
|
||||
## Keyboard Events
|
||||
|
||||
Handle key presses:
|
||||
|
||||
```python
|
||||
from textual import events
|
||||
|
||||
class MyApp(App):
|
||||
def on_key(self, event: events.Key) -> None:
|
||||
"""Any key pressed."""
|
||||
if event.key == "escape":
|
||||
self.exit()
|
||||
elif event.key == "space":
|
||||
self.toggle_pause()
|
||||
|
||||
def key_r(self, event: events.Key) -> None:
|
||||
"""Specific key handler (press 'r')."""
|
||||
self.reset()
|
||||
```
|
||||
|
||||
## Focus Events
|
||||
|
||||
Track focus changes:
|
||||
|
||||
```python
|
||||
def on_focus(self, event: events.Focus) -> None:
|
||||
"""Widget gained focus."""
|
||||
self.border_title = "Focused"
|
||||
|
||||
def on_blur(self, event: events.Blur) -> None:
|
||||
"""Widget lost focus."""
|
||||
self.border_title = ""
|
||||
```
|
||||
|
||||
Programmatic focus:
|
||||
|
||||
```python
|
||||
widget.focus() # Give focus to widget
|
||||
widget.can_focus = True # Enable focusing (default for inputs)
|
||||
```
|
||||
|
||||
## Widget Messages
|
||||
|
||||
Handle messages from specific widgets:
|
||||
|
||||
```python
|
||||
from textual.widgets import Button, Input, Switch
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
"""Any button pressed."""
|
||||
button_id = event.button.id
|
||||
self.notify(f"Button {button_id} pressed")
|
||||
|
||||
def on_input_changed(self, event: Input.Changed) -> None:
|
||||
"""Input text changed."""
|
||||
self.update_preview(event.value)
|
||||
|
||||
def on_input_submitted(self, event: Input.Submitted) -> None:
|
||||
"""User pressed Enter in input."""
|
||||
self.process(event.value)
|
||||
|
||||
def on_switch_changed(self, event: Switch.Changed) -> None:
|
||||
"""Switch toggled."""
|
||||
self.feature_enabled = event.value
|
||||
|
||||
def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
|
||||
"""Row in table selected."""
|
||||
row_key = event.row_key
|
||||
```
|
||||
|
||||
### Message Naming Convention
|
||||
|
||||
Handler method: `on_{widget_type}_{message_name}`
|
||||
- Converts to snake_case
|
||||
- Example: `Button.Pressed` → `on_button_pressed`
|
||||
- Custom widget: `MyWidget.ValueChanged` → `on_my_widget_value_changed`
|
||||
|
||||
## Custom Messages
|
||||
|
||||
Define custom messages for your widgets:
|
||||
|
||||
```python
|
||||
from textual.message import Message
|
||||
from textual.widget import Widget
|
||||
|
||||
class Card(Widget):
|
||||
class Selected(Message):
|
||||
"""Posted when card is selected."""
|
||||
def __init__(self, card_id: str, value: int) -> None:
|
||||
super().__init__()
|
||||
self.card_id = card_id
|
||||
self.value = value
|
||||
|
||||
def on_click(self) -> None:
|
||||
self.post_message(self.Selected(self.id, self.value))
|
||||
|
||||
# Handle in parent
|
||||
def on_card_selected(self, event: Card.Selected) -> None:
|
||||
self.notify(f"Card {event.card_id} (value: {event.value}) selected")
|
||||
```
|
||||
|
||||
## Message Control
|
||||
|
||||
### Stop Propagation
|
||||
|
||||
Prevent message from bubbling to parent:
|
||||
|
||||
```python
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
event.stop() # Don't propagate to parent
|
||||
# Handle locally
|
||||
```
|
||||
|
||||
### Prevent Messages
|
||||
|
||||
Temporarily suppress messages:
|
||||
|
||||
```python
|
||||
with widget.prevent(Switch.Changed):
|
||||
widget.value = True # Won't emit Changed message
|
||||
```
|
||||
|
||||
Useful when programmatically updating to avoid infinite loops.
|
||||
|
||||
## Actions
|
||||
|
||||
Actions are methods that can be triggered by bindings or programmatically:
|
||||
|
||||
```python
|
||||
class MyApp(App):
|
||||
BINDINGS = [
|
||||
("n", "next_page", "Next"),
|
||||
("p", "prev_page", "Previous"),
|
||||
]
|
||||
|
||||
def action_next_page(self) -> None:
|
||||
self.page += 1
|
||||
self.refresh_view()
|
||||
|
||||
def action_prev_page(self) -> None:
|
||||
self.page -= 1
|
||||
self.refresh_view()
|
||||
```
|
||||
|
||||
### Parameterized Actions
|
||||
|
||||
Pass parameters to actions:
|
||||
|
||||
```python
|
||||
BINDINGS = [
|
||||
("r", "add_color('red')", "Red"),
|
||||
("g", "add_color('green')", "Green"),
|
||||
("b", "add_color('blue')", "Blue"),
|
||||
]
|
||||
|
||||
def action_add_color(self, color: str) -> None:
|
||||
self.add_widget(ColorBar(color))
|
||||
```
|
||||
|
||||
### Programmatic Action Calls
|
||||
|
||||
```python
|
||||
self.run_action("save") # Trigger action by name
|
||||
```
|
||||
|
||||
## Notifications
|
||||
|
||||
Show temporary messages to user:
|
||||
|
||||
```python
|
||||
self.notify("File saved successfully!")
|
||||
self.notify("Error occurred", severity="error")
|
||||
self.notify("Warning!", severity="warning")
|
||||
self.notify("Info message", severity="information", timeout=5)
|
||||
```
|
||||
|
||||
## Timers
|
||||
|
||||
Schedule repeated actions:
|
||||
|
||||
```python
|
||||
def on_mount(self) -> None:
|
||||
self.set_interval(1.0, self.update_timer) # Every 1 second
|
||||
|
||||
def update_timer(self) -> None:
|
||||
self.elapsed += 1
|
||||
self.query_one("#timer").update(str(self.elapsed))
|
||||
```
|
||||
|
||||
One-time delayed action:
|
||||
|
||||
```python
|
||||
self.set_timer(2.0, self.delayed_action) # After 2 seconds
|
||||
|
||||
def delayed_action(self) -> None:
|
||||
self.notify("Timer complete!")
|
||||
```
|
||||
|
||||
## Example: Interactive Card Selection
|
||||
|
||||
```python
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.containers import Horizontal
|
||||
from textual.widget import Widget
|
||||
from textual.widgets import Label, Static
|
||||
from textual.message import Message
|
||||
|
||||
class Card(Widget):
|
||||
DEFAULT_CSS = """
|
||||
Card {
|
||||
width: 12;
|
||||
height: 10;
|
||||
border: round white;
|
||||
background: $panel;
|
||||
}
|
||||
Card:hover {
|
||||
background: $boost;
|
||||
}
|
||||
Card.selected {
|
||||
border: double cyan;
|
||||
background: $accent;
|
||||
}
|
||||
"""
|
||||
|
||||
class Selected(Message):
|
||||
def __init__(self, card: "Card") -> None:
|
||||
super().__init__()
|
||||
self.card = card
|
||||
|
||||
def __init__(self, suit: str, value: str) -> None:
|
||||
super().__init__()
|
||||
self.suit = suit
|
||||
self.value = value
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Label(f"{self.value}\n{self.suit}")
|
||||
|
||||
def on_click(self) -> None:
|
||||
self.post_message(self.Selected(self))
|
||||
|
||||
class CardGame(App):
|
||||
def compose(self) -> ComposeResult:
|
||||
with Horizontal(id="hand"):
|
||||
yield Card("♠", "A")
|
||||
yield Card("♥", "K")
|
||||
yield Card("♣", "Q")
|
||||
|
||||
def on_card_selected(self, event: Card.Selected) -> None:
|
||||
# Deselect all
|
||||
for card in self.query(Card):
|
||||
card.remove_class("selected")
|
||||
# Select clicked
|
||||
event.card.add_class("selected")
|
||||
self.notify(f"Selected {event.card.value} of {event.card.suit}")
|
||||
```
|
||||
298
skills/textual-builder/references/layout.md
Normal file
298
skills/textual-builder/references/layout.md
Normal file
@@ -0,0 +1,298 @@
|
||||
# Layout and Positioning
|
||||
|
||||
## Layout Types
|
||||
|
||||
### Vertical (Default)
|
||||
|
||||
Stacks widgets vertically:
|
||||
|
||||
```css
|
||||
Container {
|
||||
layout: vertical;
|
||||
}
|
||||
```
|
||||
|
||||
### Horizontal
|
||||
|
||||
Arranges widgets side-by-side:
|
||||
|
||||
```css
|
||||
Container {
|
||||
layout: horizontal;
|
||||
}
|
||||
```
|
||||
|
||||
### Grid
|
||||
|
||||
Grid layout with rows and columns:
|
||||
|
||||
```css
|
||||
Grid {
|
||||
layout: grid;
|
||||
grid-size: 3 2; /* 3 columns, 2 rows */
|
||||
grid-gutter: 1 2; /* vertical horizontal spacing */
|
||||
}
|
||||
```
|
||||
|
||||
#### Grid Cell Spanning
|
||||
|
||||
Make widgets span multiple cells:
|
||||
|
||||
```css
|
||||
#header {
|
||||
column-span: 3; /* Span 3 columns */
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
row-span: 2; /* Span 2 rows */
|
||||
}
|
||||
```
|
||||
|
||||
#### Grid Rows and Columns
|
||||
|
||||
Define row heights and column widths:
|
||||
|
||||
```css
|
||||
Grid {
|
||||
grid-size: 2 3;
|
||||
grid-rows: 1fr 6 25%; /* Flexible, fixed 6, 25% */
|
||||
grid-columns: 1fr 2fr; /* 1:2 ratio */
|
||||
}
|
||||
```
|
||||
|
||||
## Alignment
|
||||
|
||||
### Screen/Container Alignment
|
||||
|
||||
Center content within screen:
|
||||
|
||||
```css
|
||||
Screen {
|
||||
align: center middle; /* horizontal vertical */
|
||||
}
|
||||
```
|
||||
|
||||
Options: `left`, `center`, `right` × `top`, `middle`, `bottom`
|
||||
|
||||
### Content Alignment
|
||||
|
||||
Align content within a widget:
|
||||
|
||||
```css
|
||||
MyWidget {
|
||||
content-align: center middle;
|
||||
text-align: center;
|
||||
}
|
||||
```
|
||||
|
||||
## Docking
|
||||
|
||||
Pin widgets to screen edges:
|
||||
|
||||
```css
|
||||
#header {
|
||||
dock: top;
|
||||
height: 3;
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
dock: left;
|
||||
width: 20;
|
||||
}
|
||||
|
||||
#footer {
|
||||
dock: bottom;
|
||||
}
|
||||
```
|
||||
|
||||
Docking order matters - earlier docked widgets take priority.
|
||||
|
||||
## Sizing
|
||||
|
||||
### Fixed Sizes
|
||||
|
||||
```css
|
||||
Widget {
|
||||
width: 50; /* 50 cells */
|
||||
height: 10; /* 10 rows */
|
||||
}
|
||||
```
|
||||
|
||||
### Relative Sizes
|
||||
|
||||
```css
|
||||
Widget {
|
||||
width: 50%; /* 50% of parent */
|
||||
height: 100%;
|
||||
}
|
||||
```
|
||||
|
||||
### Fractional Units
|
||||
|
||||
Share available space proportionally:
|
||||
|
||||
```css
|
||||
#left {
|
||||
width: 1fr; /* Gets 1 part */
|
||||
}
|
||||
|
||||
#right {
|
||||
width: 2fr; /* Gets 2 parts (twice as wide) */
|
||||
}
|
||||
```
|
||||
|
||||
### Auto Sizing
|
||||
|
||||
Fit content:
|
||||
|
||||
```css
|
||||
Widget {
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
```
|
||||
|
||||
### Min/Max Constraints
|
||||
|
||||
```css
|
||||
Widget {
|
||||
min-width: 20;
|
||||
max-width: 80;
|
||||
min-height: 5;
|
||||
max-height: 30;
|
||||
}
|
||||
```
|
||||
|
||||
## Spacing
|
||||
|
||||
### Margin
|
||||
|
||||
Space outside widget border:
|
||||
|
||||
```css
|
||||
Widget {
|
||||
margin: 1; /* All sides */
|
||||
margin: 1 2; /* vertical horizontal */
|
||||
margin: 1 2 3 4; /* top right bottom left */
|
||||
}
|
||||
```
|
||||
|
||||
### Padding
|
||||
|
||||
Space inside widget border:
|
||||
|
||||
```css
|
||||
Widget {
|
||||
padding: 1; /* All sides */
|
||||
padding: 1 2; /* vertical horizontal */
|
||||
}
|
||||
```
|
||||
|
||||
## Visibility
|
||||
|
||||
### Display
|
||||
|
||||
Show or hide widgets:
|
||||
|
||||
```css
|
||||
#hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#visible {
|
||||
display: block;
|
||||
}
|
||||
```
|
||||
|
||||
Toggle in Python:
|
||||
|
||||
```python
|
||||
widget.display = False # Hide
|
||||
widget.display = True # Show
|
||||
```
|
||||
|
||||
### Visibility
|
||||
|
||||
Similar to display but reserves space:
|
||||
|
||||
```css
|
||||
Widget {
|
||||
visibility: hidden; /* Hidden but takes space */
|
||||
visibility: visible;
|
||||
}
|
||||
```
|
||||
|
||||
## Layers
|
||||
|
||||
Control stacking order:
|
||||
|
||||
```css
|
||||
#background {
|
||||
layer: below;
|
||||
}
|
||||
|
||||
#popup {
|
||||
layer: above;
|
||||
}
|
||||
```
|
||||
|
||||
## Scrolling
|
||||
|
||||
### Enable Scrolling
|
||||
|
||||
```css
|
||||
Container {
|
||||
overflow-x: auto; /* Horizontal scrolling */
|
||||
overflow-y: auto; /* Vertical scrolling */
|
||||
overflow: auto auto; /* Both */
|
||||
}
|
||||
```
|
||||
|
||||
### Programmatic Scrolling
|
||||
|
||||
```python
|
||||
# Scroll to specific position
|
||||
container.scroll_to(x=0, y=100)
|
||||
|
||||
# Scroll widget into view
|
||||
widget.scroll_visible()
|
||||
|
||||
# Scroll to end
|
||||
self.screen.scroll_end(animate=True)
|
||||
```
|
||||
|
||||
## Example: Card Game Layout
|
||||
|
||||
```css
|
||||
Screen {
|
||||
layout: vertical;
|
||||
}
|
||||
|
||||
#opponent-hand {
|
||||
dock: top;
|
||||
height: 12;
|
||||
layout: horizontal;
|
||||
align: center top;
|
||||
}
|
||||
|
||||
#play-area {
|
||||
height: 1fr;
|
||||
layout: grid;
|
||||
grid-size: 5 3;
|
||||
align: center middle;
|
||||
}
|
||||
|
||||
#player-hand {
|
||||
dock: bottom;
|
||||
height: 15;
|
||||
layout: horizontal;
|
||||
align: center bottom;
|
||||
padding: 1;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 12;
|
||||
height: 10;
|
||||
margin: 0 1;
|
||||
}
|
||||
```
|
||||
323
skills/textual-builder/references/styling.md
Normal file
323
skills/textual-builder/references/styling.md
Normal file
@@ -0,0 +1,323 @@
|
||||
# Styling with CSS
|
||||
|
||||
## CSS Files
|
||||
|
||||
Link external CSS file:
|
||||
|
||||
```python
|
||||
class MyApp(App):
|
||||
CSS_PATH = "app.tcss" # Textual CSS file
|
||||
```
|
||||
|
||||
Or inline CSS:
|
||||
|
||||
```python
|
||||
class MyApp(App):
|
||||
CSS = """
|
||||
Screen {
|
||||
background: $background;
|
||||
}
|
||||
"""
|
||||
```
|
||||
|
||||
## Selectors
|
||||
|
||||
### Type Selectors
|
||||
|
||||
Target all widgets of a type:
|
||||
|
||||
```css
|
||||
Button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
Label {
|
||||
color: cyan;
|
||||
}
|
||||
```
|
||||
|
||||
### ID Selectors
|
||||
|
||||
Target specific widget:
|
||||
|
||||
```css
|
||||
#my-button {
|
||||
background: red;
|
||||
}
|
||||
|
||||
#header {
|
||||
dock: top;
|
||||
}
|
||||
```
|
||||
|
||||
### Class Selectors
|
||||
|
||||
Target widgets with specific class:
|
||||
|
||||
```css
|
||||
.card {
|
||||
border: round white;
|
||||
padding: 1;
|
||||
}
|
||||
|
||||
.selected {
|
||||
background: yellow;
|
||||
}
|
||||
```
|
||||
|
||||
Add classes in Python:
|
||||
|
||||
```python
|
||||
widget = Label("Text", classes="card selected")
|
||||
# or
|
||||
widget.add_class("highlighted")
|
||||
widget.remove_class("selected")
|
||||
widget.toggle_class("active")
|
||||
```
|
||||
|
||||
### Pseudo-classes
|
||||
|
||||
Style based on state:
|
||||
|
||||
```css
|
||||
Button:hover {
|
||||
background: $accent;
|
||||
}
|
||||
|
||||
Button:focus {
|
||||
border: double green;
|
||||
}
|
||||
|
||||
Input:disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
```
|
||||
|
||||
Common pseudo-classes: `:hover`, `:focus`, `:focus-within`, `:disabled`, `:enabled`
|
||||
|
||||
### Combinators
|
||||
|
||||
```css
|
||||
/* Direct children */
|
||||
Container > Label {
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Descendants */
|
||||
Container Label {
|
||||
margin: 1;
|
||||
}
|
||||
|
||||
/* Class and type */
|
||||
Label.card {
|
||||
border: round;
|
||||
}
|
||||
```
|
||||
|
||||
## Colors
|
||||
|
||||
### Named Colors
|
||||
|
||||
```css
|
||||
Widget {
|
||||
color: red;
|
||||
background: blue;
|
||||
border: green;
|
||||
}
|
||||
```
|
||||
|
||||
### Hex Colors
|
||||
|
||||
```css
|
||||
Widget {
|
||||
color: #ff0000;
|
||||
background: #00ff0088; /* With alpha */
|
||||
}
|
||||
```
|
||||
|
||||
### RGB/RGBA
|
||||
|
||||
```css
|
||||
Widget {
|
||||
color: rgb(255, 0, 0);
|
||||
background: rgba(0, 255, 0, 0.5);
|
||||
}
|
||||
```
|
||||
|
||||
### Theme Variables
|
||||
|
||||
Use built-in theme colors:
|
||||
|
||||
```css
|
||||
Widget {
|
||||
background: $background;
|
||||
color: $text;
|
||||
border: $primary;
|
||||
}
|
||||
```
|
||||
|
||||
Common theme variables:
|
||||
- `$background` - Main background
|
||||
- `$surface` - Surface color
|
||||
- `$panel` - Panel background
|
||||
- `$boost` - Highlighted background
|
||||
- `$primary` - Primary accent
|
||||
- `$secondary` - Secondary accent
|
||||
- `$accent` - Accent color
|
||||
- `$text` - Main text color
|
||||
- `$text-muted` - Muted text
|
||||
- `$foreground-muted` - Dimmed foreground
|
||||
|
||||
## Borders
|
||||
|
||||
### Border Styles
|
||||
|
||||
```css
|
||||
Widget {
|
||||
border: solid red; /* Style and color */
|
||||
border: round cyan; /* Rounded border */
|
||||
border: double white; /* Double line */
|
||||
border: dashed yellow; /* Dashed */
|
||||
border: heavy green; /* Heavy/thick */
|
||||
border: tall blue; /* Tall characters */
|
||||
}
|
||||
```
|
||||
|
||||
### Border Sides
|
||||
|
||||
```css
|
||||
Widget {
|
||||
border-top: solid red;
|
||||
border-bottom: round blue;
|
||||
border-left: double green;
|
||||
border-right: dashed yellow;
|
||||
}
|
||||
```
|
||||
|
||||
### Border Title
|
||||
|
||||
```css
|
||||
Widget {
|
||||
border: round white;
|
||||
border-title-align: center;
|
||||
}
|
||||
```
|
||||
|
||||
Set title in Python:
|
||||
|
||||
```python
|
||||
widget.border_title = "My Widget"
|
||||
```
|
||||
|
||||
## Text Styling
|
||||
|
||||
### Text Properties
|
||||
|
||||
```css
|
||||
Label {
|
||||
text-style: bold;
|
||||
text-style: italic;
|
||||
text-style: bold italic;
|
||||
text-style: underline;
|
||||
text-style: strike;
|
||||
}
|
||||
```
|
||||
|
||||
### Text Alignment
|
||||
|
||||
```css
|
||||
Static {
|
||||
text-align: left;
|
||||
text-align: center;
|
||||
text-align: right;
|
||||
}
|
||||
```
|
||||
|
||||
## Keylines
|
||||
|
||||
Add separators between grid cells or flex items:
|
||||
|
||||
```css
|
||||
Grid {
|
||||
keyline: thin green;
|
||||
keyline: thick $primary;
|
||||
}
|
||||
```
|
||||
|
||||
Note: Must be on a container with a layout.
|
||||
|
||||
## Opacity
|
||||
|
||||
```css
|
||||
Widget {
|
||||
opacity: 0.5; /* 50% transparent */
|
||||
opacity: 0; /* Fully transparent */
|
||||
opacity: 1; /* Fully opaque */
|
||||
}
|
||||
```
|
||||
|
||||
## Tint
|
||||
|
||||
Apply color overlay:
|
||||
|
||||
```css
|
||||
Widget {
|
||||
tint: rgba(255, 0, 0, 0.3); /* Red tint */
|
||||
}
|
||||
```
|
||||
|
||||
## Rich Markup
|
||||
|
||||
Use Rich markup in text:
|
||||
|
||||
```python
|
||||
label = Label("[bold cyan]Hello[/bold cyan] [red]World[/red]")
|
||||
label.update("[underline]Updated[/underline]")
|
||||
```
|
||||
|
||||
Common markup:
|
||||
- `[bold]...[/bold]` - Bold
|
||||
- `[italic]...[/italic]` - Italic
|
||||
- `[color]...[/color]` - Colored (e.g., `[red]`, `[#ff0000]`)
|
||||
- `[underline]...[/underline]` - Underline
|
||||
- `[strike]...[/strike]` - Strikethrough
|
||||
- `[link=...]...[/link]` - Link
|
||||
|
||||
## Example: Card Styling
|
||||
|
||||
```css
|
||||
.card {
|
||||
width: 12;
|
||||
height: 10;
|
||||
border: round $secondary;
|
||||
background: $panel;
|
||||
padding: 1;
|
||||
content-align: center middle;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
background: $boost;
|
||||
border: heavy $primary;
|
||||
}
|
||||
|
||||
.card.selected {
|
||||
background: $accent;
|
||||
border: double $primary;
|
||||
}
|
||||
|
||||
.card.disabled {
|
||||
opacity: 0.5;
|
||||
tint: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
text-style: bold;
|
||||
text-align: center;
|
||||
color: $text;
|
||||
}
|
||||
|
||||
.card-value {
|
||||
text-align: center;
|
||||
color: $text-muted;
|
||||
}
|
||||
```
|
||||
241
skills/textual-builder/references/widgets.md
Normal file
241
skills/textual-builder/references/widgets.md
Normal file
@@ -0,0 +1,241 @@
|
||||
# Common Widgets
|
||||
|
||||
## Display Widgets
|
||||
|
||||
### Label / Static
|
||||
|
||||
Display static or updatable text:
|
||||
|
||||
```python
|
||||
from textual.widgets import Label, Static
|
||||
|
||||
# Label is just an alias for Static
|
||||
label = Label("Hello World")
|
||||
static = Static("Initial text")
|
||||
|
||||
# Update later
|
||||
static.update("New text")
|
||||
static.update("[bold]Rich markup[/bold]")
|
||||
```
|
||||
|
||||
### Placeholder
|
||||
|
||||
Useful for prototyping layouts:
|
||||
|
||||
```python
|
||||
from textual.widgets import Placeholder
|
||||
|
||||
# Shows widget ID and size info
|
||||
yield Placeholder("Custom label", id="p1")
|
||||
yield Placeholder(variant="size") # Shows dimensions
|
||||
yield Placeholder(variant="text") # Shows placeholder text
|
||||
```
|
||||
|
||||
## Input Widgets
|
||||
|
||||
### Button
|
||||
|
||||
```python
|
||||
from textual.widgets import Button
|
||||
|
||||
yield Button("Click Me", id="my-button")
|
||||
yield Button("Disabled", disabled=True)
|
||||
|
||||
# Handle click
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
button_id = event.button.id
|
||||
self.notify(f"{button_id} clicked!")
|
||||
```
|
||||
|
||||
### Input
|
||||
|
||||
Single-line text input:
|
||||
|
||||
```python
|
||||
from textual.widgets import Input
|
||||
|
||||
yield Input(placeholder="Enter text...", id="name-input")
|
||||
|
||||
def on_input_changed(self, event: Input.Changed) -> None:
|
||||
self.text_value = event.value
|
||||
|
||||
def on_input_submitted(self, event: Input.Submitted) -> None:
|
||||
# User pressed Enter
|
||||
self.process_input(event.value)
|
||||
```
|
||||
|
||||
### TextArea
|
||||
|
||||
Multi-line text editor:
|
||||
|
||||
```python
|
||||
from textual.widgets import TextArea
|
||||
|
||||
text_area = TextArea()
|
||||
text_area.load_text("Initial content")
|
||||
|
||||
# Get content
|
||||
content = text_area.text
|
||||
```
|
||||
|
||||
### Switch
|
||||
|
||||
Toggle switch (like checkbox):
|
||||
|
||||
```python
|
||||
from textual.widgets import Switch
|
||||
|
||||
yield Switch(value=True) # Initially on
|
||||
|
||||
def on_switch_changed(self, event: Switch.Changed) -> None:
|
||||
is_on = event.value
|
||||
self.toggle_feature(is_on)
|
||||
```
|
||||
|
||||
## Data Display
|
||||
|
||||
### DataTable
|
||||
|
||||
Display tabular data:
|
||||
|
||||
```python
|
||||
from textual.widgets import DataTable
|
||||
|
||||
table = DataTable()
|
||||
|
||||
# Add columns
|
||||
table.add_columns("Name", "Age", "Country")
|
||||
|
||||
# Add rows
|
||||
table.add_row("Alice", 30, "USA")
|
||||
table.add_row("Bob", 25, "UK")
|
||||
|
||||
# Add row with custom label
|
||||
from rich.text import Text
|
||||
label = Text("1", style="bold cyan")
|
||||
table.add_row("Charlie", 35, "Canada", label=label)
|
||||
|
||||
# Configuration
|
||||
table.zebra_stripes = True # Alternating row colors
|
||||
table.cursor_type = "row" # "cell", "row", "column", or "none"
|
||||
table.show_header = True
|
||||
table.show_row_labels = True
|
||||
|
||||
# Handle selection
|
||||
def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
|
||||
row_key = event.row_key
|
||||
row_data = table.get_row(row_key)
|
||||
|
||||
def on_data_table_cell_selected(self, event: DataTable.CellSelected) -> None:
|
||||
value = event.value
|
||||
coordinate = event.coordinate
|
||||
```
|
||||
|
||||
## Layout Containers
|
||||
|
||||
### Container
|
||||
|
||||
Generic container for grouping widgets:
|
||||
|
||||
```python
|
||||
from textual.containers import Container
|
||||
|
||||
with Container(id="sidebar"):
|
||||
yield Label("Title")
|
||||
yield Button("Action")
|
||||
```
|
||||
|
||||
### Vertical / Horizontal / VerticalScroll / HorizontalScroll
|
||||
|
||||
Directional containers:
|
||||
|
||||
```python
|
||||
from textual.containers import Vertical, Horizontal, VerticalScroll
|
||||
|
||||
with Horizontal():
|
||||
yield Button("Left")
|
||||
yield Button("Right")
|
||||
|
||||
with VerticalScroll():
|
||||
for i in range(100):
|
||||
yield Label(f"Item {i}")
|
||||
```
|
||||
|
||||
### Grid
|
||||
|
||||
Grid layout container:
|
||||
|
||||
```python
|
||||
from textual.containers import Grid
|
||||
|
||||
with Grid(id="my-grid"):
|
||||
yield Label("A")
|
||||
yield Label("B")
|
||||
yield Label("C")
|
||||
yield Label("D")
|
||||
|
||||
# Style in CSS:
|
||||
# Grid {
|
||||
# grid-size: 2 2; /* 2 columns, 2 rows */
|
||||
# }
|
||||
```
|
||||
|
||||
## App Widgets
|
||||
|
||||
### Header / Footer
|
||||
|
||||
Standard app chrome:
|
||||
|
||||
```python
|
||||
from textual.widgets import Header, Footer
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header()
|
||||
# ... content ...
|
||||
yield Footer()
|
||||
```
|
||||
|
||||
Footer automatically shows key bindings defined in BINDINGS.
|
||||
|
||||
## Custom Widgets
|
||||
|
||||
Create reusable components:
|
||||
|
||||
```python
|
||||
from textual.widget import Widget
|
||||
from textual.widgets import Label, Button
|
||||
|
||||
class Card(Widget):
|
||||
"""A card widget with title and content."""
|
||||
|
||||
DEFAULT_CSS = """
|
||||
Card {
|
||||
width: 30;
|
||||
height: 15;
|
||||
border: round white;
|
||||
padding: 1;
|
||||
}
|
||||
"""
|
||||
|
||||
def __init__(self, title: str, content: str) -> None:
|
||||
super().__init__()
|
||||
self.title = title
|
||||
self.content = content
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Label(self.title, classes="card-title")
|
||||
yield Label(self.content, classes="card-content")
|
||||
yield Button("Select", id=f"select-{self.title}")
|
||||
```
|
||||
|
||||
### Render Method
|
||||
|
||||
For simple custom widgets that just render text:
|
||||
|
||||
```python
|
||||
from textual.widget import Widget
|
||||
|
||||
class FizzBuzz(Widget):
|
||||
def render(self) -> str:
|
||||
return "FizzBuzz!"
|
||||
```
|
||||
Reference in New Issue
Block a user