py-sparcli 0.3.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.
- py_sparcli-0.3.0.dist-info/METADATA +257 -0
- py_sparcli-0.3.0.dist-info/RECORD +61 -0
- py_sparcli-0.3.0.dist-info/WHEEL +4 -0
- py_sparcli-0.3.0.dist-info/licenses/LICENSE +21 -0
- sparcli/__init__.py +184 -0
- sparcli/core/__init__.py +92 -0
- sparcli/core/border.py +160 -0
- sparcli/core/color.py +306 -0
- sparcli/core/cursor.py +59 -0
- sparcli/core/geometry.py +141 -0
- sparcli/core/markup.py +215 -0
- sparcli/core/render.py +245 -0
- sparcli/core/style.py +128 -0
- sparcli/core/terminal.py +127 -0
- sparcli/core/text.py +143 -0
- sparcli/core/theme.py +117 -0
- sparcli/core/width.py +244 -0
- sparcli/errors.py +38 -0
- sparcli/input/__init__.py +42 -0
- sparcli/input/calc.py +214 -0
- sparcli/input/confirm.py +188 -0
- sparcli/input/datepicker.py +298 -0
- sparcli/input/editor.py +189 -0
- sparcli/input/event.py +551 -0
- sparcli/input/field.py +90 -0
- sparcli/input/fuzzy.py +401 -0
- sparcli/input/guard.py +103 -0
- sparcli/input/history.py +160 -0
- sparcli/input/line_edit.py +229 -0
- sparcli/input/number.py +232 -0
- sparcli/input/outcome.py +107 -0
- sparcli/input/password.py +233 -0
- sparcli/input/prompt.py +134 -0
- sparcli/input/select.py +336 -0
- sparcli/input/shortcut.py +123 -0
- sparcli/input/text.py +564 -0
- sparcli/input/textarea.py +242 -0
- sparcli/input/validate.py +67 -0
- sparcli/output/__init__.py +56 -0
- sparcli/output/alert.py +107 -0
- sparcli/output/badge.py +82 -0
- sparcli/output/box.py +196 -0
- sparcli/output/columns.py +157 -0
- sparcli/output/compose.py +111 -0
- sparcli/output/diff.py +229 -0
- sparcli/output/kv.py +175 -0
- sparcli/output/layout.py +77 -0
- sparcli/output/list.py +274 -0
- sparcli/output/live.py +129 -0
- sparcli/output/multiprogress.py +73 -0
- sparcli/output/pager.py +83 -0
- sparcli/output/panel.py +125 -0
- sparcli/output/progress.py +233 -0
- sparcli/output/rule.py +134 -0
- sparcli/output/spinner.py +138 -0
- sparcli/output/table/__init__.py +17 -0
- sparcli/output/table/plan.py +198 -0
- sparcli/output/table/render.py +302 -0
- sparcli/output/table/table.py +351 -0
- sparcli/output/tree.py +157 -0
- sparcli/py.typed +0 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
"""
|
|
2
|
+
sparcli.output.table.table
|
|
3
|
+
===========================
|
|
4
|
+
|
|
5
|
+
Defines the public table classes :class:`Table`, :class:`Column` and
|
|
6
|
+
:class:`Cell`.
|
|
7
|
+
|
|
8
|
+
A :class:`Table` lays out rows of :class:`Cell` values under a set of
|
|
9
|
+
:class:`Column` definitions, honoring per-column alignment, width constraints
|
|
10
|
+
and word wrap, plus zebra striping, a title, footer rows and horizontal or
|
|
11
|
+
vertical cell spanning. Each class offers both a keyword constructor and fluent
|
|
12
|
+
builder methods, so ``Column("Name").align(Align.RIGHT)`` reads like the Rust
|
|
13
|
+
original. The heavy lifting lives in :mod:`sparcli.output.table.plan` (grid and
|
|
14
|
+
width computation) and :mod:`sparcli.output.table.render` (line assembly).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from collections.abc import Iterable
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
|
|
22
|
+
from sparcli.core.border import BorderType
|
|
23
|
+
from sparcli.core.geometry import Align
|
|
24
|
+
from sparcli.core.render import Renderable, Rendered
|
|
25
|
+
from sparcli.core.style import Style
|
|
26
|
+
from sparcli.core.text import IntoText, Text, into_text
|
|
27
|
+
from sparcli.core.theme import theme
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Column:
|
|
31
|
+
"""
|
|
32
|
+
A single table column definition.
|
|
33
|
+
|
|
34
|
+
Attributes
|
|
35
|
+
----------
|
|
36
|
+
header : Text
|
|
37
|
+
The column header text.
|
|
38
|
+
alignment : Align
|
|
39
|
+
Horizontal alignment applied to header and body cells.
|
|
40
|
+
width_min : int
|
|
41
|
+
The minimum column width in columns.
|
|
42
|
+
width_max : int | None
|
|
43
|
+
The maximum column width, or ``None`` for unbounded.
|
|
44
|
+
width_fixed : int | None
|
|
45
|
+
A fixed column width that overrides the natural width.
|
|
46
|
+
wrapping : bool
|
|
47
|
+
Whether overflow reflows onto more lines instead of truncating.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
__slots__ = (
|
|
51
|
+
"header",
|
|
52
|
+
"alignment",
|
|
53
|
+
"width_min",
|
|
54
|
+
"width_max",
|
|
55
|
+
"width_fixed",
|
|
56
|
+
"wrapping",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
*,
|
|
62
|
+
header: IntoText = "",
|
|
63
|
+
alignment: Align = Align.LEFT,
|
|
64
|
+
width_min: int = 0,
|
|
65
|
+
width_max: int | None = None,
|
|
66
|
+
width_fixed: int | None = None,
|
|
67
|
+
wrapping: bool = False,
|
|
68
|
+
) -> None:
|
|
69
|
+
self.header: Text = into_text(header)
|
|
70
|
+
self.alignment: Align = alignment
|
|
71
|
+
self.width_min: int = width_min
|
|
72
|
+
self.width_max: int | None = width_max
|
|
73
|
+
self.width_fixed: int | None = width_fixed
|
|
74
|
+
self.wrapping: bool = wrapping
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def new(cls, header: IntoText) -> Column:
|
|
78
|
+
"""Returns a left-aligned column with the given header."""
|
|
79
|
+
return cls(header=header)
|
|
80
|
+
|
|
81
|
+
def align(self, align: Align) -> Column:
|
|
82
|
+
"""Sets the column alignment and returns the column."""
|
|
83
|
+
self.alignment = align
|
|
84
|
+
return self
|
|
85
|
+
|
|
86
|
+
def min_width(self, width: int) -> Column:
|
|
87
|
+
"""Sets the minimum column width and returns the column."""
|
|
88
|
+
self.width_min = width
|
|
89
|
+
return self
|
|
90
|
+
|
|
91
|
+
def max_width(self, width: int) -> Column:
|
|
92
|
+
"""Sets the maximum column width and returns the column."""
|
|
93
|
+
self.width_max = width
|
|
94
|
+
return self
|
|
95
|
+
|
|
96
|
+
def fixed_width(self, width: int) -> Column:
|
|
97
|
+
"""Sets a fixed column width and returns the column."""
|
|
98
|
+
self.width_fixed = width
|
|
99
|
+
return self
|
|
100
|
+
|
|
101
|
+
def wrap(self) -> Column:
|
|
102
|
+
"""Enables word wrapping instead of truncation and returns self."""
|
|
103
|
+
self.wrapping = True
|
|
104
|
+
return self
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class Cell:
|
|
108
|
+
"""
|
|
109
|
+
A single table cell.
|
|
110
|
+
|
|
111
|
+
Attributes
|
|
112
|
+
----------
|
|
113
|
+
content : Text
|
|
114
|
+
The cell content.
|
|
115
|
+
alignment : Align | None
|
|
116
|
+
An override alignment, or ``None`` to inherit the column alignment.
|
|
117
|
+
span_cols : int
|
|
118
|
+
The number of columns the cell spans (at least ``1``).
|
|
119
|
+
span_rows : int
|
|
120
|
+
The number of rows the cell spans (at least ``1``).
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
__slots__ = ("content", "alignment", "span_cols", "span_rows")
|
|
124
|
+
|
|
125
|
+
def __init__(
|
|
126
|
+
self,
|
|
127
|
+
*,
|
|
128
|
+
content: IntoText = "",
|
|
129
|
+
alignment: Align | None = None,
|
|
130
|
+
span_cols: int = 1,
|
|
131
|
+
span_rows: int = 1,
|
|
132
|
+
) -> None:
|
|
133
|
+
self.content: Text = into_text(content)
|
|
134
|
+
self.alignment: Align | None = alignment
|
|
135
|
+
self.span_cols: int = max(span_cols, 1)
|
|
136
|
+
self.span_rows: int = max(span_rows, 1)
|
|
137
|
+
|
|
138
|
+
@classmethod
|
|
139
|
+
def new(cls, content: IntoText) -> Cell:
|
|
140
|
+
"""Returns a cell holding ``content`` with no span."""
|
|
141
|
+
return cls(content=content)
|
|
142
|
+
|
|
143
|
+
def align(self, align: Align) -> Cell:
|
|
144
|
+
"""Overrides the cell alignment and returns the cell."""
|
|
145
|
+
self.alignment = align
|
|
146
|
+
return self
|
|
147
|
+
|
|
148
|
+
def colspan(self, columns: int) -> Cell:
|
|
149
|
+
"""Spans the cell across ``columns`` columns and returns the cell."""
|
|
150
|
+
self.span_cols = max(columns, 1)
|
|
151
|
+
return self
|
|
152
|
+
|
|
153
|
+
def rowspan(self, rows: int) -> Cell:
|
|
154
|
+
"""Spans the cell across ``rows`` rows and returns the cell."""
|
|
155
|
+
self.span_rows = max(rows, 1)
|
|
156
|
+
return self
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
IntoColumn = Column | IntoText
|
|
160
|
+
IntoCell = Cell | IntoText
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def into_column(value: IntoColumn) -> Column:
|
|
164
|
+
"""Coerces a string, rich text or column into a :class:`Column`."""
|
|
165
|
+
if isinstance(value, Column):
|
|
166
|
+
return value
|
|
167
|
+
return Column.new(value)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def into_cell(value: IntoCell) -> Cell:
|
|
171
|
+
"""Coerces a string, rich text or cell into a :class:`Cell`."""
|
|
172
|
+
if isinstance(value, Cell):
|
|
173
|
+
return value
|
|
174
|
+
return Cell.new(value)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@dataclass(slots=True)
|
|
178
|
+
class Row:
|
|
179
|
+
"""A table row of cells, flagged as body or footer."""
|
|
180
|
+
|
|
181
|
+
cells: list[Cell]
|
|
182
|
+
footer: bool
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@dataclass(slots=True)
|
|
186
|
+
class TableOpts:
|
|
187
|
+
"""The styling and layout options shared with the plan and renderer."""
|
|
188
|
+
|
|
189
|
+
border: BorderType = BorderType.ROUNDED
|
|
190
|
+
border_style: Style = field(default_factory=Style)
|
|
191
|
+
header: bool = True
|
|
192
|
+
header_style: Style = field(default_factory=Style)
|
|
193
|
+
striped: bool = False
|
|
194
|
+
stripe_style: Style = field(default_factory=lambda: Style().dim())
|
|
195
|
+
title: Text | None = None
|
|
196
|
+
title_style: Style = field(default_factory=Style)
|
|
197
|
+
pad: int = 1
|
|
198
|
+
row_separators: bool = False
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@dataclass(slots=True)
|
|
202
|
+
class TableLayout:
|
|
203
|
+
"""The column definitions and options handed to plan and render."""
|
|
204
|
+
|
|
205
|
+
columns: list[Column]
|
|
206
|
+
opts: TableOpts
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class Table(Renderable):
|
|
210
|
+
"""
|
|
211
|
+
A data table with headers, footers, borders, alignment and wrapping.
|
|
212
|
+
|
|
213
|
+
A table that fits the render width is left untouched; an overflowing one
|
|
214
|
+
shrinks its flexible columns so its borders stay within the terminal.
|
|
215
|
+
``wrap`` columns reflow first, then the rest truncate; ``fixed_width``
|
|
216
|
+
columns never shrink and no column falls below its ``min_width``.
|
|
217
|
+
|
|
218
|
+
Examples
|
|
219
|
+
--------
|
|
220
|
+
>>> from sparcli.output.table import Table
|
|
221
|
+
>>> out = (
|
|
222
|
+
... Table()
|
|
223
|
+
... .columns(["Name", "Status"])
|
|
224
|
+
... .row(["web-1", "online"])
|
|
225
|
+
... .render(40)
|
|
226
|
+
... )
|
|
227
|
+
>>> "web-1" in out.plain()
|
|
228
|
+
True
|
|
229
|
+
"""
|
|
230
|
+
|
|
231
|
+
__slots__ = ("_columns", "_rows", "_opts")
|
|
232
|
+
|
|
233
|
+
def __init__(
|
|
234
|
+
self,
|
|
235
|
+
*,
|
|
236
|
+
border: BorderType | None = None,
|
|
237
|
+
border_style: Style | None = None,
|
|
238
|
+
header: bool = True,
|
|
239
|
+
header_style: Style | None = None,
|
|
240
|
+
striped: bool = False,
|
|
241
|
+
stripe_style: Style | None = None,
|
|
242
|
+
title: IntoText | None = None,
|
|
243
|
+
title_style: Style | None = None,
|
|
244
|
+
pad: int = 1,
|
|
245
|
+
row_separators: bool = False,
|
|
246
|
+
) -> None:
|
|
247
|
+
active = theme()
|
|
248
|
+
self._columns: list[Column] = []
|
|
249
|
+
self._rows: list[Row] = []
|
|
250
|
+
self._opts = TableOpts(
|
|
251
|
+
border=border if border is not None else active.border,
|
|
252
|
+
border_style=(
|
|
253
|
+
border_style if border_style is not None else active.secondary
|
|
254
|
+
),
|
|
255
|
+
header=header,
|
|
256
|
+
header_style=(
|
|
257
|
+
header_style if header_style is not None else active.heading
|
|
258
|
+
),
|
|
259
|
+
striped=striped,
|
|
260
|
+
stripe_style=(
|
|
261
|
+
stripe_style if stripe_style is not None else Style().dim()
|
|
262
|
+
),
|
|
263
|
+
title=into_text(title) if title is not None else None,
|
|
264
|
+
title_style=(
|
|
265
|
+
title_style if title_style is not None else active.heading
|
|
266
|
+
),
|
|
267
|
+
pad=pad,
|
|
268
|
+
row_separators=row_separators,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
def column(self, column: IntoColumn) -> Table:
|
|
272
|
+
"""Adds a column and returns the table."""
|
|
273
|
+
self._columns.append(into_column(column))
|
|
274
|
+
return self
|
|
275
|
+
|
|
276
|
+
def columns(self, columns: Iterable[IntoColumn]) -> Table:
|
|
277
|
+
"""Adds several columns from an iterable and returns the table."""
|
|
278
|
+
self._columns.extend(into_column(c) for c in columns)
|
|
279
|
+
return self
|
|
280
|
+
|
|
281
|
+
def row(self, cells: Iterable[IntoCell]) -> Table:
|
|
282
|
+
"""Adds a data row and returns the table."""
|
|
283
|
+
self._rows.append(Row([into_cell(c) for c in cells], False))
|
|
284
|
+
return self
|
|
285
|
+
|
|
286
|
+
def footer_row(self, cells: Iterable[IntoCell]) -> Table:
|
|
287
|
+
"""Adds a footer row (drawn after a separator) and returns the table."""
|
|
288
|
+
self._rows.append(Row([into_cell(c) for c in cells], True))
|
|
289
|
+
return self
|
|
290
|
+
|
|
291
|
+
def border(self, border: BorderType) -> Table:
|
|
292
|
+
"""Sets the border type and returns the table."""
|
|
293
|
+
self._opts.border = border
|
|
294
|
+
return self
|
|
295
|
+
|
|
296
|
+
def border_style(self, style: Style) -> Table:
|
|
297
|
+
"""Sets the border glyph style and returns the table."""
|
|
298
|
+
self._opts.border_style = style
|
|
299
|
+
return self
|
|
300
|
+
|
|
301
|
+
def header(self, show: bool) -> Table:
|
|
302
|
+
"""Enables or disables the header row and returns the table."""
|
|
303
|
+
self._opts.header = show
|
|
304
|
+
return self
|
|
305
|
+
|
|
306
|
+
def header_style(self, style: Style) -> Table:
|
|
307
|
+
"""Sets the header row text style and returns the table."""
|
|
308
|
+
self._opts.header_style = style
|
|
309
|
+
return self
|
|
310
|
+
|
|
311
|
+
def striped(self, striped: bool) -> Table:
|
|
312
|
+
"""Enables zebra striping of body rows and returns the table."""
|
|
313
|
+
self._opts.striped = striped
|
|
314
|
+
return self
|
|
315
|
+
|
|
316
|
+
def stripe_style(self, style: Style) -> Table:
|
|
317
|
+
"""Sets the style used for striped rows and returns the table."""
|
|
318
|
+
self._opts.stripe_style = style
|
|
319
|
+
return self
|
|
320
|
+
|
|
321
|
+
def title_style(self, style: Style) -> Table:
|
|
322
|
+
"""Sets the title text style and returns the table."""
|
|
323
|
+
self._opts.title_style = style
|
|
324
|
+
return self
|
|
325
|
+
|
|
326
|
+
def title(self, title: IntoText) -> Table:
|
|
327
|
+
"""Sets a table title and returns the table."""
|
|
328
|
+
self._opts.title = into_text(title)
|
|
329
|
+
return self
|
|
330
|
+
|
|
331
|
+
def pad(self, pad: int) -> Table:
|
|
332
|
+
"""Sets the horizontal cell padding and returns the table."""
|
|
333
|
+
self._opts.pad = pad
|
|
334
|
+
return self
|
|
335
|
+
|
|
336
|
+
def row_separators(self, on: bool) -> Table:
|
|
337
|
+
"""Draws separators between body rows and returns the table."""
|
|
338
|
+
self._opts.row_separators = on
|
|
339
|
+
return self
|
|
340
|
+
|
|
341
|
+
def render(self, max_width: int) -> Rendered:
|
|
342
|
+
"""Renders the table into at most ``max_width`` columns."""
|
|
343
|
+
if not self._columns:
|
|
344
|
+
return Rendered.empty()
|
|
345
|
+
from sparcli.output.table.plan import build_plan, column_widths
|
|
346
|
+
from sparcli.output.table.render import Builder
|
|
347
|
+
|
|
348
|
+
layout = TableLayout(self._columns, self._opts)
|
|
349
|
+
grid = build_plan(self._rows, len(self._columns))
|
|
350
|
+
widths = column_widths(layout, grid, max_width)
|
|
351
|
+
return Builder(layout, grid, widths).build()
|
sparcli/output/tree.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""
|
|
2
|
+
sparcli.output.tree
|
|
3
|
+
===================
|
|
4
|
+
|
|
5
|
+
Defines :class:`Tree` and :class:`TreeNode`, hierarchical views drawn with
|
|
6
|
+
box-drawing connectors.
|
|
7
|
+
|
|
8
|
+
A tree renders each node's content on its own row, prefixed by ``├─``/``└─``
|
|
9
|
+
connectors taken from a border glyph set. Vertical ``│`` guides carry the branch
|
|
10
|
+
color down through nested descendants; they can be switched off, and the dash
|
|
11
|
+
count and connector style are configurable.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from sparcli.core.border import BorderType
|
|
17
|
+
from sparcli.core.render import Renderable, Rendered
|
|
18
|
+
from sparcli.core.style import Style
|
|
19
|
+
from sparcli.core.text import IntoText, Line, Span, into_text
|
|
20
|
+
from sparcli.core.theme import theme
|
|
21
|
+
|
|
22
|
+
# A connector spans the branch glyph, its dashes and a trailing space.
|
|
23
|
+
_CONNECTOR_FIXED_CELLS = 2
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TreeNode:
|
|
27
|
+
"""
|
|
28
|
+
A node in a :class:`Tree`.
|
|
29
|
+
|
|
30
|
+
Attributes
|
|
31
|
+
----------
|
|
32
|
+
content : Text
|
|
33
|
+
The node's rich-text content.
|
|
34
|
+
children : list[TreeNode]
|
|
35
|
+
The node's direct children, drawn indented beneath it.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
__slots__ = ("content", "children")
|
|
39
|
+
|
|
40
|
+
def __init__(self, content: IntoText) -> None:
|
|
41
|
+
self.content = into_text(content)
|
|
42
|
+
self.children: list[TreeNode] = []
|
|
43
|
+
|
|
44
|
+
def child(self, node: TreeNode) -> TreeNode:
|
|
45
|
+
"""Adds a child node and returns this node for chaining."""
|
|
46
|
+
self.children.append(node)
|
|
47
|
+
return self
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Tree(Renderable):
|
|
51
|
+
"""A tree of :class:`TreeNode` objects drawn with connector glyphs."""
|
|
52
|
+
|
|
53
|
+
__slots__ = (
|
|
54
|
+
"_roots",
|
|
55
|
+
"_border",
|
|
56
|
+
"_connector_style",
|
|
57
|
+
"_dashes",
|
|
58
|
+
"_guides",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def __init__(
|
|
62
|
+
self,
|
|
63
|
+
*,
|
|
64
|
+
border: BorderType = BorderType.SINGLE,
|
|
65
|
+
connector_style: Style | None = None,
|
|
66
|
+
dashes: int = 1,
|
|
67
|
+
guides: bool = True,
|
|
68
|
+
) -> None:
|
|
69
|
+
self._roots: list[TreeNode] = []
|
|
70
|
+
self._border = border
|
|
71
|
+
self._connector_style = (
|
|
72
|
+
connector_style
|
|
73
|
+
if connector_style is not None
|
|
74
|
+
else theme().secondary
|
|
75
|
+
)
|
|
76
|
+
self._dashes = dashes
|
|
77
|
+
self._guides = guides
|
|
78
|
+
|
|
79
|
+
def node(self, node: TreeNode) -> Tree:
|
|
80
|
+
"""Adds a top-level (root) node and returns the tree."""
|
|
81
|
+
self._roots.append(node)
|
|
82
|
+
return self
|
|
83
|
+
|
|
84
|
+
def border(self, border: BorderType) -> Tree:
|
|
85
|
+
"""Sets the connector glyph set and returns the tree."""
|
|
86
|
+
self._border = border
|
|
87
|
+
return self
|
|
88
|
+
|
|
89
|
+
def connector_style(self, style: Style) -> Tree:
|
|
90
|
+
"""Sets the connector style and returns the tree."""
|
|
91
|
+
self._connector_style = style
|
|
92
|
+
return self
|
|
93
|
+
|
|
94
|
+
def dashes(self, dashes: int) -> Tree:
|
|
95
|
+
"""Sets the number of horizontal dashes per connector."""
|
|
96
|
+
self._dashes = dashes
|
|
97
|
+
return self
|
|
98
|
+
|
|
99
|
+
def no_guides(self) -> Tree:
|
|
100
|
+
"""Disables the vertical continuation guides and returns the tree."""
|
|
101
|
+
self._guides = False
|
|
102
|
+
return self
|
|
103
|
+
|
|
104
|
+
def render(self, max_width: int) -> Rendered:
|
|
105
|
+
"""Renders the tree; ``max_width`` is accepted but not constraining."""
|
|
106
|
+
del max_width
|
|
107
|
+
lines: list[Line] = []
|
|
108
|
+
for root in self._roots:
|
|
109
|
+
for content_line in root.content.lines:
|
|
110
|
+
lines.append(Line(list(content_line.spans)))
|
|
111
|
+
self._render_children(root.children, "", lines)
|
|
112
|
+
return Rendered(lines)
|
|
113
|
+
|
|
114
|
+
def _connector_width(self) -> int:
|
|
115
|
+
"""Returns the column width of a connector."""
|
|
116
|
+
return _CONNECTOR_FIXED_CELLS + self._dashes
|
|
117
|
+
|
|
118
|
+
def _render_children(
|
|
119
|
+
self, children: list[TreeNode], prefix: str, lines: list[Line]
|
|
120
|
+
) -> None:
|
|
121
|
+
"""Renders the children of a node beneath ``prefix``."""
|
|
122
|
+
chars = self._border.chars()
|
|
123
|
+
dash = chars.horizontal * self._dashes
|
|
124
|
+
for index, child in enumerate(children):
|
|
125
|
+
last = index + 1 == len(children)
|
|
126
|
+
branch = chars.bottom_left if last else chars.tee_right
|
|
127
|
+
connector = f"{branch}{dash} "
|
|
128
|
+
continuation = self._continuation(last)
|
|
129
|
+
self._push_node_lines(child, prefix, connector, continuation, lines)
|
|
130
|
+
child_prefix = f"{prefix}{continuation}"
|
|
131
|
+
self._render_children(child.children, child_prefix, lines)
|
|
132
|
+
|
|
133
|
+
def _push_node_lines(
|
|
134
|
+
self,
|
|
135
|
+
node: TreeNode,
|
|
136
|
+
prefix: str,
|
|
137
|
+
connector: str,
|
|
138
|
+
continuation: str,
|
|
139
|
+
lines: list[Line],
|
|
140
|
+
) -> None:
|
|
141
|
+
"""Emits the content lines of a single node."""
|
|
142
|
+
for row, content_line in enumerate(node.content.lines):
|
|
143
|
+
lead = connector if row == 0 else continuation
|
|
144
|
+
spans: list[Span] = [
|
|
145
|
+
Span.styled(prefix, self._connector_style),
|
|
146
|
+
Span.styled(lead, self._connector_style),
|
|
147
|
+
]
|
|
148
|
+
spans.extend(content_line.spans)
|
|
149
|
+
lines.append(Line(spans))
|
|
150
|
+
|
|
151
|
+
def _continuation(self, last: bool) -> str:
|
|
152
|
+
"""Returns the continuation cell shown under a node's connector."""
|
|
153
|
+
width = self._connector_width()
|
|
154
|
+
if last or not self._guides:
|
|
155
|
+
return " " * width
|
|
156
|
+
guide = self._border.chars().vertical
|
|
157
|
+
return f"{guide}{' ' * (width - 1)}"
|
sparcli/py.typed
ADDED
|
File without changes
|