CLI screenshots

You can produce this screenshot of a git diff:

A macOS terminal window showing git diff comparing two fruit baskets, its removed lines red and its added lines green

By invoking click-extra screenshot in your terminal, without installing it:

$ uvx click-extra screenshot --output git-diff.svg --preset macos -- git diff --color=always --no-index basket.txt basket.new.txt

The screenshot is written to an SVG file, where:

  • --output git-diff.svg names the file to write.

  • --preset macos sets the window’s chrome to the macOS style.

  • -- separates those options from the command to capture.

  • --color=always keeps git printing colors even though its output is a pipe.

  • --no-index lets git diff compare two plain files instead of a repository.

Capture a command

The screenshot command

click-extra screenshot runs a command and writes its colored output to a file. Use -- to separate the screenshot options from the command’s own options:

$ click-extra screenshot --output cli-help.svg -- my-cli --help
$ click-extra screenshot --help
Usage: click-extra screenshot [OPTIONS] COMMAND_LINE...

  Capture a command's colored output and write it as an image or HTML.

  Runs COMMAND_LINE with colors forced on and its terminal width pinned, then
  writes the captured output where --output points. Its extension picks the
  format:

  - .svg: a picture of a terminal window, for a surface that strips inline
  HTML. A README on GitHub or PyPI has no other option;

  - .html: selectable, searchable, copy-pasteable text, for a page you own.

  Put -- before the command line so its own options are not mistaken for this
  command's:

    click-extra screenshot --output shot.svg -- my-cli --help

  COMMAND_LINE is anything the shell can run, Click CLI or not. A Click CLI not
  built on Click Extra prints its help uncolored, so --wrap routes it through
  the wrap subcommand first and captures the colored rendering.

  An SVG starts each run of text on its own column, so it renders correctly
  outside a web browser, where a file manager, a git client or a thumbnailer
  would otherwise slide the columns out of place.

  Neither format needs an optional dependency.

  --record runs the command under a pseudo-terminal instead, and writes an
  animated SVG of every screen it drew: the frames a spinner or a progress bar
  asks a terminal for, which a plain capture never sees. The invocation is drawn
  above every frame, and the loop pauses on the final screen for as long as its
  line count asks, see --hold.

Options:
  --output FILE                Path of the file to write. Its extension picks
                               the format: .svg for an image, .html for
                               selectable text, .ansi for the escape sequences
                               themselves. Pass - to print those to the
                               terminal, which draws no window.  [required]
  --columns [auto|INTEGER]     Terminal width, in characters, the command wraps
                               its output to and the image is laid out at. Pass
                               auto to pin neither: the command finds its own
                               width, and the image is laid out at the longest
                               line it printed, so nothing folds inside the
                               picture.  [default: 80]
  --background [dark|light]    Terminal chrome the capture is drawn on, and the
                               palette its colors resolve against. Match it to
                               the theme the captured CLI renders with: a light-
                               background theme washes out on the dark default.
                               [default: dark]
  --preset [linux|macos|plain|windows]
                               Terminal to draw the capture as: its window
                               decorations, palette, font and prompt sigil.
                               Anything stated alongside wins over it. Left out,
                               the capture keeps the renderer's own neutral
                               window.
  --border COLOR               Color of the frame drawn around the terminal
                               window, as CSS names it. Pass none to draw no
                               frame. Defaults to the one the chrome can show.
  --border-width PIXELS        Thickness of that frame, in pixels.  [default: 1;
                               x>=0]
  --radius PIXELS              How round the window's corners are, in pixels.
                               Zero squares them. Defaults to 8, or to the
                               rounding --preset terminal draws.  [x>=0]
  --backdrop COLOR             Color filling the image behind the window, margin
                               included, as CSS names it. Left transparent by
                               default, so the page shows through.  [default:
                               none]
  --shadow COLOR               Color of the drop shadow lifting the window off
                               the page, as CSS names it. Pass none to draw no
                               shadow. Defaults to the one the chrome calls for.
  --margin PIXELS              Transparent pixels left around the window, on all
                               four sides. The room the drop shadow falls into,
                               so a capture drawing one wants some.  [default:
                               48; x>=0]
  --padding PIXELS             Pixels added inside the window, around the drawn
                               text, on top of the few the renderer adds on its
                               own.  [default: 8; x>=0]
  --opacity FLOAT RANGE        How solid the window's body is. Under 1 it turns
                               see-through, the way a terminal set to
                               transparency does: whatever the capture sits on
                               shows through it, while its text, frame and title
                               bar keep their own paint.  [default: 1.0;
                               0<=x<=1]
  --watermark TEXT             Credit line drawn in the image's bottom-right
                               corner, in the margin around the window. Pass an
                               empty string to draw none, or your own text to
                               credit your project instead.  [default: generated
                               with click-extra 9.2.1]
  --watermark-color COLOR      Color that credit line is drawn in, as CSS names
                               it, alpha included. Defaults to a neutral gray:
                               the line sits in the transparent margin, so it
                               answers to the page embedding the image rather
                               than to the chrome.
  --head INTEGER RANGE         Keep only the first N lines.  [x>=1]
  --tail INTEGER RANGE         Keep only the last N lines.  [x>=1]
  --truncation [auto|TEXT]     Line standing in for what --head or --tail cut
                               away, or auto to rule one across the width the
                               kept lines span.  [default: auto]
  --line-numbers               Number the drawn lines in a gutter, the way
                               Pygments does inline. Line 1 is the first line
                               the picture shows.
  --emphasize-lines LINES      Draw a band behind the lines named, as 2,4-5.
                               Counted from 1 as the picture draws them. Ranges
                               are closed: state both ends.
  --title TEXT                 Caption drawn in an SVG's window chrome, or an
                               HTML document's title.  [default: ""]
  --fragment                   For HTML, emit the bare block instead of a
                               standalone document, to paste into a page that
                               has its own.
  --prompt TEXT                Command line to display above the output, when it
                               differs from the one that is run. Pass an empty
                               string to draw no prompt at all. Defaults to the
                               command line itself.
  --merge-stderr               Fold the command's stderr into the capture, for a
                               CLI printing its help there. Off by default,
                               which is what keeps a wrapper's build chatter out
                               of the image.
  --wrap                       Route COMMAND_LINE through the wrap subcommand,
                               so a Click CLI that is not built on Click Extra
                               is captured with its colors. Only works on a
                               target wrap can resolve.
  --timeout SECONDS            Seconds before the command is killed. Waits
                               forever by default. With --record, this is also
                               where the recording stops.  [x>0]
  --record                     Run the command under a pseudo-terminal and write
                               an animated SVG of the screens it draws, spinners
                               and progress bars included. Needs an .svg
                               --output and a numeric --columns. Unix only.
  --rows INTEGER RANGE         With --record, the height of the terminal the
                               command runs in, in characters. Defaults to 24.
                               [x>=1]
  --hold [auto|FLOAT]          With --record, extra seconds the last frame stays
                               up before the animation starts over, or auto to
                               scale them to that frame's line count. Defaults
                               to auto.
  --blank SECONDS              With --record, seconds of empty screen closing
                               the cycle. Defaults to 0.6.  [x>=0]
  --cursor [auto|block|bar|underline]
                               Draw a terminal cursor where the command left it.
                               Bare, it takes the shape the --preset terminal
                               draws; name one to override that. Omitted, no
                               cursor is drawn.
  --blink SECONDS              With --cursor, seconds one blink takes. Pass 0 to
                               draw a steady cursor. Defaults to 1.0.  [x>=0]
  --closing-prompt / --no-closing-prompt
                               Draw the shell's prompt on the row under the
                               output, where it comes back once the command
                               exits. Costs no height alongside --cursor, which
                               already leaves that row for the cursor to wait
                               on.  [default: no-closing-prompt]
  --typing SECONDS             With --record, open the animation by typing the
                               command line out, this many seconds per
                               character. Omitted, the prompt stands there from
                               the first frame.  [x>0]
  --submit SECONDS             With --typing, seconds the finished command line
                               waits before its output starts. Defaults to 0.4.
                               [x>0]
  --speed FLOAT RANGE          With --record, how much faster to play than
                               recorded: 2 halves every frame's time. Defaults
                               to 1.0.  [x>0]
  -h, --help                   Show this message and exit.

