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)),
),
)
â•──────────────────────────────────────╮
│ Building web v4.2.0 │
│ [##########] 100% │
│ ✓ compiled 42 modules │
│ ! 2 warnings in src/app.ts │
│ ✓ wrote dist/web.js (184 kB) │
╰──────────────────────────────────────╯
exited 0
â•──────────────────────────────────────╮
│ ===== 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'scolumns, which the slide and the runner read from the box their output is shown in, by passingcommand_outputthe sameviewporthandle they measure withuse_rects(viewport). It's read when the run starts, and a run waits to start until it's positive, sinceuse_rectsreports 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 isrows, 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
columnswide, 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. PAGERandGIT_PAGERarecat, sogit logand other commands that start a pager print instead. Other tools read their own variables, such asMANPAGER, whichenvcan 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, underenv, as forrun_pty.clear_env=Trueleaves 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
TYPE:
|
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:
|
env
|
Variables to set in the program's environment.
It gets
TYPE:
|
clear_env
|
Leave out the app's environment (see
TYPE:
|
timeout
|
How long the program may run before it's hung up and its outcome is
TYPE:
|
max_lines
|
How many lines of output a run keeps, dropping the oldest, at most
TYPE:
|
hangup_grace
|
How long the program has to exit after each signal that hangs it up
(see
TYPE:
|
counterweight.commands.Command
dataclass
Command(
run: Callable[[Sequence[str]], None],
cancel: Callable[[], None],
latest: CommandRun | None,
)
What use_command returns.
run
instance-attribute
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.
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
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
TYPE:
|
style
|
Merged over the outer box, which holds the scroll area and the line beneath it,
as for a size (
TYPE:
|
styles
|
How the box and the line beneath it are drawn.
TYPE:
|
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
area: ScrollAreaStyles = ScrollAreaStyles()
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.
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
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.
history
class-attribute
instance-attribute
The rows that scrolled off the top of the screen, oldest first, at most max_lines of them.
screen
class-attribute
instance-attribute
The screen's rows, down to its last row that isn't blank.
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.
of
classmethod
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
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.