CLI screenshots¶
click-extra produces colored terminal output, and inside this Sphinx documentation the click:run directive executes each CLI and renders its real output at build time, so these pages need no screenshots. A README on GitHub or PyPI, a slide, a social post or a page of your own cannot run code, and those surfaces need a capture instead. click-extra ships the command that produces one, as an image or as HTML:
Every part of that window answers to an option: the terminal it is drawn as, the chrome under its colors, the backdrop, the caption, the line numbers, the transparency, the border, the shadow, the corner radius and the room around it. The block that produced it sits further down this page, and rewrites the image on every build.
The screenshot command¶
click-extra screenshot runs a CLI, captures its colored output and writes it out. Point it at any command, with -- separating your CLI’s own options from the ones above:
$ click-extra screenshot --output cli-help.svg -- my-cli --help
Two formats, two surfaces¶
The extension of --output picks what gets written, and the two are not interchangeable:
Format |
Text is |
Goes where |
|---|---|---|
|
a picture |
a surface that strips inline HTML: a README on GitHub or PyPI |
|
selectable, searchable, copyable |
a page you own: your site, a blog post, a slide deck |
So the choice is really made for you. GitHub and PyPI render an image and drop inline styling, which leaves a README no option but SVG. Everywhere you control the markup, HTML is the better artifact: a reader can select a flag out of the help screen and paste it into their terminal, and search finds it.
Neither format needs an optional dependency. Both read the same click_extra.styling.split_ansi() stream: SVG is laid out on a character grid by click_extra.screenshot.render_svg(), HTML is inline-styled markup from click_extra.styling.ansi_to_html().
$ click-extra screenshot --output cli-help.html -- my-cli --help
That writes a standalone document. Add --fragment to get the bare <pre> instead, styled inline so it needs no stylesheet from the page you paste it into.
$ 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.
Options:
--output FILE Path of the file to write. Its extension picks
the format: .svg for an image, .html for
selectable text. [required]
--columns INTEGER|auto 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 TEXT 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 INTEGER RANGE Thickness of that frame, in pixels. [default:
1; x>=0]
--radius INTEGER RANGE 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 TEXT 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 TEXT 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 INTEGER RANGE 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 INTEGER RANGE Pixels added inside the window, around the
captured 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.0.0]
--watermark-color TEXT 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.
--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.
--head INTEGER RANGE Keep only the first N lines of output. [x>=1]
--tail INTEGER RANGE Keep only the last N lines of output. [x>=1]
--truncation TEXT Line standing in for what --head or --tail cut
away. [default: [...]]
--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.
--line-numbers Number the captured lines in a gutter, the way
Pygments does inline. The prompt counts as line
1, being the invocation everything under it came
from.
--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.
--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 FLOAT RANGE Seconds before the command is killed. Waits
forever by default. [x>0]
--time / --no-time Measure and print elapsed execution time.
[default: no-time]
--config CONFIG_PATH Location of the configuration file. Supports
local path with glob patterns or remote URL.
[default: ~/.config/click-extra/{*.toml,*.yaml,*
.yml,*.json,*.json5,*.jsonc,*.hjson,*.ini,*.xml,
*.plist,*.sqlite,*.sqlite3,*.conf,pyproject.toml
}]
--no-config Ignore all configuration files and only use
command line parameters and environment
variables.
--validate-config FILE Validate the configuration file and exit.
--export-config FORMAT Export the configuration in the selected format
to <stdout>, then exit.
--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]
--params Show all CLI parameters, their provenance,
defaults and value, then exit.
--table-format [aligned|asciidoc|colon-grid|csv|csv-excel|csv-excel-tab|csv-unix|double-grid|double-outline|fancy-grid|fancy-outline|github|grid|heavy-grid|heavy-outline|hjson|html|jira|json|json5|jsonc|latex|latex-booktabs|latex-longtable|latex-raw|mediawiki|mixed-grid|mixed-outline|moinmoin|orgtbl|outline|pipe|plain|presto|pretty|psql|rounded-grid|rounded-outline|rst|simple|simple-grid|simple-outline|textile|toml|tsv|unsafehtml|vertical|xml|yaml|youtrack]
Rendering style of tables. [default: rounded-
outline]
--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]
--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.
-h, --help Show this message and exit.
Three things it settles that a general-purpose capture tool leaves to you:
Colors, which a command strips on its own the moment its output is a pipe rather than a terminal. The capture runs under
click_extra.color.forced_color(), setting theFORCE_COLORlever every mainstream color system obeys and clearing anyNO_COLORthe environment carries.Width, where
--columnspins what the command wraps to and what the image is drawn at. Let those two disagree and the rendered lines overrun the image.stderr, which stays out of the capture unless--merge-stderrasks for it. That is what keeps a wrapper’s build chatter out of the picture with no shell redirection to remember.
Every SVG it writes also gives each column its own offset, rather than padding a line with spaces and leaning on textLength to hold the rest of it in place. Written the other way, a column only lands where it belongs if the reader’s renderer both honors textLength and resolves the font the file names. A web browser does both; librsvg (and through it rsvg-convert and ImageMagick) ignores textLength outright, and a file manager, a git client or a thumbnailer commonly falls back to a proportional font. Either way a gutter paid for in glyphs collapses and the columns slide onto each other. click_extra.screenshot.column_segments() documents where the cut falls.
Three smaller things travel with that, each one a way a capture used to depend on the reader being a browser:
Nothing is fetched. The text is set in the first family of
click_extra.screenshot.CAPTURE_FONT_STACKthe reader already has, so a capture renders the same offline and on a page that forbids third-party requests.The encoding is declared outright. A standalone SVG carries no HTTP header to state it, and a reader that assumes the platform’s own turns every multi-byte character into mojibake, a full block becoming
â.Nothing important is left to a stylesheet or a filter. The terminal text names its face, size and color as attributes as well as in the stylesheet, because a renderer that ignores a
<style>block would otherwise fall back to a proportional face in default black. And the window’s drop shadow is cast by a rectangle of its own rather than by a filter on the window: an element whose filter a renderer cannot resolve is an element in error, which the spec answers by not drawing it at all, so a filter hung on the window would take the background and the frame down with it. macOS Finder’s thumbnailer and ImageMagick both do exactly that.
Capturing a CLI that is not yours¶
screenshot runs whatever the shell runs, Click CLI or not, so git --help and docker ps capture as readily as your own tool. What it captures is what the command prints: a Click CLI that is not built on Click Extra prints its help uncolored, and that is what lands in the file.
To picture it with colors, run it through wrap first, which 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
which is the 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 drawn above the output is the wrap invocation, not the bare command, because that is what reproduces the colored screen: running flask --help on its own gives back the plain one.
Note
The two commands stay separate because they answer different questions. wrap decides how a CLI renders, and only reaches Click commands it can import: a module:function, a project directory, a .py file, an entry point. screenshot decides where the output goes, and reaches anything executable. Composing them covers the overlap; merging them would cost every CLI that wrap cannot import, which is most of what a README wants to show.
That separation 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.
Note
HTML carries two limitations SVG does not. An OSC 8 hyperlink loses its URL and keeps its visible text, and the eight base ANSI colors render as their CSS names, so the browser’s palette decides their exact shade rather than the terminal’s. Neither shows up on a help screen, which is why the format is worth having anyway.
Width¶
--columns pins the width twice over: the command wraps its output to it, and the image is laid out at the same one. They have to 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 own 80 through a pipe), and the image is laid out at the longest line that came back:
$ click-extra screenshot --output params.svg --columns auto -- my-cli --params
Reach for it when the output holds a line the command does not wrap on its own, which a pinned width folds mid-word: a long invocation drawn as the prompt, a wide table, a machine-readable dump. The trade-off is that the picture stops being a fixed-width terminal, so captures meant to sit side by side at the same width should name that width instead.
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.screenshot.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.screenshot 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.
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.
Light and dark chrome¶
A capture freezes the colors of the run it pictures, so the window it is drawn in has to answer to the theme that run rendered for. --background light swaps the dark chrome for white, along with the ANSI palette the capture’s own colors resolve against:
$ click-extra screenshot --output light-help.svg --background light -- my-cli --theme light --help
Both halves are needed, and they are not the same half: --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, in one direction or the other.
The prompt line follows the chrome on its own. It is the one line a capture draws itself rather than collects, so on white it would otherwise land in the dark theme’s near-white invoked_command style and vanish.
So does a CLI that asks. A capture states its chrome to the command the way a terminal would, through the CLITHEME and COLORFGBG variables background detection reads, on top of the width it pins and the colors it forces. A CLI passing --theme auto then renders for the window it lands in, and needs telling only once:
$ 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 were shot from the same command line, --background apart, and the CLI picked its palette from the terminal each capture claimed to be:
$ 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
$ 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, and deliberately: a CLI that never asks for it keeps rendering exactly as it does everywhere else, which is why --theme dark and --theme light are spelled out below rather than left to detection.
Here is one help screen taken both ways, with the CLI’s theme and the image’s chrome moving together:
Both blocks hide their results, which is the exception to what :screenshot: is usually for: a results block on this page is colored by the site’s stylesheet and follows the reader’s own theme, so the one thing being compared here is the one thing it cannot show.
The window itself¶
A capture is drawn as a terminal window: a rounded rectangle in the chrome’s background color, framed with a one-pixel border and lifted off the page by a drop shadow. Every part of that 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 |
|---|---|---|---|
|
CSS color |
the chrome’s |
The frame around the window. |
|
pixels |
|
How thick that frame is. |
|
pixels |
|
How round the window’s corners are. |
|
CSS color |
the chrome’s |
The drop shadow under the window. |
|
CSS color |
none |
A page behind the window, margin included. |
|
pixels |
|
Transparent space around the window. |
|
|
|
How solid the window’s body is. Under |
|
text |
the click-extra credit |
A credit line in the image’s bottom-right corner. Empty draws none. |
|
CSS color |
a neutral gray |
The ink that line is drawn in. |
|
pixels |
|
Space inside it, on top of the renderer’s own. |
|
flag |
off |
A dim gutter numbering the captured lines. |
|
text |
none |
A caption centered in the window’s title bar. |
Two of them carry a default that is not a fixed value but the chrome’s own. That is the whole reason they are not constants: a renderer frames its window in a translucent white, and a light capture wearing it is a white window on a white page, its edge left for the reader to infer.
The rest answer to what the capture is for. A shadow needs --margin to fall into, since a filter paints outside the shape it is applied to and the image’s own box cuts whatever lands past it. --backdrop fills that same space instead of leaving the page through, which is what 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 set of options (click_extra.screenshot.render_svg() draws them, click_extra.screenshot.render_html() translates them), so 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. A renderer that skips filters, and a few outside the browser do, still draws the border, so the window keeps an edge either way.
Gradients¶
--backdrop also takes a CSS gradient, which is what turns a capture into a picture that carries its own page:
$ click-extra screenshot --output card.svg --backdrop "linear-gradient(135deg, #667eea, #764ba2)" -- my-cli --help
An SVG fill has no syntax for that, so the CSS is read and re-emitted as the paint server SVG does understand (click_extra.screenshot.gradient_svg()), placed in user space rather than approximated: the gradient line runs through the image’s center at the angle asked for, as long as the image measures along it, and a radial one reaches the farthest corner. Understood are linear-gradient, opening with an angle (135deg) or a side keyword (to bottom right), and radial-gradient, both followed by two or more color stops, each pinnable at a percentage (#667eea 30%). Anything else is taken for the plain color it presumably is, and HTML captures pass the value through to CSS untouched either way.
Transparency¶
--opacity thins the window’s body out, the way a terminal set to transparency does. Below 1, whatever the capture sits on comes through it, 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 tinting the terminal instead of stopping at its edge. With no backdrop it is the page that comes through, which is what a capture dropped on a surface you do not control wants: an image holding no opinion on the color behind it. What limits the value is legibility, and that is the reader’s screen deciding, not the capture: a body much under half solid hands the text whatever contrast the backdrop happens to have.
An HTML capture thins the block’s background color with CSS color-mix() rather than a rectangle’s fill, 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, the invocation everything under it came from:
$ 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, which is 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.
The credit line¶
Every capture the command writes carries a credit in its bottom-right corner, in the margin around the window. Here is one, reading generated with pantry 1.4.2 because that is what shot it:
Left alone, the line names click-extra and the release that drew the image, which is what a capture needs once it has travelled: on a slide, in a README or on a social card it is a long way from the page that explains where it came from. --watermark replaces the text, and an empty string draws none at all:
$ 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 in a capture drawn outside the window, so it is also the one paint that cannot answer to the chrome: the margin is transparent, and what sits behind it is a page this command never sees. Hence a neutral gray, which reads on a white README and a dark one alike, and --watermark-color for a capture whose backdrop it has to sit on.
A capture written by a click:run block carries none of this by default. That image is regenerated and committed on every documentation build, so a release number in it would rewrite every asset the day the release changes, 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, on a gradient, captioned, numbered, rounded, given room to breathe, and left see-through enough for the gradient to tint it. The second tab is the block that wrote it, options and assertions included:
```{click:run}
:screenshot: styled-window-screen
:screenshot-columns: auto
:screenshot-title: 🍎 pantry --help
:screenshot-backdrop: 'linear-gradient(135deg, #667eea, #764ba2)'
: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 own choice, the window being the thing on show here: leave it out and the block renders its live text below the fence as well.
Terminal presets¶
A capture is a picture of a terminal, and terminals do not look alike. --preset draws one as a named desktop’s:
$ 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 sigil its usual shell prompts with. Anything 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 rather than to the terminal. Which is what makes the fourth preset the odd one out: plain mimics no desktop, wears no buttons, and drops the strip entirely unless a --title gives it something to hold.
Round buttons on the left, Apple’s Pro and Basic palettes, SF Mono, and a $ prompt.
Minimize, maximize and close on the right, square corners, the Campbell and One Half Light schemes, Cascadia Code, and a PS C:\> prompt.
A single close button, the Tango palette GNOME Terminal ships, Ubuntu Mono, and a $ prompt.
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.
Caution
A preset’s palette is the scheme its terminal ships with, not the one your reader has configured theirs to. It also decides how the captured CLI’s own colors land: a screen rendered for a dark theme on a light preset washes out exactly as the chrome section describes, since the two halves have to agree either way.
Stating a default once¶
screenshot is itself a Click Extra CLI, so every option above is also a configuration key. A project drawing all of its captures the same way says so 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.
Inside a Sphinx documentation the captures are written by click:run blocks rather than by the command, so the same wish is a conf.py value:
click_extra_screenshot_preset = "macos"
It covers every block whose :screenshot: names no preset of its own, which leaves :screenshot-preset: for the pages that mean to 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"]
The captures on this page¶
Here is the command turned on click-extra itself, whose 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:
$ 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:
$ 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:
$ click-extra screenshot --output docs/assets/theme-gallery-screen.svg --head 34 --prompt "click-extra themes" -- uv run --frozen -- click-extra themes
--prompt is what 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 the [...] marker admits that 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 rather than shoot them again, the blocks maintain them: a :screenshot: option writes each image on every documentation build, which is what keeps the readme’s front page in step with the code nobody thought to re-check.
Reach for the command when the CLI you want to picture has no live block, and for 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.
Keeping a capture honest¶
Once committed, an image goes stale the first time the CLI’s help changes, and nothing about it complains. 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, failing on the first line that diverged. Re-running the command above is then all it takes to refresh 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 tyre: it is what every surface without the switch shows, PyPI and most editors’ previews included, so it holds the capture that reads on the light background those 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 a hundred forks of the page, none of which resolve a repository-relative path, which 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¶
Width a capture is taken and rendered at, see AUTO_COLUMNS.
- click_extra.screenshot.TColumns: TypeAlias = int | typing.Literal['auto']
Width a capture is taken and rendered at, see
AUTO_COLUMNS.
- class click_extra.screenshot.CaptureFormat(*values)[source]
Bases:
EnumDocument formats a capture can be rendered to.
The value doubles as the file extension
format_from_path()matches on.- 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:
EnumTerminal 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
lightandmanpagethemes wash out on the dark chrome a renderer defaults to.The value doubles as the
--backgroundchoice 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, '$', "'Fira Code', 'Cascadia Code', Menlo, Consolas, monospace", ('#292929', '#c5c8c6', ('#2e3436', '#cc0000', '#4e9a06', '#c4a000', '#3465a4', '#75507b', '#06989a', '#d3d7cf', '#555753', '#ef2929', '#8ae234', '#fce94f', '#729fcf', '#ad7fa8', '#34e2e2', '#eeeeec'), '#292929'), ('#ffffff', '#000000', ('#2e3436', '#cc0000', '#4e9a06', '#c4a000', '#3465a4', '#75507b', '#06989a', '#d3d7cf', '#555753', '#ef2929', '#8ae234', '#fce94f', '#729fcf', '#ad7fa8', '#34e2e2', '#eeeeec'), '#ffffff'))
Terminal a capture with no
--presetis drawn as.plainmimics no desktop, which is what a capture wearing no decoration should resolve its colors against. Naming it here is what keeps the two formats looking like the same terminal, and keeps one catalog answering for every palette a capture can use: without it the default colors would be a second set of literals free to drift from the one the presets publish.
- click_extra.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
blueleaves 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.
- 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'), search=Style(fg='green'), success=Style(fg='green'), cross_ref_highlight=True, subheading=Style(fg='blue', dim))}
Theme the prompt line is drawn with, per chrome.
The captured output arrives already colored by the CLI that produced it, under whatever theme that run was told to use. The prompt is the one line this process draws itself, so it is the one that would otherwise land on white chrome in the dark default’s near-white
invoked_commandstyle, invisible.Nonekeeps whatever theme the invocation already runs under. So does a missing entry: the mapping is read throughdict.get(), andBUILTIN_THEMESis empty when a trimmed install dropsthemes.toml.
- 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
strokeattribute 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.0leaves 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
#292929and against nothing else: a light capture framed with it is a white window on a white page, the shape of the terminal only guessable from its text. Each chrome names a frame its own background can show.
- click_extra.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 bottomis what a gradient opening with no direction at all means, which is why it doubles as the default. Seegradient_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=0has no such band, and the line lands on the window’s own corner instead of beside it.
- click_extra.screenshot.DEFAULT_WATERMARK = 'generated with click-extra 9.0.0'
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.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-themeCLITHEME, andCOLORFGBGcarryingforeground;backgroundpalette indices.So a CLI asking for –theme auto renders for the chrome its picture is drawn on, instead of falling back to dark inside a light window. A CLI that never asks is unaffected: the variables only answer a question it does not put.
- click_extra.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
styleattribute, which a double quote here would terminate early.
- 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_STACKasks for. Every monospaced fallback behind it is close enough that the grid holds, andtextLengthpins each run to its columns for the ones that are not.
- click_extra.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.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
dimrun’s ink is mixed toward the background, seeblend().
- click_extra.screenshot.RTL_BIDI_CLASSES = frozenset({'AL', 'AN', 'R'})
Unicode bidirectional classes written right to left.
Right-to-left letters, Arabic letters and Arabic-Indic numbers, as
unicodedata.bidirectional()names them. Seeis_bidirectional().
- click_extra.screenshot.WINDOW_PADDING = 8
Pixels every window keeps between its frame and its text, on three sides.
The fourth is the top, where
TITLEBAR_HEIGHTanswers instead. This is the window’s own breathing room, before thepaddinga capture may ask for on top.
- click_extra.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_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_COLUMNSas much as on an explicit width: a command printing nothing but blank lines would otherwise ask for an image no glyph fits in.
- click_extra.screenshot.LINE_NUMBER_SEPARATOR = ' │ '
Rule drawn between a line’s number and the line itself.
A vertical bar rather than a bare space, so the gutter reads as a column of its own even where the output is itself indented.
- click_extra.screenshot.DEFAULT_TRUNCATION = '[...]'
Marker standing in for the lines
trim_lines()cut away.
- click_extra.screenshot.PADDING = ' \xa0'
Characters separating one column of a capture from the next.
render_svg()emits every space as a non-breaking one, so the padding survives an XML round-trip and no renderer collapses a run of them.
- click_extra.screenshot.number_lines(text, start=1)[source]
Prefix each line of
textwith its number, in a dim gutter.The numbers are drawn into the terminal text rather than into a column of the image, which is the same trade Pygments makes with its inline line numbers: every renderer places them for free, and every reader copying the capture copies them too.
Right-aligned on the widest number, so the gutter is one column whatever the output’s length, and separated by
LINE_NUMBER_SEPARATOR.
- click_extra.screenshot.preset_palette(preset, background)[source]
The colors a preset shows on the given chrome.
- Return type:
- 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 byrender()andrender_html(), so the two formats cannot disagree on what a chrome looks like.- Return type:
- click_extra.screenshot.is_bidirectional(text)[source]
Whether
textcarries a character written right to left.Arabic, Hebrew and their neighbours are reordered by whoever draws them, and the cursive ones are shaped: a letter’s form depends on what it joins. A terminal grid describes neither, which is why
render_svg()stops pinning such a run to an exact width.
- click_extra.screenshot.cell_width(text)[source]
Columns
textoccupies on a terminal’s character grid.Not its length: a CJK ideograph is drawn two cells wide, a combining mark none at all.
wcwidth.wcswidth()answers for both, and returns-1for a string carrying a control character, where the count of characters is the closest thing to an answer left.
- click_extra.screenshot.fit_columns(text)[source]
Width, in characters, of the longest line in
text.ANSI escapes are stripped first: they style the glyphs around them and occupy no cell of their own. Measured in terminal cells, so a line of CJK asks for the two columns per glyph it is drawn with. Floored at
MIN_COLUMNS.
- 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 theFORCE_COLORlever every mainstream color system obeys and clears any opt-out the environment carries, whileCOLUMNSfixes the width the command wraps to.Only
stdoutis captured by default. That is what keeps a capture free of the progress lines and build chatter a wrapper likeuvwrites tostderr, with no shell redirection to remember.- Parameters:
args (
str|Path|None|Iterable[str|Path|None|Iterable[Iterable[str|Path|None|Iterable[TNestedArgs]]]]) – the command line, in the nested formrun_cli()accepts.columns (
int|Literal['auto']) – terminal width, in characters, the command wraps its output to.AUTO_COLUMNSpins nothing and lets the command find its own.background (
CaptureBackground) – chrome the capture is headed for, stated to the command the way a terminal would, seeCAPTURE_TERMINAL_HINTS.merge_stderr (
bool) – foldstderrinto the captured output, for a command printing its help there.timeout (
float|None) – seconds before the command is killed.Nonewaits forever.
- Return type:
- Returns:
the completed process, whose
stdoutholds the captured text.
- click_extra.screenshot.trim_lines(text, *, head=None, tail=None, truncation='[...]')[source]
Keep only the first
headand lasttaillines oftext.Whatever is dropped is replaced by a single
truncationline, so the image admits that it was cut rather than pretending to be the whole output. Text short enough to survive both bounds comes back untouched, with no marker.- Parameters:
- Return type:
- Returns:
the trimmed text.
- click_extra.screenshot.palette_color(color, palette)[source]
Resolve any color a
Stylecarries to a hex string.The 16 named and indexed ANSI slots are not colors, they are names: a terminal decides what its
redlooks like, and a capture has no terminal, so they answer topalette. Every other form a style can carry (a 24-bit triplet, a 256-cube index, a hex string) already states its own color and passes through.- Parameters:
palette (
TerminalPalette) – the terminal colors to resolve names against.
- Return type:
- Returns:
the color, as
#rrggbb.- Raises:
ValueError – when the value names no color.
- click_extra.screenshot.blend(color, into, ratio)[source]
Mix
colortowardinto, the way a terminal fades dim text.SVG has no dim, and thinning the glyphs with
opacitywould let whatever sits behind the capture show through them. Mixing the two colors up front keeps the text opaque and lands the same shade.
- click_extra.screenshot.grid(text, columns)[source]
Lay ANSI text out on a terminal’s character grid.
The one place a capture stops being a stream and becomes a picture. Each styled run of
split_ansi()is split at newlines into rows, then placed on the column it starts at, measured in cells rather than characters so a wide glyph takes the two it is drawn with.A line reaching past
columnssoft-wraps onto the next row, the way it would on a terminal that narrow, rather than being cropped: a command is free to print a line it never wraps itself (a long URL, a wide table, a machine-readable dump), and a picture that silently swallowed the overflow would be lying about what ran. A glyph straddling the edge moves down whole.Returning the column with each run is what lets
render_svg()place a run without measuring anything back out of its own output.
- 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
filltakes a paint: a color, or a reference to a gradient declared as an element of its own. The syntax a page’s CSS carries,linear-gradient(135deg, #ff9a9e, #fad0c4), means nothing to it, and a capture handed one would come out unpainted. So the CSS is read here and re-emitted as the element SVG does understand, which is what lets the same--backdropvalue serve both formats.Understood:
linear-gradientopening with an optional angle (135deg) or side keyword (to bottom right, seeCSS_SIDE_ANGLES), andradial-gradient, both followed by two or more color stops, each pinnable at a percentage. Anything else returnsNoneand is left alone, being a plain color as far as this is concerned.The gradient is placed in user space, which is what makes it exact rather than approximated: the CSS line runs through the image’s center at the given angle, and is as long as the box measures along it (
|W·sinθ| + |H·cosθ|), while a radial one reaches the farthest corner.- Parameters:
- Return type:
- Returns:
the
<defs>markup and thefillvalue referencing it, orNonewhen the value is not a gradient this understands.
- click_extra.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:
- Return type:
- Returns:
the SVG markup.
- click_extra.screenshot.watermark_svg(text, *, width, height, paint, font_stack="'Fira Code', 'Cascadia Code', Menlo, Consolas, monospace")[source]
Draw the credit line in the image’s bottom-right corner.
Placed in the margin rather than over the terminal, which is what keeps it from covering a line of output: a capture is a picture of text, and a mark crossing that text costs the reader the thing being shown.
Carries a
watermarkclass, so a reader taking a capture apart can tell the one run the renderer never captured from the ones it did.- Parameters:
- Return type:
- 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:
- Returns:
the SVG markup, empty when the window wears none.
- 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
textLengthand resolves the font, because the gutter’s width is being paid for in glyphs.librsvgdoes neither, and the columns collapse onto each other.Cutting the run at its gutters and giving each piece its own offset asks nothing of the renderer but to draw glyphs at coordinates.
- click_extra.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
textLengthholds the result to the width it occupies.Text carrying a tile (
_TILING_RE) is cut into groups of at mostTILE_RUNcells, each landing on a stated offset. A<text>element is the smallest thing some renderers position at all:librsvg(and through itrsvg-convertand ImageMagick) honors the firstxof an element and then lays every following glyph out at the font’s own advance, ignoring bothtextLengthand any furtherx. A rule of 75 tiles drawn a tenth of a pixel narrow therefore ends a whole cell short of the│below it, and the table’s corners miss. Restating the offset every few cells bounds that error to well under a pixel, whatever the font, and costs a tile nothing since none of them ligate.
- click_extra.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.
- click_extra.screenshot.style_rules(style, palette)[source]
Compile a style to the CSS an SVG text run is drawn with.
- Parameters:
style (
Style) – the run’s style, assplit_ansi()yields it.palette (
TerminalPalette) – the terminal colors to resolve names against.
- Return type:
- Returns:
the CSS declarations, semicolon-separated.
- click_extra.screenshot.run_paint(style, palette)[source]
The color painted behind a run, or
Nonewhere it shows the terminal’s own.- Parameters:
style (
Style) – the run’s style.palette (
TerminalPalette) – the terminal colors to resolve names against.
- Return type:
- Returns:
the background color, as
#rrggbb, orNoneto paint nothing.
- click_extra.screenshot.render_svg(text, *, columns, title='', unique_id=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 timesCELL_WIDTH.Two primitives draw everything. A
<rect>fills the cells behind a run that carries a background, and a<text>draws its glyphs, pinned to its columns withtextLengthso the layout survives a reader who does not have the font.Note
A run’s padding is left out of its
<text>and paid for in thexoffset instead. Written the other way, a column only lands where it belongs if the glyphs are exactly the width assumed here, which asks the renderer to both honortextLengthand resolve the font. A web browser does both.librsvg(and through itrsvg-convertand ImageMagick) ignorestextLength, and a file manager, a git client or a thumbnailer commonly falls back to a proportional font. Starting each run on its own column asks neither.- Parameters:
text (
str) – captured output, ANSI escape sequences included.columns (
int) – width of the terminal, in characters.title (
str) – caption drawn in the window’s title bar. Empty draws none.unique_id (
str|None) – prefix namespacing this document’s CSS classes and element IDs, seerender(). Derived from the content when not given.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_PAINTdraws 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, seegradient_svg().NO_PAINTleaves 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.Nonetakes the palette’s foreground.titlebar (
str) – paint for the strip the title and buttons sit in.NO_PAINTleaves it the terminal’s own color.collapse_titlebar (
bool) – drop that strip, closing the window over the first line of text. For a capture wearing neither decoration nor caption.opacity (
float) – how solid the window’s body is, fromOPAQUEdown to0.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:
- Returns:
the SVG source.
- click_extra.screenshot.render_html(text, *, title='', full=True, background=CaptureBackground.DARK, preset=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)')[source]
Render captured terminal text to HTML.
The
<pre>carries its own inline styling, so a fragment pasted into an existing page needs no stylesheet and cannot be restyled out of legibility by the host. Nothing else is needed either: a<pre>preserves the capture’s own spacing, which is what spares HTML the column arithmeticrender_svg()performs for a picture.Caution
The text is escaped before its ANSI is translated, the order
click_extra.tableuses for itshtmlformat. Skip it and any<a CLI prints opens a tag: click-extra’s own--export-confighelp says it writesto <stdout>.Note
An OSC 8 hyperlink loses its URL and keeps its visible text: the escape is dropped rather than turned into an
<a>.- Parameters:
text (
str) – captured output, ANSI escape sequences included.title (
str) –<title>of the document. Ignored for a fragment.full (
bool) – wrap the<pre>in a standalone document.Falsereturns the<pre>alone, to paste into a page that has its own.background (
CaptureBackground) – chrome to draw on, seeCaptureBackground.border (
str) – color of the block’s frame, seerender_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, seerender_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, seebuttons.titlebar (
str) – ignored, seebuttons.collapse_titlebar (
bool) – ignored, seebuttons.opacity (
float) – how solid the block’s background is, fromOPAQUEdown to0.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.
- Return type:
- Returns:
the rendered markup.
- click_extra.screenshot.render(text, *, format=CaptureFormat.SVG, columns=80, 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.0.0', watermark_color=None)[source]
Render captured terminal text to the document
formatnames.- Parameters:
text (
str) – captured output, ANSI escape sequences included.format (
CaptureFormat) – which document to produce.columns (
int|Literal['auto']) – terminal width, in characters, an SVG is laid out at, orAUTO_COLUMNSfor the width its own longest line asks for. HTML reflows, so it ignores 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.full (
bool) – HTML only. Seerender_html().background (
CaptureBackground) – chrome to draw on, seeCaptureBackground.border (
str|None) – color of the window’s frame.Nonetakes the one the chrome can show, seeCAPTURE_BORDERS;NO_PAINTdraws 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_PAINTleaves it transparent.shadow (
str|None) – color of the window’s drop shadow.Nonetakes the chrome’s own, seeCAPTURE_SHADOWS;NO_PAINTdraws 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, fromOPAQUEdown to0.0. Below it, whatever the capture is laid over shows through.watermark (
str) – credit line drawn in the image’s bottom-right corner, seeDEFAULT_WATERMARK. An empty string draws none.watermark_color (
str|None) – color that line is drawn in.NonetakesWATERMARK_INK, which reads on a page of either color.
- Return type:
- Returns:
the rendered document.
- Raises:
ImportError – rendering SVG without the
screenshotextra installed.
- click_extra.screenshot.capture(args, *, format=CaptureFormat.SVG, columns=80, prompt=None, head=None, tail=None, truncation='[...]', merge_stderr=False, timeout=None, line_numbers=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.0.0', watermark_color=None)[source]
Run a command and render its output as a document.
Chains
capture_output(),trim_lines()andrender(). The invocation is drawn above the output as a shell prompt, styled by the active theme throughformat_cli_prompt(), so the capture shows what to type to reproduce it.- Parameters:
args (
str|Path|None|Iterable[str|Path|None|Iterable[Iterable[str|Path|None|Iterable[TNestedArgs]]]]) – the command line to run.format (
CaptureFormat) – which document to produce.columns (
int|Literal['auto']) – terminal width, in characters, orAUTO_COLUMNSto pin none and lay the image out at what the command printed.prompt (
str|None) – command line to display, when it differs from the one run.uv run --frozen -- my-clireproduces a capture from a checkout, butmy-cliis what a reader types. An empty string draws no prompt at all.tail (
int|None) – number of trailing output lines to keep.truncation (
str) – line standing in for the lines cut byheadortail.merge_stderr (
bool) – foldstderrinto the captured output.timeout (
float|None) – seconds before the command is killed.line_numbers (
bool) – draw each line’s number in a gutter, seenumber_lines(). The prompt counts as the first of them, being the invocation everything under it came from.background (
CaptureBackground) – seerender().
- Return type:
- 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.
- Parameters:
path (
Path) – where the capture is to be written.- Return type:
- Returns:
the
CaptureFormatits extension names.- Raises:
ValueError – when the extension names no format.
click_extra.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 four things that make one recognizable, so a reader placing the image knows which desktop it came from:
the window decorations, three round buttons on the left for macOS, three glyphs on the right for Windows, a single one for GNOME;
the palette its colors resolve against, which is what turns a bright blue into Campbell’s
#3B78FFor Tango’s#729FCF;the font the terminal ships with;
the prompt its shell draws,
$againstPS C:\>.
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:
NamedTupleThe colors a terminal resolves a capture’s ANSI codes against.
The 16
ansientries are the standard palette in the canonical order: black, red, green, yellow, blue, magenta, cyan, white, then the same eight in their bright variants.Create new instance of TerminalPalette(background, foreground, ansi, titlebar)
- background: str
Color behind the text, as CSS spells it.
- foreground: str
Color of the text a CLI leaves unstyled.
- 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:
NamedTupleThe decorations a terminal draws in its title bar.
Two shapes cover the desktops: macOS draws filled circles on the left, Windows and GNOME draw glyphs on the right.
Nonein either field leaves that half undrawn, which is what a bare window asks for.Create new instance of WindowButtons(circles, glyphs)
- 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.
- 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
BasicandProschemes.
- 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, font_stack: str, dark: TerminalPalette, light: TerminalPalette)[source]
Bases:
NamedTupleA terminal a capture can be drawn as.
Pass one to
click-extra screenshot --preset, or to aclick:runblock as:screenshot-preset:. Anything stated alongside it wins: a preset picks the defaults, it does not lock them.Create new instance of TerminalPreset(label, buttons, radius, prompt, 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.
- font_stack: str
Fonts the capture asks for, the terminal’s own first.
Nothing is embedded, so a reader without the family falls back down the list. Which is why each ends with the same generic
monospacea browser always resolves.
- dark: TerminalPalette
Colors the terminal shows on its dark scheme.
- light: TerminalPalette
Colors it shows on its light one.
- click_extra.screenshot_presets.PRESETS: Final[dict[str, TerminalPreset]] = {'linux': ('GNOME Terminal', ((), '✕'), 6, '$', "'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, '$', "'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, '$', "'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:\\>', "'Cascadia Code', 'Cascadia Mono', Consolas, monospace", ('#0c0c0c', '#cccccc', ('#0c0c0c', '#c50f1f', '#13a10e', '#c19c00', '#0037da', '#881798', '#3a96dd', '#cccccc', '#767676', '#e74856', '#16c60c', '#f9f1a5', '#3b78ff', '#b4009e', '#61d6d6', '#f2f2f2'), '#202020'), ('#fafafa', '#383a42', ('#383a42', '#e45649', '#50a14f', '#c18301', '#0184bc', '#a626a4', '#0997b3', '#fafafa', '#4f525d', '#df6c75', '#98c379', '#e4c07a', '#61afef', '#c577dd', '#56b5c1', '#ffffff'), '#f3f3f3'))}
Every terminal a capture can be drawn as, alphabetically.
plainis the odd one out: it mimics no desktop, dropping the buttons and the rounded corners for a capture that has to read as a block of output rather than as a window, on a slide or in a paper.