Configuration options:
  --config LOCATION            Location of the configuration file. Supports
                               local path with glob patterns or remote URL.
                               [default: ~/.config/click-extra/]
  --no-config                  Ignore all configuration files and only use
                               command line parameters and environment
                               variables.
  --validate-config LOCATION   Validate the configuration file and exit.
  --export-config FORMAT       Export the configuration in the selected format
                               to <stdout>, then exit.

Output options:
  --accessible                 Accessibility mode: disable colors and render
                               tables in a borderless, screen-reader-friendly
                               format.
  --color [auto|always|never]  Colorize the output. A bare --color is the same
                               as --color=always.  [default: auto]
  --no-color                   Disable colorization (alias of --color=never).
  --progress / --no-progress   Show progress indicators during long operations.
                               Disabled for non-interactive output (pipes, dumb
                               terminals, CI) and by --accessible.  [default:
                               progress]
  --theme [auto|dark|dracula|light|manpage|monokai|nord|solarized-dark]
                               Color theme used for help screens.  [default:
                               dark]
  --table-format FORMAT        Rendering style of tables.  [default: rounded-
                               outline]

Logging options:
  --verbosity LEVEL            Either CRITICAL, ERROR, WARNING, INFO, DEBUG.
                               [default: WARNING]
  -v, --verbose                Increase the default WARNING verbosity by one
                               level for each additional repetition of the
                               option.  [default: 0]
  -q, --quiet                  Decrease the default WARNING verbosity by one
                               level for each additional repetition of the
                               option.  [default: 0]
  --debug                      Shorthand for --verbosity DEBUG.

Introspection options:
  --time / --no-time           Measure and print elapsed execution time.
                               [default: no-time]
  --params                     Show all CLI parameters, their provenance,
                               defaults and value, then exit.
  --tree                       Show the tree of nested subcommands and exit.
  --man                        Read the command's manual page and exit.
  --help-format [carapace|json|json-full|man|markdown|markdown-full]
                               Render the command in the given format and exit.
  --version                    Show the version and exit.

Examples:

  Draw a help screen as a picture a README can show:
    $ click-extra screenshot --output my-cli.svg -- my-cli --help
  Draw it as selectable text, for a page you own:
    $ click-extra screenshot --output my-cli.html -- my-cli --help
  Capture a CLI that is not built on Click Extra, colored all the same:
    $ click-extra screenshot --output flask.svg --wrap -- flask run --help
  Record the frames a spinner draws, as an animation:
    $ click-extra screenshot --output ripen.svg --record --columns 80 -- ripen

It settles three things a general capture tool leaves to you:

  • Colors. A command strips colors as soon as its output is a pipe. The capture runs under click_extra.color.forced_color(), which sets FORCE_COLOR and clears any NO_COLOR in the environment.

  • Width. --columns sets what the command wraps to and what the image is drawn at. If the two disagree, the rendered lines overrun the image.

  • stderr. It stays out of the capture unless --merge-stderr asks for it, which keeps a wrapper’s build chatter out of the picture.

SVG or HTML

The extension of --output selects the format:

Format

Text is

Goes where

.svg

a picture

a surface that strips inline HTML: a README on GitHub or PyPI

.html

selectable, searchable, copyable

a page you own: your site, a blog post, a slide deck

GitHub and PyPI render SVG as an image and strip inline styling, so a README needs SVG, and the capture’s text becomes pixels there: no page search, no copy. Where you control the markup, HTML keeps the text as text, so a reader can find a flag with the page’s own search and copy it straight into a terminal.

Neither format needs an optional dependency.

$ click-extra screenshot --output cli-help.html -- my-cli --help

That writes a standalone document. Add --fragment to get a bare <pre> with inline styles, so it needs no stylesheet.

Tip

An SVG capture needs no web font and no HTTP request to render, so it draws the same in any viewer that implements the whole of SVG’s text styling.

Caution

Not every viewer does. macOS has two SVG engines, and they disagree. Preview, Quick Look and Safari go through WebKit and draw a capture in full. Finder’s icon thumbnails and any app reading the file through NSImage go through Core Graphics instead, which implements fill and font-weight but neither font-style nor text-decoration: italic renders upright and underline, overline and strikethrough render as nothing. Colors, bold, dim and reverse survive.

The file is the same either way, so there is nothing to fix in a capture that looks flat in a file manager or a git client. Open it in a browser to see what a reader on a documentation page gets. Animated captures and OSC 8 hyperlinks reach no static thumbnail at all, whichever engine draws it.

Note

HTML has two limitations SVG does not. An OSC 8 hyperlink loses its URL and keeps its visible text. The eight base ANSI colors render as their CSS names, so the browser’s palette decides their shade, not the terminal’s. Neither shows up on a help screen, which is why the format is worth having anyway.

Straight to the terminal

--output - draws no window and prints the captured escape sequences, so you can see what a capture holds before committing it to a file. --output shot.ansi writes the same thing out.

$ click-extra screenshot --output - -- my-cli --help

The escapes are dropped when the destination turns out to be a pipe, unless --color=always keeps them. Nothing describing a window applies here, so the frame, the chrome and the credit line are all ignored.

See also

To picture source code instead of a command’s output, see code snippets. Both are drawn in the same window, and the whole styling vocabulary below applies to either.

Any command, any CLI

screenshot runs anything the shell runs, Click CLI or not. git --help and docker ps capture as readily as your own tool. The file holds exactly what the command prints, so a Click CLI not built on Click Extra lands in it uncolored.

To capture it with colors, run it through wrap first. wrap patches Click’s help rendering without touching the target’s code. --wrap does that for you:

$ click-extra screenshot --output flask-help.svg --wrap -- flask --help

This is shorthand for composing the two commands by hand:

$ click-extra screenshot --output flask-help.svg --prompt "click-extra wrap -- flask --help" -- click-extra wrap -- flask --help

The prompt line shows the wrap invocation, not the bare command. The wrap invocation is what reproduces the colored screen: flask --help on its own prints the plain one.

--prompt also stands on its own. It draws its text as the command line above the output. Use it when the invocation you ran is not the one the reader should type.

Note

wrap decides how a CLI renders, and reaches only the Click commands it can import. screenshot decides where the output goes, and reaches anything executable. Composing the two covers the overlap. This is also why --wrap insists on the installed click-extra command rather than falling back to python -m click_extra: the two resolve a target differently, so the fallback would quietly capture a different CLI.

Style the capture

Width

--columns pins the width twice: the command wraps its output to it, and the image is laid out at the same width. The two must agree, or the rendered lines overrun the picture.

--columns auto pins neither. The command finds its own width (the terminal it runs in, or Click’s default of 80 through a pipe). The image is laid out at the longest line that came back:

$ click-extra screenshot --output params.svg --columns auto -- my-cli --params

Use it when the output holds a line the command does not wrap on its own: a long invocation drawn as the prompt, a wide table, a machine-readable dump. A pinned width folds such a line mid-word. The trade-off: the picture stops being a fixed-width terminal, so captures meant to sit side by side should name a width instead.

A long invocation

The prompt is a line like any other, so it counts toward the picture’s width. A script passed inline is the usual cause: the command line ends up longer than anything it prints. --columns auto then lays the image out at the invocation instead of at the output, and a pinned width folds that invocation across several rows.

--prompt settles it. It states the line a reader would type, in place of the one that ran:

$ click-extra screenshot --output market.svg --prompt "python market.py" -- python -c 'print("苹果    apple")'

Only the drawn line changes. The capture still holds what the inline script printed, so the image is laid out at that output alone. This is the same option a foreign CLI uses to hide the wrapper it was reached through.

Each argument is quoted as a shell needs it, so the drawn line pastes back as the command it pictures. An argument holding a space stays one argument, rather than spilling into the line as several.

--prompt "" draws no prompt at all. Reach for it when the prose around the capture already carries the command. A documentation block spells that one :hide-prompt:.

Light and dark chrome

A capture freezes the colors of the run it shows, so the window must match the theme that run rendered for. --background light swaps the dark chrome for white, along with the ANSI palette the capture’s colors resolve against:

