or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

content.mdcore-framework.mdevents.mdindex.mdstyling.mdtesting.mdwidgets.md

content.mddocs/

0

# Content and Rendering

1

2

Rich content system for displaying formatted text, custom renderables for specialized visualizations, content processing utilities, and DOM manipulation functions for widget tree traversal.

3

4

## Capabilities

5

6

### Rich Content System

7

8

Textual's content system allows for rich text display with styling spans and formatting.

9

10

```python { .api }

11

class Content:

12

"""Rich text content with styling spans."""

13

14

def __init__(self, text: str = "", spans: list[Span] | None = None):

15

"""

16

Initialize content.

17

18

Parameters:

19

- text: Plain text content

20

- spans: List of styling spans

21

"""

22

23

def __str__(self) -> str:

24

"""Get plain text representation."""

25

26

def __len__(self) -> int:

27

"""Get content length."""

28

29

def copy(self) -> Content:

30

"""Create a copy of the content."""

31

32

def append(self, text: str, *, style: Style | None = None) -> None:

33

"""

34

Append text with optional styling.

35

36

Parameters:

37

- text: Text to append

38

- style: Rich Style object for formatting

39

"""

40

41

def append_text(self, text: str) -> None:

42

"""Append plain text without styling."""

43

44

def extend(self, content: Content) -> None:

45

"""

46

Extend with another Content object.

47

48

Parameters:

49

- content: Content to append

50

"""

51

52

def split(self, separator: str = "\n") -> list[Content]:

53

"""

54

Split content by separator.

55

56

Parameters:

57

- separator: String to split on

58

59

Returns:

60

List of Content objects

61

"""

62

63

def truncate(self, max_length: int, *, overflow: str = "ellipsis") -> Content:

64

"""

65

Truncate content to maximum length.

66

67

Parameters:

68

- max_length: Maximum character length

69

- overflow: How to handle overflow ("ellipsis", "ignore")

70

71

Returns:

72

Truncated content

73

"""

74

75

# Properties

76

text: str # Plain text without styling

77

spans: list[Span] # List of styling spans

78

cell_length: int # Length in terminal cells

79

80

class Span:

81

"""Text styling span."""

82

83

def __init__(self, start: int, end: int, style: Style):

84

"""

85

Initialize a span.

86

87

Parameters:

88

- start: Start character index

89

- end: End character index

90

- style: Rich Style object

91

"""

92

93

def __contains__(self, index: int) -> bool:

94

"""Check if index is within span."""

95

96

# Properties

97

start: int # Start character index

98

end: int # End character index

99

style: Style # Rich Style object

100

```

101

102

### Strip Rendering

103

104

Strip system for efficient line-by-line rendering in Textual.

105

106

```python { .api }

107

class Strip:

108

"""Horizontal line of styled text cells."""

109

110

def __init__(self, segments: list[Segment] | None = None):

111

"""

112

Initialize strip.

113

114

Parameters:

115

- segments: List of styled text segments

116

"""

117

118

@classmethod

119

def blank(cls, length: int, *, style: Style | None = None) -> Strip:

120

"""

121

Create blank strip.

122

123

Parameters:

124

- length: Strip length in characters

125

- style: Optional styling

126

127

Returns:

128

Blank Strip instance

129

"""

130

131

def __len__(self) -> int:

132

"""Get strip length."""

133

134

def __bool__(self) -> bool:

135

"""Check if strip has content."""

136

137

def crop(self, start: int, end: int | None = None) -> Strip:

138

"""

139

Crop strip to range.

140

141

Parameters:

142

- start: Start index

143

- end: End index (None for end of strip)

144

145

Returns:

146

Cropped strip

147

"""

148

149

def extend(self, strips: Iterable[Strip]) -> Strip:

150

"""

151

Extend with other strips.

152

153

Parameters:

154

- strips: Strips to append

155

156

Returns:

157

Extended strip

158

"""

159

160

def apply_style(self, style: Style) -> Strip:

161

"""

162

Apply style to entire strip.

163

164

Parameters:

165

- style: Style to apply

166

167

Returns:

168

Styled strip

169

"""

170

171

# Properties

172

text: str # Plain text content

173

cell_length: int # Length in terminal cells

174

175

class Segment:

176

"""Text segment with styling."""

177

178

def __init__(self, text: str, style: Style | None = None):

179

"""

180

Initialize segment.

181

182

Parameters:

183

- text: Text content

184

- style: Optional Rich Style

185

"""

186

187

# Properties

188

text: str # Text content

189

style: Style | None # Styling information

190

```

