Skip to main content

Console

Direct access to the process's standard streams. Useful for CLI-style manifests, interactive demos, and tests that want to print without a logger layer.

Why use this

  • Stdout and stdin primitivesWriteLine and ReadLine for line-oriented I/O; no logger between you and the terminal.
  • Stream sinkWriteStream drains any Stream<string | Uint8Array> straight to stdout; producers own framing.
  • Loading animationsStreamWait animates a single-cell spinner while the next stream blocks, then forwards every byte unchanged.
  • TTY-aware markup — a small {style content} syntax renders to ANSI on a TTY and strips to plain text otherwise; one source string, right thing happens at the sink.

Kinds

KindPurpose
Console.WriteLineWrite a templated string to stdout, followed by a newline.
Console.ReadLineWrite a prompt and read a single line from stdin.
Console.WriteStreamDrain a Stream<string | Uint8Array> to stdout.
Console.StreamWaitAnimate a one-cell spinner while waiting for the first item of an input stream, then forward the stream verbatim.

Exported instances

WriteLine and ReadLine are config-free, so the library also ships ready-made singletons via exports.resources. Reference them directly with !ref Console.<name> instead of declaring your own instance — no boilerplate, one shared console per import:

ExportKindUse
Console.writeLineConsole.WriteLineshared stdout line writer
Console.readLineConsole.ReadLineshared stdin line reader
kind: Telo.Application
metadata: { name: Hello, version: 1.0.0 }
imports:
Console: std/console@0.9.0
targets:
- invoke: !ref Console.writeLine
inputs:
output: "Hello!"

Reach for the Console.WriteLine / Console.ReadLine kinds directly only when you want a distinctly-named instance of your own; for the common case the exported singleton is all you need.

Example

kind: Run.Sequence
metadata:
name: Greeter
steps:
- name: Ask
invoke: { kind: Console.ReadLine }
inputs:
prompt: "Name: "
- name: Greet
invoke: { kind: Console.WriteLine }
inputs:
output: "Hello, ${{ steps.Ask.result.value }}!"

Console.WriteLine

Writes inputs.output to stdout followed by a newline. Pass output via the step's inputs: so ${{ }} expressions resolve against the caller's scope (variables, secrets, resource snapshots, and — inside a Run.Sequencesteps.<name>.result).

- name: Greet
invoke: { kind: Console.WriteLine }
inputs:
output: "Hello, ${{ steps.Ask.result.value }}!"

Console.ReadLine

Reads a single line from stdin. Pass prompt via the step's inputs:. The prompt is written to stdout character-for-character — no trailing newline, no auto-appended : — so the caret stays on the same line wherever you put it.

- name: AskName
invoke: { kind: Console.ReadLine }
inputs:
prompt: "What's your name? "

The captured value surfaces as steps.<name>.result.value. Markup tags inside prompt are rendered at write time. On a TTY, prompt: "{cyan you} > " shows the label in cyan; piped to a file the same prompt is plain text.

Console.WriteStream

Drains a Stream<string | Uint8Array> to stdout. Strings go through Node's native UTF-8 path; Uint8Array chunks pass through unchanged. No newline policy — producers control framing.

kind: Console.WriteStream
metadata:
name: Stdout

Inside a Run.Sequence, wire an upstream stream to the resource's input:

- name: Print
invoke: { kind: Console.WriteStream, name: Stdout }
inputs:
input: "${{ steps.SomeProducer.result.output }}"

WriteStream pairs naturally with text producers like RecordStream.ExtractText (Stream<string>) and with byte-producing codecs like Ndjson.Encoder / Sse.Encoder / Octet.Encoder (Stream<Uint8Array>).

Console.StreamWait

Stream passthrough that animates a single-cell frame sequence on stdout while waiting for the first item from its input, then clears the cell and forwards every item unchanged. Useful for "loading" indicators in CLI flows where the next step is a stream that has measurable startup latency (HTTP requests, AI completions, file reads, queue drains).

kind: Console.StreamWait
metadata:
name: ChatSpinner
prefix: "{magenta.bold ai} > "
- name: Spin
invoke: { kind: Console.StreamWait, name: ChatSpinner }
inputs:
input: "${{ steps.SomeProducer.result.output }}"
- name: Print
invoke: { kind: Console.WriteStream }
inputs:
input: "${{ steps.Spin.result.output }}"