$ click-extra screenshot --output light-help.svg --background light -- my-cli --theme light --help

Both halves are needed, and they are different halves: --theme light is what the CLI renders with, --background light is what the image is drawn on. Pass one without the other and you get the washed-out screen the theme gallery warns about.

The prompt line follows the chrome on its own. It is the one line a capture draws rather than collects, so on white it would otherwise vanish in the dark theme’s near-white invoked_command style.

A CLI that asks gets the same answer. A capture states its chrome to the command the way a terminal would, through the CLITHEME and COLORFGBG variables that background detection reads. A CLI with --theme auto then renders for the window it lands in:

$ click-extra screenshot --output light-help.svg --background light -- my-cli --theme auto --help

Here is that at work on one of click-extra’s own help screens. The two images below came from the same command line, --background apart. The CLI picked its palette from the terminal each capture claimed to be:

A help screen under --theme auto, drawn on dark chrome

$ click-extra screenshot --output docs/assets/auto-theme-dark-screen.svg --prompt "click-extra --theme auto themes --help" -- uv run --frozen -- click-extra --theme auto themes --help

The same command under --theme auto, drawn on light chrome

$ click-extra screenshot --output docs/assets/auto-theme-light-screen.svg --background light --prompt "click-extra --theme auto themes --help" -- uv run --frozen -- click-extra --theme auto themes --help

auto is not the default, on purpose. A CLI that never asks for it keeps rendering exactly as it does everywhere else. That is why the examples below spell out --theme dark and --theme light rather than rely on detection.

Here is one help screen taken both ways, with the CLI’s theme and the image’s chrome moving together:

The pantry help screen under the dark theme, drawn on dark chrome

The same screen under the light theme, drawn on light chrome

Both blocks hide their results, an exception to what :screenshot: is usually for. A results block on this page takes its colors from the site’s stylesheet and follows the reader’s own theme, so it cannot show the one thing being compared here.

The window

A capture is drawn as a terminal window: a rounded rectangle, framed with a border and lifted off the page by a drop shadow. Every part of it is an option:

$ click-extra screenshot --output shot.svg --title "my-cli --help" --backdrop "#1f6feb" --radius 0 --border-width 2 --margin 28 -- my-cli --help

Option

Takes

Default

Draws

--border

CSS color

the chrome’s

The frame around the window. none leaves it bare.

--border-width

pixels

1

How thick that frame is.

--radius

pixels

8, or the preset’s

How round the window’s corners are. 0 squares them.

--shadow

CSS color

the chrome’s

The drop shadow under the window. none leaves it flat.

--backdrop

CSS color

none

A page behind the window, margin included.

--margin

pixels

48

Transparent space around the window.

--opacity

0 to 1

1

How solid the window’s body is. Under 1 it lets what is behind it through.

--watermark

text

the click-extra credit

A credit line in the image’s bottom-right corner. Empty draws none.

--watermark-color

CSS color

a neutral gray

The ink that line is drawn in.

--padding

pixels

8

Space inside it, on top of the renderer’s own.

--line-numbers

flag

off

A dim gutter numbering the captured lines.

--title

text

none

A caption centered in the window’s title bar.

Two of them default to the chrome’s own value rather than a fixed one. That is why they are not constants: a fixed translucent white would make a light capture a white window on a white page, with no visible edge.

The rest depend on what the capture is for. A shadow needs --margin to fall into, because the image’s own box cuts whatever lands past it. --backdrop fills that same space instead of leaving it transparent, which turns a capture into a self-contained picture for a slide or a social card. --radius 0 drops the desktop-window look, for a capture meant to read as a plain block of output.

Both formats take the same options. An HTML capture lands them on the block’s border, border-radius, box-shadow, margin, padding and the page’s background.

Tip

A shadow is an SVG filter. Some renderers skip filters, but they still draw the border, so the window keeps an edge either way.

Gradients

--backdrop also takes a CSS gradient, which gives a capture its own page:

$ click-extra screenshot --output card.svg --backdrop "linear-gradient(135deg, #667eea, #764ba2)" -- my-cli --help