191

192

### Custom Renderables

193

194

Specialized rendering components for charts, progress bars, and other visualizations.

195

196

```python { .api }

197

class Bar:

198

"""Progress bar renderable."""

199

200

def __init__(

201

self,

202

size: Size,

203

*,

204

highlight_range: tuple[float, float] | None = None,

205

foreground_style: Style | None = None,

206

background_style: Style | None = None,

207

complete_style: Style | None = None,

208

):

209

"""

210

Initialize bar renderable.

211

212

Parameters:

213

- size: Bar dimensions

214

- highlight_range: Range to highlight (0.0-1.0)

215

- foreground_style: Foreground styling

216

- background_style: Background styling

217

- complete_style: Completed portion styling

218

"""

219

220

def __rich_console__(self, console, options) -> RenderResult:

221

"""Render the bar for Rich console."""

222

223

class Digits:

224

"""Digital display renderable for large numbers."""

225

226

def __init__(

227

self,

228

text: str,

229

*,

230

style: Style | None = None

231

):

232

"""

233

Initialize digits display.

234

235

Parameters:

236

- text: Text/numbers to display

237

- style: Display styling

238

"""

239

240

def __rich_console__(self, console, options) -> RenderResult:

241

"""Render digits for Rich console."""

242

243

class Sparkline:

244

"""Sparkline chart renderable."""

245

246

def __init__(

247

self,

248

data: Sequence[float],

249

*,

250

width: int | None = None,

251

min_color: Color | None = None,

252

max_color: Color | None = None,

253

summary_color: Color | None = None,

254

):

255

"""

256

Initialize sparkline.

257

258

Parameters:

259

- data: Numeric data points

260

- width: Chart width (None for auto)

261

- min_color: Color for minimum values

262

- max_color: Color for maximum values

263

- summary_color: Color for summary statistics

264

"""

265

266

def __rich_console__(self, console, options) -> RenderResult:

267

"""Render sparkline for Rich console."""

268

269

class Gradient:

270

"""Color gradient renderable."""

271

272

def __init__(

273

self,

274

size: Size,

275

stops: Sequence[tuple[float, Color]],

276

direction: str = "horizontal"

277

):

278

"""

279

Initialize gradient.

280

281

Parameters:

282

- size: Gradient dimensions

283

- stops: Color stops as (position, color) tuples

284

- direction: "horizontal" or "vertical"

285

"""

286

287

def __rich_console__(self, console, options) -> RenderResult:

288

"""Render gradient for Rich console."""

289

290

class Blank:

291

"""Empty space renderable."""

292

293

def __init__(self, width: int, height: int, *, style: Style | None = None):

294

"""

295

Initialize blank space.

296

297

Parameters:

298

- width: Width in characters

299

- height: Height in lines

300

- style: Optional background styling

301

"""

302

303

def __rich_console__(self, console, options) -> RenderResult:

304

"""Render blank space for Rich console."""

305

```

306

307

### DOM Tree Traversal

308

309

Utilities for walking and querying the widget DOM tree.

310

311

