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 themes and 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 text occupies 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.width answers 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 -1 for it, which is every styled string. Reach for it only where a consumer reads that value back, as tabulate does.

Parameters:
  • text (str) – the text to measure.

  • term_program (str | None) – name of the terminal drawing the text, as $TERM_PROGRAM holds it. A few of them advance an emoji-presentation sequence by one column where the Unicode tables say two, and naming the terminal applies wcwidth’s correction for it. None measures what the tables say.

Return type:

int

Returns:

the number of cells it occupies, never negative.

click_extra.layout.pad_to(text, width)[source]ΒΆ

Pad text with blanks until it occupies width cells.

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 least width cells 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.

Parameters:
  • text (str) – the text to pad, styled or not.

  • width (int) – cells the result occupies.

Return type:

str

Returns:

the padded text.

click_extra.layout.center_in_rule(label, width, rule='─', opening='[ ', closing=' ]', color='bright_black')[source]ΒΆ

One line of width cells: label centered in a rule drawn with rule.

label may arrive already styled: cell_width() discounts its escapes, so a caller paints the label its own way and hands the whole thing over. color paints the rule and the two brackets, which is the half a caller cannot pre-style without knowing where they fall.

A None or 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 len by one column per glyph.

Parameters:
  • label (str | None) – the text the rule is drawn around, styled or not, or None for an unbroken rule.

  • width (int) – columns the line occupies.

  • rule (str) – character the rule is drawn with.

  • opening (str) – bracket written between the rule and label.

  • closing (str) – bracket written between label and the rule.

  • color (str | None) – color the rule and brackets are painted, or None to leave them as they are. label is never repainted: a caller styles it itself, or leaves it in the terminal’s own ink.

Return type:

str

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. See is_bidirectional().

click_extra.layout.number_lines(text, start=1)[source]ΒΆ

Prefix each line of text with 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.

Parameters:
  • text (str) – the text to number, ANSI escape sequences included.

  • start (int) – number given to the first line.

Return type:

str

Returns:

the numbered text.

click_extra.layout.is_bidirectional(text)[source]ΒΆ

Whether text carries 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.

Parameters:

text (str) – the text to inspect.

Return type:

bool

Returns:

True when at least one character is right-to-left.

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:
  • text (str) – the text to measure, ANSI escape sequences included.

  • floor (int) – width to return when every line is narrower than it. A caller laying the text out somewhere with a minimum of its own states that minimum here; the default floors at nothing.

Return type:

int

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 columns soft-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.

Parameters:
  • text (str) – the text to lay out, ANSI escape sequences included.

  • columns (int) – width of the grid, in cells.

Return type:

list[list[tuple[Style, str, int]]]

Returns:

one list of (style, text, column) runs per row.

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.wrap measures 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.

Return type:

list[str]