Understood are linear-gradient, opening with an angle (135deg) or a side keyword (to bottom right), and radial-gradient. Both take two or more color stops, each pinnable at a percentage (#667eea 30%). Anything else is treated as a plain color. HTML captures pass the value through to CSS untouched.

Transparency

--opacity makes the window’s body translucent, the way a terminal set to transparency does. Below 1, whatever sits behind the capture shows through, while its text, frame and title bar keep their own paint:

$ click-extra screenshot --output glass.svg --opacity 0.7 --backdrop "linear-gradient(135deg, #667eea, #764ba2)" -- my-cli --help

Over a backdrop it reads as frosted glass: the gradient tints the terminal instead of stopping at its edge. With no backdrop, the page shows through, which is what a capture dropped on a surface you do not control wants. What limits the value is legibility: a body much under half solid gives the text whatever contrast the backdrop happens to have.

An HTML capture thins the block’s background color with CSS color-mix(), so the page it is pasted into shows through the same way.

Line numbers

--line-numbers draws each line’s number in a dim gutter, the way Pygments does inline. Line 1 is the prompt:

$ click-extra screenshot --output numbered.svg --line-numbers -- my-cli --help

The numbers land in the terminal text rather than in a column of their own, the same trade Pygments makes: every renderer places them for free, and a reader copying an HTML capture copies them too.

It also means the gutter spends columns the command already used: a screen wrapped at 80 comes back a few characters too wide and folds. Pair the flag with --columns auto, or with a width that leaves room for the gutter, so the image grows instead of the lines breaking.

Emphasized lines

--emphasize-lines draws a band behind the lines it names, the way Pygments marks a highlighted line in a code block. Line 1 is the prompt, the same line the gutter counts first:

$ click-extra screenshot --output marked.svg --emphasize-lines 2,4-5 -- my-cli --help

The band is mixed from the chrome it is drawn on rather than stated outright, so one setting serves both: a shade lighter than a dark terminal, a shade darker than a light one. It runs from one edge of the window to the other, because what is emphasized is the row, not the column of text in it. It sits behind the text and follows the window’s own rounding, so a band on the last line cannot square off the corners.

A help screen with its Options heading and the two lines of --crates banded

Lines are counted on the canvas, blanks included, the way a gutter numbers them. Turn :screenshot-line-numbers: on while choosing them and the two counts agree.

Ranges on the command line are closed, so state both ends: the capture’s height is only known once the command has run, too late for 4- to mean anything. Inside a documentation block the height is known, so :screenshot-emphasize-lines: takes the open-ended form too.

An animated capture bands the same way, one frame at a time. A band appears with the frame that first draws the row it marks, and it is gone wherever the row is. The empty beat closing a cycle carries none: banding a row before the animation reaches it would read as a stray rectangle in blank space.

The credit line

Every capture the command writes carries a credit line in the margin at its bottom-right corner. Here is one, reading generated with pantry 1.4.2:

A help screen credited in the margin under its bottom-right corner

By default, the line names click-extra and the release that drew the image. A capture needs that once it has travelled: on a slide, in a README or on a social card it sits far from the page that explains where it came from. --watermark replaces the text, and an empty string draws none:

$ click-extra screenshot --output shot.svg --watermark "pantry 1.4.2 · example.com" -- my-cli --help
$ click-extra screenshot --output shot.svg --watermark "" -- my-cli --help

Crediting your own project rather than the tool that drew it is the expected case, not an exception. Set it once in your configuration file and every capture carries it.

The mark is the one thing drawn outside the window, so it is also the one paint that cannot follow the chrome: the margin is transparent, and behind it sits a page this command never sees. Hence a neutral gray, which reads on a white README and a dark one alike. --watermark-color covers a capture whose backdrop it has to sit on.

A capture written by a click:run block carries no watermark by default. That image is regenerated on every documentation build, so a release number in it would rewrite every asset on release day, and the page around it already says what drew it. :screenshot-watermark:, or the click_extra_screenshot_watermark conf.py value, turns it on for a project that wants it anyway.

All of it at once

Here is the pantry screen again, wearing every option above: on a gradient, captioned, numbered, rounded, padded, see-through enough for the gradient to tint it, and with its Options: heading and its --crates entry picked out. The second tab shows the block that wrote it:

The pantry help screen, captioned, numbered and part-highlighted on a gradient backdrop

```{click:run}
:screenshot: styled-window-screen
:screenshot-columns: auto
:screenshot-title: 🍎 pantry --help
:screenshot-backdrop: 'linear-gradient(135deg, #667eea, #764ba2)'
:screenshot-emphasize-lines: 9,15-16
:screenshot-line-numbers:
:screenshot-opacity: 0.75
:screenshot-radius: 12
:screenshot-padding: 24
:hide-results:
result = invoke(pantry, args=["--help"])
assert result.exit_code == 0
assert "--crates" in result.stdout
```

A caption takes any text a terminal can draw, emoji included. :hide-results: is this page’s choice, since the window is the thing on show: leave it out and the block renders its live text below the fence as well.

None of that is limited to the still. An animated capture is drawn through the same window, so it carries the gradient, the caption, the rounding, the transparency and the gutter exactly as a screenshot does. Here is a pantry being restocked, wearing everything above:

A spinner restocking a pantry, numbered and see-through on a gradient backdrop, its third line picked out

The gutter counts each frame’s own rows, so it grows as the trail does, and the band on the third line arrives on the same frame its number does.

Terminal presets

A capture is a picture of a terminal, and terminals do not look alike. --preset styles the window as a named desktop’s terminal:

$ click-extra screenshot --output shot.svg --preset windows -- my-cli --help

Each preset carries the four things that make a terminal recognizable: its window decorations, the palette its colors resolve against, the font it ships with, and the prompt sigil of its usual shell. An option stated alongside wins, so --preset windows --radius 8 rounds the corners Windows squares.

A preset also paints the strip its buttons and caption sit in, a shade off the terminal’s own background, because that strip belongs to the desktop. That makes plain the odd one out: it mimics no desktop, wears no buttons, and drops the strip entirely unless a --title gives it something to hold.

Each tab below shows its preset twice: bare, and then wearing everything else this page offers. The second picture states no --radius, so each terminal keeps its own corners: Windows stays square under the same gradient that rounds the other three.

Transparency lands least alike across them, because each preset brings its own background for the gradient to show through. At 75% it barely lifts Apple Terminal’s black and visibly lifts GNOME’s lighter #2e3436. All four still clear WCAG AA for their text, from 14.4:1 down to 6.4:1, so what changes is the character of the window, not whether it can be read.

The pantry screen drawn as an Apple Terminal window

Round buttons on the left, Apple’s Pro and Basic palettes, SF Mono, and a $ prompt.

An Apple Terminal window on a gradient, captioned, numbered and part-highlighted

The pantry screen drawn as a Windows Terminal window

Minimize, maximize and close on the right, square corners, the Campbell and One Half Light schemes, Cascadia Code, and a PS C:\> prompt.

A Windows Terminal window on a gradient, captioned, numbered and part-highlighted

The pantry screen drawn as a GNOME Terminal window

A single close button, the Tango palette GNOME Terminal ships, Ubuntu Mono, and a $ prompt.

A GNOME Terminal window on a gradient, captioned, numbered and part-highlighted

The pantry screen drawn as a plain block of output

No buttons, no rounded corners, no title bar: for a capture that has to read as a block of output rather than as a window, on a slide or in a paper.

A plain block of output on a gradient, captioned, numbered and part-highlighted

Caution

A preset’s palette is the scheme its terminal ships with, not the one your reader configured. It also decides how the captured CLI’s colors land: a screen rendered for a dark theme on a light preset washes out, exactly as the chrome section describes.

Wide glyphs and writing systems

A terminal cell is not a character. A Chinese ideograph, a Hangul syllable, a fullwidth Latin letter and most emoji are each drawn two cells wide, while a combining accent is drawn in none at all. A capture measures its runs in cells, through click_extra.layout.cell_width(), which is the same wcwidth measurement click-extra already uses to align tables and the command tree.

The CLI below pads with that measurement, so every name lands on column 12 whatever script precedes it:

from click_extra import command, echo
from click_extra.layout import cell_width

FRUITS = (
    ("苹果", "apple"),
    ("バナナ", "banana"),
    ("체리", "cherry"),
    ("KIWI", "kiwi"),
    ("🍎🍌🍒", "basket"),
    ("┌──┬──┐", "crate"),
    ("مشمش", "apricot"),
)

@command
def market():
    """List each fruit beside its name, aligned on a fixed column."""
    for glyphs, name in FRUITS:
        echo(f"{glyphs}{' ' * (12 - cell_width(glyphs))}{name}")
$ market
苹果        apple
バナナ      banana
체리        cherry
KIWI    kiwi
🍎🍌🍒      basket
┌──┬──┐     crate
مشمش        apricot

Size a run by its character count instead and the picture drifts: the seven lines above would land on four different columns, the wide scripts drawn at half their width and stacking on each other. That is rich#2742, open upstream since 2023 and one of the reasons this renderer is click-extra’s own. The upstream page collects the rest.

Two spaces are what mark a column. A capture cuts each line at its gutters (click_extra.screenshot.column_segments()), then pins every piece to its own offset. One space is ordinary word spacing rather than a gutter, so a wide glyph and the word after it stay a single run, and where that word sits inside the run is left to the font. Pad a column to a fixed width, the way market does above, instead of separating it by one space. Captured output rarely trips on this: a table or a help screen already leaves two spaces or a rule between its columns. A hand-written example is where it shows.

Note

Right-to-left scripts are the case cell arithmetic cannot settle. Arabic and Hebrew are reordered by whoever draws them, and the cursive ones are shaped: a letter’s form depends on the letters it joins. A run carrying any of them is therefore left to size itself, rather than pinned to an exact width that would pay for the difference in letter spacing and pull the word apart at its joins. The run still starts on its own column, so the grid around it holds; only its own width floats.

Box-drawing and block characters are not letters but tiles: a table’s rule, a tree’s elbow and a gradient’s bar are drawn by butting them edge to edge. They are emitted in short groups, each landing on a stated offset, so a font drawing them a fraction of a pixel off the grid cannot accumulate that error across a rule and leave the table’s corners missing their own border. What remains is whether two adjacent tiles join cleanly, which is the font’s business and no renderer’s: that one is rich#2536, where the answer upstream is that it would take replacing the characters with drawn shapes. click-extra does not do that either.

Animate a capture

Animated captures

An SVG capture can hold more than one frame. Pass frames a sequence of captured texts and interval how long each is shown. The window and its caption are drawn once, and every frame is stacked inside them.

from pathlib import Path

from click_extra.screenshot import render_svg
from click_extra.spinner_presets import SPINNERS
from click_extra.styling import Style

preset = SPINNERS["moon"]
lemon = Style(fg="#f1fa8c")
Path("brewing.svg").write_text(
    render_svg(
        columns=34,
        title="brewing",
        unique_id="brewing",
        frames=[lemon(f"{frame}  brewing tea…") for frame in preset.frames],
        interval=preset.interval,
    ),
    encoding="utf-8",
)

One number for interval 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.

The frames differ in nothing but their text, so one stylesheet covers all of them. Everything is namespaced by unique_id, keyframes included, so two animations inlined into one page keep their own timing.

A row drawn the same in every frame is drawn once. Only the rows that move are copied per frame, which keeps a recording with little motion small.

One frame stays visible wherever the animation does not run, so a capture is never a blank rectangle. The capture also honors a system asking for reduced motion, by keeping every animation rule behind a prefers-reduced-motion guard.

A recorded animation also pauses on its last frame and then closes on an empty beat, 2.6 seconds by default. A loop restarting the instant it arrives gives a reader no time to read the end. The empty beat says plainly where the loop comes round.

Three knobs state all of it, on render_svg, on the --record command line and on a documentation block alike:

render_svg

CLI

Directive

What it sets

hold

--hold

:screenshot-hold:

Extra seconds on the last frame, or auto.

blank

--blank

:screenshot-blank:

Seconds of empty screen closing the cycle.

speed

--speed

:screenshot-speed:

How much faster to play than recorded: 2 halves every frame.

speed scales the replay only. The two pauses are stated in real seconds and stay untouched. A declared spinner cycles in place and ends nowhere, so it holds and blanks for nothing unless a page asks.

hold also takes auto, which scales the pause to the final frame itself: a quarter second per populated line, clamped between 2 and 30 seconds. A fixed number serves an ending the author has seen; auto serves one that changes with every retake, like a recorded command whose closing report grows with what there is to report. A :screenshot-record: block and a --record capture therefore default to it, and a stated number overrides it.

The frame left visible is the last one, because an animation that accumulates says most once it has finished. An animated capture is therefore always a still as well.

A blinking cursor

A capture draws no cursor unless it is asked for one. Pass a Cursor and it draws one:

from click_extra.screenshot import render_svg
from click_extra.screenshot_presets import Cursor, CursorShape

render_svg("$ pantry restock --crates 4", columns=44, cursor=Cursor())

Where the cursor stands is never stated: it is read off each frame’s own text. A screen ends with whatever was written to it last, so the cursor sits after that. Output closing on a newline puts it on the row underneath, and the window grows a line to hold it, the way a terminal’s does.

Three fields say what it looks like:

Field

What it sets

shape

BLOCK, BAR or UNDERLINE. None takes the preset’s own.

blink

Seconds one blink takes. 0 draws a steady cursor.

color

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

A preset names the shape its terminal draws, so --preset windows gets Windows Terminal’s bar and the rest get a block. Cursor(CursorShape.BAR) overrides that.

The blink runs on a clock of its own rather than on the animation’s. The two 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. The rule sits behind the same prefers-reduced-motion guard as every other animation here, so a reader who asked their system for less of it gets a cursor that is lit and still.

A still capture takes a cursor too, and leaves it after the last thing the command printed, which is where the shell finds it. :screenshot-cursor: asks a documentation block for one, and :screenshot-blink: says how fast. A block draws its own invocation already, so nothing else is needed:

A still capture with a block cursor waiting under its output

The cursor sits on the row under the output, because the command’s last line ended on a newline. That is where a terminal leaves it, so the window grows a line to hold it.

The shell coming back

A command exits and the shell prints its prompt again. --closing-prompt, and :screenshot-closing-prompt: on a documentation block, draw that last row:

A capture closing on the shell's prompt, with the cursor waiting on it

Alongside a cursor it is free. Output ending on a newline already leaves the row the cursor waits on, and the sigil fills it instead of leaving it blank. A command that never ended its line is given one, exactly as a shell prints its own newline before prompting.

An animation closes its last frame and no other. The shell has not come back while the command is still drawing, and a sigil on an earlier frame would say it had.

Recording an animation

The frames above were declared. They can also be recorded, from a command that draws them.

This 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, because that answers a different question.

For a spinner this process hosts, hand it a ScreenRecorder. It claims to be a terminal without being one, so no pseudo-terminal is involved and it works on every platform:

from click_extra import SPINNERS, Spinner
from click_extra.recording import ScreenRecorder

recorder = ScreenRecorder()
with Spinner("Brewing tea", spinner=SPINNERS["moon"], stream=recorder):
    steep()
frames = recorder.frames()

For a command this process does not host, record_command runs it under a pseudo-terminal and reads both its output and its errors, the spinner drawing on the latter:

from click_extra.recording import record_command

frames = record_command(("kettle", "boil", "--slowly"), columns=60, duration=5.0)

That path is Unix only: a pseudo-terminal is termios and pty, neither of which Windows ships, and reaching ConPTY would mean a dependency. The in-process recorder above covers Windows.

Each Frame carries the screen it held and how long it held it, so render_svg takes them directly:

render_svg(
    columns=60,
    frames=[frame.text for frame in frames],
    interval=[frame.duration for frame in frames],
)

Caution

A recording is timed by the wall clock, so the same command records slightly different durations every run. That is fine for a one-off image, and a problem for a committed one that is rewritten on every build: see keeping a recording committable.

Type the command first

A recording holds what a command drew and never the invocation that drew it, so an animation opens on output arriving from nowhere. typing types the command line out first, one character per frame:

from click_extra.recording import record_and_render
from click_extra.screenshot_presets import Cursor

svg, returncode = record_and_render(
    ("pantry", "restock", "--crates", "4"),
    columns=44,
    typing=0.05,
    submit=0.45,
    cursor=Cursor(),
)

typing is how long a character takes to appear, and submit is the beat the finished line waits before the output starts, which is the pause before the return key. Leave typing out and the prompt stands there from the first frame, which is what a recording shows without it.

The typed screens are ordinary frames. Everything the picture does for a frame therefore reaches them: a gutter numbers them, and the cursor walks along the line with no caret of its own to state.

For an animation assembled by hand, type_line makes those frames from a prompt line and nothing else:

from click_extra.recording import type_line

opening = type_line("$ pantry restock --crates 4", typing=0.05)

A documentation block asks for the same with three options. :screenshot-prompt: states the invocation to draw, :screenshot-typing: types it, and :screenshot-submit: sets the beat before the output starts:

A command line typing itself, then a spinner turning underneath

The cursor walks the line as it is typed, then drops to the spinner it started. Nothing states that: the cursor is read off each frame’s text, so it follows whatever the frame last drew.

Tip

A typed opening costs one frame per character. Those frames differ by a single character each, so they compress to almost nothing over the wire: the raw file roughly doubles while the gzipped one grows about a kilobyte.

Keeping a recording committable

A committed capture is rewritten on every build, which keeps it from drifting away from the CLI. A recording cannot live that way, and not because of its timings: the scheduler decides which spinner glyph pairs with which screen, so the same command records a different set of frames on every other run. Rounding the durations with quantize settles the jitter but cannot touch that.

So a recording is written once and then kept. :screenshot-record: writes the asset the first time and leaves it alone afterwards. It does not even evaluate its expression once the file exists. To take a fresh recording, delete the file and build again.

A recording states what it pictures on a line beside the generator tag:

<!-- @generated by Click Extra 9.0.1.dev0 -->
<!-- @recording frames=43 period=6.3s digest=90877cf3f14015d6 -->

The digest covers the frames a cycle holds and the beat it holds them on, not their order or their count.

Caution

Written once means frozen: nothing re-checks a recording against the code it pictures, so it rots the way any hand-made screenshot does. A declared animation has no such problem, being composed rather than timed, and is regenerated on every build like every other capture.

Recording from the command line

--record turns the screenshot command into a recorder: the command runs under a pseudo-terminal, so a spinner or a progress bar draws the frames it would draw for you, and every screen it leaves behind lands in one animated SVG.

$ click-extra screenshot --record --output recorded-trail-screen.svg -- click-extra trail

The trail demo recorded live: outcomes stream above a turning spinner

--cursor and --typing turn a recording into a session: the command line types itself at the prompt, a cursor follows it along, and the output arrives underneath.

$ click-extra screenshot --record --cursor --closing-prompt --typing 0.05 --columns 46 --output typed-trail-screen.svg -- click-extra trail

The trail demo typed at a prompt, a cursor blinking through it

--cursor on its own takes the shape the --preset terminal draws, and --cursor bar or --cursor underline overrides it. --blink 0 leaves the cursor lit and still. Both apply to a still capture as well, where the cursor lands after the last thing the command printed.

The invocation is drawn above every frame, exactly as a still capture draws its own prompt, and --prompt overrides or hides it the same way. --rows states the terminal’s height, --timeout stops a recording that would run on, and the pacing knobs above apply as given. Two of a still capture’s arrangements do not carry over: the width must be a number, since the pseudo-terminal exists before the command draws its first line, and --head/--tail stay out, a recording being made of whole screens.

Caution

Unix only, for the reasons record_command() states. The same pipeline is scriptable through record_and_render().

Publish and maintain

Stating a default once

screenshot is itself a Click Extra CLI, so every option above is also a configuration key. To draw every capture the same way, state it once in the pyproject.toml click-extra finds by walking up from the working directory:

[tool.click-extra.screenshot]
preset = "macos"
margin = 64
opacity = 0.85
watermark = "pantry 1.4.2 · example.com"

Any dedicated configuration file does the same under a [click-extra.screenshot] table. A flag on the command line still wins over both, following the usual precedence, so a single capture can break the house style without editing anything.

In Sphinx documentation, click:run blocks write the captures rather than the command, so the same setting is a conf.py value:

click_extra_screenshot_preset = "macos"

It covers every block whose :screenshot: names no preset of its own, leaving :screenshot-preset: for the pages that depart from it. A project keeping its settings in one place can read the value back out of its pyproject.toml, conf.py being Python:

import tomllib
from pathlib import Path

pyproject = tomllib.loads(Path("../pyproject.toml").read_text(encoding="utf-8"))
click_extra_screenshot_preset = pyproject["tool"]["click-extra"]["screenshot"]["preset"]

Keeping captures fresh

Here is the command pointed at click-extra itself. Its bundled CLI doubles as a live demo of the rendering features. Each image below was produced by the line printed under it, run from a checkout.

gradient puts 24-bit ramps next to their 256-color quantized equivalents, so the stepping the smaller palette introduces is there to see:

Color gradients rendered in 24-bit and quantized to 256 colors

$ click-extra screenshot --output docs/assets/color-gradient-screen.svg --prompt "click-extra gradient" -- uv run --frozen -- click-extra gradient

styles crosses every color with every text style. Its table wants far more than 80 columns, so the capture is taken at the width it actually needs:

Every color rendered under each text style

$ click-extra screenshot --output docs/assets/text-styles-screen.svg --columns 160 --head 14 --prompt "click-extra styles" -- uv run --frozen -- click-extra styles

themes renders a sample help screen under each built-in palette, and this capture keeps the first two of them, which is what --theme does to a screen:

The same CLI help screen captured under the dark and dracula themes

$ click-extra screenshot --output docs/assets/theme-gallery-screen.svg --head 34 --prompt "click-extra themes" -- uv run --frozen -- click-extra themes

--prompt makes each image show the bare click-extra a reader would type, while uv run --frozen -- is what actually ran. The plumbing that reaches a checkout’s copy of the CLI is not worth picturing. --head bounds the two long ones, and a dotted rule closing on says the rest was cut.

The before/after pair opening the readme takes the other route. Those two screens already exist as live click:run blocks in the tutorial, so the blocks maintain them instead of shooting them again: a :screenshot: option writes each image on every documentation build. That keeps the readme’s front page in step with the code.

Use the command when the CLI you want to picture has no live block, and the directive when it does.

Whichever route, a full click-extra help screen carries --table-format, whose choice list is a single 463-character line. Click never wraps an option’s own term, so the renderer folds it across six rows, exactly as a terminal would.

Once committed, an image goes stale the first time the CLI’s help changes. tests/test_screenshots.py reads the SVG back, rebuilds the terminal text from the glyph coordinates, and compares it to what the command prints today. It fails on the first line that diverged, and re-running the command above refreshes the picture.

GitHub integration

A README can track the reader’s theme. Capture the same screen twice, once per chrome, with the CLI’s own theme moving along:

$ click-extra screenshot --output docs/assets/help-dark.svg -- my-cli --theme dark --help
$ click-extra screenshot --output docs/assets/help-light.svg --background light -- my-cli --theme light --help

Then hand both to a <picture> element, which GitHub renders in a README and which picks between them on prefers-color-scheme:

<picture>
 <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/me/my-cli/main/docs/assets/help-dark.svg"/>
 <img alt="my-cli help screen" src="https://raw.githubusercontent.com/me/my-cli/main/docs/assets/help-light.svg"/>
</picture>

The <img> is not a spare: every surface without the switch shows it, PyPI and most editors’ previews included. So it holds the capture that reads on the light background those surfaces default to. The <source> is the one a reader in dark mode gets instead.

Both URLs are absolute on purpose. A README is read on PyPI and in many forks of the page, and none of them resolve a repository-relative path. That is why every capture in this project’s own README is addressed through raw.githubusercontent.com.

Note

The switch keys on the browser’s color scheme, not on the theme toggle of a documentation site, so it belongs to a README rather than to these pages: Furo’s own switch would leave it unmoved.

click_extra.screenshot API

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

click_extra.screenshot.TColumns: TypeAlias = int | typing.Literal['auto']

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

click_extra.screenshot.THold: TypeAlias = float | typing.Literal['auto']

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

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

Bases: Enum

Document formats a capture can be rendered to.

The value doubles as the file extension format_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().

class click_extra.screenshot.CaptureBackground(*values)[source]

Bases: Enum

Terminal chrome a capture is drawn on.

A capture freezes the colors of the run it pictures, so the chrome has to answer to the palette that run was colored for. Neither direction survives the other: a screen colored for a dark terminal is unreadable on white, and click-extra’s own light and manpage themes wash out on the dark chrome a renderer defaults to.

The value doubles as the --background choice the CLI offers.

DARK = 'dark'

What a terminal, and this package’s default theme, usually look like.

LIGHT = 'light'

For a CLI rendered with a light-background theme.

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

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

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

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

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

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

click_extra.screenshot.CAPTURE_BACKGROUND = '#292929'

Background a dark capture is drawn on.

Stating it is not optional: a help screen colored for a dark terminal is unreadable on a page that defaults to white.

click_extra.screenshot.CAPTURE_FOREGROUND = '#c5c8c6'

Color of the text a dark capture leaves unstyled. See CAPTURE_BACKGROUND.

click_extra.screenshot.LIGHT_CAPTURE_BACKGROUND = '#ffffff'

Background a light capture is drawn on.

See CAPTURE_BACKGROUND: an SVG and an HTML capture of the same run have to look like the same terminal.

click_extra.screenshot.LIGHT_CAPTURE_FOREGROUND = '#000000'

Color of the text a light capture leaves unstyled.

See LIGHT_CAPTURE_BACKGROUND.

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

Theme the prompt line is drawn with, per chrome.

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

None keeps whatever theme the invocation already runs under. So does a missing entry: the mapping is read through dict.get(), and BUILTIN_THEMES is empty when a trimmed install drops themes.toml.

click_extra.screenshot.STDOUT_PATH = '-'

Destination naming the terminal rather than a file.

The convention every command-line tool reading or writing a stream already follows, and the one destination that states no extension, so it is what format_from_path() reads as CaptureFormat.ANSI.

click_extra.screenshot.NO_PAINT = 'none'

Border or shadow value asking for none to be drawn.

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

click_extra.screenshot.OPAQUE = 1.0

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

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

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

Color the window frame is drawn in, per chrome.

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

click_extra.screenshot.CAPTURE_SHADOWS: dict[CaptureBackground, str] = {CaptureBackground.DARK: 'rgba(0,0,0,0.5)', CaptureBackground.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.

click_extra.screenshot.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.screenshot.DEFAULT_BORDER_WIDTH = 1

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

click_extra.screenshot.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.screenshot.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.screenshot.SHADOW_BLUR = 6

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

click_extra.screenshot.SHADOW_OFFSET = 3

Downward offset, in pixels, of the drop shadow.

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

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

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

click_extra.screenshot.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.screenshot.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.screenshot.TITLE_SIZE = 18

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

click_extra.screenshot.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.screenshot.WATERMARK_INSET = 12

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

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

click_extra.screenshot.PACKAGE_NAME = 'click-extra'

Name this package is distributed and credited under.

click_extra.screenshot.DEFAULT_WATERMARK = 'generated with click-extra 9.2.1'

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 names the release that drew it, so a reader can tell an image shot two years ago from one shot today.

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.screenshot.WATERMARK_URL = 'https://kdeldycke.github.io/click-extra/screenshots.html'

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

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

Note

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

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

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

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

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

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

Environment variables naming the terminal a capture is taken from.

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

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

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

click_extra.screenshot.CAPTURE_FONT_STACK = "'Fira Code', 'Cascadia Code', Menlo, Consolas, monospace"

Monospaced fonts a capture asks for, best first.

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

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

Note

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

The fallback cost is measured. 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: 26 of the 89 tiles under docs/assets/spinner-*.svg draw 11% to 80% 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") was measured and rejected: across the 1407 runs in the committed captures it distorts 425 of them by more than 6%, some past 100%, because it stretches a narrow glyph as readily as it squeezes a wide one.

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

click_extra.screenshot.CELL_HEIGHT = 20.0

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

click_extra.screenshot.FONT_ASPECT_RATIO = 0.61

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

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

click_extra.screenshot.CELL_WIDTH = 12.2

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

click_extra.screenshot.LINE_HEIGHT = 24.4

Vertical distance between two consecutive text baselines, in pixels.

click_extra.screenshot.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.screenshot.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.screenshot.CURSOR_THICKNESS = 2.0

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

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

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

Cursor shape asking for the one the terminal preset decides.

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

click_extra.screenshot.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.screenshot.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.screenshot.DIM_RATIO = 0.4

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

click_extra.screenshot.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.screenshot.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, and the split was found the hard way. visibility alone was tried first and satisfies a browser, librsvg and macOS Quick Look’s thumbnailer. Two other readers ignored it and drew every frame on top of the last: a git client’s SVG diff view, and macOS Finder’s preview pane, which is a different code path from the thumbnailer that was already working. Adding opacity fixed both, so each property covers a reader the other misses and dropping either takes a class of reader with it.

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

The line an animated capture states its own identity on.

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

click_extra.screenshot.FRAME_TIMING_FUNCTION = 'step-end'

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

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

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

Guard every animation rule an animated capture emits sits behind.

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

click_extra.screenshot.WINDOW_PADDING = 8

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

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

click_extra.screenshot.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.screenshot.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.screenshot.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.screenshot.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.screenshot.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.screenshot.auto_hold(text)[source]

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

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

Parameters:

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

Return type:

float

Returns:

the pause, in seconds.

click_extra.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.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.screenshot.MIN_COLUMNS = 20

Narrowest width a capture is rendered at.

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

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

Marker asking for a rule as wide as the lines it stands between.

TRUNCATION_LABEL centered in a run of TRUNCATION_RULE, spanning the widest line the capture kept. 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.

Measured on the kept lines alone, so the marker can never be what decides the image width. That also makes it track an explicit columns only as far as the text does: a capture whose lines all stop short draws a rule that stops there too.

click_extra.screenshot.TRUNCATION_LABEL = '✂'

What AUTO_TRUNCATION centers in its rule.

click_extra.screenshot.TRUNCATION_RULE = '·'

Character AUTO_TRUNCATION draws its rule with.

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

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

click_extra.screenshot.DEFAULT_TRUNCATION: str = 'auto'

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

click_extra.screenshot.preset_palette(preset, background)[source]

The colors a preset shows on the given chrome.

Return type:

TerminalPalette

click_extra.screenshot.resolve_palette(preset, background)[source]

The colors a capture resolves its ANSI codes against.

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

Return type:

TerminalPalette

click_extra.screenshot.cursor_cell(picture, columns)[source]

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

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

Note

Derived rather than recorded, and exactly rather than nearly: the position is a property of the text a screen holds, so it survives a frame travelling as a bare string and costs the recorder no second channel to carry it on. Checked against the screen’s own column across a spinner’s writes.

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

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

Return type:

tuple[int, int] | None

Returns:

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

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

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

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

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

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

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

Return type:

int

Returns:

the width, in characters.

click_extra.screenshot.append_prompt(text, *, background=CaptureBackground.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 (CaptureBackground) – chrome the capture is headed for, which picks the theme the sigil is styled with.

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

Return type:

str

Returns:

the same text, closed by a prompt.

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

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

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

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

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

Parameters:
Return type:

CompletedProcess[str]

Returns:

the completed process, whose stdout holds the captured text.

click_extra.screenshot.trim_lines(text, *, head=None, tail=None, truncation='auto')[source]

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

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

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

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

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

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

Return type:

str

Returns:

the trimmed text.

click_extra.screenshot.palette_color(color, palette)[source]

Resolve any color a Style carries to a hex string.

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

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

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

Return type:

str

Returns:

the color, as #rrggbb.

Raises:

ValueError – when the value names no color.

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

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

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

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

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

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

Return type:

str

Returns:

the blended color, as #rrggbb.

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

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

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

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

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

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

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

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

  • height (float) – its height.

Return type:

tuple[str, str] | None

Returns:

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

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

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

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

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

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

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

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

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

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

Return type:

str

Returns:

the SVG markup.

click_extra.screenshot.credit_segments(text)[source]

Split a credit line around the package name it credits.

The one part of the line worth pointing anywhere is the name, so both formats link that and leave the rest as prose. Splitting is what keeps a custom credit out of it: a line not naming the package has nothing to link.

Parameters:

text (str) – the credit line.

Return type:

tuple[str, str, str] | None

Returns:

what precedes the name, the name, and what follows it, or None when the line does not name the package.

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

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

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

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

The package name is wrapped in a link, see WATERMARK_URL. Written as a plain href rather than the xlink:href of SVG 1.1, which every current browser reads and which needs no second namespace on the root element.

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

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

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

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

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

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

Return type:

str

Returns:

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

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

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

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

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

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

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

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

Return type:

str

Returns:

the SVG markup, empty when the window wears none.

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

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

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

Note

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

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

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

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

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

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

Return type:

str

Returns:

the SVG source for the cursor.

click_extra.screenshot.column_segments(text, column)[source]

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

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

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

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

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

Return type:

Iterator[tuple[str, int]]

Returns:

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

click_extra.screenshot.tile_runs(text, column)[source]

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

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

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

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

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

Return type:

Iterator[tuple[str, int]]

Returns:

each piece, with the column it starts on.

click_extra.screenshot.glyph_offsets(text, column)[source]

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

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

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

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

Return type:

str

Returns:

the x attribute, and a textLength where one applies.

click_extra.screenshot.style_rules(style, palette)[source]

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

Parameters:
Return type:

str

Returns:

the CSS declarations, semicolon-separated.

click_extra.screenshot.run_paint(style, palette)[source]

The color painted behind a run, or None where it shows the terminal’s own.

Parameters:
  • style (Style) – the run’s style.

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

Return type:

str | None

Returns:

the background color, as #rrggbb, or None to paint nothing.

click_extra.screenshot.animation_digest(frames, durations)[source]

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

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

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

Note

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

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

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

Return type:

str

Returns:

the fingerprint, as hexadecimal.

click_extra.screenshot.animation_metadata(svg)[source]

Read back what an animated capture states about itself.

Parameters:

svg (str) – source of a rendered capture.

Return type:

dict[str, str]

Returns:

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

click_extra.screenshot.frame_animation_css(unique_id, durations)[source]

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

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

Note

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

Every rule sits behind REDUCED_MOTION_QUERY, and nothing else here hides anything: the frames a still is not made of carry visibility="hidden" as a presentation attribute 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.

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

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

Return type:

str

Returns:

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

Raises:

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

click_extra.screenshot.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 opacity and never touches visibility. A cursor sits inside the group of the frame it belongs to, and that group is hidden by both properties at once, see HIDDEN_FRAME_ATTRIBUTES. Opacity multiplies down into the group, so a hidden frame’s cursor stays hidden; a rule restoring visibility would instead override the inherited value and show every frame’s cursor at once.

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

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

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

Return type:

str

Returns:

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

Raises:

ValueError – when the period is not positive.

click_extra.screenshot.render_svg(text='', *, columns, title='', unique_id=None, frames=None, interval=None, hold=0.0, blank=0.0, speed=1.0, emphasize=(), cursor=None, 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", border='none', border_width=1, radius=8, backdrop='none', shadow='none', margin=0, padding=0, buttons=(('#ff5f57', '#febc2e', '#28c840'), ''), buttons_color=None, titlebar='none', collapse_titlebar=False, opacity=1.0, watermark='', watermark_color='rgba(128,128,128,0.85)')[source]

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

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

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

Note

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

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

Note

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  • border (str) – paint for the window’s frame. NO_PAINT draws none.

  • border_width (int) – thickness of that frame, in pixels.

  • radius (int) – how round the window’s corners are, in pixels.

  • backdrop (str) – paint filling the whole image, margin included, or a CSS gradient, see gradient_svg(). NO_PAINT leaves it transparent.

  • shadow (str) – color the window’s drop shadow floods with.

  • margin (int) – transparent pixels left around the window, on all four sides.

  • padding (int) – pixels added inside the window, around the text.

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

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

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

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

  • opacity (float) – how solid the window’s body is, from OPAQUE down to 0.0. Only the body thins out: the frame, the title bar and the text keep their own paint.

  • watermark (str) – credit line drawn in the image’s bottom-right corner.

  • watermark_color (str) – color that line is drawn in, alpha included.

Return type:

str

Returns:

the SVG source.

Raises:

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

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

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

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

Caution

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

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

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

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

Return type:

str

Returns:

the text, banded.

click_extra.screenshot.render_html(text, *, title='', full=True, background=CaptureBackground.DARK, preset=None, palette=None, border='none', border_width=1, radius=8, backdrop='none', shadow='none', margin=0, padding=0, buttons=None, buttons_color='#c5c8c6', font_stack="'Fira Code', 'Cascadia Code', Menlo, Consolas, monospace", titlebar='none', collapse_titlebar=False, opacity=1.0, watermark='', watermark_color='rgba(128,128,128,0.85)', watermark_url='https://kdeldycke.github.io/click-extra/screenshots.html')[source]

Render captured terminal text to HTML.

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

Caution

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

Note

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

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

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

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

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

  • palette (TerminalPalette | None) – colors the text resolves against. None takes the ones the preset and chrome name, which is what a terminal capture wants. Stated by a capture whose colors come from somewhere else, as a snippet one takes them from a syntax style.

  • border (str) – color of the block’s frame, see render_svg().

  • border_width (int) – thickness of that frame, in pixels.

  • radius (int) – how round the block’s corners are, in pixels.

  • backdrop (str) – paint filling the page behind the block.

  • shadow (str) – color of the block’s drop shadow, see render_svg().

  • margin (int) – pixels left around the block, on all four sides.

  • padding (int) – pixels added inside the block, on top of its own.

  • buttons (WindowButtons | None) – ignored. HTML reflows with the page embedding it, so it carries the text and its colors, not a window drawn around them.

  • buttons_color (str) – ignored, see buttons.

  • titlebar (str) – ignored, see buttons.

  • collapse_titlebar (bool) – ignored, see buttons.

  • opacity (float) – how solid the block’s background is, from OPAQUE down to 0.0, where the page shows straight through the text.

  • watermark (str) – credit line drawn under the block, against its right edge, where an SVG draws it in the margin. Empty draws none.

  • watermark_color (str) – color that line is drawn in, alpha included.

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

Return type:

str

Returns:

the rendered markup.

click_extra.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, full=True, background=CaptureBackground.DARK, preset=None, palette=None, border=None, border_width=1, radius=None, backdrop='none', shadow=None, margin=48, padding=8, opacity=1.0, watermark='generated with click-extra 9.2.1', watermark_color=None)[source]

Render captured terminal text to the document format names.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  • border (str | None) – color of the window’s frame. None takes the one the chrome can show, see CAPTURE_BORDERS; NO_PAINT draws none.

  • border_width (int) – thickness of that frame, in pixels.

  • radius (int | None) – how round the window’s corners are, in pixels. Zero squares them.

  • backdrop (str) – paint filling the image behind the window, margin included. NO_PAINT leaves it transparent.

  • shadow (str | None) – color of the window’s drop shadow. None takes the chrome’s own, see CAPTURE_SHADOWS; NO_PAINT draws none.

  • margin (int) – transparent pixels left around the window, on all four sides.

  • padding (int) – pixels added inside the window, around the text.

  • opacity (float) – how solid the window’s body is, from OPAQUE down to 0.0. Below it, whatever the capture is laid over shows through.

  • watermark (str) – credit line drawn in the image’s bottom-right corner, see DEFAULT_WATERMARK. An empty string draws none.

  • watermark_color (str | None) – color that line is drawn in. None takes WATERMARK_INK, which reads on a page of either color.

Return type:

str

Returns:

the rendered document.

Raises:

ValueError – asking an HTML capture to animate.

click_extra.screenshot.prompt_line(args, *, prompt=None, background=CaptureBackground.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. Each drew its own before, which is three chances for the sigil, the theme or the preset to disagree between an image and the image beside it.

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

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

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

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

Return type:

str

Returns:

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

click_extra.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, closing_prompt=False, title='', unique_id=None, full=True, background=CaptureBackground.DARK, preset=None, border=None, border_width=1, radius=None, backdrop='none', shadow=None, margin=48, padding=8, opacity=1.0, watermark='generated with click-extra 9.2.1', watermark_color=None)[source]

Run a command and render its output as a document.

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

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

  • format (CaptureFormat) – which document to produce.

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

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

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

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

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

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

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

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

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

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

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

  • title (str) – see render().

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

  • full (bool) – see render().

  • background (CaptureBackground) – see render().

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

  • border_width (int) – see render().

  • radius (int | None) – see render().

  • backdrop (str) – see render().

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

  • margin (int) – see render().

  • padding (int) – see render().

  • opacity (float) – see render().

  • watermark (str) – see render().

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

Return type:

tuple[str, int]

Returns:

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

click_extra.screenshot.format_from_path(path)[source]

Pick the capture format a file name asks for.

STDOUT_PATH names the terminal, whose format is the escape sequences themselves. It is answered here rather than at the call site so the one question “what does this destination want?” has one answer.

Parameters:

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

Return type:

CaptureFormat

Returns:

the CaptureFormat its extension names.

Raises:

ValueError – when the extension names no format.

click_extra.screenshot_presets API

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

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

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

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

  • the font the terminal ships with;

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

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

None of it is applied unless asked for: a capture with no preset keeps the renderer’s own neutral window, which is what every image in this project’s documentation is drawn as.

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.screenshot_presets.TerminalPalette(background: str, foreground: str, ansi: tuple[str, ...], titlebar: str)[source]

Bases: NamedTuple

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

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

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

background: str

Color behind the text, as CSS spells it.

foreground: str

Color of the text a CLI leaves unstyled.

ansi: tuple[str, ...]

The 16 palette entries, normal eight first.

titlebar: str

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

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

class click_extra.screenshot_presets.WindowButtons(circles: tuple[str, ...] = (), glyphs: str = '')[source]

Bases: NamedTuple

The decorations a terminal draws in its title bar.

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

Create new instance of WindowButtons(circles, glyphs)

circles: tuple[str, ...]

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

glyphs: str

Characters drawn from the right, closing button last.

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

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

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

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

click_extra.screenshot_presets.GNOME_BUTTONS: Final = ((), '✕')

The single close button a GNOME window carries by default.

class click_extra.screenshot_presets.CursorShape(*values)[source]

Bases: Enum

The shape a terminal draws its cursor as.

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

BLOCK = 'block'

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

BAR = 'bar'

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

UNDERLINE = 'underline'

A thin line along the cell’s bottom edge.

class click_extra.screenshot_presets.Cursor(shape: CursorShape | None = None, blink: float = 1.0, color: str | None = None)[source]

Bases: NamedTuple

The cursor a capture draws, and how it behaves.

Passed to render_svg() to draw one at all: a capture shows no cursor unless asked, which is what keeps an image taken before this existed byte-identical to the one taken after.

Note

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

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

shape: CursorShape | None

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

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_QUERY like every other animation this package emits, which leaves the cursor lit and still. That guard is also what answers WCAG 2.2.2, which asks that anything blinking past five seconds can be stopped.

color: str | None

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

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

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

click_extra.screenshot_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.screenshot_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.screenshot_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.screenshot_presets.TerminalPreset(label: str, buttons: WindowButtons, radius: int, prompt: str, cursor: CursorShape, font_stack: str, dark: TerminalPalette, light: TerminalPalette)[source]

Bases: NamedTuple

A terminal a capture can be drawn as.

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

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

label: str

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

buttons: WindowButtons

Decorations drawn in the title bar, see WindowButtons.

radius: int

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

prompt: str

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

cursor: CursorShape

Shape it draws its cursor as, see CursorShape.

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

font_stack: str

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

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

dark: TerminalPalette

Colors the terminal shows on its dark scheme.

light: TerminalPalette

Colors it shows on its light one.

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

Every terminal a capture can be drawn as, alphabetically.

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