```python { .api }

312

def walk_children(

313

node: DOMNode,

314

*,

315

reverse: bool = False,

316

with_root: bool = True

317

) -> Iterator[DOMNode]:

318

"""

319

Walk immediate children of a DOM node.

320

321

Parameters:

322

- node: Starting DOM node

323

- reverse: Walk in reverse order

324

- with_root: Include the root node

325

326

Yields:

327

Child DOM nodes

328

"""

329

330

def walk_depth_first(

331

root: DOMNode,

332

*,

333

reverse: bool = False,

334

with_root: bool = True

335

) -> Iterator[DOMNode]:

336

"""

337

Walk DOM tree depth-first.

338

339

Parameters:

340

- root: Root node to start from

341

- reverse: Traverse in reverse order

342

- with_root: Include the root node

343

344

Yields:

345

DOM nodes in depth-first order

346

"""

347

348

def walk_breadth_first(

349

root: DOMNode,

350

*,

351

reverse: bool = False,

352

with_root: bool = True

353

) -> Iterator[DOMNode]:

354

"""

355

Walk DOM tree breadth-first.

356

357

Parameters:

358

- root: Root node to start from

359

- reverse: Traverse in reverse order

360

- with_root: Include the root node

361

362

Yields:

363

DOM nodes in breadth-first order

364

"""

365

366

class DOMNode:

367

"""Base DOM node with CSS query support."""

368

369

def query(self, selector: str) -> DOMQuery[DOMNode]:

370

"""

371

Query descendant nodes with CSS selector.

372

373

Parameters:

374

- selector: CSS selector string

375

376

Returns:

377

Query result set

378

"""

379

380

def query_one(

381

self,

382

selector: str,

383

expected_type: type[ExpectedType] = DOMNode

384

) -> ExpectedType:

385

"""

386

Query for single descendant node.

387

388

Parameters:

389

- selector: CSS selector string

390

- expected_type: Expected node type

391

392

Returns:

393

Single matching node

394

395

Raises:

396

NoMatches: If no nodes match

397

WrongType: If node is wrong type

398

TooManyMatches: If multiple nodes match

399

"""

400

401

def remove_class(self, *class_names: str) -> None:

402

"""

403

Remove CSS classes.

404

405

Parameters:

406

- *class_names: Class names to remove

407

"""

408

409

def add_class(self, *class_names: str) -> None:

410

"""

411

Add CSS classes.

412

413

Parameters:

414

- *class_names: Class names to add

415

"""

416

417

def toggle_class(self, *class_names: str) -> None:

418

"""

419

Toggle CSS classes.

420

421

Parameters:

422

- *class_names: Class names to toggle

423

"""

424

425

def has_class(self, class_name: str) -> bool:

426

"""

427

Check if node has CSS class.

428

429

Parameters:

430

- class_name: Class name to check

431

432

Returns:

433

True if class is present

434

"""

435

436

# Properties

437

id: str | None # Unique identifier

438

classes: set[str] # CSS classes

439

parent: DOMNode | None # Parent node

440

children: list[DOMNode] # Child nodes

441

ancestors: list[DOMNode] # All ancestor nodes

442

ancestors_with_self: list[DOMNode] # Ancestors including self

443

444

class DOMQuery:

445

"""Result set from DOM queries."""

446

447

def __init__(self, nodes: Iterable[DOMNode]):

448

"""

449

Initialize query result.

450

451

Parameters:

452

- nodes: Matching DOM nodes

453

"""

454

455

def __len__(self) -> int:

456

"""Get number of matching nodes."""

457

458

def __iter__(self) -> Iterator[DOMNode]:

459

"""Iterate over matching nodes."""

460

461

def __bool__(self) -> bool:

462

"""Check if query has results."""

463

464

def first(self, expected_type: type[ExpectedType] = DOMNode) -> ExpectedType:

465

"""

466

Get first matching node.

467

468

Parameters:

469

- expected_type: Expected node type

470

471

Returns:

472

First matching node

473

"""

474

475

def last(self, expected_type: type[ExpectedType] = DOMNode) -> ExpectedType:

476

"""

477

Get last matching node.

478

479

Parameters:

480

- expected_type: Expected node type

481

482

Returns:

483

Last matching node

484

"""

485

486

def remove(self) -> None:

487

"""Remove all matching nodes from DOM."""

488

489

def add_class(self, *class_names: str) -> None:

490

"""Add CSS classes to all matching nodes."""

491

492

def remove_class(self, *class_names: str) -> None:

493

"""Remove CSS classes from all matching nodes."""

494

495

def set_styles(self, **styles) -> None:

496

"""Set styles on all matching nodes."""

497

498

class NoMatches(Exception):

499

"""Raised when DOM query finds no matches."""

500

501

class WrongType(Exception):

502

"""Raised when DOM node is wrong type."""

503

504

class TooManyMatches(Exception):

505

"""Raised when DOM query finds too many matches."""

506

```

