LayoutΒΆ
A terminal is a grid of cells. Anything a CLI draws to a stated width has to agree with the terminal on how many cells a string takes, and that number is not the stringβs length.
Measuring a lineΒΆ
cell_width answers in cells. An ideograph takes two of them, and a combining mark none:
from click_extra import command, echo
from click_extra.layout import cell_width
@command
def measure():
"""Compare a string's length with the cells it occupies."""
for text in ("apricot", "ζ", "e\u0301"):
echo(f"{len(text):2} characters, {cell_width(text):2} cells")
$ measure
7 characters, 7 cells
1 characters, 2 cells
2 characters, 1 cells
Reach for it over len wherever a column has to line up: a table gutter, a padded label, a rule.
Styling costs nothing either, which is what lets you measure text as it arrived instead of stripping it first. An ANSI escape and an OSC 8 hyperlink each occupy no cell, and a tab reaches the next stop eight columns along:
Note
wcwidth.wcswidth, the older call, refuses any string carrying a control character and answers -1 for it, which is every styled string. Reach for it only where a consumer reads that value back, as tabulate does.
Naming the terminal applies wcwidthβs correction for the emoji-presentation sequences a few of them advance by one column where the Unicode tables say two:
Padding a lineΒΆ
pad_to fills a line out to a stated width. str.ljust counts characters, so on a styled line it counts the escapes it cannot see and pads short, or not at all:
Whatever style the text left open is closed before the blanks, so a background cannot bleed across the gap into whatever sits beside it:
Ruling a lineΒΆ
center_in_rule composes a line of a stated width with a label centered in it. It is what click-extra themes heads each palette with, and what a capture writes in place of the lines --head cut away:
from click_extra import command, echo, option
from click_extra.layout import center_in_rule
@command
@option("--columns", type=int, default=60, help="Width of the rule.")
def harvest(columns):
"""Separate two baskets with a named rule."""
echo(center_in_rule("apricots", columns))
echo(" Alberge, Moorpark, Tilton")
echo(center_in_rule("plums", columns))
echo(" Damson, Greengage, Mirabelle")
$ harvest --columns 44
ββββββββββββββββ[ apricots ]ββββββββββββββββ
Alberge, Moorpark, Tilton
βββββββββββββββββ[ plums ]ββββββββββββββββββ
Damson, Greengage, Mirabelle
Every part of the line is a parameter: the character the rule repeats, the two brackets around the label, and the color the rule and brackets are painted.
A rule with nothing to nameΒΆ
Pass no label and the brackets go too, leaving one unbroken line. A divider naming nothing should not look like a frame around nothing:
The same happens when the width cannot hold the brackets, and a width too narrow for the label leaves the label alone rather than drawing a rule that cannot close.
A label you styled yourselfΒΆ
The label is measured with its escape sequences stripped, so a caller paints it whichever way it likes and hands the whole thing over. color reaches only the rule and the brackets, which is the half a caller cannot pre-style without knowing where they fall:
from click_extra import command, echo
from click_extra.layout import center_in_rule
from click_extra.styling import Style
@command
def basket():
"""Rule a line around a label the caller colored itself."""
echo(center_in_rule(Style(fg="green", bold=True)("ripe"), 40))
$ basket
ββββββββββββββββ[ ripe ]ββββββββββββββββ
That label is 4 cells of text carrying 17 characters, so a rule built on len would come out 13 columns short of the width asked for.
Tip
A rule drawn with a Box Drawing dash (β, β, β) carries two or three strokes inside a single cell. In a capture each stroke is a pixel or two wide and a cell advances a fractional number of device pixels, so neighbouring dashes merge on some cells and separate on others, and the rule shimmers. An unbroken β has nothing to merge, and Β· puts one mark per cell with room around it. Both stay even at any scale.
Wrapping a styled lineΒΆ
wrap_ansi(text, width) wraps a styled string to a visible width. textwrap.wrap() counts every byte of an escape toward the line length, so it breaks a styled string several words early. Here an escape measures no cells and a double-width character two, and each line opens and closes the styling it needs:
'\x1b[31mOvercast in the morning, with\x1b[0m'
'\x1b[31ma light drizzle after midday.\x1b[0m'
It powers the wrapping of the vertical table format, which has no rendering backend to delegate its line breaking to.
click_extra.layout APIΒΆ
Measure a line of terminal text, and compose one of a stated width.
A terminal is a grid of cells, and everything a CLI draws on it has to agree on
how many cells a string occupies. That number is not the stringβs length: an
ideograph takes two cells and a combining mark none. cell_width() answers
it once, and every line this package composes is built on that answer.
The module holds the composition helpers built on it, starting with
center_in_rule(). They are terminal-text primitives, not capture
machinery: click_extra.screenshot consumes them to lay a picture out on
the same grid, and so can any CLI drawing a divider of its own.
- click_extra.layout.PADDING = ' \xa0'ΒΆ
Characters that separate one column of terminal text from the next.
Both are one cell wide and draw nothing.
render_svg()emits every space as a non-breaking one, so the padding survives an XML round-trip and no renderer collapses a run of them.
- click_extra.layout.RULE_GLYPH = 'β'ΒΆ
Character
center_in_rule()draws a rule with, absent a better one.An unbroken line, which is what a divider between two whole things is.
- click_extra.layout.RULE_COLOR = 'bright_black'ΒΆ
Color
center_in_rule()paints a rule and its brackets.A rule is the one line of a screen nothing printed, so it is drawn to recede: dimmer than the text it separates, in both the gallery of
click-extra themesand the marker standing in for what a capture cut. A rule a caller spells out in full is written as given, color included, since a caller naming one has already decided how it should look.
- click_extra.layout.RESET = '\x1b[0m'ΒΆ
Escape closing every style a line left open, written before any padding.
- click_extra.layout.cell_width(text, term_program=None)[source]ΒΆ
Columns
textoccupies on a terminalβs character grid.Not its length. A CJK ideograph is drawn two cells wide, a combining mark none, an ANSI escape or an OSC 8 hyperlink none at all while adding several characters, and a tab as many as it takes to reach the next stop eight columns along.
wcwidth.widthanswers for all of them, which is what lets a caller measure text as it arrived rather than stripping it first.Note
wcwidth.wcswidth, the older call, refuses any string carrying a control character and answers-1for it, which is every styled string. Reach for it only where a consumer reads that value back, astabulatedoes.- Parameters:
text (
str) β the text to measure.term_program (
str|None) β name of the terminal drawing the text, as$TERM_PROGRAMholds it. A few of them advance an emoji-presentation sequence by one column where the Unicode tables say two, and naming the terminal applieswcwidthβs correction for it.Nonemeasures what the tables say.
- Return type:
- Returns:
the number of cells it occupies, never negative.
- click_extra.layout.pad_to(text, width)[source]ΒΆ
Pad
textwith blanks until it occupieswidthcells.str.ljust()counts characters, so on a styled line it counts the escapes it cannot see and pads too little, or nothing at all. Text already at leastwidthcells wide comes back untouched.Any style the text left open is closed before the blanks, so a background cannot bleed across the gap into whatever sits beside it.
- click_extra.layout.center_in_rule(label, width, rule='β', opening='[ ', closing=' ]', color='bright_black')[source]ΒΆ
One line of
widthcells:labelcentered in a rule drawn withrule.labelmay arrive already styled:cell_width()discounts its escapes, so a caller paints the label its own way and hands the whole thing over.colorpaints the rule and the two brackets, which is the half a caller cannot pre-style without knowing where they fall.A
Noneor empty label draws no brackets and one contiguous rule: a divider naming nothing should not look like a frame around nothing. A width too narrow for the brackets drops them the same way, and one too narrow for the label leaves the label alone rather than drawing a rule that cannot close.Measured in cells, not characters: a label carrying a wide glyph shifts a rule built on
lenby one column per glyph.- Parameters:
label (
str|None) β the text the rule is drawn around, styled or not, orNonefor an unbroken rule.width (
int) β columns the line occupies.rule (
str) β character the rule is drawn with.opening (
str) β bracket written between the rule andlabel.closing (
str) β bracket written betweenlabeland the rule.color (
str|None) β color the rule and brackets are painted, orNoneto leave them as they are.labelis never repainted: a caller styles it itself, or leaves it in the terminalβs own ink.
- Return type:
- Returns:
the whole line.
- click_extra.layout.LINE_NUMBER_SEPARATOR = ' β 'ΒΆ
Rule drawn between a lineβs number and the line itself.
A vertical bar rather than a bare space, so the gutter reads as a column of its own even where the output is itself indented.
- click_extra.layout.RTL_BIDI_CLASSES = frozenset({'AL', 'AN', 'R'})ΒΆ
Unicode bidirectional classes written right to left.
Right-to-left letters, Arabic letters and Arabic-Indic numbers, as
unicodedata.bidirectional()names them. Seeis_bidirectional().
- click_extra.layout.number_lines(text, start=1)[source]ΒΆ
Prefix each line of
textwith its number, in a dim gutter.The numbers are drawn into the terminal text rather than into a column beside it, which is the same trade Pygments makes with its inline line numbers: every renderer places them for free, and every reader copying the text copies them too.
Right-aligned on the widest number, so the gutter is one column whatever the outputβs length, and separated by
LINE_NUMBER_SEPARATOR.
- click_extra.layout.is_bidirectional(text)[source]ΒΆ
Whether
textcarries a character written right to left.Arabic, Hebrew and their neighbours are reordered by whoever draws them, and the cursive ones are shaped: a letterβs form depends on what it joins. A terminal grid describes neither, which is why
render_svg()stops pinning such a run to an exact width.
- click_extra.layout.fit_columns(text, floor=0)[source]ΒΆ
Width, in characters, of the longest line in
text.ANSI escapes style the glyphs around them and occupy no cell of their own, so they are discounted. Measured in terminal cells, so a line of CJK asks for the two columns per glyph it is drawn with.
- Parameters:
- Return type:
- Returns:
the width laying every line out without folding any.
- click_extra.layout.grid(text, columns)[source]ΒΆ
Lay ANSI text out on a terminalβs character grid.
Where a stream of styled text stops being a stream and becomes a picture. Each styled run of
split_ansi()is split at newlines into rows, then placed on the column it starts at, measured in cells rather than characters so a wide glyph takes the two it is drawn with.A line reaching past
columnssoft-wraps onto the next row, the way it would on a terminal that narrow, rather than being cropped: a command is free to print a line it never wraps itself (a long URL, a wide table, a machine-readable dump), and a layout that silently swallowed the overflow would be lying about what ran. A glyph straddling the edge moves down whole.Returning the column with each run is what lets a renderer place a run without measuring anything back out of its own output.
- click_extra.layout.wrap_ansi(text, width)[source]ΒΆ
Wrap text to width terminal cells, preserving its ANSI styling.
textwrap.wrap()counts every byte of an ANSI escape toward the line length, so a styled string wraps far earlier than its visible width warrants.wcwidth.wrapmeasures an escape at no cells and every character between escapes at the width a terminal advances by. It reopens on each line the styling still in effect, so no escape sequence crosses a line boundary: each returned line carries the styling it needs, opened and closed within the line.Returns a list of lines, empty text yielding a single empty one.
Note
Breaks land where
textwrap.wrap()puts them on plain ASCII, so long-word breaking and whitespace handling match it exactly. The two measures part on a double-width character, which counts for the two cells it takes, and on an OSC 8 hyperlink, which counts for none.