Every byte emitted by StreamWait flows through its output stream — the resource never writes to stdout directly. The downstream sink (typically Console.WriteStream) is the sole writer, so there's no two-writer race.

Reserved-cell mechanics

The animation occupies one cell, reserved by the head of the output:

prefix       -> written verbatim (with markup rendered if TTY)
' \b' -> reserve the next column with a space, park the cursor on it
frames[0]+\b -> initial frame, painted immediately (no `intervalMs` blank gap)
... ticks -> each tick: frame[i] + \b, overwriting the same cell
' \b' -> clear the cell when first input item arrives
items... -> every input item forwarded verbatim, starting at the cleared column

Field reference

FieldDefaultNotes
prefix""Markup-aware. Must not contain \n \r \b \x1b.
framesbraille spinner cycleEach frame must be exactly one character (length === 1).
intervalMs80Tick period. Minimum 16.

Caveats

  • \b cursor parking assumes a TTY-like terminal. Piped output captures the literal \b bytes — readable but not visually clean.
  • Frames are validated as single-character strings; terminal cell width isn't checked. Pick width-1 glyphs (Braille, ASCII, simple punctuation).
  • Same goes for prefix: control chars are rejected, but multi-cell glyphs aren't validated.

Markup

Every Console.* text path runs strings through a tiny chalk-template-style markup parser before writing. On a TTY (process.stdout.isTTY === true) tags become ANSI SGR codes; otherwise the markup is stripped to plain text. The manifest author writes one source string; the right thing happens at the sink.

Syntax

{red error}                 red foreground
{red.bold ERROR} dot-chained styles
{red.bgWhite warning} background via bgRed / bgWhite / ...
{#ff8800 highlight} truecolor hex foreground
{bg#222244 banner} truecolor hex background
hi {red {bold WORLD}!} nesting (LIFO)
literal: \{red\} not a tag escaped braces - backslash also escapes itself

Recognized styles

CategoryNames
Foregroundblack red green yellow blue magenta cyan white gray (also grey)
Bright fgbrightBlack brightRed brightGreen brightYellow brightBlue brightMagenta brightCyan brightWhite
BackgroundbgBlack bgRed bgGreen bgYellow bgBlue bgMagenta bgCyan bgWhite bgGray (+ bgBright<Color>)
Hex#RRGGBB (foreground), bg#RRGGBB (background)
Attributebold dim italic underline reverse strikethrough

Behaviour

  • Open-close pairing. Every { opens a tag; the next whitespace separates the style chain from the content; the matching } closes the tag. Tags must be balanced and properly nested (LIFO).
  • Unknown styles fall through to literal. A typo ({notARealStyle hi}) or a future grammar addition this implementation doesn't yet recognize renders the entire tag as literal text — the consumer sees what they wrote, no crash.
  • Same-axis nesting reverts to default on inner close. {red {green X}} more emits red, then green, then resets foreground to terminal default (not back to red). Avoid nesting same-axis styles; nest cross-axis instead ({red {bold X}} more red is fine — bold and color are independent).
  • CEL coexists. ${{ ... }} (dollar + double brace) is CEL; { ... } (single brace, no $) is markup. CEL evaluation runs first; markup runs at sink write time on the post-CEL string.

Render targets

  • TTY: ANSI SGR codes. 16-color baseline + 256-color and truecolor for hex variants.
  • Non-TTY (piped, redirected): all markup stripped, content emitted verbatim.

Detection happens once per controller invocation by checking ctx.stdout.isTTY. No environment variables, no --color flag plumbing required.

Notes

  • Intended for the root Application process. When a kernel runs inside a non-interactive environment (a detached container, a Temporal worker), Console.ReadLine will block indefinitely — wrap it with an outer sequence that only runs in interactive contexts.
  • Output is unbuffered line-by-line. Each Console.WriteLine call is a single stdout.write of the rendered string + \n. Console.WriteStream writes one chunk per iteration — chunk boundaries are upstream-defined.
  • If a manifest needs literal { / } characters in console output, escape them with \{ and \}.