click_extra.capture package¶
Draw terminal text as a static document: an SVG picture, HTML or ANSI.
click_extra.capture.screenshotruns a command, captures its output and renders it.click_extra.capture.recordingrebuilds the frames of an animation from the stream a command writes at a terminal.click_extra.capture.snippetcolors a source file to render it the same way.click_extra.capture.svgandclick_extra.capture.htmlare the two renderers.click_extra.capture.presetsdescribes the window a capture is drawn in: the terminal presets, the chrome and its palettes.
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 arithmeticrender_svg()performs for a picture.Caution
The text is escaped before its ANSI is translated, the order
click_extra.tableuses for itshtmlformat. Skip it and any<a CLI prints opens a tag: click-extra’s own--export-confighelp says it writesto <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.Falsereturns the<pre>and its credit line alone, to paste into a page that has its own.palette (
TerminalPalette) – terminal colors the text resolves against, seeresolve_palette().chrome (
Chrome) – the block around the text, resolved, seeresolve(). Itsmarginis left around the block, itspaddingadded inside the block on top of its own, itsbackdropfills the page behind it, and itsopacitythins the block’s background alone, down to0.0where 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 toBARE_CHROME.font_stack (
str) – fonts the text is set in, best first.
- Return type:
- Returns:
the rendered markup.
- Raises:
ValueError – when
chromeis 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
#3B78FFor Tango’s#729FCF;the font the terminal ships with;
the prompt its shell draws,
$againstPS 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:
NamedTupleThe colors a terminal resolves a capture’s ANSI codes against.
The 16
ansientries 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)
- class click_extra.capture.presets.WindowButtons(circles=(), glyphs='')[source]¶
Bases:
NamedTupleThe 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)
- 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:
EnumThe 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:
NamedTupleThe 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.
Nonetakes the shape the terminal preset names.
- blink: float¶
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_QUERYlike 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.
- 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
BasicandProschemes.
- 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:
NamedTupleA terminal a capture can be drawn as.
Pass one to
click-extra screenshot --preset, or to aclick:runblock 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)
- buttons: WindowButtons¶
Decorations drawn in the title bar, see
WindowButtons.
- 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
monospacea 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.
plainis 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
--presetis drawn as.plainmimics 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
styleattribute, 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_URLdocuments the other half of this: a capture embedded with<img>never sees the page’s own@font-faceeither, 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
blueleaves 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 rulerender()andstyle_palette()share, so the SVG and HTML formats cannot disagree on what a chrome looks like.- Parameters:
preset (
TerminalPreset|None) – terminal being pictured, orNonefor the default one.background (
TerminalBackground) – chrome the capture is drawn on.
- Return type:
- 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
strokeattribute 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.0leaves 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
#292929and 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:
objectHow the window around a capture’s text is drawn.
One value for the decoration
render(),capture(),record_and_render()andrender_snippet()all take, so its defaults live in one place. A field left atNonetakes what the capture’s background and preset draw, whichresolve()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.
Nonetakes the one the background can show, seeCAPTURE_BORDERS;NO_PAINTdraws none.
- radius: int | None = None¶
How round the window’s corners are, in pixels. Zero squares them.
Nonetakes the preset’s own, orDEFAULT_RADIUSwithout one.
- backdrop: str = 'none'¶
Paint filling the image behind the window, margin included.
NO_PAINTleaves it transparent.
- shadow: str | None = None¶
Color of the window’s drop shadow.
Nonetakes the background’s own, seeCAPTURE_SHADOWS;NO_PAINTdraws none.
- opacity: float = 1.0¶
How solid the window’s body is, from
OPAQUEdown to0.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.
NonetakesWATERMARK_INK, which reads on a page of either color.
- resolve(background, preset)[source]¶
This chrome, with every
Nonereplaced by what the capture draws.render_svg()andrender_html()take a resolved chrome, which makes this the one step between the two layers.- Parameters:
background (
TerminalBackground) – chrome the capture is headed for.preset (
TerminalPreset|None) – terminal being pictured, orNone.
- Return type:
- 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
Nonenames no paint and no size: onlyresolve()knows what it stands for.- Raises:
ValueError – naming each field still set to
None.- Return type:
- 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()andrender_html()draw when they get no chrome. A capture taken throughrender()resolves aChromeinstead, 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_PATTERNstates, so a sequence with an intermediate byte, like theESC [ 2 SP qsetting 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
durationor by a Ctrl+C, getsSIGTERMfirst, 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: seeclick_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:
objectThe 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.
- 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.
- class click_extra.capture.recording.Frame(text, duration)[source]¶
Bases:
NamedTupleOne screen a recording held, and how long it held it.
Create new instance of Frame(text, duration)
- class click_extra.capture.recording.ScreenRecorder(*, clock=<built-in function monotonic>)[source]¶
Bases:
TextIOBaseA 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 astreamargument 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.
- 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.
- 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.
- 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
termiosandpty, neither of which Windows ships, and reaching ConPTY means a dependency. Recording a spinner this process hosts needs none of that: hand aScreenRecorderto itsstreamand 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:
args (
str|Path|None|Iterable[str|Path|None|Iterable[Iterable[str|Path|None|Iterable[TNestedArgs]]]]) – the command line, in the nested formrun_cli()accepts.columns (
int) – width of the terminal it is run in, in characters.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, seeCAPTURE_TERMINAL_HINTS.duration (
float|None) – seconds to record before stopping the command.Nonerecords until it exits on its own.clock (
Callable[[],float]) – what the recorder reads the time off.
- Return type:
- 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.
- click_extra.capture.recording.ansi_prefix(text, count)[source]¶
The first
countprintable characters oftext, 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:
- Return type:
- 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, seecursor_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, seeformat_cli_prompt().typing (
float) – seconds each character takes to appear.submit (
float) – seconds the finished line waits before whatever follows.
- Return type:
- 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()andrender(), the waycapture()chains the still pipeline. The invocation is drawn above every frame as a shell prompt, styled by the active theme throughformat_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 noautohere: 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.Nonerecords until it exits on its own.quantum (
float) – grid the frame durations are rounded onto, seequantize().hold (
float|Literal['auto']) – extra seconds the last frame stays up, orAUTO_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. Seetype_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, seerender_svg().cursor (
Cursor|None) – the terminal cursor to draw, seeCursor. 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, seeappend_prompt(). Only that frame carries it: the shell has not returned while the command is still drawing.preset (
TerminalPreset|None) – seerender().
- Return type:
- 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
typingorsubmitis 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:
capture_output()runs the command throughrun_cli(), underforced_color()and a pinned terminal width, and hands back its raw ANSI text.render()turns that text into a document, in one of theCaptureFormatmembers.
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_COLUMNSas 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_COLUMNSandAUTO_HOLD: the caller says a cursor is wanted and leaves what it looks like to whatever knows the terminal. It is whatshapeofNonespells on a command line, which cannot passNone.
- click_extra.capture.screenshot.AUTO_TRUNCATION: Literal['auto'] = 'auto'¶
Marker asking for a rule across the width of the picture.
TRUNCATION_LABELcentered in a run ofTRUNCATION_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 underAUTO_COLUMNS. See_rule_marker().
- click_extra.capture.screenshot.TRUNCATION_LABEL = '✂'¶
What
AUTO_TRUNCATIONcenters in its rule.
- click_extra.capture.screenshot.TRUNCATION_RULE = '·'¶
Character
AUTO_TRUNCATIONdraws 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_LABELat 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_MARKERwrites 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:deltaships it as its own--wrap-left-symbol, and paints it in a color of its own for the same reason this one is drawn inRULE_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_commandstyle, invisible.Nonekeeps whatever theme the invocation already runs under. So does a missing entry: the mapping is read throughdict.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-themeCLITHEME, andCOLORFGBGcarryingforeground;backgroundpalette 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_PROGRAMis 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:
EnumDocument 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_SENTINELnames the terminal, whose format is the escape sequences themselves, seeis_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:
- Returns:
the
CaptureFormatits 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 howrun_cli()takes them: the chrome’sCAPTURE_TERMINAL_HINTS, everyCAPTURE_HIDDEN_TERMINAL_VARSentry mapped toNoneso it is removed rather than set, and the width the command wraps to.- Parameters:
background (
TerminalBackground) – chrome the capture is headed for.columns (
int|Literal['auto']) – terminal width, in characters, pinned throughCOLUMNS.AUTO_COLUMNSpins nothing.
- Return type:
- Returns:
the overrides, by variable name.
- click_extra.capture.screenshot.resolve_wrap_marker(marker)[source]¶
Read a
wrap_markeroption into the characters written at a fold.The one reading of the option every entry point shares: the
--wrap-markerof 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:
- 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
columnsoption every entry point shares: the--columnsof 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 wordAUTO_COLUMNS.- Return type:
- 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
picturesasks 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.
- 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 formrun_cli()accepts.prompt (
str|None) – command line to display, when it differs from the one run. An empty string draws no prompt at all;Noneshows 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.Nonekeeps this platform’s.
- Return type:
- 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.Nonekeeps this platform’s.
- Return type:
- 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 theFORCE_COLORlever every mainstream color system obeys and clears any opt-out the environment carries, whileCOLUMNSfixes the width the command wraps to.Only
stdoutis captured by default. That is what keeps a capture free of the progress lines and build chatter a wrapper likeuvwrites tostderr, 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:
args (
str|Path|None|Iterable[str|Path|None|Iterable[Iterable[str|Path|None|Iterable[TNestedArgs]]]]) – the command line, in the nested formrun_cli()accepts.columns (
int|Literal['auto']) – terminal width, in characters, the command wraps its output to.AUTO_COLUMNSpins nothing and lets the command find its own.background (
TerminalBackground) – chrome the capture is headed for, stated to the command the way a terminal would, seeCAPTURE_TERMINAL_HINTS.merge_stderr (
bool) – foldstderrinto the captured output, for a command printing its help there.timeout (
float|None) – seconds before the command is killed.Nonewaits forever.
- Return type:
- Returns:
the completed process, whose
stdoutholds 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
headand lasttaillines oftext.Whatever is dropped is replaced by a single
truncationline, 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, orNonefor no head bound.tail (
int|None) – number of trailing lines to keep, orNonefor 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 ruleAUTO_TRUNCATIONdraws, see_rule_marker().wrap_marker (
str) – what closes a folded row, seerender(). It takes a cell of every row, so it narrows the rule by as much.width_floor (
int) – underAUTO_COLUMNS, the width of a line the picture draws besidestext, like the prompt, which the rule spans too.
- Return type:
- 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.
- 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
formatnames.- Parameters:
text (
str) – captured output, ANSI escape sequences included.format (
CaptureFormat) – which document to produce.CaptureFormat.ANSIdraws 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 isemphasize, 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, orAUTO_COLUMNSfor 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, seerender_svg().interval (
float|Sequence[float] |None) – SVG only. How long each of them is shown, seerender_svg().hold (
float|Literal['auto']) – SVG only. Extra seconds the last frame stays up, orAUTO_HOLDto scale them to that frame’s line count, seerender_svg().blank (
float) – SVG only. Seconds of empty screen closing the cycle, seerender_svg().speed (
float) – SVG only. How much faster to play than recorded, seerender_svg().emphasize (
Sequence[int]) – SVG and ANSI. Lines to draw a band behind, seerender_svg()andemphasize_ansi().cursor (
Cursor|None) – SVG only. The terminal cursor to draw, seeCursor.Nonedraws none. A cursor naming no shape takes the one thepresetsays that terminal draws, so--preset windowsgets its bar without stating it.wrap_marker (
str) – SVG only. What closes each row a fold cut short,AUTO_WRAP_MARKERforWRAP_MARKER_LABEL, or empty to fold silently, seegrid(). 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. Seerender_html().background (
TerminalBackground) – chrome to draw on, seeTerminalBackground.preset (
TerminalPreset|None) – terminal to draw the capture as, seeTerminalPreset.Nonekeeps the renderer’s own neutral window.palette (
TerminalPalette|None) – colors the text resolves against.Nonetakes 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, seeChrome.
- Return type:
- 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()andrender(). The invocation is drawn above the output as a shell prompt, styled by the active theme throughformat_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, orAUTO_COLUMNSto 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-clireproduces a capture from a checkout, butmy-cliis what a reader types. An empty string draws no prompt at all.tail (
int|None) – number of trailing output lines to keep.truncation (
str) – line standing in for the lines cut byheadortail.merge_stderr (
bool) – foldstderrinto the captured output.timeout (
float|None) – seconds before the command is killed.line_numbers (
bool) – draw each line’s number in a gutter, seenumber_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, seerender_svg(). The prompt is line 1 here too, and a gutter does not shift the count.cursor (
Cursor|None) – seerender(). A still capture leaves its cursor after the last thing the command printed, which is where the shell finds it.closing_prompt (
bool) – draw the shell’s prompt on the row under the output, which is where it comes back once the command exits, seeappend_prompt().background (
TerminalBackground) – seerender().preset (
TerminalPreset|None) – seerender().
- Return type:
- 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:
highlight_code()colors the source with Pygments, which writes the same ANSI escape sequences a terminal would.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.
monokaipaints#272822, two shades off the#292929a dark capture is drawn on, and Pygments’ owndefaultpaints#f8f8f8against 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 cellscell_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.
- 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
pythnas plain text hides the typo in a picture nobody re-reads.- Parameters:
- Return type:
Lexer- Returns:
the lexer.
- Raises:
ValueError – when
languagenames 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, orNonefor the chrome’s default.background (
TerminalBackground) – chrome the snippet is drawn on.
- Return type:
- Returns:
the style’s name.
- Raises:
ValueError – when
stylenames 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#272822and 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.Textcolor 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:
- 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;bsequences, which is the one interchange format both capture renderers already read. So the output of this drops straight intorender(), with nothing in between.- Parameters:
- Return type:
- Returns:
the source, ANSI escape sequences included.
- Raises:
ValueError – when
languagenames 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()andrender().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:
code (
str) – the source to draw.format (
CaptureFormat) – which document to produce.language (
str|None) – the language, as Pygments names it. Seeresolve_lexer()for what answers when it is left out.filename (
str|None) – name the code was read from, extension included.style (
str|None) – name of the Pygments style to color with.Nonetakes the chrome’s own, seeDEFAULT_SYNTAX_STYLES.columns (
int|Literal['auto']) – width, in characters, an SVG is laid out at.truncation (
str) – line standing in for the lines cut byheadortail.line_numbers (
bool) – draw each line’s number in a gutter, seenumber_lines().emphasize (
Sequence[int]) – lines to draw a band behind, counted from1.background (
TerminalBackground) – seerender().preset (
TerminalPreset|None) – seerender().
- Return type:
- Returns:
the rendered document.
- Raises:
ValueError – when
languageorstylenames 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_HEIGHTanswers instead. This is the window’s own breathing room, before thepaddinga 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 bottomis what a gradient opening with no direction at all means, which is why it doubles as the default. Seegradient_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=0has 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_NAMEis 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_STACKasks for. Every monospaced fallback behind it is close enough that the grid holds, andtextLengthpins 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
BARandUNDERLINEare 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
dimrun’s ink is mixed toward the background, seeblend().
- 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.
visibilityalone satisfies a browser,librsvgand 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 honoropacity, 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
@generatedline, 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-endholds 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.accessibilitycounts an endlessly repeating spinner among the things--accessibleexists 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_LINEfor each, clamped betweenAUTO_HOLD_MINandAUTO_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.
- 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:
- 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_HOLDscales the pause to the final frame’s own line count, seeauto_hold().- Parameters:
value (
str) – the text, a number of seconds or the wordauto.- Return type:
- 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
colortowardinto, the way a terminal fades dim text.SVG has no dim, and thinning the glyphs with
opacitywould let whatever sits behind the capture show through them. Mixing the two colors up front keeps the text opaque and lands the same shade.
- click_extra.capture.svg.emphasis_paint(palette)[source]¶
The color an emphasized line’s band is painted, on
palette.EMPHASIS_RATIOof the way from the terminal’s background toward its ink, seeblend().- Parameters:
palette (
TerminalPalette) – the terminal colors the capture is drawn with.- Return type:
- Returns:
the band’s color, as
#rrggbb.
- click_extra.capture.svg.palette_color(color, palette)[source]¶
Resolve any color a
Stylecarries to a hex string.The 16 named and indexed ANSI slots are not colors, they are names: a terminal decides what its
redlooks like, and a capture has no terminal, so they answer topalette. 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:
palette (
TerminalPalette) – the terminal colors to resolve names against.
- Return type:
- 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:
style (
Style) – the run’s style, assplit_ansi()yields it.palette (
TerminalPalette) – the terminal colors to resolve names against.
- Return type:
- 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
TerminalScreenjoins 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:
- Return type:
- Returns:
the cursor’s row and column, both counted from zero, or
Nonefor 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
filltakes 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--backdropvalue serve both formats.Understood:
linear-gradientopening with an optional angle (135deg) or side keyword (to bottom right, seeCSS_SIDE_ANGLES), andradial-gradient, both followed by two or more color stops, each pinnable at a percentage. Anything else returnsNoneand 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:
- Return type:
- Returns:
the
<defs>markup and thefillvalue referencing it, orNonewhen 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:
- Return type:
- 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:
- 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
watermarkclass, 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_URLandcredit_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:
- 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:
- 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:
- Return type:
- 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
textLengthand resolves the font, because the gutter’s width is being paid for in glyphs.librsvgdoes 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.
- 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
textLengthholds the result to the width it occupies.Text carrying a tile (
_TILING_RE) is cut into groups of at mostTILE_RUNcells, each landing on a stated offset. A<text>element is the smallest thing some renderers position at all:librsvg(and through itrsvg-convertand ImageMagick) honors the firstxof an element and then lays every following glyph out at the font’s own advance, ignoring bothtextLengthand any furtherx. 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.
- 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.
- 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.
- click_extra.capture.svg.animation_metadata(svg)[source]¶
Read back what an animated capture states about itself.
- 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 carryHIDDEN_FRAME_ATTRIBUTESas 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:
- Return type:
- Returns:
the stylesheet fragment, indented to sit in a
<style>block.- Raises:
ValueError – when a frame is given a duration that is not positive.
- click_extra.capture.svg.blink_css(unique_id, period)[source]¶
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
opacityand never touchesvisibility. A cursor sits inside the group of the frame it belongs to, and that group is hidden by both properties at once, seeHIDDEN_FRAME_ATTRIBUTES. Opacity multiplies down into the group, so a hidden frame’s cursor stays hidden; a rule restoringvisibilitywould 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:
- Return type:
- 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 timesCELL_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 withtextLengthso 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 thexoffset 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 honortextLengthand resolve the font. A web browser does both.librsvg(and through itrsvg-convertand ImageMagick) ignorestextLength, 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
framesdraws 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 byframe_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 whenframesis 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, seerender(). 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 stilltextwould.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 alongsideframes.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_HOLDscales the pause to the final frame’s own line count, seeauto_hold().blank (
float) – seconds of empty screen closing the cycle, afterhold. 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, so2halves every frame’s time and0.5doubles it.holdandblankare 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 from1the 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, seeCursor.Nonedraws none. Where it stands is read off each frame’s own text, seecursor_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, seegrid(). 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, seeresolve(): its frame, corners, backdrop, shadow, margin, padding, opacity and credit line. Itsbackdroptakes a CSS gradient too, seegradient_svg(). Itsopacitythins the window’s body alone: the frame, the title bar and the text keep their own paint. Defaults toBARE_CHROME.buttons (
WindowButtons) – decorations drawn in the title bar.buttons_color (
str|None) – paint for the glyph decorations. Circles carry their own colors.Nonetakes the palette’s foreground.titlebar (
str) – paint for the strip the title and buttons sit in.NO_PAINTleaves 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:
- Returns:
the SVG source.
- Raises:
ValueError – when
chromeis not resolved, whenframesis given without aninterval, when the two disagree on how many frames there are, when no frame is given, whenholdorblankis negative, whenemphasizenames a line the capture does not have, or when acursoris given a negative blink.