Skip to content

One-Shot Commands

A one-shot command runs a program that's expected to exit, though not necessarily to succeed, and shows everything it printed, colors and all, as a terminal would have shown it. A slide can run an example command when it's shown, and an editor can run the code in a text area and show what it printed.

This page covers running a command, how its output is sized, the terminal it runs on, cancelling and timing out a run, and showing output recorded ahead of time.

Running a command

use_command runs a program when its run is called, and returns the latest run, a CommandRun value holding the program's argv, everything it printed so far as output, and its outcome. command_output shows a run: its output in a scroll_area that sticks to its end, so it follows the output while the program runs, with a line beneath that says how the run is going, or how it ended.

A slide that runs its command when it's shown calls run from an effect:

@component
def slide(title: str, argv: tuple[str, ...]) -> Div:
    # The box the output is shown in, which sets how wide the program's terminal is.
    viewport, _ = use_state(ElementHandle)
    command = use_command(columns=use_rects(viewport).content.width)

    async def start() -> None:
        command.run(argv)

    # Runs the command when the slide is shown, and again if it's shown with another one.
    use_effect(start, deps=(argv,))

    return Div(
        style=col | full | gap(1) | pad(1),
        children=(
            Text(content=title, style=text_color("sky", 300)),
            command_output(command.latest, viewport, style=fill(1)),
        ),
    )

A build's colored output in a box, with "exited 0" beneath it

