libtodraw 1.0.0__py3-none-any.whl

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.
libtodraw/__init__.py ADDED
@@ -0,0 +1,58 @@
1
+ """libtodraw — Rich on steroids. Full-featured terminal rendering library."""
2
+
3
+ __version__ = "1.0.0"
4
+
5
+ from .color import Color
6
+ from .style import Style, StyleType
7
+ from .text import Text
8
+ from .segment import Segment
9
+ from .measure import Measurement
10
+ from .options import ConsoleOptions, JustifyMethod
11
+ from .console import Console
12
+ from .live import Live
13
+ from .status import Status
14
+ from .capture import Capture
15
+ from .syntax import Syntax, SyntaxTheme
16
+ from .markdown_render import Markdown
17
+ from .inspector import inspect as pretty_inspect
18
+ from .traceback_handler import install as install_traceback
19
+ from .highlighter import Highlighter
20
+ from .theme import Theme, DEFAULT_THEME
21
+ from .markup import parse as parse_markup
22
+ from .terminal import get_size, detect_truecolor, detect_unicode
23
+ from .export import export_text, export_html
24
+
25
+ from .renderables.table import Table, Column
26
+ from .renderables.panel import Panel, Borders
27
+ from .renderables.tree import Tree, TreeNode
28
+ from .renderables.columns import Columns
29
+ from .renderables.layout import Layout
30
+ from .renderables.rule import Rule
31
+ from .renderables.progress import Progress, TaskID, BarColumn, SpinnerColumn, TextColumn, TimeRemainingColumn
32
+ from .renderables.spinner import Spinner
33
+ from .canvas import Canvas, Cell, CanvasRegion
34
+
35
+ console = Console()
36
+
37
+ print = console.print
38
+ log = console.log
39
+ input = console.input
40
+ clear = console.clear
41
+ rule = console.rule
42
+ status = console.status
43
+
44
+ __all__ = [
45
+ "Console", "ConsoleOptions", "Measurement", "Capture", "Status",
46
+ "Color", "Style", "Text", "Segment",
47
+ "Table", "Column", "Panel", "Borders",
48
+ "Tree", "TreeNode", "Columns", "Layout", "Rule",
49
+ "Progress", "TaskID", "BarColumn", "SpinnerColumn", "TextColumn", "TimeRemainingColumn",
50
+ "Spinner", "Live",
51
+ "Canvas", "Cell", "CanvasRegion",
52
+ "Markdown", "Syntax", "SyntaxTheme",
53
+ "Theme", "DEFAULT_THEME", "Highlighter",
54
+ "parse_markup", "get_size", "detect_truecolor", "detect_unicode",
55
+ "export_text", "export_html",
56
+ "pretty_inspect", "install_traceback",
57
+ "console", "print", "log", "input", "clear", "rule", "status",
58
+ ]
libtodraw/canvas.py ADDED
@@ -0,0 +1,530 @@
1
+ """Canvas — pixel-level control over every character in the terminal."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ import shutil
7
+ from typing import Optional, Tuple, List
8
+
9
+ from .style import Style
10
+ from .color import Color
11
+
12
+
13
+ def _visible_len(s: str) -> int:
14
+ return len(re.sub(r'\033\[[0-9;]*m', '', s))
15
+
16
+
17
+ class Cell:
18
+ """A single terminal cell with full style control."""
19
+
20
+ __slots__ = ("char", "style", "z_index")
21
+
22
+ def __init__(self, char: str = " ", style: Optional[Style] = None, z_index: int = 0):
23
+ self.char = char
24
+ self.style = style
25
+ self.z_index = z_index
26
+
27
+ def __repr__(self):
28
+ return f"Cell({self.char!r}, {self.style!r})"
29
+
30
+
31
+ class Canvas:
32
+ """Full character-level terminal buffer. Control every cell."""
33
+
34
+ def __init__(self, width: int = 80, height: int = 24):
35
+ self.width = width
36
+ self.height = height
37
+ self._cells: List[List[Cell]] = [
38
+ [Cell() for _ in range(width)] for _ in range(height)
39
+ ]
40
+
41
+ def _in_bounds(self, x: int, y: int) -> bool:
42
+ return 0 <= x < self.width and 0 <= y < self.height
43
+
44
+ # ── Pixel-level control ──
45
+
46
+ def set(self, x: int, y: int, char: str, style: Optional[Style] = None) -> Canvas:
47
+ """Set a single cell. Returns self for chaining."""
48
+ if self._in_bounds(x, y):
49
+ self._cells[y][x].char = char
50
+ self._cells[y][x].style = style
51
+ return self
52
+
53
+ def get(self, x: int, y: int) -> Cell:
54
+ """Get a single cell."""
55
+ if self._in_bounds(x, y):
56
+ return self._cells[y][x]
57
+ return Cell()
58
+
59
+ def set_style(self, x: int, y: int, style: Style) -> Canvas:
60
+ """Set style on a cell without changing its character."""
61
+ if self._in_bounds(x, y):
62
+ self._cells[y][x].style = style
63
+ return self
64
+
65
+ def set_char(self, x: int, y: int, char: str) -> Canvas:
66
+ """Set a character without changing its style."""
67
+ if self._in_bounds(x, y):
68
+ self._cells[y][x].char = char
69
+ return self
70
+
71
+ def clear_cell(self, x: int, y: int) -> Canvas:
72
+ """Reset a cell to blank with no style."""
73
+ if self._in_bounds(x, y):
74
+ self._cells[y][x] = Cell()
75
+ return self
76
+
77
+ # ── Bulk operations ──
78
+
79
+ def fill(self, x: int, y: int, w: int, h: int, char: str = " ",
80
+ style: Optional[Style] = None) -> Canvas:
81
+ """Fill a rectangular region."""
82
+ for dy in range(h):
83
+ for dx in range(w):
84
+ self.set(x + dx, y + dy, char, style)
85
+ return self
86
+
87
+ def clear(self, x: int = 0, y: int = 0, w: Optional[int] = None,
88
+ h: Optional[int] = None) -> Canvas:
89
+ """Clear a region (or entire canvas if no args)."""
90
+ w = w or self.width
91
+ h = h or self.height
92
+ for dy in range(h):
93
+ for dx in range(w):
94
+ if self._in_bounds(x + dx, y + dy):
95
+ self._cells[y + dy][x + dx] = Cell()
96
+ return self
97
+
98
+ def clear_all(self) -> Canvas:
99
+ """Clear entire canvas."""
100
+ return self.clear(0, 0, self.width, self.height)
101
+
102
+ # ── Drawing primitives ──
103
+
104
+ def draw_rect(self, x: int, y: int, w: int, h: int,
105
+ border: Optional[Style] = None, fill: Optional[Style] = None,
106
+ chars: Optional[dict] = None) -> Canvas:
107
+ """Draw a rectangle with optional border and fill.
108
+
109
+ chars: override border characters.
110
+ keys: tl, tr, bl, br, hor, ver, fill
111
+ """
112
+ c = chars or {}
113
+ tl = c.get("tl", "┌")
114
+ tr = c.get("tr", "┐")
115
+ bl = c.get("bl", "└")
116
+ br = c.get("br", "┘")
117
+ hor = c.get("hor", "─")
118
+ ver = c.get("ver", "│")
119
+ fill_ch = c.get("fill", " ")
120
+
121
+ # Corners
122
+ self.set(x, y, tl, border)
123
+ self.set(x + w - 1, y, tr, border)
124
+ self.set(x, y + h - 1, bl, border)
125
+ self.set(x + w - 1, y + h - 1, br, border)
126
+
127
+ # Horizontal edges
128
+ for i in range(1, w - 1):
129
+ self.set(x + i, y, hor, border)
130
+ self.set(x + i, y + h - 1, hor, border)
131
+
132
+ # Vertical edges
133
+ for i in range(1, h - 1):
134
+ self.set(x, y + i, ver, border)
135
+ self.set(x + w - 1, y + i, ver, border)
136
+
137
+ # Fill interior
138
+ if fill:
139
+ for dy in range(1, h - 1):
140
+ for dx in range(1, w - 1):
141
+ self.set(x + dx, y + dy, fill_ch, fill)
142
+
143
+ return self
144
+
145
+ def draw_hline(self, x: int, y: int, length: int, char: str = "─",
146
+ style: Optional[Style] = None) -> Canvas:
147
+ """Draw a horizontal line."""
148
+ for i in range(length):
149
+ self.set(x + i, y, char, style)
150
+ return self
151
+
152
+ def draw_vline(self, x: int, y: int, length: int, char: str = "│",
153
+ style: Optional[Style] = None) -> Canvas:
154
+ """Draw a vertical line."""
155
+ for i in range(length):
156
+ self.set(x, y + i, char, style)
157
+ return self
158
+
159
+ def draw_box(self, x: int, y: int, w: int, h: int,
160
+ style: Optional[Style] = None, box_type: str = "simple") -> Canvas:
161
+ """Draw a box using predefined box types.
162
+
163
+ box_type: simple, rounded, heavy, double, dashed, none
164
+ """
165
+ boxes = {
166
+ "simple": {"tl": "┌", "tr": "┐", "bl": "└", "br": "┘", "hor": "─", "ver": "│"},
167
+ "rounded": {"tl": "╭", "tr": "╮", "bl": "╰", "br": "╯", "hor": "─", "ver": "│"},
168
+ "heavy": {"tl": "┏", "tr": "┓", "bl": "┗", "br": "┛", "hor": "━", "ver": "┃"},
169
+ "double": {"tl": "╔", "tr": "╗", "bl": "╚", "br": "╝", "hor": "═", "ver": "║"},
170
+ "dashed": {"tl": "┌", "tr": "┐", "bl": "└", "br": "┘", "hor": "╌", "ver": "╎"},
171
+ "dotted": {"tl": "•", "tr": "•", "bl": "•", "br": "•", "hor": "·", "ver": "⋮"},
172
+ }
173
+ chars = boxes.get(box_type, boxes["simple"])
174
+ return self.draw_rect(x, y, w, h, border=style, chars=chars)
175
+
176
+ # ── Text placement ──
177
+
178
+ def put(self, x: int, y: int, text: str, style: Optional[Style] = None) -> Canvas:
179
+ """Place text at position, character by character. Handles newlines."""
180
+ cx, cy = x, y
181
+ for ch in text:
182
+ if ch == "\n":
183
+ cx = x
184
+ cy += 1
185
+ continue
186
+ self.set(cx, cy, ch, style)
187
+ cx += 1
188
+ return self
189
+
190
+ def put_centered(self, y: int, text: str, style: Optional[Style] = None) -> Canvas:
191
+ """Place text centered horizontally on row y."""
192
+ tw = _visible_len(text)
193
+ x = max(0, (self.width - tw) // 2)
194
+ return self.put(x, y, text, style)
195
+
196
+ def put_right(self, y: int, text: str, style: Optional[Style] = None) -> Canvas:
197
+ """Place text right-aligned on row y."""
198
+ tw = _visible_len(text)
199
+ x = max(0, self.width - tw)
200
+ return self.put(x, y, text, style)
201
+
202
+ def put_at(self, x: int, y: int, text: str, style: Optional[Style] = None,
203
+ align: str = "left", max_width: Optional[int] = None) -> Canvas:
204
+ """Place text with alignment control.
205
+
206
+ align: left, center, right
207
+ max_width: if set, pad/truncate to this width
208
+ """
209
+ tw = _visible_len(text)
210
+ if max_width and tw > max_width:
211
+ text = text[:max_width]
212
+ tw = max_width
213
+
214
+ if align == "center":
215
+ pad = (max_width or self.width) // 2 - tw // 2
216
+ x = x + pad
217
+ elif align == "right":
218
+ pad = (max_width or self.width) - tw
219
+ x = x + max(0, pad)
220
+
221
+ return self.put(x, y, text, style)
222
+
223
+ def write_line(self, y: int, x: int, text: str, style: Optional[Style] = None,
224
+ justify: str = "left", width: Optional[int] = None) -> Canvas:
225
+ """Write a full line with justification and padding.
226
+
227
+ justify: left, center, right, full
228
+ width: line width for justification
229
+ """
230
+ w = width or self.width
231
+ tw = _visible_len(text)
232
+
233
+ if justify == "center":
234
+ pad = (w - tw) // 2
235
+ full = " " * pad + text + " " * (w - tw - pad)
236
+ elif justify == "right":
237
+ full = " " * (w - tw) + text
238
+ elif justify == "full":
239
+ words = text.split()
240
+ if len(words) <= 1:
241
+ full = text.ljust(w)
242
+ else:
243
+ total_chars = sum(len(w) for w in words)
244
+ total_spaces = w - total_chars
245
+ space_each = total_spaces // (len(words) - 1) if len(words) > 1 else 0
246
+ extra = total_spaces % (len(words) - 1) if len(words) > 1 else 0
247
+ parts = []
248
+ for i, word in enumerate(words):
249
+ parts.append(word)
250
+ if i < len(words) - 1:
251
+ spaces = space_each + (1 if i < extra else 0)
252
+ parts.append(" " * spaces)
253
+ full = "".join(parts)
254
+ else:
255
+ full = text.ljust(w)
256
+
257
+ return self.put(x, y, full, style)
258
+
259
+ # ── Gradient fill ──
260
+
261
+ def gradient_fill(self, x: int, y: int, w: int, h: int,
262
+ color_start: Color, color_end: Color,
263
+ char: str = "█", horizontal: bool = True) -> Canvas:
264
+ """Fill a region with a color gradient."""
265
+ length = w if horizontal else h
266
+ for i in range(length):
267
+ t = i / max(length - 1, 1)
268
+ color = color_start.lerp(color_end, t)
269
+ style = Style(color=color)
270
+ if horizontal:
271
+ for dy in range(h):
272
+ self.set(x + i, y + dy, char, style)
273
+ else:
274
+ for dx in range(w):
275
+ self.set(x + dx, y + i, char, style)
276
+ return self
277
+
278
+ def gradient_border(self, x: int, y: int, w: int, h: int,
279
+ color_start: Color, color_end: Color,
280
+ box_type: str = "simple") -> Canvas:
281
+ """Draw a box border with gradient colors along the perimeter."""
282
+ boxes = {
283
+ "simple": {"tl": "┌", "tr": "┐", "bl": "└", "br": "┘", "hor": "─", "ver": "│"},
284
+ "rounded": {"tl": "╭", "tr": "╮", "bl": "╰", "br": "╯", "hor": "─", "ver": "│"},
285
+ "heavy": {"tl": "┏", "tr": "┓", "bl": "┗", "br": "┛", "hor": "━", "ver": "┃"},
286
+ "double": {"tl": "╔", "tr": "╗", "bl": "╚", "br": "╝", "hor": "═", "ver": "║"},
287
+ }
288
+ c = boxes.get(box_type, boxes["simple"])
289
+
290
+ # Horizontal gradient for top/bottom
291
+ for i in range(w):
292
+ t = i / max(w - 1, 1)
293
+ color = color_start.lerp(color_end, t)
294
+ style = Style(color=color)
295
+ ch = c["hor"] if 0 < i < w - 1 else (c["tl"] if i == 0 else c["tr"])
296
+ self.set(x + i, y, ch, style)
297
+ ch = c["hor"] if 0 < i < w - 1 else (c["bl"] if i == 0 else c["br"])
298
+ self.set(x + i, y + h - 1, ch, style)
299
+
300
+ # Vertical gradient for sides
301
+ for i in range(1, h - 1):
302
+ t = i / max(h - 1, 1)
303
+ color = color_start.lerp(color_end, t)
304
+ style = Style(color=color)
305
+ self.set(x, y + i, c["ver"], style)
306
+ self.set(x + w - 1, y + i, c["ver"], style)
307
+
308
+ return self
309
+
310
+ # ── Circle / ellipse ──
311
+
312
+ def draw_circle(self, cx: int, cy: int, radius: int,
313
+ char: str = "●", style: Optional[Style] = None,
314
+ filled: bool = False) -> Canvas:
315
+ """Draw a circle (or filled circle)."""
316
+ if filled:
317
+ for y in range(-radius, radius + 1):
318
+ for x in range(-radius, radius + 1):
319
+ if x * x + y * y <= radius * radius:
320
+ self.set(cx + x, cy + y, char, style)
321
+ else:
322
+ x, y = radius, 0
323
+ err = 1 - radius
324
+ while x >= y:
325
+ for dx, dy in [(x, y), (y, x), (-y, x), (-x, y),
326
+ (-x, -y), (-y, -x), (y, -x), (x, -y)]:
327
+ self.set(cx + dx, cy + dy, char, style)
328
+ y += 1
329
+ if err < 0:
330
+ err += 2 * y + 1
331
+ else:
332
+ x -= 1
333
+ err += 2 * (y - x) + 1
334
+ return self
335
+
336
+ def draw_line(self, x0: int, y0: int, x1: int, y1: int,
337
+ char: str = "·", style: Optional[Style] = None) -> Canvas:
338
+ """Draw a line between two points using Bresenham's algorithm."""
339
+ dx = abs(x1 - x0)
340
+ dy = abs(y1 - y0)
341
+ sx = 1 if x0 < x1 else -1
342
+ sy = 1 if y0 < y1 else -1
343
+ err = dx - dy
344
+
345
+ while True:
346
+ self.set(x0, y0, char, style)
347
+ if x0 == x1 and y0 == y1:
348
+ break
349
+ e2 = 2 * err
350
+ if e2 > -dy:
351
+ err -= dy
352
+ x0 += sx
353
+ if e2 < dx:
354
+ err += dx
355
+ y0 += sy
356
+ return self
357
+
358
+ # ── Resize ──
359
+
360
+ def resize(self, width: int, height: int) -> Canvas:
361
+ """Resize the canvas, preserving existing content where possible."""
362
+ new_cells = [
363
+ [Cell() for _ in range(width)] for _ in range(height)
364
+ ]
365
+ for y in range(min(self.height, height)):
366
+ for x in range(min(self.width, width)):
367
+ new_cells[y][x] = self._cells[y][x]
368
+ self.width = width
369
+ self.height = height
370
+ self._cells = new_cells
371
+ return self
372
+
373
+ # ── Overlay / compositing ──
374
+
375
+ def overlay(self, other: Canvas, x: int = 0, y: int = 0,
376
+ overwrite: bool = True) -> Canvas:
377
+ """Overlay another canvas onto this one.
378
+
379
+ overwrite: if False, only paint non-blank cells.
380
+ """
381
+ for oy in range(other.height):
382
+ for ox in range(other.width):
383
+ tx, ty = x + ox, y + oy
384
+ if self._in_bounds(tx, ty):
385
+ src = other._cells[oy][ox]
386
+ if overwrite or src.char != " ":
387
+ self._cells[ty][tx] = Cell(src.char, src.style)
388
+ return self
389
+
390
+ def copy(self) -> Canvas:
391
+ """Create a deep copy of this canvas."""
392
+ c = Canvas(self.width, self.height)
393
+ for y in range(self.height):
394
+ for x in range(self.width):
395
+ src = self._cells[y][x]
396
+ c._cells[y][x] = Cell(src.char, src.style, src.z_index)
397
+ return c
398
+
399
+ # ── Inspection ──
400
+
401
+ def get_row(self, y: int) -> List[Cell]:
402
+ """Get all cells in a row."""
403
+ if 0 <= y < self.height:
404
+ return list(self._cells[y])
405
+ return []
406
+
407
+ def get_col(self, x: int) -> List[Cell]:
408
+ """Get all cells in a column."""
409
+ if 0 <= x < self.width:
410
+ return [self._cells[y][x] for y in range(self.height)]
411
+ return []
412
+
413
+ def get_region(self, x: int, y: int, w: int, h: int) -> List[List[Cell]]:
414
+ """Get a rectangular region of cells."""
415
+ result = []
416
+ for dy in range(h):
417
+ row = []
418
+ for dx in range(w):
419
+ if self._in_bounds(x + dx, y + dy):
420
+ row.append(self._cells[y + dy][x + dx])
421
+ else:
422
+ row.append(Cell())
423
+ result.append(row)
424
+ return result
425
+
426
+ def find(self, char: str) -> List[Tuple[int, int]]:
427
+ """Find all positions of a character."""
428
+ positions = []
429
+ for y in range(self.height):
430
+ for x in range(self.width):
431
+ if self._cells[y][x].char == char:
432
+ positions.append((x, y))
433
+ return positions
434
+
435
+ def to_text(self) -> str:
436
+ """Convert canvas to plain text (no ANSI)."""
437
+ lines = []
438
+ for y in range(self.height):
439
+ line = "".join(self._cells[y][x].char for x in range(self.width))
440
+ lines.append(line.rstrip())
441
+ return "\n".join(lines)
442
+
443
+ # ── Render ──
444
+
445
+ def render(self) -> str:
446
+ """Render canvas to ANSI string for terminal output."""
447
+ lines = []
448
+ for y in range(self.height):
449
+ parts = []
450
+ last_style = None
451
+ for x in range(self.width):
452
+ cell = self._cells[y][x]
453
+ if cell.style != last_style:
454
+ if last_style is not None:
455
+ parts.append(Style.RESET)
456
+ if cell.style is not None:
457
+ parts.append(cell.style.to_ansi())
458
+ last_style = cell.style
459
+ parts.append(cell.char)
460
+ if last_style is not None:
461
+ parts.append(Style.RESET)
462
+ lines.append("".join(parts))
463
+ return "\n".join(lines)
464
+
465
+ def print(self, file=None) -> None:
466
+ """Print canvas to file (or stdout)."""
467
+ import sys
468
+ f = file or sys.stdout
469
+ f.write("\033[H")
470
+ f.write(self.render())
471
+ f.write("\n")
472
+ f.flush()
473
+
474
+ # ── Context ──
475
+
476
+ def region(self, x: int, y: int, w: int, h: int) -> "CanvasRegion":
477
+ """Get a sub-region for scoped drawing."""
478
+ return CanvasRegion(self, x, y, w, h)
479
+
480
+ def __getitem__(self, pos: Tuple[int, int]) -> Cell:
481
+ x, y = pos
482
+ return self.get(x, y)
483
+
484
+ def __setitem__(self, pos: Tuple[int, int], value):
485
+ x, y = pos
486
+ if isinstance(value, tuple):
487
+ char, style = value
488
+ self.set(x, y, char, style)
489
+ else:
490
+ self.set_char(x, y, str(value))
491
+
492
+ def __repr__(self):
493
+ return f"Canvas({self.width}x{self.height})"
494
+
495
+
496
+ class CanvasRegion:
497
+ """A sub-region of a canvas for scoped drawing."""
498
+
499
+ def __init__(self, canvas: Canvas, x: int, y: int, width: int, height: int):
500
+ self._canvas = canvas
501
+ self._ox = x
502
+ self._oy = y
503
+ self.width = width
504
+ self.height = height
505
+
506
+ def set(self, x: int, y: int, char: str, style: Optional[Style] = None) -> CanvasRegion:
507
+ self._canvas.set(self._ox + x, self._oy + y, char, style)
508
+ return self
509
+
510
+ def put(self, x: int, y: int, text: str, style: Optional[Style] = None) -> CanvasRegion:
511
+ self._canvas.put(self._ox + x, self._oy + y, text, style)
512
+ return self
513
+
514
+ def fill(self, char: str = " ", style: Optional[Style] = None) -> CanvasRegion:
515
+ for y in range(self.height):
516
+ for x in range(self.width):
517
+ self._canvas.set(self._ox + x, self._oy + y, char, style)
518
+ return self
519
+
520
+ def clear(self) -> CanvasRegion:
521
+ return self.fill()
522
+
523
+ def draw_box(self, style: Optional[Style] = None,
524
+ box_type: str = "simple") -> CanvasRegion:
525
+ self._canvas.draw_box(self._ox, self._oy, self.width, self.height,
526
+ style=style, box_type=box_type)
527
+ return self
528
+
529
+ def __repr__(self):
530
+ return f"CanvasRegion({self._ox},{self._oy} {self.width}x{self.height})"
libtodraw/capture.py ADDED
@@ -0,0 +1,37 @@
1
+ """Output capture context manager."""
2
+
3
+ import sys
4
+ from io import StringIO
5
+ from typing import Optional, TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from .console import Console
9
+
10
+
11
+ class Capture:
12
+ """Context manager for capturing console output."""
13
+
14
+ def __init__(self, console: "Console"):
15
+ self._console = console
16
+ self._buffer: Optional[StringIO] = None
17
+ self._original_capture: Optional[StringIO] = None
18
+
19
+ def __enter__(self) -> "Capture":
20
+ self._buffer = StringIO()
21
+ self._original_capture = self._console._capture_buffer
22
+ self._console._capture_buffer = self._buffer
23
+ return self
24
+
25
+ def __exit__(self, exc_type: object, exc_val: object, exc_tb: object) -> None:
26
+ self._console._capture_buffer = self._original_capture
27
+
28
+ def get(self) -> str:
29
+ """Get captured output."""
30
+ if self._buffer is not None:
31
+ return self._buffer.getvalue()
32
+ return ""
33
+
34
+ @property
35
+ def text(self) -> str:
36
+ """Get captured text."""
37
+ return self.get()