click_extra.capture package

Draw terminal text as a static document: an SVG picture, HTML or ANSI.

Submodules

click_extra.capture.html module

Render captured terminal text as self-contained HTML.

click_extra.capture.screenshot decides what a capture shows, then hands the text, its palette and the resolved Chrome to render_html(), which writes them as an inline-styled <pre>: selectable, searchable text that needs no stylesheet and reflows with the page. The window around it is drawn with the geometry click_extra.capture.svg publishes, so the two documents look like the same terminal.

click_extra.capture.html.render_html(text, *, title='', full=True, palette=('#292929', '#c5c8c6', ('#2e3436', '#cc0000', '#4e9a06', '#c4a000', '#3465a4', '#75507b', '#06989a', '#d3d7cf', '#555753', '#ef2929', '#8ae234', '#fce94f', '#729fcf', '#ad7fa8', '#34e2e2', '#eeeeec'), '#292929'), chrome=Chrome(border='none', border_width=1, radius=8, backdrop='none', shadow='none', margin=0, padding=0, opacity=1.0, watermark='', watermark_color='rgba(128,128,128,0.85)'), font_stack="'Fira Code', 'Cascadia Code', Menlo, Consolas, monospace")[source]

Render captured terminal text to HTML.

The <pre> carries its own inline styling, so a fragment pasted into an existing page needs no stylesheet and cannot be restyled out of legibility by the host. Nothing else is needed either: a <pre> preserves the capture’s own spacing, which is what spares HTML the column arithmetic render_svg() performs for a picture.

Caution

The text is escaped before its ANSI is translated, the order click_extra.table uses for its html format. Skip it and any < a CLI prints opens a tag: click-extra’s own --export-config help says it writes to <stdout>.

Note

An OSC 8 hyperlink loses its URL and keeps its visible text: the escape is dropped rather than turned into an <a>.

Parameters:
  • text (str) – captured output, ANSI escape sequences included.

  • title (str) – <title> of the document. Ignored for a fragment.

  • full (bool) – wrap the <pre> in a standalone document. False returns the <pre> and its credit line alone, to paste into a page that has its own.

  • palette (TerminalPalette) – terminal colors the text resolves against, see resolve_palette().

  • chrome (Chrome) – the block around the text, resolved, see resolve(). Its margin is left around the block, its padding added inside the block on top of its own, its backdrop fills the page behind it, and its opacity thins the block’s background alone, down to 0.0 where the page shows straight through the text. The credit line sits under the block, against its right edge, where an SVG draws it in the margin. Defaults to BARE_CHROME.

  • font_stack (str) – fonts the text is set in, best first.

Return type:

str

Returns:

the rendered markup.

Raises:

ValueError – when chrome is not resolved.

click_extra.capture.presets module

The bundled catalog of terminal presets a capture can be drawn as.

A capture is a picture of a terminal, and terminals do not look alike. A preset carries the five things that make one recognizable, so a reader placing the image knows which desktop it came from:

  • the window decorations, three round buttons on the left for macOS, three glyphs on the right for Windows, a single one for GNOME;

  • the palette its colors resolve against, which is what turns a bright blue into Campbell’s #3B78FF or Tango’s #729FCF;

  • the font the terminal ships with;

  • the prompt its shell draws, $ against PS C:\>;

  • the cursor it draws, a block everywhere but Windows Terminal, which opens on a bar.

None of it is applied unless asked for: a capture with no preset keeps the renderer’s own neutral window. That window is described here too: DEFAULT_PRESET is the terminal it resolves its colors against, TerminalBackground names the dark or light chrome a capture is drawn on, and resolve_palette() answers the palette any preset shows on either. Chrome draws the rest of the window: its frame, shadow, margins and credit line.

Caution

A palette here is a published default, transcribed from the scheme each terminal ships (Campbell and One Half Light for Windows Terminal, Basic and Pro for Apple’s Terminal, Tango for GNOME), and cross-checked against iTerm2-Color-Schemes. It is what the terminal looks like out of the box, not what any given reader has configured theirs to.

class click_extra.capture.presets.TerminalPalette(background, foreground, ansi, titlebar)[source]

Bases: NamedTuple

The colors a terminal resolves a capture’s ANSI codes against.

The 16 ansi entries are the standard palette in the canonical order: black, red, green, yellow, blue, magenta, cyan, white, then the same eight in their bright variants.

Create new instance of TerminalPalette(background, foreground, ansi, titlebar)

background: str

Color behind the text, as CSS spells it.

foreground: str

Color of the text a CLI leaves unstyled.

ansi: tuple[str, ...]

The 16 palette entries, normal eight first.

titlebar: str

Color of the strip the window’s title and buttons sit in.

A window’s chrome, which the desktop paints rather than the terminal: a shade off the background, so the top of the window reads as a window rather than as the first line of output.

class click_extra.capture.presets.WindowButtons(circles=(), glyphs='')[source]

Bases: NamedTuple

The decorations a terminal draws in its title bar.

Two shapes cover the desktops: macOS draws filled circles on the left, Windows and GNOME draw glyphs on the right. An empty field leaves that half undrawn, which is what a bare window asks for.

Create new instance of WindowButtons(circles, glyphs)

circles: tuple[str, ...]

Colors of the round buttons drawn from the left, in order.

glyphs: str

Characters drawn from the right, closing button last.

click_extra.capture.presets.MACOS_BUTTONS: Final = (('#ff5f57', '#febc2e', '#28c840'), '')

Close, minimize and zoom, the three round buttons of an Aqua title bar.

click_extra.capture.presets.WINDOWS_BUTTONS: Final = ((), '-□✕')

Minimize, maximize and close, the three glyphs of a Windows title bar.

click_extra.capture.presets.GNOME_BUTTONS: Final = ((), '✕')

The single close button a GNOME window carries by default.

class click_extra.capture.presets.CursorShape(*values)[source]

Bases: Enum

The shape a terminal draws its cursor as.

The three every terminal offers, under the names they are configured by. Which one a terminal picks out of the box is part of what makes it recognizable, so a preset states it alongside the buttons and the font.

BLOCK = 'block'

A filled cell, the shape a terminal draws unless told otherwise.

BAR = 'bar'

A thin upright line on the cell’s leading edge.

UNDERLINE = 'underline'

A thin line along the cell’s bottom edge.

class click_extra.capture.presets.Cursor(shape=None, blink=1.0, color=None)[source]

Bases: NamedTuple

The cursor a capture draws, and how it behaves.

Passed to render_svg() to draw one at all: a capture shows no cursor unless asked.

Note

Where the cursor is is never stated here. A frame’s text already says so, see cursor_cell(), so a caller states what the cursor looks like and the picture answers for the rest.

Create new instance of Cursor(shape, blink, color)

shape: CursorShape | None

How it is drawn. None takes the shape the terminal preset names.

Seconds one blink takes, half of it lit. Zero draws a steady cursor.

A second is what the desktops settle around, and it is deliberately no factor of any frame interval: the two clocks drift against each other across a loop, which is what a terminal showing a cursor over a running command looks like.

Caution

Blinking is motion, and a reader may have asked their system for less of it. The rule sits behind REDUCED_MOTION_QUERY like every other animation this package emits, which leaves the cursor lit and still. That guard is also what answers WCAG 2.2.2, which asks that anything blinking past five seconds can be stopped.

color: str | None

Paint it is drawn with. None takes the terminal’s foreground.

click_extra.capture.presets.APPLE_ANSI: Final = ('#000000', '#c23621', '#25bc24', '#adad27', '#492ee1', '#d338d3', '#33bbc8', '#cbcccd', '#818383', '#fc391f', '#31e722', '#eaec23', '#5833ff', '#f935f8', '#14f0f0', '#e9ebeb')

Palette shared by Apple Terminal’s Basic and Pro schemes.

click_extra.capture.presets.CAMPBELL_ANSI: Final = ('#0c0c0c', '#c50f1f', '#13a10e', '#c19c00', '#0037da', '#881798', '#3a96dd', '#cccccc', '#767676', '#e74856', '#16c60c', '#f9f1a5', '#3b78ff', '#b4009e', '#61d6d6', '#f2f2f2')

Palette of Campbell, the scheme Windows Terminal opens with.

click_extra.capture.presets.ONE_HALF_LIGHT_ANSI: Final = ('#383a42', '#e45649', '#50a14f', '#c18301', '#0184bc', '#a626a4', '#0997b3', '#fafafa', '#4f525d', '#df6c75', '#98c379', '#e4c07a', '#61afef', '#c577dd', '#56b5c1', '#ffffff')

Palette of One Half Light, the light scheme Windows Terminal ships.

click_extra.capture.presets.TANGO_ANSI: Final = ('#2e3436', '#cc0000', '#4e9a06', '#c4a000', '#3465a4', '#75507b', '#06989a', '#d3d7cf', '#555753', '#ef2929', '#8ae234', '#fce94f', '#729fcf', '#ad7fa8', '#34e2e2', '#eeeeec')

Palette of Tango, which GNOME Terminal ships in a dark and a light dress.

class click_extra.capture.presets.TerminalPreset(label, buttons, radius, prompt, cursor, font_stack, dark, light)[source]

Bases: NamedTuple

A terminal a capture can be drawn as.

Pass one to click-extra screenshot --preset, or to a click:run block as :screenshot-preset:. Anything stated alongside it wins: a preset picks the defaults, it does not lock them.

Create new instance of TerminalPreset(label, buttons, radius, prompt, cursor, font_stack, dark, light)

label: str

Human name of the terminal, for the documentation and the help screen.

buttons: WindowButtons

Decorations drawn in the title bar, see WindowButtons.

radius: int

How round the window’s corners are, in pixels.

prompt: str

Sigil the terminal’s usual shell draws before a command.

cursor: CursorShape

Shape it draws its cursor as, see CursorShape.

Only consulted by a capture that asked for a cursor: a preset picks the shape, it never turns one on.

font_stack: str

Fonts the capture asks for, the terminal’s own first.

Nothing is embedded, so a reader without the family falls back down the list. Which is why each ends with the same generic monospace a browser always resolves.

dark: TerminalPalette

Colors the terminal shows on its dark scheme.

light: TerminalPalette

Colors it shows on its light one.

click_extra.capture.presets.PRESETS: Final[dict[str, TerminalPreset]] = {'linux': ('GNOME Terminal', ((), '✕'), 6, '$', CursorShape.BLOCK, "'Ubuntu Mono', 'DejaVu Sans Mono', monospace", ('#2e3436', '#d3d7cf', ('#2e3436', '#cc0000', '#4e9a06', '#c4a000', '#3465a4', '#75507b', '#06989a', '#d3d7cf', '#555753', '#ef2929', '#8ae234', '#fce94f', '#729fcf', '#ad7fa8', '#34e2e2', '#eeeeec'), '#303030'), ('#ffffff', '#2e3436', ('#2e3436', '#cc0000', '#4e9a06', '#c4a000', '#3465a4', '#75507b', '#06989a', '#d3d7cf', '#555753', '#ef2929', '#8ae234', '#fce94f', '#729fcf', '#ad7fa8', '#34e2e2', '#eeeeec'), '#ebebeb')), 'macos': ('Apple Terminal', (('#ff5f57', '#febc2e', '#28c840'), ''), 10, '$', CursorShape.BLOCK, "'SF Mono', Menlo, Monaco, monospace", ('#000000', '#f2f2f2', ('#000000', '#c23621', '#25bc24', '#adad27', '#492ee1', '#d338d3', '#33bbc8', '#cbcccd', '#818383', '#fc391f', '#31e722', '#eaec23', '#5833ff', '#f935f8', '#14f0f0', '#e9ebeb'), '#3a3a3a'), ('#ffffff', '#000000', ('#000000', '#c23621', '#25bc24', '#adad27', '#492ee1', '#d338d3', '#33bbc8', '#cbcccd', '#818383', '#fc391f', '#31e722', '#eaec23', '#5833ff', '#f935f8', '#14f0f0', '#e9ebeb'), '#e9e9e9')), 'plain': ('No terminal at all', ((), ''), 0, '$', CursorShape.BLOCK, "'Fira Code', 'Cascadia Code', Menlo, Consolas, monospace", ('#292929', '#c5c8c6', ('#2e3436', '#cc0000', '#4e9a06', '#c4a000', '#3465a4', '#75507b', '#06989a', '#d3d7cf', '#555753', '#ef2929', '#8ae234', '#fce94f', '#729fcf', '#ad7fa8', '#34e2e2', '#eeeeec'), '#292929'), ('#ffffff', '#000000', ('#2e3436', '#cc0000', '#4e9a06', '#c4a000', '#3465a4', '#75507b', '#06989a', '#d3d7cf', '#555753', '#ef2929', '#8ae234', '#fce94f', '#729fcf', '#ad7fa8', '#34e2e2', '#eeeeec'), '#ffffff')), 'windows': ('Windows Terminal', ((), '-□✕'), 0, 'PS C:\\>', CursorShape.BAR, "'Cascadia Code', 'Cascadia Mono', Consolas, monospace", ('#0c0c0c', '#cccccc', ('#0c0c0c', '#c50f1f', '#13a10e', '#c19c00', '#0037da', '#881798', '#3a96dd', '#cccccc', '#767676', '#e74856', '#16c60c', '#f9f1a5', '#3b78ff', '#b4009e', '#61d6d6', '#f2f2f2'), '#202020'), ('#fafafa', '#383a42', ('#383a42', '#e45649', '#50a14f', '#c18301', '#0184bc', '#a626a4', '#0997b3', '#fafafa', '#4f525d', '#df6c75', '#98c379', '#e4c07a', '#61afef', '#c577dd', '#56b5c1', '#ffffff'), '#f3f3f3'))}