507

508

### Content Processing Utilities

509

510

```python { .api }

511

def strip_links(content: Content) -> Content:

512

"""

513

Remove all links from content.

514

515

Parameters:

516

- content: Content to process

517

518

Returns:

519

Content with links removed

520

"""

521

522

def highlight_words(

523

content: Content,

524

words: Iterable[str],

525

*,

526

style: Style,

527

case_sensitive: bool = False

528

) -> Content:

529

"""

530

Highlight specific words in content.

531

532

Parameters:

533

- content: Content to process

534

- words: Words to highlight

535

- style: Highlight style

536

- case_sensitive: Whether matching is case sensitive

537

538

Returns:

539

Content with highlighted words

540

"""

541

542

def truncate_middle(

543

text: str,

544

length: int,

545

*,

546

ellipsis: str = "…"

547

) -> str:

548

"""

549

Truncate text in the middle.

550

551

Parameters:

552

- text: Text to truncate

553

- length: Target length

554

- ellipsis: Ellipsis character

555

556

Returns:

557

Truncated text

558

"""

559

```

560

561

## Usage Examples

562

563

### Rich Content Creation

564

565

```python

566

from textual.app import App

567

from textual.widgets import Static

568

from textual.content import Content, Span

569

from rich.style import Style

570

571

class ContentApp(App):

572

def compose(self):

573

# Create rich content with spans

574

content = Content("Hello, bold world!")

575

content.spans.append(

576

Span(7, 11, Style(bold=True, color="red"))

577

)

578

579

yield Static(content, id="rich-text")

580

581

# Alternative: build content incrementally

582

content2 = Content()

583

content2.append("Normal text ")

584

content2.append("highlighted", style=Style(bgcolor="yellow"))

585

content2.append(" and back to normal")

586

587

yield Static(content2, id="incremental")

588

589

def on_mount(self):

590

# Manipulate content after creation

591

static = self.query_one("#rich-text", Static)

592

current_content = static.renderable

593

594

# Truncate if too long

595

if len(current_content.text) > 20:

596

truncated = current_content.truncate(20)

597

static.update(truncated)

598

```

599

600

### Custom Renderables

601

602

```python

603

from textual.app import App

604

from textual.widgets import Static

605

from textual.renderables import Sparkline, Digits, Bar

606

from textual.geometry import Size

607

608

class RenderableApp(App):

609

def compose(self):

610

# Sparkline chart

611

data = [1, 3, 2, 5, 4, 6, 3, 2, 4, 1]

612

sparkline = Sparkline(data, width=20)

613

yield Static(sparkline, id="chart")

614

615

# Digital display

616

digits = Digits("12:34")

617

yield Static(digits, id="clock")

618

619

# Progress bar

620

bar = Bar(Size(30, 1), highlight_range=(0.0, 0.7))

621

yield Static(bar, id="progress")

622

623

def update_displays(self):

624

"""Update the displays with new data."""

625

import time

626

627

# Update clock

628

current_time = time.strftime("%H:%M")

629

digits = Digits(current_time)

630

self.query_one("#clock", Static).update(digits)

631

632

# Update progress

633

import random

634

progress = random.random()

635

bar = Bar(Size(30, 1), highlight_range=(0.0, progress))

636

self.query_one("#progress", Static).update(bar)

637

```

638

639

### DOM Tree Traversal

640

641