╭──────────────────────────────────────╮
│ Building web v4.2.0                  │
│ [##########] 100%                    │
│ ✓ compiled 42 modules                │
│ ! 2 warnings in src/app.ts           │
│ ✓ wrote dist/web.js (184 kB)         │
╰──────────────────────────────────────╯
exited 0                                

A test run's output in a box, with "exited 1" in red beneath it

╭──────────────────────────────────────╮
│ ===== test session starts =====      │
│ tests/test_math.py .F.               │
│ FAILED test_math.py::test_divide     │
│ ===== 1 failed, 2 passed =====       │
╰──────────────────────────────────────╯
exited 1                                

A code runner runs its code on a key press instead:

@component
def code_runner() -> Div:
    code, set_code = use_state('for n in range(3):\n    print(f"{n} squared is {n * n}")')
    viewport, _ = use_state(ElementHandle)
    command = use_command(columns=use_rects(viewport).content.width, timeout=timedelta(seconds=10))

    def on_input(event: InputEvent, receiver: Receiver) -> InputResponse:
        match event:
            case KeyPressed(key=Key.F5):
                command.run((sys.executable, "-c", code))
                return ()
            case KeyPressed(key=Key.Escape):
                command.cancel()
                return ()
            case _:
                return None

    return Div(
        style=col | full,
        on_input=first(on_input, tab_focus),
        children=(
            text_area(code, set_code, enter_inserts_newline=True, auto_focus=True, style=max_height(10)),
            command_output(command.latest, viewport, style=fill(1)),
            Text(content="F5 runs the code, Escape stops it, Tab moves to the output", style=text_color("slate", 500)),
        ),
    )

This follows React Query's hooks for a fetch. useMutation runs on an event, through its mutate(), and exposes the latest run's status and result, which is run and latest here, and useQuery runs when its component mounts and again when its key changes, which is the slide's effect. A run differs from a fetch in two ways: its output streams, so latest shows what the program has printed so far while it runs, updating at most once a frame, and a program that exits with a failing status is a result like any other, not an error.

A run is a value that outlives its program, so a component can keep finished runs in state, show one again, or show it beside the next. Calling run again starts another run, even with the same argv, and hangs up the run in progress.

How a run is sized

A run has two sizes, and only the first comes from its box.

  • The terminal's size is what the program formats its output for (wrapping, tables, progress bars). Its width is use_command's columns, which the slide and the runner read from the box their output is shown in, by passing command_output the same viewport handle they measure with use_rects(viewport). It's read when the run starts, and a run waits to start until it's positive, since use_rects reports zero on the first render. A later change doesn't rewrap a run's output: columns past the box's edge are cut off, and running again at the new width is up to the caller. Its height is rows, 24 by default, as a fresh xterm's is, which a program drawing a live display sizes it to. It doesn't come from the box, since the box scrolls rather than being the program's screen.
  • The output's size is how much the program printed: the run's columns wide, and as tall as the lines it printed, which grow while it runs. Its canvas takes that height, and the scroll area shows as many lines as fit.

Size the box from its parent

The output's width is the run's columns, which comes from the box, so the box has to take its width from its parent, by stretching, fill, or a styled width. A box sized by its content stays zero wide, and its run never starts.

The terminal it runs on

A run gets a pseudo-terminal rather than pipes, so it prints what it would in a terminal:

  • Colors. Most programs color their output only when their output is a terminal.
  • Cursor movement. A progress bar that rewrites its line with \r, or a live display that moves the cursor up and redraws, leaves its last frame in the output, rather than every frame run together.

Its standard output and standard error arrive together, as they do in a terminal. A program that has to tell them apart is better run with asyncio's subprocess functions, which need no terminal.

The program sees a terminal, so it acts as it would on one:

  • Its standard input is /dev/null, so a program that reads input gets end of file rather than waiting for keys. A program that has to talk to its terminal can still open /dev/tty.
  • PAGER and GIT_PAGER are cat, so git log and other commands that start a pager print instead. Other tools read their own variables, such as MANPAGER, which env can set.
  • A program that asks its terminal something, such as where its cursor is, gets an answer from the emulator.
  • Its environment is the app's, under the terminal's variables (TERM=xterm-256color, COLORTERM=truecolor) and the pager defaults, under env, as for run_pty. clear_env=True leaves out the app's, which keeps a run's output from depending on who runs it.

Cancelling and timing out

cancel hangs up the run in progress and makes its outcome Cancelled, keeping what it printed. Starting another run and unmounting the component hang it up too. Hanging up sends the program's process group SIGHUP, as closing a terminal window does, and SIGTERM and then SIGKILL to whatever is still running a second apart, so nothing a run started outlives it. That goes on in the background, so a program slow to exit doesn't hold up the app, which waits for it when it stops (see Hanging up in the background).

A program never gets the keys the app reads, so it can't be interrupted with Ctrl+C: bind a key to cancel, as the runner does with Escape. Ctrl+C reaches the app as a key press, so an app can bind it to cancel, as a shell does, rather than leaving it to quit the app. timeout bounds a run: a program still running when it runs out is hung up, and its outcome is TimedOut. Only the program's own run counts against it: one that exits in time has exited, even if a background job it left keeps its terminal open past the timeout.

A run keeps the last max_lines lines of its output, 10,000 by default, dropping the oldest, so a program that never stops printing can't grow it without bound.

Recorded output

Transcript.of turns output recorded ahead of time, such as with script, into the same output a run has, with no program run, so a slide can show a command's output without running it. That's how the screenshots on this page were drawn:

BUILD = (
    b"\x1b[1mBuilding\x1b[0m web v4.2.0\r\n"
    b"[####......] 40%\r[##########] 100%\r\n"
    b"\x1b[32m\xe2\x9c\x93\x1b[0m compiled 42 modules\r\n"
    b"\x1b[33m!\x1b[0m 2 warnings in \x1b[4msrc/app.ts\x1b[0m\r\n"
    b"\x1b[32m\xe2\x9c\x93\x1b[0m wrote \x1b[36mdist/web.js\x1b[0m (184 kB)\r\n"
)


@component
def recorded_build() -> Div:
    viewport, _ = use_state(ElementHandle)
    # Recorded at the width of the box's content: 40 columns, less its border and padding.
    run = CommandRun(argv=("./build.sh",), output=Transcript.of(BUILD, columns=36), outcome=Exited(status=0))
    return Div(children=(command_output(run, viewport, style=size(40, 8)),))

A Transcript is a Canvas painter, so a canvas can show it without command_output.

API

counterweight.commands.use_command

use_command(
    columns: int,
    rows: int = 24,
    env: Mapping[str, str] = frozendict(),
    clear_env: bool = False,
    timeout: timedelta | None = None,
    max_lines: int = MAX_LINES,
    hangup_grace: timedelta = HANGUP_GRACE,
) -> Command

Runs a program, when its run is called, on a pseudo-terminal columns by rows, with its standard input on /dev/null, so a program that reads input gets end of file rather than waiting, and returns the latest run, with everything it printed so far, which updates at most once a frame while it runs. Starting another run, cancelling, and unmounting the component each hang the program up (see run_pty) in the background, so a program slow to exit doesn't hold up the app's rendering, and an app that stops waits for it.

PARAMETER DESCRIPTION
columns

How wide the program's terminal is, which it formats its output for, such as the content width that use_rects reports for the box the output is shown in. It's read when the run starts, and a run waits to start until it's positive, since use_rects reports zero on the first render. A later change doesn't rewrap a run's output: running it again at the new width does.

TYPE: int

rows

How tall the program's terminal is, which a program that draws a live display sizes it to. It doesn't bound the output, which grows with what the program prints.

TYPE: int DEFAULT: 24

env

Variables to set in the program's environment. It gets PAGER=cat and GIT_PAGER=cat, so a command such as git log prints rather than paging, over the environment run_pty gives it, and env over those.

TYPE: Mapping[str, str] DEFAULT: frozendict()

clear_env

Leave out the app's environment (see run_pty).

TYPE: bool DEFAULT: False

timeout

How long the program may run before it's hung up and its outcome is TimedOut, or None for no limit (see run_pty).

TYPE: timedelta | None DEFAULT: None

max_lines

How many lines of output a run keeps, dropping the oldest, at most MAX_SCROLLBACK.

TYPE: int DEFAULT: MAX_LINES

hangup_grace

How long the program has to exit after each signal that hangs it up (see run_pty).

TYPE: timedelta DEFAULT: HANGUP_GRACE

counterweight.commands.Command dataclass

Command(
    run: Callable[[Sequence[str]], None],
    cancel: Callable[[], None],
    latest: CommandRun | None,
)

What use_command returns.

run instance-attribute

run: Callable[[Sequence[str]], None]

Starts running argv, cancelling the run in progress, if there is one. It's the same function on every render.

cancel instance-attribute

cancel: Callable[[], None]

Cancels the run in progress, hanging its program up, and makes its outcome Cancelled. It does nothing if no run is in progress, and is the same function on every render.

latest instance-attribute

latest: CommandRun | None

The latest run, while it runs and after it ends, or None before the first.

counterweight.commands.CommandRun dataclass

CommandRun(
    argv: tuple[str, ...],
    output: Transcript,
    outcome: Outcome,
)

One run of a program, which outlives the program, so it can be kept, shown again, or shown beside the next.

argv instance-attribute

argv: tuple[str, ...]

output instance-attribute

output: Transcript

Everything the program printed, so far while it runs.

outcome instance-attribute

outcome: Outcome

counterweight.commands.Outcome

How a run is going, or how it ended. A program that exits with a failing status has exited as much as one that succeeds: check its status.

counterweight.commands.Cancelled dataclass

Cancelled()

The run was cancelled, by Command.cancel, before the program exited.

counterweight.commands.command_output

command_output(
    run: CommandRun | None,
    viewport: ElementHandle,
    style: Style = default,
    styles: CommandOutputStyles = CommandOutputStyles(),
    auto_focus: bool = False,
    handle: FocusHandle | None = None,
    is_focusable: bool = True,
    in_focus_order: bool = True,
) -> Div

A run from use_command, its output in a scroll_area that sticks to its end, so it follows the output while the program runs, with a line beneath that says how the run is going, or how it ended. The scroll area takes viewport and the focus parameters, handle being a focus handle.

The program formats its output for the scroll area's content width, which the component that calls use_command reads with use_rects(viewport), so the box has to take its width from its parent (by stretching, fill, or a styled width), never from its content, whose width is the run's: a box sized by its content stays zero wide, and the run never starts.

PARAMETER DESCRIPTION
run

The run to show, such as Command.latest, or None for an empty box.

TYPE: CommandRun | None

style

Merged over the outer box, which holds the scroll area and the line beneath it, as for a size (fill(1), height(12)): the scroll area takes the height left over, and scrolls the output that doesn't fit.

TYPE: Style DEFAULT: default

styles

How the box and the line beneath it are drawn.

TYPE: CommandOutputStyles DEFAULT: CommandOutputStyles()

counterweight.commands.CommandOutputStyles dataclass

CommandOutputStyles(
    *,
    area: ScrollAreaStyles = ScrollAreaStyles(),
    in_progress: Style = text_color("slate", 400),
    succeeded: Style = text_color("green", 400),
    failed: Style = text_color("red", 400),
    cancelled: Style = text_color("amber", 400),
)

How command_output draws the box around a run's output, and the line beneath it that says how the run is going, or how it ended.

area class-attribute instance-attribute

The box around the output.

in_progress class-attribute instance-attribute

in_progress: Style = text_color('slate', 400)

The line beneath, while the program starts and runs.

succeeded class-attribute instance-attribute

succeeded: Style = text_color('green', 400)

The line beneath, once the program exits with status 0.

failed class-attribute instance-attribute

failed: Style = text_color('red', 400)

The line beneath, once the program exits with any other status, fails to start, or times out.

cancelled class-attribute instance-attribute

cancelled: Style = text_color('amber', 400)

The line beneath, once the run is cancelled.

outcome_style

outcome_style(outcome: Outcome) -> Style

The style of the line beneath for outcome.

counterweight.commands.COMMAND_ENVIRONMENT module-attribute

COMMAND_ENVIRONMENT = frozendict(
    {"PAGER": "cat", "GIT_PAGER": "cat"}
)

What every one-shot command's environment sets, under the caller's env: pagers that print rather than wait for keys that never come.

counterweight.painters.terminal.Transcript dataclass

Transcript(
    columns: int,
    history: Rows = (),
    screen: Rows = (),
    max_lines: int = MAX_LINES,
)

A Canvas painter that draws everything a program printed, as a terminal would have shown it: the rows that scrolled off the top of its terminal's screen, then the screen, down to its last row that isn't blank. It keeps the last max_lines of them, dropping the oldest.

Its natural size is columns wide, the width of the terminal the program printed to, and as tall as its lines, so a canvas takes its height from them, and a scroll_area around it scrolls what doesn't fit. It keeps no aspect ratio, so a canvas wider than columns doesn't make it taller. Cells in the program's default colors follow the canvas's text_style.

Build one from output recorded ahead of time with of, and from a running program with use_command.

columns instance-attribute

columns: int

history class-attribute instance-attribute

history: Rows = ()

The rows that scrolled off the top of the screen, oldest first, at most max_lines of them.

screen class-attribute instance-attribute

screen: Rows = ()

The screen's rows, down to its last row that isn't blank.

max_lines class-attribute instance-attribute

max_lines: int = MAX_LINES

lines class-attribute instance-attribute

lines: Rows = field(init=False, repr=False, compare=False)

The history, then the screen, keeping the last max_lines rows.

natural_size property

natural_size: tuple[int, int]

keeps_aspect_ratio property

keeps_aspect_ratio: bool

__post_init__

__post_init__() -> None

of classmethod

of(
    output: bytes,
    columns: int,
    rows: int = 24,
    max_lines: int = MAX_LINES,
) -> Self

The transcript of output, recorded from a program run on a terminal columns by rows, such as with script, as a slide or a docs screenshot shows a command's output without running it. Queries in the output, which a program would wait for its terminal to answer, go unanswered.

followed_by

followed_by(scrolled_off: Rows, screen: Screen) -> Self

This transcript, after the rows in scrolled_off scrolled off the top of the terminal's screen, which now shows screen, as Emulator.scrolled_off and Emulator.snapshot report them.

draw

draw(width: int, height: int) -> Rows

The transcript's first height lines.