Every terminal a capture can be drawn as, alphabetically.

plain is the odd one out: it mimics no desktop, dropping the buttons and the rounded corners for a capture that has to read as a block of output rather than as a window, on a slide or in a paper.

click_extra.capture.presets.DEFAULT_PRESET: Final = ('No terminal at all', ((), ''), 0, '$', CursorShape.BLOCK, "'Fira Code', 'Cascadia Code', Menlo, Consolas, monospace", ('#292929', '#c5c8c6', ('#2e3436', '#cc0000', '#4e9a06', '#c4a000', '#3465a4', '#75507b', '#06989a', '#d3d7cf', '#555753', '#ef2929', '#8ae234', '#fce94f', '#729fcf', '#ad7fa8', '#34e2e2', '#eeeeec'), '#292929'), ('#ffffff', '#000000', ('#2e3436', '#cc0000', '#4e9a06', '#c4a000', '#3465a4', '#75507b', '#06989a', '#d3d7cf', '#555753', '#ef2929', '#8ae234', '#fce94f', '#729fcf', '#ad7fa8', '#34e2e2', '#eeeeec'), '#ffffff'))

Terminal a capture with no --preset is drawn as.

plain mimics no desktop, which is what a capture wearing no decoration should resolve its colors against. Naming it here is what keeps the two formats looking like the same terminal, and keeps one catalog answering for every palette a capture can use: without it the default colors would be a second set of literals free to drift from the one the presets publish.

click_extra.capture.presets.CAPTURE_FONT_STACK: Final = "'Fira Code', 'Cascadia Code', Menlo, Consolas, monospace"

Monospaced fonts a capture asks for, best first.

Nothing is embedded and nothing is fetched, so both formats set the text in the first family the reader already has, and a capture renders the same offline, on a page forbidding third-party requests, and in a viewer that speaks no CSS @font-face.

Family names are single-quoted on purpose: this lands in a double-quoted style attribute, which a double quote here would terminate early.

Note

Not embedding is a decision, not an omission: a subsetted font would ship inside this package, and redistribute someone else’s font under its own license. The bytes are small; the license management is not worth a terminal picture. The JuliaMono subset under docs/_static/ does not change this: it sets the documentation’s own HTML, where a stylesheet reaches the text, and reusing it in a capture would be that same redistribution.

The fallback has a cost. A stock macOS falls through to Menlo, which carries no Braille and none of the Mathematical Operators the spinner catalog draws, so those resolve to the proportional Apple Symbols: nearly a third of the spinner tiles draw wider than their column, and the window’s clip cuts the overflow. A reader with either of the first two families sees none of it, and emoji stay out of reach of every monospaced font.

Fitting each run to its columns instead (lengthAdjust="spacingAndGlyphs") is no cure: it stretches a narrow glyph as readily as it squeezes a wide one, and distorts nearly a third of the runs in the committed captures by more than 6%.

WATERMARK_URL documents the other half of this: a capture embedded with <img> never sees the page’s own @font-face either, so a stylesheet cannot fix this from the outside.

click_extra.capture.presets.CAPTURE_PALETTES: dict[TerminalBackground, TerminalPalette] = {TerminalBackground.DARK: ('#292929', '#c5c8c6', ('#2e3436', '#cc0000', '#4e9a06', '#c4a000', '#3465a4', '#75507b', '#06989a', '#d3d7cf', '#555753', '#ef2929', '#8ae234', '#fce94f', '#729fcf', '#ad7fa8', '#34e2e2', '#eeeeec'), '#292929'), TerminalBackground.LIGHT: ('#ffffff', '#000000', ('#2e3436', '#cc0000', '#4e9a06', '#c4a000', '#3465a4', '#75507b', '#06989a', '#d3d7cf', '#555753', '#ef2929', '#8ae234', '#fce94f', '#729fcf', '#ad7fa8', '#34e2e2', '#eeeeec'), '#ffffff')}

Colors each chrome resolves a capture’s ANSI codes against.

A palette carries the 16 ANSI colors alongside the background and foreground, which is the other half of the job: a CLI naming blue leaves the shade to whoever draws it, and the one that reads on white is not the one that reads on #292929.

click_extra.capture.presets.resolve_palette(preset, background)[source]

The colors a capture resolves its ANSI codes against.

The preset’s palette on the given chrome, or the default terminal’s (CAPTURE_PALETTES) when no preset dresses the capture. The one resolution rule render() and style_palette() share, so the SVG and HTML formats cannot disagree on what a chrome looks like.

Parameters:
Return type:

TerminalPalette

Returns:

the palette.

click_extra.capture.presets.NO_PAINT = 'none'

Border or shadow value asking for none to be drawn.

SVG’s own keyword for an absent paint, so it reaches the stroke attribute unchanged, and CSS’s for an absent shadow.

click_extra.capture.presets.OPAQUE = 1.0

Opacity of a window showing nothing of what sits behind it.

Anything under it is what a terminal calls transparency: the backdrop, or the page embedding the capture, comes through the window’s body while its text, frame and title bar stay as they are. 0.0 leaves the text alone on the page.

click_extra.capture.presets.WATERMARK_INK = 'rgba(128,128,128,0.85)'

Color the credit line is drawn in.

The one paint in a capture that answers to neither chrome, because it is the one thing drawn outside the window: the margin is transparent, so the mark sits on whatever page embeds the image, which the capture never gets to see. A white mark suits the dark chrome it was picked for and disappears on a README; a neutral gray reads on both, and dims into a backdrop when one is painted.

click_extra.capture.presets.DEFAULT_BORDER_WIDTH = 1

Thickness, in pixels, of the frame drawn around the window.

click_extra.capture.presets.DEFAULT_RADIUS = 8

How round the window’s corners are, in pixels.

The radius a renderer draws on its own, which is what a terminal on a desktop looks like. Zero squares them, for a capture meant to read as a plain block.

click_extra.capture.presets.PACKAGE_NAME = 'click-extra'

Name this package is distributed and credited under.

click_extra.capture.presets.DEFAULT_WATERMARK = 'generated with click-extra'

Credit line every capture carries unless another one, or none, is asked for.

A capture travels: it lands on a slide, in a README, on a social card, far from the page that explains where it came from. The mark is what still says so, and carries the link back.

The line names the tool, never its release. A capture is committed beside the project that drew it and regenerated at every release, and a release number would rewrite every asset on release day for nothing but the bump.

Note

This is a default, not a fixture. --watermark "" draws none, and any other text replaces it: a project crediting itself rather than its tooling is the expected case, not an exception.

click_extra.capture.presets.DEFAULT_MARGIN = 48

Transparent pixels left around the window, on all four sides.

Room for the shadow to fall into, first of all: a filter draws outside the shape it is applied to, and anything past the image’s own box is cut. It is also what a backdrop has to show through, and what keeps the window from touching the text of the page embedding it.

click_extra.capture.presets.DEFAULT_PADDING = 8

Pixels added inside the window, around the captured text.

On top of the few a renderer adds on its own (8, and 40 above for the title bar), which leaves a help screen’s first column tight against the frame.

click_extra.capture.presets.CAPTURE_BORDERS: dict[TerminalBackground, str] = {TerminalBackground.DARK: 'rgba(255,255,255,0.35)', TerminalBackground.LIGHT: 'rgba(0,0,0,0.25)'}

Color the window frame is drawn in, per chrome.

The dark entry is a translucent white that reads against #292929 and against nothing else: a light capture framed with it is a white window on a white page, the shape of the terminal only guessable from its text. Each chrome names a frame its own background can show.

click_extra.capture.presets.CAPTURE_SHADOWS: dict[TerminalBackground, str] = {TerminalBackground.DARK: 'rgba(0,0,0,0.5)', TerminalBackground.LIGHT: 'rgba(0,0,0,0.25)'}

Color the window’s drop shadow floods with, per chrome.

Where the frame states the window’s edge, the shadow lifts it off whatever page embeds the capture, which is the other half of not dissolving into it. A reader whose renderer drops the filter still gets the frame.

class click_extra.capture.presets.Chrome(border=None, border_width=1, radius=None, backdrop='none', shadow=None, margin=48, padding=8, opacity=1.0, watermark='generated with click-extra', watermark_color=None)[source]

Bases: object

How the window around a capture’s text is drawn.

One value for the decoration render(), capture(), record_and_render() and render_snippet() all take, so its defaults live in one place. A field left at None takes what the capture’s background and preset draw, which resolve() fills in.

from click_extra.capture.presets import Chrome
from click_extra.capture.screenshot import render

svg = render(text, chrome=Chrome(margin=0, watermark=""))
border: str | None = None

Color of the window’s frame.

None takes the one the background can show, see CAPTURE_BORDERS; NO_PAINT draws none.

border_width: int = 1

Thickness of that frame, in pixels.

radius: int | None = None

How round the window’s corners are, in pixels. Zero squares them.

None takes the preset’s own, or DEFAULT_RADIUS without one.

backdrop: str = 'none'

Paint filling the image behind the window, margin included.

NO_PAINT leaves it transparent.

shadow: str | None = None

Color of the window’s drop shadow.

None takes the background’s own, see CAPTURE_SHADOWS; NO_PAINT draws none.

margin: int = 48

Transparent pixels left around the window, on all four sides.

padding: int = 8

Pixels added inside the window, around the text.

opacity: float = 1.0

How solid the window’s body is, from OPAQUE down to 0.0.

Below it, whatever the capture is laid over shows through.

watermark: str = 'generated with click-extra'

Credit line drawn in the image’s bottom-right corner.

See DEFAULT_WATERMARK. An empty string draws none.

watermark_color: str | None = None

Color that line is drawn in.

None takes WATERMARK_INK, which reads on a page of either color.

resolve(background, preset)[source]

This chrome, with every None replaced by what the capture draws.

render_svg() and render_html() take a resolved chrome, which makes this the one step between the two layers.

Parameters:
Return type:

Chrome

Returns:

a chrome carrying no None.

check_resolved()[source]

Refuse a chrome that still carries a None.

A renderer draws the values it is given, and a None names no paint and no size: only resolve() knows what it stands for.

Raises:

ValueError – naming each field still set to None.

Return type:

None

click_extra.capture.presets.BARE_CHROME = Chrome(border='none', border_width=1, radius=8, backdrop='none', shadow='none', margin=0, padding=0, opacity=1.0, watermark='', watermark_color='rgba(128,128,128,0.85)')

A resolved chrome that draws the window alone.

No frame, no shadow, no margin, no padding and no credit line: what render_svg() and render_html() draw when they get no chrome. A capture taken through render() resolves a Chrome instead, which frames the window and credits the package.

click_extra.capture.recording module

Rebuild a terminal’s screen from the stream a command writes at it.