```python

642

from textual.app import App

643

from textual.containers import Container, Horizontal

644

from textual.widgets import Button, Static

645

from textual.walk import walk_depth_first, walk_breadth_first

646

647

class TraversalApp(App):

648

def compose(self):

649

with Container(id="main"):

650

yield Static("Header", id="header")

651

with Horizontal(id="buttons"):

652

yield Button("Button 1", id="btn1")

653

yield Button("Button 2", id="btn2")

654

yield Static("Footer", id="footer")

655

656

def on_mount(self):

657

# Walk all widgets depth-first

658

main_container = self.query_one("#main")

659

660

self.log("Depth-first traversal:")

661

for widget in walk_depth_first(main_container):

662

self.log(f" {widget.__class__.__name__}: {widget.id}")

663

664

self.log("Breadth-first traversal:")

665

for widget in walk_breadth_first(main_container):

666

self.log(f" {widget.__class__.__name__}: {widget.id}")

667

668

def on_button_pressed(self, event: Button.Pressed):

669

# Find all buttons in the app

670

all_buttons = self.query("Button")

671

self.log(f"Found {len(all_buttons)} buttons")

672

673

# Find parent container of clicked button

674

parent = event.button.parent

675

while parent and not parent.id == "main":

676

parent = parent.parent

677

678

if parent:

679

self.log(f"Button is in container: {parent.id}")

680

```

681

682

### CSS Queries and DOM Manipulation

683

684

```python

685

from textual.app import App

686

from textual.widgets import Button, Static

687

from textual.containers import Container

688

689

class QueryApp(App):

690

def compose(self):

691

with Container():

692

yield Static("Status: Ready", classes="status info")

693

yield Button("Success", classes="action success")

694

yield Button("Warning", classes="action warning")

695

yield Button("Error", classes="action error")

696

697

def on_button_pressed(self, event: Button.Pressed):

698

# Query by class

699

status = self.query_one(".status", Static)

700

701

# Update based on button type

702

if event.button.has_class("success"):

703

status.update("Status: Success!")

704

status.remove_class("info", "warning", "error")

705

status.add_class("success")

706

707

elif event.button.has_class("warning"):

708

status.update("Status: Warning!")

709

status.remove_class("info", "success", "error")

710

status.add_class("warning")

711

712

elif event.button.has_class("error"):

713

status.update("Status: Error!")

714

status.remove_class("info", "success", "warning")

715

status.add_class("error")

716

717

# Style all action buttons

718

action_buttons = self.query(".action")

719

action_buttons.set_styles(opacity=0.7)

720

721

# Highlight clicked button

722

event.button.styles.opacity = 1.0

723

```

724

725

### Strip-based Custom Widget

726

727

```python

728

from textual.widget import Widget

729

from textual.strip import Strip

730

from textual.geometry import Size

731

from rich.segment import Segment

732

from rich.style import Style

733

734

class ProgressWidget(Widget):

735

"""Custom widget using Strip rendering."""

736

737

def __init__(self, progress: float = 0.0):

738

super().__init__()

739

self.progress = max(0.0, min(1.0, progress))

740

741

def render_line(self, y: int) -> Strip:

742

"""Render a single line using Strip."""

743

if y != 0: # Only render on first line

744

return Strip.blank(self.size.width)

745

746

# Calculate progress bar dimensions

747

width = self.size.width

748

filled_width = int(width * self.progress)

749

750

# Create segments for filled and empty portions

751

segments = []

752

753

if filled_width > 0:

754

segments.append(

755

Segment("█" * filled_width, Style(color="green"))

756

)

757

758

if filled_width < width:

759

segments.append(

760

Segment("░" * (width - filled_width), Style(color="gray"))

761

)

762

763

return Strip(segments)

764

765

def get_content_width(self, container: Size, viewport: Size) -> tuple[int, int]:

766

"""Get content width."""

767

return (20, 20) # Fixed width

768

769

def get_content_height(self, container: Size, viewport: Size, width: int) -> tuple[int, int]:

770

"""Get content height."""

771

return (1, 1) # Single line

772

```