A command that animates does not print its frames one after another: it prints one, returns the cursor to the start of the line, and prints the next over it. The stream therefore says what changed, and the frames only exist once something replays those changes against a screen. That replay is what this module does.

Note

The vocabulary is deliberately small. A spinner and a progress bar move the cursor with a carriage return, clear what they are about to redraw with an erase-in-line, and end a kept line with a newline. Following those, plus the color codes that travel with the text, is enough to recover their frames, and it costs no dependency. Anything addressing the cursor by coordinate, clearing the screen or switching to the alternate buffer is out of scope: a full-screen program is not what a capture pictures.

Todo

Read a full-screen program through pyte behind an optional extra, should picturing one ever be asked for. The screen below would stay the default, so the common case keeps costing no dependency.

click_extra.capture.recording.CSI_RE = re.compile('\\x1b\\[[0-?]*[ -/]*[@-~]')

One control sequence, from the escape that opens it to the final byte ending it.

Broad on purpose: the sequences this screen ignores have to be recognized to be dropped, or their parameter digits would land on the screen as text. The grammar is the one CSI_PATTERN states, so a sequence with an intermediate byte, like the ESC [ 2 SP q setting the cursor’s shape, is dropped whole too.

click_extra.capture.recording.PARTIAL_CSI_RE = re.compile('(?:\\x1b\\[[0-?]*[ -/]*|\\x1b)$')

The opening of a control sequence the rest of which has not arrived yet.

A recorder reads whatever the terminal hands it, on boundaries that answer to the pipe rather than to the stream: a sequence routinely straddles two reads. Recognizing the half that arrived first is what keeps its bracket and digits off the screen, where they would otherwise read as text a command never printed.

click_extra.capture.recording.DEFAULT_QUANTUM = 0.01

Grid a recording’s frame durations are rounded onto, in seconds.

Ten milliseconds is coarser than the jitter two runs of one unchanged command differ by, and finer than any frame worth picturing: the fastest bundled spinner holds one for eighty. See quantize().

click_extra.capture.recording.DEFAULT_RECORDING_HOLD: float | Literal['auto'] = 'auto'

How long a recorded animation holds its last frame before starting over.

A recording ends somewhere, and the end is usually its point: the trail filled in, the bar run out, the outcome landed. Looping straight back gives a reader no time to read any of it, and how much time reading takes depends on how much the ending shows, so the default scales with it: see auto_hold(). A declared animation cycles in place and ends nowhere, so it holds for nothing unless a page asks.

click_extra.capture.recording.DEFAULT_RECORDING_BLANK = 0.6

Seconds of empty screen closing a recorded animation’s cycle.

Long enough to read as the loop turning over, short enough not to read as the image going blank.

click_extra.capture.recording.DEFAULT_ROWS = 24

Height of the terminal a recording runs its command in, in characters.

A width is what a CLI wraps to and therefore what a capture pictures, so it is stated per recording. A height only has to be tall enough that nothing scrolls away before the screen is read, which this is.

click_extra.capture.recording.DEFAULT_SUBMIT = 0.4

Seconds a finished command line waits before its output starts, by default.

The beat between the last character and the return key. Long enough that the two read as separate acts, short enough that the animation does not appear to have stalled. See type_line().

click_extra.capture.recording.DEFAULT_TYPING = 0.05

Seconds one character of a typed command line takes to appear, by default.

A brisk but readable rate: a reader follows what is being typed rather than watching a line materialize. Slower than any real typist, which is the point, since the line is there to be read and not to be raced. See type_line().

click_extra.capture.recording.READ_POLL = 0.02

Seconds a recording waits on the terminal before checking the command again.

Short enough to time a frame of the fastest bundled spinner, whose interval is 80 milliseconds, and long enough that waiting costs no measurable spin.

click_extra.capture.recording.READ_SIZE = 65536

Bytes read from the terminal at once.

Well past any single frame, so a frame arrives whole in the ordinary case. The screen handles a sequence split across two reads regardless, see PARTIAL_CSI_RE.

click_extra.capture.recording.TERMINATE_GRACE = 2.0

Seconds a recording waits for the command to exit after SIGTERM.

A command still running when the recording ends, by its duration or by a Ctrl+C, gets SIGTERM first, to clean up after itself. One that ignores it is killed with its descendants once this grace runs out, so the recording never waits forever: see click_extra.execution._stop_process.

click_extra.capture.recording.ERASE_IN_LINE = 'K'

Final letter of the sequence clearing a line from the cursor onward.

click_extra.capture.recording.SELECT_GRAPHIC_RENDITION = 'm'

Final letter of the sequence setting colors and attributes.

The one class of sequence kept in the screen’s text rather than acted on: it carries no cursor movement, and a capture renders it as the color it names.

class click_extra.capture.recording.TerminalScreen[source]

Bases: object

The text a terminal shows, rebuilt from what was written at it.

Rows accumulate as a command writes. A carriage return moves the cursor back to the start of the row it is on, so what follows lands over what is already there, which is how an animation redraws itself in place.

Caution

Overwriting is whole-row rather than per-cell: a write landing on column zero replaces the row instead of covering its first few cells. Every animation this screen exists for redraws its row in full and ends with an erase-in-line, so the two agree there. They part on a command that overwrites a prefix and leaves a longer tail behind, where a terminal keeps the tail and this screen drops it. Tracking that faithfully means holding a style per cell, which is a full emulator and the dependency this one avoids.

Start on a single empty row, with the cursor at its start.

rows: list[str]
property display: str

The screen as it stands, rows joined by newlines.

feed(text)[source]

Replay what a command wrote, moving the screen to what it now shows.

Safe to call with the stream cut anywhere: a control sequence straddling two calls is held until the rest of it arrives. A sequence still unfinished when the stream ends never reaches the screen, having named nothing to draw.

Parameters:

text (str) – the stream, control sequences and all.

Return type:

None

class click_extra.capture.recording.Frame(text, duration)[source]

Bases: NamedTuple

One screen a recording held, and how long it held it.

Create new instance of Frame(text, duration)

text: str

What the terminal showed, ANSI escape sequences included.

duration: float

Seconds the screen stayed that way before the next frame replaced it.

class click_extra.capture.recording.ScreenRecorder(*, clock=<built-in function monotonic>)[source]

Bases: TextIOBase

A stream that keeps every screen written at it, and when.

Stands in for a terminal without being one. It answers isatty() in the affirmative, which is the whole question a spinner or a progress bar asks before deciding to animate, so handing one to a stream argument records an animation in-process: no pseudo-terminal, and therefore every platform.

Note

This is the path a documentation build takes. record_command() is for a command this process does not host, which has no such stream to be handed and needs a pseudo-terminal instead.

Start on a blank screen, with nothing recorded.

Parameters:

clock (Callable[[], float]) – what the recorder reads the time off. Stated so a test can hand it a clock that does not depend on how busy the machine is.

isatty()[source]

Claim to be a terminal, which is what makes a CLI animate at all.

Return type:

bool

writable()[source]

Claim to be writable, as a terminal is.

Return type:

bool

write(text)[source]

Replay what was written and note the screen it leaves behind.

A write changing nothing on screen is not noted: a cursor hidden or a style opened leaves the picture as it was, and the screen it did not change keeps the time it first appeared. What is kept is one sample per distinct screen, so a long animation costs as much memory as it has screens rather than as many as it has writes.

Parameters:

text (str) – the stream, control sequences and all.

Return type:

int

Returns:

how much was written, as a stream is expected to report.

frames(*, end=None)[source]

Coalesce what was recorded into the frames the screen held.

A write changing nothing on screen is not a frame, see write(), and neither is a screen holding nothing but blanks: an animation erases its line before printing the result underneath, and neither that erasure nor the newline ending it is a picture. What is left is one frame per distinct screen, each lasting until the next replaced it.

Caution

The durations are wall-clock, so they answer to how busy the machine was and differ a little between two runs of the same command. Quantize them before committing what they time.

Parameters:

end (float | None) – when the recording stopped, which is how long the last frame lasted. Defaults to now.

Return type:

tuple[Frame, ...]

Returns:

the frames, in the order they were drawn.

click_extra.capture.recording.record_command(args, *, columns=80, rows=24, background=TerminalBackground.DARK, duration=None, clock=<built-in function monotonic>)[source]

Run a command under a pseudo-terminal and record the screens it draws.

An animation is the one thing a pipe cannot capture. A spinner asks whether its stream is a terminal and stays silent when it is not, so a command run the ordinary way prints its result and none of the frames leading to it. Forcing color through the environment does not help: that answers a different question. Only a pseudo-terminal answers this one.

Caution

Unix only. A pseudo-terminal is termios and pty, neither of which Windows ships, and reaching ConPTY means a dependency. Recording a spinner this process hosts needs none of that: hand a ScreenRecorder to its stream and it animates on any platform.

The terminal this runs from is hidden from the command, see CAPTURE_HIDDEN_TERMINAL_VARS, so one machine’s recording matches another’s.

Both the command’s output and its errors are read, the spinner drawing on the latter. What comes back is timed by the wall clock and therefore differs a little between two runs: quantize the durations before committing them.

Parameters:
Return type:

tuple[Frame, ...]

Returns:

the frames the terminal held, in order.

Raises:

NotImplementedError – on a platform with no pseudo-terminal.

click_extra.capture.recording.quantize(frames, quantum=0.01)[source]

Round each frame’s duration onto a grid, so a rerun times it the same.

A recording is timed by the wall clock, so two runs of one unchanged command differ by a few milliseconds a frame. Rounding onto a grid coarser than that jitter lands both on the same numbers, which is what an asset rewritten on every build needs to stay byte-identical.

Caution

This settles jitter and nothing else. A frame the scheduler dropped leaves a shorter recording of the same frames, which no rounding recovers. That case is answered a layer up, by animation_digest(), which fingerprints the frames a cycle holds rather than how many of them were caught.

Parameters:
  • frames (Sequence[Frame]) – what was recorded.

  • quantum (float) – seconds per grid step.

Return type:

tuple[Frame, ...]

Returns:

the same frames, timed on the grid.

Raises:

ValueError – when the grid step is not positive.

click_extra.capture.recording.ansi_prefix(text, count)[source]

The first count printable characters of text, its styling kept.

Slicing a styled line by index cuts an escape sequence in half, and the remains land on the screen as the digits and brackets they are made of. Counting the printable characters alone and keeping every escape passed on the way is what avoids that, and it is also correct rather than merely safe: an escape is drawn nowhere, so the ones before a character are exactly the styling in force at it.

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

  • count (int) – how many printable characters to keep. Counted as characters rather than as cells, a keystroke producing one of either.

Return type:

str

Returns:

the prefix, closed by a reset so the styling it opened ends with it.

click_extra.capture.recording.type_line(line, *, typing=0.05, submit=0.4)[source]

Frames of a command line appearing one keystroke at a time.

The opening act a terminal recording is usually missing. A command records what it drew and never the invocation that drew it, so an animation starts on output arriving from nowhere. Typing the line first says where it came from, and reads as a session rather than as a clip.

Note

Ordinary frames, carrying no mechanism of their own: they prepend to a recording’s and travel through render_svg() like any others. Everything the picture does for a frame therefore reaches these too, the gutter numbering them and the cursor following the text along, see cursor_cell().

The last frame holds the finished line for submit, which is the beat before the return key. A recording’s own frames follow it.

Parameters:
  • line (str) – the command line to type, already styled as a prompt draws it, see format_cli_prompt().

  • typing (float) – seconds each character takes to appear.

  • submit (float) – seconds the finished line waits before whatever follows.

Return type:

tuple[Frame, ...]

Returns:

one frame per character, in the order they are typed. Empty for an empty line, there being nothing to type.

Raises:

ValueError – when either duration is not positive.

click_extra.capture.recording.record_and_render(args, *, columns=80, rows=24, background=TerminalBackground.DARK, prompt=None, duration=None, quantum=0.01, hold='auto', blank=0.6, speed=1.0, typing=0.0, submit=0.4, line_numbers=False, emphasize=(), cursor=None, closing_prompt=False, title='', unique_id=None, preset=None, chrome=Chrome(border=None, border_width=1, radius=None, backdrop='none', shadow=None, margin=48, padding=8, opacity=1.0, watermark='generated with click-extra', watermark_color=None))[source]

Record a command under a pseudo-terminal and render it as an animated SVG.

Chains record_command(), quantize() and render(), the way capture() chains the still pipeline. The invocation is drawn above every frame as a shell prompt, styled by the active theme through format_cli_prompt(), so the recording shows what to type to reproduce it.

Parameters:
  • args (str | Path | None | Iterable[str | Path | None | Iterable[Iterable[str | Path | None | Iterable[TNestedArgs]]]]) – the command line to record.

  • columns (int) – width of the terminal it runs in, in characters. A recording pins its width up front, so there is no auto here: the pseudo-terminal must exist before the command draws its first line.

  • rows (int) – height of that terminal, in characters.

  • background (TerminalBackground) – chrome the recording is headed for, stated to the command the way a terminal would.

  • prompt (str | None) – command line to display, when it differs from the one run. An empty string draws no prompt at all.

  • duration (float | None) – seconds to record before stopping the command. None records until it exits on its own.

  • quantum (float) – grid the frame durations are rounded onto, see quantize().

  • hold (float | Literal['auto']) – extra seconds the last frame stays up, or AUTO_HOLD (the default here) to scale them to that frame’s line count.

  • blank (float) – seconds of empty screen closing the cycle.

  • speed (float) – how much faster to play than recorded. The typed opening is replayed at the same rate as everything else, being part of what the animation shows rather than a pause laid over it.

  • typing (float) – seconds each character of the prompt takes to appear, opening the animation by typing the command out. Zero draws the prompt whole from the first frame, which is what a recording carrying no opening shows. See type_line().

  • submit (float) – seconds the finished command line waits before its output starts. Unused when nothing is typed.

  • line_numbers (bool) – draw each line’s number in a gutter, the prompt counting as the first of them.

  • emphasize (Sequence[int]) – lines to draw a band behind, see render_svg().

  • cursor (Cursor | None) – the terminal cursor to draw, see Cursor. It follows the text from screen to screen on its own, so a typed opening gets its caret from this and nothing else.

  • closing_prompt (bool) – draw the shell’s prompt under the last frame, which is where it comes back once the command exits, see append_prompt(). Only that frame carries it: the shell has not returned while the command is still drawing.

  • title (str) – see render().

  • unique_id (str | None) – see render().

  • preset (TerminalPreset | None) – see render().

  • chrome (Chrome) – see render().

Return type:

tuple[str, int]

Returns:

the rendered SVG document, and the command’s exit code.

Raises:
  • NotImplementedError – on a platform with no pseudo-terminal.

  • ValueError – when the command drew nothing to record, or when a stated typing or submit is not positive.

click_extra.capture.screenshot module

Run a CLI, capture its colors, and render the result as a static document.

In Sphinx the click:run directive executes each CLI and renders its real output at build time, so a documentation page never needs a screenshot. A README on GitHub or PyPI, a slide, or a social post cannot run code, and those surfaces need a capture instead.

The pipeline is two steps, each replaceable on its own:

  1. capture_output() runs the command through run_cli(), under forced_color() and a pinned terminal width, and hands back its raw ANSI text.

  2. render() turns that text into a document, in one of the CaptureFormat members.

capture() chains both, and is what the click-extra screenshot command calls.

Both formats read the same split_ansi() stream, and neither needs a dependency the package does not already carry: SVG is laid out on a character grid by render_svg(), HTML is inline-styled markup from render_html().

The two are not interchangeable, and neither is a fallback for the other:

  • SVG goes where you do not own the page. GitHub and PyPI render an image and strip inline HTML, so a README has no other option. It is a picture: the text is not selectable, and not searchable.

  • HTML goes where you do own the page. The text stays selectable, searchable and copy-pasteable, and reflows with the container.

click_extra.capture.screenshot.TColumns: TypeAlias

Width a capture is taken and rendered at, see AUTO_COLUMNS.

click_extra.capture.screenshot.AUTO_COLUMNS: Literal['auto'] = 'auto'

Width asking for the one the captured text itself decides.

Neither end of the pipeline is pinned: the command wraps to whatever terminal it finds (Click’s own 80 when that is a pipe, or a documentation build), and the image is laid out at the longest line that came back, see fit_columns(). Nothing the command printed folds inside the picture then, which is what a line the command does not wrap on its own needs: a prompt, a wide table, a machine-readable dump.

The cost is that the picture stops being a fixed-width terminal, so a capture meant to sit beside others at the same width should name that width instead.

click_extra.capture.screenshot.DEFAULT_COLUMNS = 80

Terminal width a capture is taken at, in characters.

Both ends of the pipeline have to agree on it: the command wraps its output to this width, and the renderer lays the image out at the same one. Let them disagree and the rendered lines overrun the image. 80 is the width Click itself falls back to off a terminal, which makes it the value a capture lands on by accident anyway.

click_extra.capture.screenshot.MIN_COLUMNS = 20

Narrowest width a capture is rendered at.

A floor on AUTO_COLUMNS as much as on an explicit width: a command printing nothing but blank lines would otherwise ask for an image no glyph fits in.

click_extra.capture.screenshot.AUTO_CURSOR: Literal['auto'] = 'auto'

Cursor shape asking for the one the terminal preset decides.

The same bargain as AUTO_COLUMNS and AUTO_HOLD: the caller says a cursor is wanted and leaves what it looks like to whatever knows the terminal. It is what shape of None spells on a command line, which cannot pass None.

click_extra.capture.screenshot.AUTO_TRUNCATION: Literal['auto'] = 'auto'

Marker asking for a rule across the width of the picture.

TRUNCATION_LABEL centered in a run of TRUNCATION_RULE, spanning the text column of the window. A bare label sits in the left margin and reads as one more line of output; a rule crosses the picture and reads as a seam, which is what a cut is. It carries no brackets, unlike a rule naming a section: there is nothing to name here, and the label is the cut itself.

Never wider than what decides the image width, so the marker cannot widen the picture it cuts: the pinned columns, or the widest line under AUTO_COLUMNS. See _rule_marker().

click_extra.capture.screenshot.TRUNCATION_LABEL = '✂'

What AUTO_TRUNCATION centers in its rule.

click_extra.capture.screenshot.TRUNCATION_RULE = '·'

Character AUTO_TRUNCATION draws its rule with.

Broken rather than RULE_GLYPH: a dotted line reads as text missing from that spot, where an unbroken one reads as a section ending.

A dot rather than one of the Box Drawing dashes, which carry two or three strokes inside a single cell. Those strokes and the hairline gaps between them are each a pixel or two wide at a normal capture scale, and a cell advances a fractional number of device pixels, so the gaps land inside a pixel on some cells and on a boundary on others: neighbouring dashes merge here and separate there, and the rule shimmers. One dot per cell has nothing to merge with.

click_extra.capture.screenshot.DEFAULT_TRUNCATION: str = 'auto'

Marker standing in for the lines trim_lines() cut away.

click_extra.capture.screenshot.AUTO_WRAP_MARKER: Literal['auto'] = 'auto'

Marker asking for WRAP_MARKER_LABEL at every fold.

A terminal folds a line wider than itself silently, so a reader of a picture cannot tell a line that wrapped from one the command printed in pieces, and a break landing mid-word reads as a typo. The marker says which it is. It costs a cell of the row it closes, see grid().

click_extra.capture.screenshot.WRAP_MARKER_LABEL = '↵'

What AUTO_WRAP_MARKER writes at the fold.

↵ rather than a right-pointing arrow: the line turns back to the left margin and continues on the row below, which is what the glyph draws. It is also the one convention with a precedent in another pager: delta ships it as its own --wrap-left-symbol, and paints it in a color of its own for the same reason this one is drawn in RULE_COLOR.

click_extra.capture.screenshot.DEFAULT_WRAP_MARKER: str = ''

What closes a folded row when nothing asks for a marker.

Empty, so a capture folds the way a terminal does. A reader drawing a terminal window is not owed a mark for something the terminal itself would not show.

click_extra.capture.screenshot.PROMPT_THEMES: dict[TerminalBackground, HelpTheme | None] = {TerminalBackground.DARK: None, TerminalBackground.LIGHT: HelpTheme(invoked_command=Style(fg='black', bold), command_help=<function identity>, heading=Style(fg='magenta', underline), constraint=Style(fg='magenta'), section_help=<function identity>, col1=<function identity>, col2=<function identity>, alias=Style(fg='blue', bold), alias_secondary=Style(fg='blue', bold, dim), epilog=<function identity>, critical=Style(fg='red', bold), error=Style(fg='red'), warning=Style(fg='magenta'), info=<function identity>, debug=Style(fg='blue', dim), option=Style(fg='blue', bold), subcommand=Style(fg='blue', bold), choice=Style(fg='magenta', bold), metavar=Style(fg='blue', dim, italic), bracket=Style(dim), envvar=Style(fg='magenta', dim), default=Style(fg='green', dim, italic), range_label=Style(fg='blue', dim), required=Style(fg='red', dim), argument=Style(fg='blue', italic), deprecated=Style(fg='red'), separator=Style(fg='black', bold, dim), search=Style(fg='green'), success=Style(fg='green'), cross_ref_highlight=True, subheading=Style(fg='blue', dim))}

Theme the prompt line is drawn with, per chrome.

The captured output arrives already colored by the CLI that produced it, under whatever theme that run was told to use. The prompt is the one line this process draws itself, so it is the one that would otherwise land on white chrome in the dark default’s near-white invoked_command style, invisible.

None keeps whatever theme the invocation already runs under. So does a missing entry: the mapping is read through dict.get().

click_extra.capture.screenshot.CAPTURE_TERMINAL_HINTS: dict[TerminalBackground, dict[str, str]] = {TerminalBackground.DARK: {'CLITHEME': 'dark', 'COLORFGBG': '15;0'}, TerminalBackground.LIGHT: {'CLITHEME': 'light', 'COLORFGBG': '0;15'}}

Environment a terminal of each chrome would carry, handed to the command.

A capture is a terminal simulated for a command that cannot see one: its width is pinned and its colors forced, because a pipe would have it wrap to a guess and print none. Its background is the third thing a terminal states and a pipe does not, through the two variables resolve_background() reads: the cli-theme CLITHEME, and COLORFGBG carrying foreground;background palette indices.

So a CLI asking for –theme auto renders for the chrome its picture is drawn on, instead of falling back to dark inside a light window. A CLI that never asks is unaffected: the variables only answer a question it does not put.

click_extra.capture.screenshot.CAPTURE_HIDDEN_TERMINAL_VARS: tuple[str, ...] = ('TERM_PROGRAM',)

Environment variables naming the terminal a capture is taken from.

A capture is drawn for a file, and read in a browser or an image viewer. The terminal that happened to run it is therefore not the terminal it is drawn for, and anything the command would tailor to that terminal has to be kept away from it, or the same capture comes out differently on every machine.

TERM_PROGRAM is the one that bites, through _paints_wider_than_it_advances(): a table carrying an emoji-presentation sequence is padded for the terminal named there, so a capture taken under Apple Terminal is wider than the same capture taken under Ghostty. Committed side by side, the two never stop rewriting each other.

Cleared rather than pinned to a value: no name is the honest answer, since a capture is drawn for no terminal in particular.

class click_extra.capture.screenshot.CaptureFormat(*values)[source]

Bases: Enum

Document formats a capture can be rendered to.

The value doubles as the file extension from_path() matches on.

ANSI = 'ansi'

The escape sequences themselves, for a terminal to paint.

The one target that needs no rendering, a terminal reading the same stream the capture is carried in. So it is the whole picture minus the window: there is no frame, no chrome and no margin to draw, and every option describing one is ignored, see render().

HTML = 'html'

Selectable, searchable text in a self-contained <pre>.

Built on ansi_to_html(), so it needs no extra.

SVG = 'svg'

A picture of a terminal window, for a surface that strips inline HTML.

Laid out on a character grid by render_svg().

classmethod from_path(path)[source]

Pick the capture format a file name asks for.

A bare STDOUT_SENTINEL names the terminal, whose format is the escape sequences themselves, see is_stdout(). A file named - in some directory is a file like any other, and answers by its extension, which it has none of. It is answered here rather than at the call site so the one question “what does this destination want?” has one answer.

Parameters:

path (Path) – where the capture is to be written.

Return type:

CaptureFormat

Returns:

the CaptureFormat its extension names.

Raises:

ValueError – when the extension names no format.

click_extra.capture.screenshot.capture_environment(background, columns)[source]

The environment a capture states to the command it runs.

The overrides env_copy() lays over the inherited environment, which is how run_cli() takes them: the chrome’s CAPTURE_TERMINAL_HINTS, every CAPTURE_HIDDEN_TERMINAL_VARS entry mapped to None so it is removed rather than set, and the width the command wraps to.

Parameters:
Return type:

dict[str, str | None]

Returns:

the overrides, by variable name.

click_extra.capture.screenshot.resolve_wrap_marker(marker)[source]

Read a wrap_marker option into the characters written at a fold.

The one reading of the option every entry point shares: the --wrap-marker of the capture commands, and the API those commands call.

Parameters:

marker (str) – the text, AUTO_WRAP_MARKER, or an empty string for no marker at all.

Return type:

str

Returns:

the characters to write at a fold, empty for none.

click_extra.capture.screenshot.parse_columns(value)[source]

Read a width written as text into a number, or into AUTO_COLUMNS.

The one reading of a columns option every entry point shares: the --columns of the capture commands and the :screenshot-columns: of the documentation directive. Each wraps the error in the exception its framework reports.

Parameters:

value (str) – the text, an integer or the word AUTO_COLUMNS.

Return type:

int | Literal['auto']

Returns:

the width, in characters, or AUTO_COLUMNS.

Raises:

ValueError – when the text is neither, or names a width under MIN_COLUMNS.

click_extra.capture.screenshot.auto_columns(pictures, cursor=None)[source]

Width, in characters, an auto-sized capture of pictures asks for.

The longest line any of them holds, which is what keeps a command’s own wrapping from folding again inside the picture. Frames are stacked in one window, so the widest is what has to fit.

A cursor standing past the end of its row needs a cell of its own on top of that. At exactly the text’s width it would otherwise wrap onto the row below, see cursor_cell(), and an auto-sized capture would show a row holding nothing but a cursor: the width is derived from the text, so the text always ends on the last column.

Parameters:
  • pictures (Sequence[str]) – the captured texts, one per frame, or the one a still draws.

  • cursor (Cursor | None) – the cursor the capture draws, if any.

Return type:

int

Returns:

the width, in characters.

click_extra.capture.screenshot.prompt_line(args, *, prompt=None, background=TerminalBackground.DARK, preset=None)[source]

Compose the invocation a capture draws above its output.

The one place three pipelines agree on what a prompt looks like: a still capture, a recording, and a documentation block that records one. Drawing it three times would be three chances for the sigil, the theme or the preset to disagree between an image and the image beside it.

Parameters:
  • args (str | Path | None | Iterable[str | Path | None | Iterable[Iterable[str | Path | None | Iterable[TNestedArgs]]]]) – the command line that was run, in the nested form run_cli() accepts.

  • prompt (str | None) – command line to display, when it differs from the one run. An empty string draws no prompt at all; None shows what was run.

  • background (TerminalBackground) – chrome the capture is headed for, which picks the theme the line is styled with.

  • preset (TerminalPreset | None) – terminal being pictured, which names the sigil its shell draws. None keeps this platform’s.

Return type:

str

Returns:

the styled line, or empty when nothing is to be drawn.

click_extra.capture.screenshot.append_prompt(text, *, background=TerminalBackground.DARK, preset=None)[source]

Put the shell’s prompt back on the row under a finished command.

What a terminal actually shows once a command exits: the shell comes back and waits, so the row under the output holds its sigil rather than nothing. Paired with a cursor it costs no height at all, the cursor having already claimed that row, see cursor_cell(). It also puts the cursor somewhere that reads: after a prompt, instead of alone on an empty line.

Caution

Only for a screen the command has finished drawing. Mid-animation the shell has not come back, and a sigil there says the command exited when it did not. record_and_render() therefore closes its last frame alone.

Parameters:
  • text (str) – the captured text to close.

  • background (TerminalBackground) – chrome the capture is headed for, which picks the theme the sigil is styled with.

  • preset (TerminalPreset | None) – terminal being pictured, which names the sigil its shell draws. None keeps this platform’s.

Return type:

str

Returns:

the same text, closed by a prompt.

click_extra.capture.screenshot.capture_output(args, *, columns=80, background=TerminalBackground.DARK, merge_stderr=False, timeout=None)[source]

Run a command and capture its output, ANSI escape sequences and all.

A command whose output is a pipe rather than a terminal strips its own colors, and wraps to whatever width it can guess. Both are pinned here: forced_color() sets the FORCE_COLOR lever every mainstream color system obeys and clears any opt-out the environment carries, while COLUMNS fixes the width the command wraps to.

Only stdout is captured by default. That is what keeps a capture free of the progress lines and build chatter a wrapper like uv writes to stderr, with no shell redirection to remember.

The terminal this runs from is hidden from the command, see CAPTURE_HIDDEN_TERMINAL_VARS, so one machine’s capture matches another’s.

Parameters:
Return type:

CompletedProcess[str]

Returns:

the completed process, whose stdout holds the captured text.

click_extra.capture.screenshot.trim_lines(text, *, head=None, tail=None, truncation='auto', columns=80, wrap_marker='', width_floor=0)[source]

Keep only the first head and last tail lines of text.

Whatever is dropped is replaced by a single truncation line, so the image admits that it was cut rather than pretending to be the whole output. Text short enough to survive both bounds comes back untouched, with no marker.

A cut leaving a style open above it closes that style before the marker, see _leaves_style_open().

Parameters:
  • text (str) – the captured output.

  • head (int | None) – number of leading lines to keep, or None for no head bound.

  • tail (int | None) – number of trailing lines to keep, or None for no tail bound.

  • truncation (str) – line standing in for what was cut.

  • columns (int | Literal['auto']) – width the text is drawn at, which bounds the rule AUTO_TRUNCATION draws, see _rule_marker().

  • wrap_marker (str) – what closes a folded row, see render(). It takes a cell of every row, so it narrows the rule by as much.

  • width_floor (int) – under AUTO_COLUMNS, the width of a line the picture draws besides text, like the prompt, which the rule spans too.

Return type:

str

Returns:

the trimmed text.

click_extra.capture.screenshot.emphasize_ansi(text, lines, paint)[source]

Band the named lines of ANSI text, the way a terminal can.

The picture’s band is a rectangle drawn behind a row. A terminal has no behind, so the band is the row’s own background color, set for the whole row and padded out to the longest line so the marked rows still square up into a block rather than ending ragged.

Caution

The band is restated after every escape sequence in the row, not just at its start. Pygments closes a colored run with a full reset (\x1b[39;00m), which clears the background along with the ink: set once, a band would stop at the row’s first keyword. Restating the same color costs nothing to look at, since the second declaration paints what the first already did.

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

  • lines (Sequence[int]) – rows to band, counted from 1. Empty bands nothing.

  • paint (str) – the band’s color, as #rrggbb.

Return type:

str

Returns:

the text, banded.

click_extra.capture.screenshot.render(text='', *, format=CaptureFormat.SVG, columns=80, title='', unique_id=None, frames=None, interval=None, hold=0.0, blank=0.0, speed=1.0, emphasize=(), cursor=None, wrap_marker='', full=True, background=TerminalBackground.DARK, preset=None, palette=None, chrome=Chrome(border=None, border_width=1, radius=None, backdrop='none', shadow=None, margin=48, padding=8, opacity=1.0, watermark='generated with click-extra', watermark_color=None))[source]

Render captured terminal text to the document format names.

Parameters:
  • text (str) – captured output, ANSI escape sequences included.

  • format (CaptureFormat) – which document to produce. CaptureFormat.ANSI draws no window, so it ignores everything describing one: the frame, the chrome, the caption, the margin, the credit line and the animation. What it keeps is emphasize, which marks rows rather than surrounding them, and whatever the caller already did to the text itself.

  • columns (int | Literal['auto']) – terminal width, in characters, an SVG is laid out at, or AUTO_COLUMNS for the width its own longest line asks for. HTML reflows and ANSI is the text itself, so both ignore this.

  • title (str) – caption drawn in an SVG’s window chrome, or an HTML document’s <title>.

  • unique_id (str | None) – SVG only. Prefix namespacing the source’s CSS classes and element IDs. Pinning it to something stable (the output file’s name, say) keeps a regenerated capture diffing line by line, instead of renaming every class as soon as a single character of output changes. Characters a CSS class name cannot carry are folded to a dash.

  • frames (Sequence[str] | None) – SVG only. The animation’s frames, see render_svg().

  • interval (float | Sequence[float] | None) – SVG only. How long each of them is shown, see render_svg().

  • hold (float | Literal['auto']) – SVG only. Extra seconds the last frame stays up, or AUTO_HOLD to scale them to that frame’s line count, see render_svg().

  • blank (float) – SVG only. Seconds of empty screen closing the cycle, see render_svg().

  • speed (float) – SVG only. How much faster to play than recorded, see render_svg().

  • emphasize (Sequence[int]) – SVG and ANSI. Lines to draw a band behind, see render_svg() and emphasize_ansi().

  • cursor (Cursor | None) – SVG only. The terminal cursor to draw, see Cursor. None draws none. A cursor naming no shape takes the one the preset says that terminal draws, so --preset windows gets its bar without stating it.

  • wrap_marker (str) – SVG only. What closes each row a fold cut short, AUTO_WRAP_MARKER for WRAP_MARKER_LABEL, or empty to fold silently, see grid(). HTML reflows with the page embedding it, and a terminal reading ANSI folds on its own, so neither has a fold of the capture’s to mark.

  • full (bool) – HTML only. See render_html().

  • background (TerminalBackground) – chrome to draw on, see TerminalBackground.

  • preset (TerminalPreset | None) – terminal to draw the capture as, see TerminalPreset. None keeps the renderer’s own neutral window.

  • palette (TerminalPalette | None) – colors the text resolves against. None takes the ones the preset and chrome name, which is what a terminal capture wants. The window’s decorations keep answering to the chrome either way: a stated palette repaints the terminal’s body, not the desktop’s frame around it.

  • chrome (Chrome) – how the window around the text is drawn, see Chrome.

Return type:

str

Returns:

the rendered document.

Raises:

ValueError – asking an ANSI or HTML capture to animate.

click_extra.capture.screenshot.capture(args, *, format=CaptureFormat.SVG, columns=80, prompt=None, head=None, tail=None, truncation='auto', merge_stderr=False, timeout=None, line_numbers=False, emphasize=(), cursor=None, wrap_marker='', closing_prompt=False, title='', unique_id=None, full=True, background=TerminalBackground.DARK, preset=None, chrome=Chrome(border=None, border_width=1, radius=None, backdrop='none', shadow=None, margin=48, padding=8, opacity=1.0, watermark='generated with click-extra', watermark_color=None))[source]

Run a command and render its output as a document.

Chains capture_output(), trim_lines() and render(). The invocation is drawn above the output as a shell prompt, styled by the active theme through format_cli_prompt(), so the capture shows what to type to reproduce it.

Parameters:
  • args (str | Path | None | Iterable[str | Path | None | Iterable[Iterable[str | Path | None | Iterable[TNestedArgs]]]]) – the command line to run.

  • format (CaptureFormat) – which document to produce.

  • columns (int | Literal['auto']) – terminal width, in characters, or AUTO_COLUMNS to pin none and lay the image out at what the command printed.

  • prompt (str | None) – command line to display, when it differs from the one run. uv run --frozen -- my-cli reproduces a capture from a checkout, but my-cli is what a reader types. An empty string draws no prompt at all.

  • head (int | None) – number of leading output lines to keep.

  • tail (int | None) – number of trailing output lines to keep.

  • truncation (str) – line standing in for the lines cut by head or tail.

  • merge_stderr (bool) – fold stderr into the captured output.

  • timeout (float | None) – seconds before the command is killed.

  • line_numbers (bool) – draw each line’s number in a gutter, see number_lines(). The prompt counts as the first of them, being the invocation everything under it came from.

  • emphasize (Sequence[int]) – lines to draw a band behind, see render_svg(). The prompt is line 1 here too, and a gutter does not shift the count.

  • cursor (Cursor | None) – see render(). A still capture leaves its cursor after the last thing the command printed, which is where the shell finds it.

  • wrap_marker (str) – see render().

  • closing_prompt (bool) – draw the shell’s prompt on the row under the output, which is where it comes back once the command exits, see append_prompt().

  • title (str) – see render().

  • unique_id (str | None) – see render().

  • full (bool) – see render().

  • background (TerminalBackground) – see render().

  • preset (TerminalPreset | None) – see render().

  • chrome (Chrome) – see render().

Return type:

tuple[str, int]

Returns:

the rendered document, and the command’s exit code.

click_extra.capture.snippet module

Draw a snippet of source code as a static document.

click_extra.capture.screenshot pictures what a command printed. This module pictures what a file says, and hands the result to the same renderer: a README, a slide or a social post that cannot run code equally cannot syntax highlight it, so both need a picture.

The pipeline is the screenshot module’s, with its first step swapped:

  1. highlight_code() colors the source with Pygments, which writes the same ANSI escape sequences a terminal would.

  2. render() turns that text into a document.

That leaves nothing to reimplement. Pygments’ terminal formatter speaks the interchange format split_ansi() already parses, so a snippet inherits the window, the presets, the light and dark chromes, the line numbers, the emphasis bands and the HTML export from the captures beside it.

render_snippet() chains both, and is what the click-extra snippet command calls.

Note

A snippet answers to a Pygments style, where a capture answers to a terminal palette. The two name colors differently: a style states every color it uses, while a terminal names sixteen and leaves their shades to whoever draws them. style_palette() is where the first becomes the second.

click_extra.capture.snippet.DEFAULT_SYNTAX_STYLES: dict[TerminalBackground, str] = {TerminalBackground.DARK: 'monokai', TerminalBackground.LIGHT: 'default'}

Pygments style a snippet is colored with, per chrome.

Both are picked to sit beside a terminal capture without a seam showing. monokai paints #272822, two shades off the #292929 a dark capture is drawn on, and Pygments’ own default paints #f8f8f8 against a light capture’s white. A darker style renders perfectly well on its own and steps visibly when the two images share a page, which is the case this default is chosen for.

click_extra.capture.snippet.TAB_WIDTH = 4

Spaces a tab is expanded to before the snippet is laid out.

A terminal expands tabs as it prints, so captured output never carries one and the renderer never had to answer for it. A file does carry them, and a tab is the one character whose width depends on where it starts: it reaches the next stop eight columns along. grid() weighs each character on its own, so it reads every tab as the eight cells cell_width() answers for one measured alone, wherever the tab actually sits. Expanding up front is what keeps the grid arithmetic true, at the width a reader expects of source code rather than of a terminal.

click_extra.capture.snippet.FALLBACK_LEXER = 'text'

Lexer a snippet is colored with when nothing identifies its language.

Emits one unstyled token per line, so an unrecognized file is drawn as the plain text it could not be proven to be, rather than miscolored as a guess.

click_extra.capture.snippet.known_styles()[source]

Every Pygments style a snippet can be colored with, sorted.

Cached: the walk behind pygments.styles.get_all_styles() reads the plugin entry points on every call, and a process never gains a style.

Return type:

tuple[str, ...]

Returns:

the style names.

click_extra.capture.snippet.resolve_lexer(code, *, language=None, filename=None)[source]

Pick the lexer a snippet is colored with.

Three sources answer, in falling order of how much they know: the language stated outright, the file name it was read from, and the code itself. A stated language that names no lexer is an error rather than a fallback, since silently drawing pythn as plain text hides the typo in a picture nobody re-reads.

Parameters:
  • code (str) – the source, used to break a tie between lexers sharing an extension and to guess when nothing else identifies it.

  • language (str | None) – the language, as Pygments names it.

  • filename (str | None) – name the code was read from, extension included.

Return type:

Lexer

Returns:

the lexer.

Raises:

ValueError – when language names no lexer Pygments knows.

click_extra.capture.snippet.resolve_style(style, background)[source]

The Pygments style a snippet is colored with.

Parameters:
  • style (str | None) – the style asked for, or None for the chrome’s default.

  • background (TerminalBackground) – chrome the snippet is drawn on.

Return type:

str

Returns:

the style’s name.

Raises:

ValueError – when style names no style Pygments knows.

click_extra.capture.snippet.style_palette(style, background, *, preset=None)[source]

The colors a snippet’s window is drawn with, taken from a syntax style.

A style states the background it was designed against, and a snippet drawn on any other one is a picture of that style in a window it was never meant for: monokai’s comment gray reads on #272822 and disappears on white.

The chrome’s own palette is where the rest comes from. Its sixteen ANSI slots are carried through untouched, being what a preset publishes and what a run captured in the same document resolves against; a snippet consults none of them, since Pygments states every color outright.

Note

A style naming no color for plain text keeps the chrome’s foreground, which is what the twenty-five light styles shipping no Token.Text color need: their code is black on near-white, and black is what a light chrome already names.

Parameters:
  • style (str) – name of the Pygments style.

  • background (TerminalBackground) – chrome the snippet is drawn on.

  • preset (TerminalPreset | None) – terminal the window is dressed as, whose palette the decorations keep answering to.

Return type:

TerminalPalette

Returns:

the palette.

click_extra.capture.snippet.highlight_code(code, *, language=None, filename=None, style='monokai')[source]

Color source code the way a terminal would print it.

Pygments’ true-color terminal formatter writes 38;2;r;g;b sequences, which is the one interchange format both capture renderers already read. So the output of this drops straight into render(), with nothing in between.

Parameters:
  • code (str) – the source to color.

  • language (str | None) – the language, as Pygments names it. See resolve_lexer() for what answers when it is left out.

  • filename (str | None) – name the code was read from, extension included.

  • style (str) – name of the Pygments style to color with.

Return type:

str

Returns:

the source, ANSI escape sequences included.

Raises:

ValueError – when language names no lexer Pygments knows.

click_extra.capture.snippet.render_snippet(code, *, format=CaptureFormat.SVG, language=None, filename=None, style=None, columns='auto', head=None, tail=None, truncation='auto', line_numbers=False, emphasize=(), wrap_marker='', title='', unique_id=None, full=True, background=TerminalBackground.DARK, preset=None, chrome=Chrome(border=None, border_width=1, radius=None, backdrop='none', shadow=None, margin=48, padding=8, opacity=1.0, watermark='generated with click-extra', watermark_color=None))[source]

Color source code and render it as a document.

Chains highlight_code(), trim_lines() and render().

Note

The width defaults to AUTO_COLUMNS, where a terminal capture pins eighty. A command wraps its own output to the width it was told about, so a capture at that width folds nothing; nobody wrapped a source file to any width, and a picture that soft-wrapped its code would break indentation the reader is meant to be reading.

Parameters:
Return type:

str

Returns:

the rendered document.

Raises:

ValueError – when language or style names nothing Pygments knows.

click_extra.capture.svg module

Draw captured terminal text as an SVG picture of a terminal window.

click_extra.capture.screenshot decides what a capture shows, then hands the text, its palette and the resolved Chrome to render_svg(), which lays the text out on a character grid and paints the window around it, one layer at a time: backdrop, shadow, frame, title bar, emphasis bands, text, cursor, credit line and animation.

The window’s geometry lives here too, since every pixel size below is the picture’s: the title bar, the shadow’s blur and offset, the credit line’s size and place, and the cell metrics the grid is measured in. click_extra.capture.html draws the same window and borrows them, so the two documents look like the same terminal. The paints and sizes a Chrome defaults to, like NO_PAINT and OPAQUE, live beside it in click_extra.capture.presets.

click_extra.capture.svg.THold: TypeAlias

Pause an animation takes on its last frame, see AUTO_HOLD.

click_extra.capture.svg.TITLEBAR_HEIGHT = 40

Height, in pixels, of the strip a title and its buttons sit in.

The padding a renderer leaves above the text, which is what a window’s chrome occupies. Restated here because a capture wearing neither decoration nor caption drops the strip, and one drawn as a real terminal paints it.

click_extra.capture.svg.SHADOW_BLUR = 6

Standard deviation, in pixels, of the drop shadow’s blur.

click_extra.capture.svg.SHADOW_OFFSET = 3

Downward offset, in pixels, of the drop shadow.

click_extra.capture.svg.WINDOW_PADDING = 8

Pixels every window keeps between its frame and its text, on three sides.

The fourth is the top, where TITLEBAR_HEIGHT answers instead. This is the window’s own breathing room, before the padding a capture may ask for on top.

click_extra.capture.svg.WINDOW_INSET = 1

Pixels between the image’s edge and the window’s frame.

A stroke straddles the shape it outlines, so a frame drawn flush with the viewBox loses its outer half to the crop. Inset by more than that half and the whole line shows.

click_extra.capture.svg.CSS_SIDE_ANGLES = {'to bottom': 180.0, 'to bottom left': 225.0, 'to bottom right': 135.0, 'to left': 270.0, 'to left bottom': 225.0, 'to left top': 315.0, 'to right': 90.0, 'to right bottom': 135.0, 'to right top': 45.0, 'to top': 0.0, 'to top left': 315.0, 'to top right': 45.0}

Angle each CSS side keyword names, in degrees clockwise from to top.

to bottom is what a gradient opening with no direction at all means, which is why it doubles as the default. See gradient_svg().

click_extra.capture.svg.TITLE_SIZE = 18

Height, in pixels, of the caption drawn in a window’s title bar.

click_extra.capture.svg.WATERMARK_SIZE = 13

Height, in pixels, of the credit line’s glyphs.

Below the terminal’s own text, since a mark competing with the screen it credits is a mark in the way.

click_extra.capture.svg.WATERMARK_INSET = 12

Pixels between the credit line and the image’s bottom-right corner.

It is drawn in the margin, the one band of a capture that carries nothing else. A capture shot with margin=0 has no such band, and the line lands on the window’s own corner instead of beside it.

click_extra.capture.svg.WATERMARK_URL = 'https://kdeldycke.github.io/click-extra/screenshots.html'

Page the credit line points at, for a reader holding only the image.

A capture travels away from whatever explained it, so the mark carries the way back. Only PACKAGE_NAME is linked, and only where the line still names it: a project crediting itself instead has no click-extra left to point at, and gets no link rather than one pointing somewhere it did not ask for.

Note

The link answers where the file is interactive: opened on its own, inlined into a page, or embedded through <object>. An <img> draws an SVG as a picture and no click reaches inside one, which is how this documentation embeds its own captures.

click_extra.capture.svg.CELL_HEIGHT = 20.0

Height of one glyph cell, in pixels, which is also the text’s font size.

click_extra.capture.svg.FONT_ASPECT_RATIO = 0.61

Width-to-height ratio of the font a capture is laid out for.

Fira Code’s, the first family CAPTURE_FONT_STACK asks for. Every monospaced fallback behind it is close enough that the grid holds, and textLength pins each run to its columns for the ones that are not.

click_extra.capture.svg.CELL_WIDTH = 12.2

Width of one glyph cell, in pixels. One character of a monospaced terminal.

click_extra.capture.svg.LINE_HEIGHT = 24.4

Vertical distance between two consecutive text baselines, in pixels.

click_extra.capture.svg.CELL_BLEED = 0.25

Pixels a cell’s background is grown by, past the line it belongs to.

Two rectangles meeting on an exact boundary leave a hairline of page showing through when a renderer rounds their edges to different pixels. Overlapping them slightly is what closes that seam, and is invisible because the color painted twice is the same color.

click_extra.capture.svg.CELL_TOP_INSET = 1.5

Pixels between a line’s top edge and the cell backgrounds drawn on it.

A glyph does not fill its line box: the leading sits above the tallest letter. Starting the paint just under that keeps a highlighted run reading as one block of color rather than as a band taller than the text it marks.

click_extra.capture.svg.CURSOR_THICKNESS = 2.0

Pixels a cursor is drawn thick, when it is a line rather than a cell.

Both BAR and UNDERLINE are the same line turned a quarter, so one number states both. Thin enough to read as a cursor beside a glyph, thick enough to survive a renderer rounding it to whole pixels.

click_extra.capture.svg.DEFAULT_CURSOR_SHAPE = CursorShape.BLOCK

Shape a cursor takes when nothing names one.

What every terminal here but Windows draws, and the shape a reader recognizes as a terminal cursor rather than as a text caret. A capture given a preset takes that terminal’s own shape instead, see cursor.

click_extra.capture.svg.TILE_RUN = 8

Cells of tiling characters drawn before their offset is restated.

Small enough that a font whose tiles are a fraction of a pixel off the grid cannot drift a visible amount before the next offset resets it, and large enough that a table’s rule stays a handful of elements rather than one per cell. See tile_runs().

click_extra.capture.svg.DIM_RATIO = 0.4

How far a dim run’s ink is mixed toward the background, see blend().

click_extra.capture.svg.EMPHASIS_RATIO = 0.16

How far an emphasized line’s band is mixed from the background toward the ink.

Mixed rather than stated outright, so one number answers for every chrome: a band a shade lighter than a dark terminal is a shade darker than a light one, and both read as the same emphasis. Far enough to find the line at a glance, near enough to leave its text the thing being read.

click_extra.capture.svg.HIDDEN_FRAME_ATTRIBUTES = ' visibility="hidden" opacity="0"'

How an animated capture hides the frames its still is not made of.

Presentation attributes rather than a stylesheet rule, so a renderer reading no CSS still shows one picture instead of every frame at once. A CSS animation outranks a presentation attribute, so frame_animation_css() restores both together and the two mechanisms never disagree.

Caution

Neither property is redundant. visibility alone satisfies a browser, librsvg and macOS Quick Look’s thumbnailer, but two other readers ignore it and draw every frame on top of the last: a git client’s SVG diff view, and macOS Finder’s preview pane, a different code path from the thumbnailer. Both honor opacity, so each property covers a reader the other misses, and dropping either takes a class of reader with it.

click_extra.capture.svg.ANIMATION_METADATA_RE = re.compile('<!-- @recording (?P<fields>[^>]*?) -->')

The line an animated capture states its own identity on.

Written beside the @generated line, and read back by whatever has to decide whether a freshly drawn animation is the one already on disk. Holding the answer in the file is what spares that decision from parsing an SVG back into frames.

click_extra.capture.svg.FRAME_TIMING_FUNCTION = 'step-end'

How an animated capture moves between two frames: it does not.

A terminal repaints a whole cell at once, so a capture of one has nothing to interpolate. step-end holds each keyframe’s value until the next one is reached, which turns a percentage ladder into discrete frames and lets every frame state its own window. That is what carries a recording, whose frames each last as long as the terminal held them, on the same machinery as a spinner, whose frames are all the same length.

click_extra.capture.svg.REDUCED_MOTION_QUERY = '@media (prefers-reduced-motion: no-preference)'

Guard every animation rule an animated capture emits sits behind.

click_extra.accessibility counts an endlessly repeating spinner among the things --accessible exists to lower, and an image looping forever on a documentation page is the same imposition on a reader who asked their system for less motion. Outside the guard the first frame stays visible and the rest stay hidden, which is the picture a renderer that reads no CSS animation already gets, so honoring the preference costs a media query and no second code path.

click_extra.capture.svg.AUTO_HOLD: Literal['auto'] = 'auto'

Hold asking for the pause the final screen’s own density decides.

Resolved by auto_hold(): the busier the closing screen, the longer the loop pauses on it before starting over. A fixed number serves an animation whose ending the author has seen; this serves one whose ending changes with every retake, like a recorded command whose closing report grows with what there is to report.

click_extra.capture.svg.AUTO_HOLD_SECONDS_PER_LINE = 0.25

Seconds auto_hold() grants per populated line of the final screen.

A scanning rate rather than a reading one: terminal output is tables and listings, which a reader sweeps at a few lines a second rather than reads word by word.

click_extra.capture.svg.AUTO_HOLD_MIN = 2.0

Shortest pause auto_hold() answers.

Even a one-line outcome deserves a beat before the loop swallows it.

click_extra.capture.svg.AUTO_HOLD_MAX = 30.0

Longest pause auto_hold() answers.

Past this, a looping animation reads as a still that never moves: whoever needs longer than half a minute on one screen wants the still, not the loop.

click_extra.capture.svg.auto_hold(text)[source]

Seconds a reader needs on a closing screen, from how much it shows.

Counts the populated lines of text (ANSI escapes stripped, blank rows ignored) and grants AUTO_HOLD_SECONDS_PER_LINE for each, clamped between AUTO_HOLD_MIN and AUTO_HOLD_MAX. Lines rather than words, because terminal output is mostly tables whose box-drawing rows would drown a word count without adding anything to read.

Parameters:

text (str) – the final frame’s captured text, ANSI escapes included.

Return type:

float

Returns:

the pause, in seconds.

click_extra.capture.svg.parse_pause(value)[source]

Read a pause written as text into seconds, zero included.

Parameters:

value (str) – the text.

Return type:

float

Returns:

the pause, in seconds.

Raises:

ValueError – when the text is not a number, or is negative.

click_extra.capture.svg.parse_hold(value)[source]

Read a hold written as text into seconds, or into auto.

AUTO_HOLD scales the pause to the final frame’s own line count, see auto_hold().

Parameters:

value (str) – the text, a number of seconds or the word auto.

Return type:

float | Literal['auto']

Returns:

the hold.

Raises:

ValueError – when the text is neither, or names a negative pause.

click_extra.capture.svg.blend(color, into, ratio)[source]

Mix color toward into, the way a terminal fades dim text.

SVG has no dim, and thinning the glyphs with opacity would let whatever sits behind the capture show through them. Mixing the two colors up front keeps the text opaque and lands the same shade.

Parameters:
  • color (str) – the color to fade, as #rrggbb.

  • into (str) – the color to fade it toward, usually the background.

  • ratio (float) – how far to go, from 0.0 (unchanged) to 1.0 (into).

Return type:

str

Returns:

the blended color, as #rrggbb.

click_extra.capture.svg.emphasis_paint(palette)[source]

The color an emphasized line’s band is painted, on palette.

EMPHASIS_RATIO of the way from the terminal’s background toward its ink, see blend().

Parameters:

palette (TerminalPalette) – the terminal colors the capture is drawn with.

Return type:

str

Returns:

the band’s color, as #rrggbb.

click_extra.capture.svg.palette_color(color, palette)[source]

Resolve any color a Style carries to a hex string.

The 16 named and indexed ANSI slots are not colors, they are names: a terminal decides what its red looks like, and a capture has no terminal, so they answer to palette. Every other form a style can carry (a 24-bit triplet, a 256-cube index, a hex string) already states its own color and passes through.

Parameters:
  • color (object) – the value to resolve, as Style holds it.

  • palette (TerminalPalette) – the terminal colors to resolve names against.

Return type:

str

Returns:

the color, as #rrggbb.

Raises:

ValueError – when the value names no color.

click_extra.capture.svg.style_rules(style, palette)[source]

Compile a style to the CSS an SVG text run is drawn with.

Parameters:
Return type:

str

Returns:

the CSS declarations, semicolon-separated.

click_extra.capture.svg.cursor_cell(picture, columns)[source]

Where a terminal left its cursor on the screen a capture pictures.

A TerminalScreen joins its rows with newlines and appends what a command writes to the last of them, so the cursor stands at the end of that row. A picture closing on a newline therefore carries an empty last row, and the cursor lands on it at column zero, which is where a terminal puts it.

Note

Derived rather than recorded, and exactly rather than nearly: the position is a property of the text a screen holds, so it survives a frame travelling as a bare string and costs the recorder no second channel to carry it on.

Parameters:
  • picture (str) – the captured text, ANSI escape sequences included.

  • columns (int) – width of the terminal, in characters.

Return type:

tuple[int, int] | None

Returns:

the cursor’s row and column, both counted from zero, or None for a screen showing nothing. The blank beat closing an animation’s cycle is the one such screen, and an empty terminal shows no cursor.

click_extra.capture.svg.gradient_svg(value, unique_id, width, height)[source]

Translate a CSS gradient into the paint server SVG draws it with.

An SVG fill takes a paint: a color, or a reference to a gradient declared as an element of its own. The syntax a page’s CSS carries, linear-gradient(135deg, #ff9a9e, #fad0c4), means nothing to it, and a capture handed one would come out unpainted. So the CSS is read here and re-emitted as the element SVG does understand, which is what lets the same --backdrop value serve both formats.

Understood: linear-gradient opening with an optional angle (135deg) or side keyword (to bottom right, see CSS_SIDE_ANGLES), and radial-gradient, both followed by two or more color stops, each pinnable at a percentage. Anything else returns None and is left alone, being a plain color as far as this is concerned.

The gradient is placed in user space, which is what makes it exact rather than approximated: the CSS line runs through the image’s center at the given angle, and is as long as the box measures along it (|W·sinθ| + |H·cosθ|), while a radial one reaches the farthest corner.

Parameters:
  • value (str) – the --backdrop value, gradient or not.

  • unique_id (str) – identifier the paint server is declared under.

  • width (float) – width of the image the gradient fills, in pixels.

  • height (float) – its height.

Return type:

tuple[str, str] | None

Returns:

the <defs> markup and the fill value referencing it, or None when the value is not a gradient this understands.

click_extra.capture.svg.titlebar_strip(left, top, width, *, paint, radius)[source]

Paint the strip a terminal seats its title and buttons in.

A capture leaves that strip the color of the terminal itself, where a real window carries a chrome of its own: the strip is what a reader’s eye reads as the top of a window rather than as the first line of output.

Drawn as a path rather than a rectangle because only its top corners follow the window’s own rounding; the bottom two meet the text and stay square.

Parameters:
  • left (float) – where the window starts, in pixels.

  • top (float) – where the window starts vertically, in pixels.

  • width (float) – how wide the window is, in pixels.

  • paint (str) – color to fill the strip with.

  • radius (int) – the window’s corner radius, in pixels.

Return type:

str

Returns:

the SVG markup.

click_extra.capture.svg.credit_markup(text, url, link, *, escape)[source]

Write a credit line as markup, the package name it credits linked.

The one part of the line worth pointing anywhere is the name, so both documents link that and leave the rest as prose. A line naming no package, or given no URL, is the escaped text and nothing more: a custom credit has nothing to link.

Parameters:
  • text (str) – the credit line.

  • url (str) – where the package name points. Empty links nothing.

  • link (Callable[[str, str], str]) – draws the anchor around the name, from the URL and the raw name. It escapes both for the document it writes.

  • escape (Callable[[str], str]) – how the rest of the line is escaped for that document.

Return type:

str

Returns:

the markup.

click_extra.capture.svg.watermark_svg(text, *, width, height, paint, font_stack="'Fira Code', 'Cascadia Code', Menlo, Consolas, monospace", url='https://kdeldycke.github.io/click-extra/screenshots.html')[source]

Draw the credit line in the image’s bottom-right corner.

Placed in the margin rather than over the terminal, which is what keeps it from covering a line of output: a capture is a picture of text, and a mark crossing that text costs the reader the thing being shown.

Carries a watermark class, so a reader taking a capture apart can tell the one run the renderer never captured from the ones it did.

The package name is wrapped in a link, see WATERMARK_URL and credit_markup().

Parameters:
  • text (str) – the credit to draw. Empty draws nothing.

  • width (float) – width of the whole image, in pixels.

  • height (float) – its height, in pixels.

  • paint (str) – color to draw the text in, alpha included.

  • font_stack (str) – fonts it is set in, the capture’s own.

  • url (str) – where the package name points. Empty links nothing.

Return type:

str

Returns:

the SVG markup, empty when there is nothing to draw.

click_extra.capture.svg.window_buttons(buttons, *, width, color, font_stack="'Fira Code', 'Cascadia Code', Menlo, Consolas, monospace")[source]

Draw a title bar’s decorations, as the terminal being mimicked draws them.

Two conventions, and a window carries one or the other: macOS fills round buttons on the left, Windows and GNOME set glyphs against the right edge. Both are placed in the window’s own coordinates, so they follow it wherever the frame moves it.

Parameters:
  • buttons (WindowButtons) – which decorations to draw.

  • width (float) – width of the window they are drawn in, in pixels.

  • color (str) – paint for the glyphs. Circles carry their own.

  • font_stack (str) – fonts the glyphs are set in, the window’s own.

Return type:

str

Returns:

the SVG markup, empty when the window wears none.

click_extra.capture.svg.cursor_svg(cursor, row, column, paint, unique_id)[source]

Draw a terminal’s cursor on the cell it stands on.

Emitted as part of that row’s cell backgrounds rather than as a layer of its own, which is what puts it under the glyphs and hands it to the machinery already deciding which rows move between frames. A row whose cursor never stirs is then drawn once for the whole animation, and a row that moves carries its cursor along at no charge. It is also why an animation typing a command line needs no caret of its own: the cursor follows the text.

Note

The cursor is never drawn over a glyph, so a block shape covering one is not a case to answer. cursor_cell() puts it at the end of a row, which is past everything written on it.

Parameters:
  • cursor (Cursor) – what the cursor looks like, see Cursor.

  • row (int) – the row it stands on, counted from zero.

  • column (int) – the column it stands on, counted from zero.

  • paint (str) – color it is drawn in.

  • unique_id (str) – prefix namespacing this document’s classes.

Return type:

str

Returns:

the SVG source for the cursor.

click_extra.capture.svg.column_segments(text, column)[source]

Cut a run of text into the columns it actually occupies.

A run carries its own padding: a help screen’s –count INTEGER Number of greetings. is one styled run holding two columns and the gutter between them. Drawn as a single element, the second column only lands where it belongs if the renderer honors textLength and resolves the font, because the gutter’s width is being paid for in glyphs. librsvg does neither, and the columns collapse onto each other.

Cutting the run at its gutters and giving each piece its own offset asks nothing of the renderer but to draw glyphs at coordinates.

Parameters:
  • text (str) – the run’s text, padding included.

  • column (int) – the terminal column the run starts on.

Return type:

Iterator[tuple[str, int]]

Returns:

each column’s text, with the column it starts on.

click_extra.capture.svg.tile_runs(text, column)[source]

Break a column’s text into the pieces drawn as one element each.

Ordinary text is one piece: the renderer lays it out and textLength holds the result to the width it occupies.

Text carrying a tile (_TILING_RE) is cut into groups of at most TILE_RUN cells, each landing on a stated offset. A <text> element is the smallest thing some renderers position at all: librsvg (and through it rsvg-convert and ImageMagick) honors the first x of an element and then lays every following glyph out at the font’s own advance, ignoring both textLength and any further x. A rule of 75 tiles drawn a tenth of a pixel narrow therefore ends a whole cell short of the │ below it, and the table’s corners miss. Restating the offset every few cells bounds that error to well under a pixel, whatever the font, and costs a tile nothing since none of them ligate.

Parameters:
  • text (str) – the column’s text.

  • column (int) – the terminal column it starts on.

Return type:

Iterator[tuple[str, int]]

Returns:

each piece, with the column it starts on.

click_extra.capture.svg.glyph_offsets(text, column)[source]

Place a piece of text on the grid, as the attributes SVG reads.

A right-to-left piece is pinned by its offset alone: it is reordered and shaped by whoever draws it, and holding it to a width fights that.

Parameters:
  • text (str) – the piece’s glyphs.

  • column (int) – the terminal column it starts on.

Return type:

str

Returns:

the x attribute, and a textLength where one applies.

click_extra.capture.svg.animation_digest(frames, durations)[source]

Fingerprint what an animation is, rather than how it happened to run.

Two recordings of one unchanged command are never byte-identical: the durations are wall-clock, so they answer to how busy the machine was. Quantizing settles the ordinary jitter and cannot settle a frame the scheduler dropped, which leaves a shorter sequence of the same frames.

So the fingerprint covers the frames a cycle holds and the beat it holds them on, not the order or the count. A dropped frame changes neither, and a command whose output actually changed changes the first.

Note

This is what lets a rebuild leave an unchanged asset alone instead of rewriting it, see ANIMATION_METADATA_RE. It is a freshness check and never a security one: a digest saying two animations match is a statement about the pixels, made by the same process that drew them.

Parameters:
  • frames (Sequence[str]) – each frame’s captured text.

  • durations (Sequence[float]) – how long each is shown, in the same order.

Return type:

str

Returns:

the fingerprint, as hexadecimal.

click_extra.capture.svg.animation_metadata(svg)[source]

Read back what an animated capture states about itself.

Parameters:

svg (str) – source of a rendered capture.

Return type:

dict[str, str]

Returns:

the @recording line’s fields, empty for a capture carrying none.

click_extra.capture.svg.frame_animation_css(unique_id, durations)[source]

Time an animated capture’s frames into CSS animation rules.

Every frame is shown for its own slice of one cycle, so a recording whose frames each lasted as long as the terminal held them rides the same machinery as a spinner whose frames are all one interval.

Note

Each boundary is computed once and handed to both the frame that ends on it and the frame that starts there. Rounding the two sides of one instant separately is what opens a gap, which the animation shows as a blank flash, or an overlap, which it shows as two frames drawn at once.

Every rule sits behind REDUCED_MOTION_QUERY, and nothing else here hides anything: the frames a still is not made of carry HIDDEN_FRAME_ATTRIBUTES as presentation attributes instead. A CSS animation outranks a presentation attribute, so the two never argue, and putting the hiding outside CSS altogether is what keeps a renderer reading no stylesheet from drawing every frame on top of the last. The keyframes restore both properties those attributes set, for the same reason.

Parameters:
  • unique_id (str) – prefix namespacing this document’s classes and keyframes.

  • durations (Sequence[float]) – seconds each frame is shown, in order.

Return type:

str

Returns:

the stylesheet fragment, indented to sit in a <style> block.

Raises:

ValueError – when a frame is given a duration that is not positive.

Time a cursor’s blink into a CSS animation rule.

One keyframe set and one rule for the whole document, however many frames it holds: a terminal has one cursor, and every frame’s copy of it therefore lights and darkens together. It steps like a frame does, being lit or dark with nothing in between, see FRAME_TIMING_FUNCTION.

Note

The blink dims opacity and never touches visibility. A cursor sits inside the group of the frame it belongs to, and that group is hidden by both properties at once, see HIDDEN_FRAME_ATTRIBUTES. Opacity multiplies down into the group, so a hidden frame’s cursor stays hidden; a rule restoring visibility would instead override the inherited value and show every frame’s cursor at once.

The rule sits behind REDUCED_MOTION_QUERY, which leaves the cursor lit and still for a reader who asked their system for less motion.

Parameters:
  • unique_id (str) – prefix namespacing this document’s classes and keyframes.

  • period (float) – seconds one blink takes, half of it lit.

Return type:

str

Returns:

the stylesheet fragment, indented to sit in a <style> block.

Raises:

ValueError – when the period is not positive.

click_extra.capture.svg.render_svg(text='', *, columns, title='', unique_id=None, frames=None, interval=None, hold=0.0, blank=0.0, speed=1.0, emphasize=(), cursor=None, wrap_marker='', palette=('#292929', '#c5c8c6', ('#2e3436', '#cc0000', '#4e9a06', '#c4a000', '#3465a4', '#75507b', '#06989a', '#d3d7cf', '#555753', '#ef2929', '#8ae234', '#fce94f', '#729fcf', '#ad7fa8', '#34e2e2', '#eeeeec'), '#292929'), font_stack="'Fira Code', 'Cascadia Code', Menlo, Consolas, monospace", chrome=Chrome(border='none', border_width=1, radius=8, backdrop='none', shadow='none', margin=0, padding=0, opacity=1.0, watermark='', watermark_color='rgba(128,128,128,0.85)'), buttons=(('#ff5f57', '#febc2e', '#28c840'), ''), buttons_color=None, titlebar='none', collapse_titlebar=False)[source]

Draw captured terminal text as a picture of a terminal window.

A terminal is a fixed grid of identically-sized cells, which is what makes this arithmetic rather than typesetting: grid() says which cell each run of same-styled characters starts on, and every coordinate below is that column times CELL_WIDTH.

Two primitives draw everything. A <rect> fills the cells behind a run that carries a background, and a <text> draws its glyphs, pinned to its columns with textLength so the layout survives a reader who does not have the font.

Note

A run’s padding is left out of its <text> and paid for in the x offset instead. Written the other way, a column only lands where it belongs if the glyphs are exactly the width assumed here, which asks the renderer to both honor textLength and resolve the font. A web browser does both. librsvg (and through it rsvg-convert and ImageMagick) ignores textLength, and a file manager, a git client or a thumbnailer commonly falls back to a proportional font. Starting each run on its own column asks neither.

Passing frames draws an animation instead of a still. The window, its caption and its clip path are drawn once and every frame is stacked inside them, so the frames differ in nothing but their text, and one stylesheet covers the lot: a color two frames share is one rule, and a frame cannot name a class the document never defines. Frames are hidden by frame_animation_css() in turn, leaving the last one that draws anything visible wherever the animation does not run.

Note

Everything a frame carries is namespaced by unique_id, keyframes included, so two animations inlined into one HTML page keep their own timing. Sharing a selector between them is what makes the shorter one run on the longer one’s clock and blank out for the frames it does not have.

Parameters:
  • text (str) – captured output, ANSI escape sequences included. The whole picture when frames is left out, and unused when it is given.

  • columns (int) – width of the terminal, in characters.

  • title (str) – caption drawn in the window’s title bar. Empty draws none.

  • unique_id (str | None) – prefix namespacing this document’s CSS classes and element IDs, see render(). Derived from the content when not given.

  • frames (Sequence[str] | None) – the animation’s frames, each captured text, in order. A single frame draws the same still text would.

  • interval (float | Sequence[float] | None) – seconds each frame is shown. One number times every frame alike, which is what a spinner asks for; a sequence gives each frame its own, which is what a recording asks for. Required alongside frames.

  • hold (float | Literal['auto']) – extra seconds the last frame stays up before the animation starts over. An animation that ends somewhere (a trail filled in, a bar run out, an outcome landed) is worth reading, and a loop that restarts the instant it arrives never lets anyone. A spinner turning in place ends nowhere, so it wants none of this and defaults to none. AUTO_HOLD scales the pause to the final frame’s own line count, see auto_hold().

  • blank (float) – seconds of empty screen closing the cycle, after hold. A loop that jumps from its last frame back to its first reads as one long animation doing something odd; an empty beat says plainly that this is where it starts over. Never the frame a still falls back to.

  • speed (float) – how much faster to play than it was recorded, so 2 halves every frame’s time and 0.5 doubles it. hold and blank are stated in real seconds and are not scaled: they are how long a reader is given, not part of what is being replayed.

  • emphasize (Sequence[int]) – lines to draw a band behind, counted from 1 the way :emphasize-lines: counts them. A band runs the full width of the window rather than of the text, the row being what is emphasized. In an animation it appears with the frame that first draws the row it marks, which is also when a gutter would first number that row, and it is gone again wherever the row is.

  • cursor (Cursor | None) – the terminal cursor to draw, see Cursor. None draws none. Where it stands is read off each frame’s own text, see cursor_cell(), so an animation carries it from screen to screen on its own. A cursor landing under the last line of output grows the window by that line, the way a terminal’s does.

  • wrap_marker (str) – characters closing each row a fold cut short, see grid(). Empty folds silently, the way a terminal does.

  • palette (TerminalPalette) – terminal colors the capture’s ANSI codes resolve against.

  • font_stack (str) – fonts the text is set in, best first.

  • chrome (Chrome) – the window around the text, resolved, see resolve(): its frame, corners, backdrop, shadow, margin, padding, opacity and credit line. Its backdrop takes a CSS gradient too, see gradient_svg(). Its opacity thins the window’s body alone: the frame, the title bar and the text keep their own paint. Defaults to BARE_CHROME.

  • buttons (WindowButtons) – decorations drawn in the title bar.

  • buttons_color (str | None) – paint for the glyph decorations. Circles carry their own colors. None takes the palette’s foreground.

  • titlebar (str) – paint for the strip the title and buttons sit in. NO_PAINT leaves it the terminal’s own color.

  • collapse_titlebar (bool) – drop that strip, closing the window over the first line of text. For a capture wearing neither decoration nor caption.

Return type:

str

Returns:

the SVG source.

Raises:

ValueError – when chrome is not resolved, when frames is given without an interval, when the two disagree on how many frames there are, when no frame is given, when hold or blank is negative, when emphasize names a line the capture does not have, or when a cursor is given a negative blink.