Build CLI tools with colored output, tables, progress bars, and live updates using Kida's terminal rendering surface.

Tip

Duration: ~30 min | Prerequisite: Kida basics, Python 3.14+

1

Terminal Rendering

Declarative terminal/CLI output with ANSI colors, components, and live rendering

Terminal Rendering

Kida's terminal rendering mode lets you build CLI dashboards, status reports, and interactive terminal UIs using the same template syntax you use for HTML. Setautoescape="terminal"and you get ANSI color filters, box-drawing components, progress bars, live re-rendering, and automatic degradation across color depths and character sets.

Terminal mode replaces HTML escaping with ANSI sanitization. Untrusted input has dangerous escape sequences (cursor movement, screen manipulation) stripped while safe SGR styling codes are preserved. TheStyled class is the terminal analogue of Markup-- it marks content as already safe for terminal output.

Quick Start

The fastest way to activate terminal mode is withterminal_env(), which creates a pre-configured Environmentwith built-in component templates:

from kida.terminal import terminal_env

env = terminal_env()
template = env.from_string("""
{{ "Build Report" | bold | cyan }}
{{ hr(40) }}
{{ "Tests" | kv("412 passed", width=35) }}
{{ "Coverage" | kv("94.2%", width=35) }}
{{ 0.85 | bar(width=25) }}
""")
print(template.render())

You can also set terminal mode on a standard Environment:

from kida import Environment

env = Environment(autoescape="terminal")

The difference is that terminal_env() adds a loader for the built-in component templates (components.txt), so you can {% from "components.txt" import panel, header, footer %}. When using Environment(autoescape="terminal")directly, you get all terminal filters and globals but need to supply your own loader if you want to import the component library.

Environment Options

Both approaches accept these terminal-specific keyword arguments:

Argument Type Default Description
terminal_color str Auto-detected Color depth:"none", "basic", "256", or "truecolor"
terminal_width int Auto-detected Terminal width in columns
terminal_unicode bool Auto-detected Whether to use Unicode box-drawing and icons
ambiguous_width int Auto-detected Width for East Asian Ambiguous characters (1 or 2)
# Force basic 16-color output at 60 columns
env = terminal_env(terminal_color="basic", terminal_width=60)

Template Globals

Terminal mode injects these globals into every template:

Global Type Description
columns int Terminal width in columns
rows int Terminal height in lines
tty bool Whether stdout is a TTY
icons IconSet Unicode/ASCII icon proxy
box BoxSet Box-drawing character set proxy
hr() function Horizontal rule generator

Terminal Filters

Terminal mode registers over 30 filters for styling, layout, and data display. All style filters returnStyledstrings that bypass ANSI sanitization.

Color Filters

Named color filters apply ANSI foreground colors. When colors are disabled (terminal_color="none"), they return the text unchanged.

Filter SGR Code Example
black 30 {{ text \| black }}
red 31 {{ text \| red }}
green 32 {{ text \| green }}
yellow 33 {{ text \| yellow }}
blue 34 {{ text \| blue }}
magenta 35 {{ text \| magenta }}
cyan 36 {{ text \| cyan }}
white 37 {{ text \| white }}
bright_red 91 {{ text \| bright_red }}
bright_green 92 {{ text \| bright_green }}
bright_yellow 93 {{ text \| bright_yellow }}
bright_blue 94 {{ text \| bright_blue }}
bright_magenta 95 {{ text \| bright_magenta }}
bright_cyan 96 {{ text \| bright_cyan }}

Extended Color:fg and bg

Thefg and bgfilters accept named colors, hex strings, 256-color indices, and RGB tuples. They degrade automatically based on the terminal's color depth (see Color Depth & Fallback).

{# Hex color #}
{{ title | fg("#00ccff") }}

{# 256-color index #}
{{ status | fg(196) }}

{# Named color #}
{{ label | bg("blue") }}

Decoration Filters

Filter Effect SGR Code
bold Bold / bright 1
dim Dim / faint 2
italic Italic 3
underline Underline 4
blink Blink 5
inverse Swap fg/bg 7
strike Strikethrough 9

Filters chain naturally:

{{ "CRITICAL" | bold | red }}
{{ filename | underline | cyan }}
{{ "(deprecated)" | dim | strike }}

Layout Filters

pad(width, align="left", fill=" ")

ANSI-aware padding. Measures visible character width (ignoring escape sequences) and pads to the target width.

{{ name | pad(20) }}                     {# left-align in 20 cols #}
{{ count | pad(10, align="right") }}     {# right-align #}
{{ title | pad(40, align="center") }}    {# center #}
wordwrap / wrap

ANSI-aware word wrapping. Tracks active style state across line breaks so colors carry over to wrapped lines.

{{ long_text | wrap(60) }}
truncate

ANSI-aware truncation. Counts visible characters, not raw bytes, so ANSI codes never cause premature truncation or broken escape sequences.

{{ description | truncate(40) }}
center

ANSI-aware centering to the specified width.

{{ title | center(80) }}

Data Display Filters

badge

Maps status keywords to colored icons. Recognized statuses:pass/success/ok, fail/error/failed, warn/warning, skip/skipped, info.

{{ "pass" | badge }}   {# green checkmark #}
{{ "fail" | badge }}   {# red cross #}
{{ "warn" | badge }}   {# yellow warning #}
{{ "skip" | badge }}   {# dim circle #}

When colors are disabled, badges render as ASCII labels: [PASS], [FAIL], [WARN], [SKIP].

bar(width=20, show_pct=True)

Progress bar from a 0.0--1.0 float value.

{{ 0.75 | bar(width=30) }}           {# block chars + percentage #}
{{ ratio | bar(width=20, show_pct=False) }}

Uses Unicode block characters when available, falls back to [####----]on ASCII terminals.

kv(value, width=40, sep=" ", fill=".")

Key-value pair with dot-leader fill.

{{ "Version" | kv("2.4.1", width=35) }}
{{ "Uptime" | kv("3h 42m", width=35) }}
table(headers=None, border="light", align=None, max_width=None)

Renders a list of dicts or list of lists as a bordered table.

{{ users | table(headers=["Name", "Role", "Status"]) }}
{{ data | table(border="heavy", align={"Score": "right"}) }}
{{ results | table(max_width=60) }}

Available border styles: light, heavy, double, round, ascii.

tree(indent=2)

Renders a nested dict as a tree with box-drawing connectors.

{{ directory_structure | tree }}

Outputs:

├── src
│   ├── main.py
│   └── utils.py
└── tests
    └── test_main.py
diff(new, context=3)

Unified diff between two strings with colored additions/deletions.

{{ old_config | diff(new_config) }}

Green + lines for additions, red - lines for deletions, cyan @@headers.

syntax(language="json")

Regex-based syntax highlighting for structured data formats. Supported languages:json and yaml (alias yml).

{{ config_json | syntax }}                    {# JSON is the default #}
{{ config_yaml | syntax(language="yaml") }}

Color mapping:

Token Color SGR Code
Keys Cyan 36
String values Green 32
Numbers Yellow 33
Booleans / null Magenta 35
Braces / brackets (JSON) Dim 2
Comments (YAML) Dim 2

Unknown languages return the content unstyled. When colors are disabled, the content is returned as-is. Input is sanitized to prevent ANSI injection.

Built-in Components

The component library lives incomponents.txt and provides reusable template macros for structured terminal layouts. Import them in any template loaded through terminal_env():

{% from "components.txt" import panel, header, footer, row, cols, rule, connector, banner, stack, two_col, dl %}

Components are organized in three layers:

Primitives -- single-line building blocks:

  • row(content, width, border="|")-- bordered line with ANSI-aware padding
  • cols(cells, sep=" ") -- multi-column layout from (content, width[, align])tuples
  • rule(width, title="", char="---")-- horizontal rule with optional title

Components -- bordered panels:

  • panel(title="", width=0, style="round", padding=1)-- box with inline title in the top border
  • header(width=0, style="double")-- double-bordered title bar
  • footer(width=0, style="heavy")-- heavy-bordered summary bar
  • box(title="", width=0, style="round", padding=1)-- bordered box with title in a separate header row
  • connector(indent=2)-- vertical pipe between panels for visual continuity

Layout helpers -- content arrangement:

  • banner(text, width=0, char="=", padding=1)-- full-width centered banner
  • two_col(left_width=0, sep=" | ") -- two-column layout split on |||
  • stack(threshold=60, sep=" | ")-- responsive: side-by-side when wide, stacked when narrow
  • dl(items, label_width=20, sep=" : ", color="cyan")-- definition list

All components default their width to thecolumns global (terminal width) when width=0.

Panel Example

{% from "components.txt" import panel %}

{% call panel(title="CPU Usage", width=60) %}
Core 0   {{ 0.67 | bar(width=20) }}
Core 1   {{ 0.25 | bar(width=20) }}
Core 2   {{ 0.91 | bar(width=20) }}
{% endcall %}

A complete dashboard layout with header, content panels, connectors, and a footer:

{% from "components.txt" import header, panel, footer, connector %}
{% set w = 64 %}

{% call header(width=w) %}
{{ icons.gear | bright_cyan }} {{ "DEPLOY PIPELINE" | bold | bright_cyan | pad(20) }}{{ version | dim }}
{% endcall %}

{% for stage in stages %}
{% call panel(title=stage.name, width=w) %}
{{ stage.name | bold | pad(26) }}{{ stage.status | badge | pad(10) }}{{ stage.duration | dim }}
{% endcall %}
{% if not loop.last %}{{ connector() }}{% endif %}
{% endfor %}

{% call footer(width=w) %}
{{ icons.zap | yellow }} {{ "SUMMARY" | bold | yellow }}   {{ "Duration" | kv(total_duration, width=32) }}
{% endcall %}

Responsive Stacking

Thestack component renders side-by-side when the terminal is wide enough, and vertically when narrow. Split content with |||:

{% from "components.txt" import stack %}

{% call stack(threshold=80) %}
{{ icons.star }} {{ "Latency" | bold }}
  p50 {{ metrics.p50 }}
  p99 {{ metrics.p99 }}
|||
{{ icons.zap }} {{ "Health" | bold }}
  Uptime     {{ metrics.uptime }}
  Error Rate {{ metrics.error_rate }}
{% endcall %}

Definition List

{% from "components.txt" import dl %}

{{ dl([
    ("Version", "2.4.1"),
    ("Uptime", "3h 42m"),
    ("Status", "running"),
], label_width=12) }}

Color Depth & Fallback

Thefg() and bg()filters degrade gracefully across four color depths. You write colors once and they render correctly everywhere:

Depth Env Variable / Detection fg("#00ccff")Emits
truecolor COLORTERM=truecolor or 24bit \033[38;2;0;204;255m
256 TERM contains 256color \033[38;5;45m(nearest cube color)
basic TTY detected \033[36m(nearest of 16 ANSI colors)
none NO_COLORset, or not a TTY Plain text, no escapes

The conversion pipeline:

  1. RGB to 256: Maps to the nearest color in the 6x6x6 color cube (indices 16--231) or the grayscale ramp (232--255), whichever is closer by Euclidean distance.
  2. RGB/256 to basic 16: Finds the nearest of the 16 standard ANSI colors by squared Euclidean distance against canonical RGB values.
  3. Named colors: Always work at any depth --red, cyan, bright_green, etc. use fixed SGR codes.
# Force a specific color depth
env = terminal_env(terminal_color="256")

# Or let auto-detection handle it (default)
env = terminal_env()

Character Width

Terminal output requires precise column alignment. ANSI escape sequences are zero-width (they occupy bytes but no screen columns), and some Unicode characters are double-width. Kida handles both.

WidthStrategy

TheWidthStrategydataclass controls how ambiguous-width characters are measured:

from kida.utils.ansi_width import WidthStrategy, configure_width

# Configure for CJK terminals where ambiguous chars render at double width
configure_width(ambiguous_width=2)

Width resolution follows a fallback chain when ambiguous_widthis not explicitly set:

  1. Explicit override:Environment(ambiguous_width=2) or terminal_env(ambiguous_width=2)
  2. Terminal probe: Writes a test character and measures cursor movement via ANSI DSR
  3. wcwidth library: Uses per-character width data if the optional wcwidthpackage is installed
  4. Locale heuristic: CJK locales (ja, ko, zh) default to 2
  5. Default: 1

ANSI-Aware String Operations

All built-in string operations in terminal mode are ANSI-aware. They measure visible width (ignoring escape sequences) and handle double-width characters:

  • visible_len(s)-- visible character count, ignoring ANSI escapes
  • ansi_ljust(s, width)-- left-justify to visible width
  • ansi_rjust(s, width)-- right-justify to visible width
  • ansi_center(s, width)-- center within visible width
  • ansi_truncate(s, width)-- truncate at visible width, preserving ANSI codes
  • ansi_wrap(s, width)-- word-wrap at visible width, re-applying styles on new lines

Thepad, truncate, center, and wordwraptemplate filters all use these functions automatically in terminal mode.

LiveRenderer

LiveRendererprovides in-place terminal re-rendering. It uses ANSI cursor movement to overwrite previously rendered output, creating smooth animation effects.

import time
from kida.terminal import terminal_env, LiveRenderer, Spinner

env = terminal_env()
tpl = env.from_string("""\
{% from "components.txt" import panel %}
{% call panel(title="Build", width=50) %}
{{ spinner() }} {{ status | bold | pad(20) }}{{ progress | bar(width=20) }}
{% endcall %}
""", name="live")

with LiveRenderer(tpl) as live:
    live.update(status="compiling", progress=0.0)
    time.sleep(1)
    live.update(status="testing", progress=0.5)
    time.sleep(1)
    live.update(status="done", progress=1.0)

How It Works

  1. On__enter__, the cursor is hidden to prevent flicker
  2. Eachupdate(**context) re-renders the template and overwrites the previous output using \033[A (cursor up) and \033[2K(erase line)
  3. On__exit__, the cursor is restored; Ctrl+C is handled gracefully
  4. Context accumulates acrossupdate()calls -- you only need to pass changed values

Constructor Options

Parameter Type Default Description
template Template required Compiled Kida template
refresh_rate float 0.1 Minimum seconds between auto-refreshes
file TextIO sys.stdout Output stream
transient bool False Clear output on exit instead of leaving it

Auto-Refresh

For continuous animation (spinners, progress), usestart_auto()to re-render in a background thread:

with LiveRenderer(tpl, refresh_rate=0.1) as live:
    live.start_auto(status="building")
    # Spinner animates automatically every 0.1s
    time.sleep(5)
    live.stop_auto()
    live.update(status="done")

Spinner

Spinner is a callable that returns a different frame on each call. It is automatically provided as spinner in LiveRenderercontext.

{{ spinner() }} Loading...

Built-in frame sets:

Name Frames Style
Spinner.BRAILLE / Spinner.DOTS Braille dot pattern Braille dots (default)
Spinner.LINE - \ | / ASCII line spinner
Spinner.ARROW Directional arrows 8-direction rotation

Custom frames:

spinner = Spinner(frames=(".", "..", "...", "...."))

Non-TTY Fallback

When output is not a TTY (piped, CI, etc.),LiveRendererappends each render separated by a blank line instead of overwriting. No cursor manipulation is attempted.

stream_to_terminal()

stream_to_terminal() writes render_stream()chunks progressively with an optional delay between them, creating a typewriter-style reveal effect.

from kida.terminal import terminal_env, stream_to_terminal

env = terminal_env()
template = env.from_string("""
{{ "Build Log" | bold | cyan }}
{{ hr(40) }}
{% for step in steps %}
{{ icons.check | green }} {{ step }}
{% endfor %}
""")

stream_to_terminal(template, {"steps": ["compile", "test", "deploy"]}, delay=0.05)
Parameter Type Default Description
template Template required Compiled Kida template
context dict None Template context variables
delay float 0.02 Seconds between chunks (0 = no delay)
file TextIO sys.stdout Output stream

When output is not a TTY, the delay is skipped and all chunks are written immediately.

Use{% flush %}in templates to create explicit chunk boundaries for streaming.

Icons & Box Drawing

Icons

Theicons global is an IconSetproxy. Access icons as attributes:

{{ icons.check }} All tests passed
{{ icons.arrow_r }} Next step
{{ icons.gear }} Settings

Available icons (Unicode / ASCII fallback):

Name Unicode ASCII Name Unicode ASCII
check [ok] arrow_r ->
cross [FAIL] arrow_l <-
warn [!] arrow_u ^
info [i] arrow_d v
dot * play >
circle o stop []
star * bullet *
diamond * gear [G]
flag [F] zap !
heart <3 spark *
lock 🔒 [L] folder 📁 [D]
key 🔑 [K] file 📄 [F]
fat_arrow =>

There is no genericicons.arrow — use a directional variant (arrow_r, arrow_l, arrow_u, arrow_d) or fat_arrow(⇒). If you reference an unknown icon name, Kida raisesAttributeErrorwith "did you mean?" suggestions.

Several icons append VS15 (U+FE0E, Variation Selector 15) to force text presentation and prevent emoji rendering in terminals that would otherwise display them as double-width color emoji.

Whenterminal_unicode=False, all icons return their ASCII fallback.

Box Drawing

Thebox global is a BoxSetproxy with five built-in styles:

{% set b = box.round %}
{{ b.tl }}{{ b.h * 38 }}{{ b.tr }}
{{ b.v }} {{ "Hello, Terminal!" | bold | pad(36) }} {{ b.v }}
{{ b.bl }}{{ b.h * 38 }}{{ b.br }}
Style Corners Lines Description
box.light +-+ -| Light single-line
box.heavy Heavy single Thick lines Heavy single-line
box.double Double-line Double lines Double-line borders
box.round Rounded corners Light lines Light with rounded corners
box.ascii + -| Pure ASCII, always available

Each style provides these characters:tl, tr, bl, br (corners), h, v (lines), lj, rj, tj, bj (junctions), and cross(four-way junction).

Whenterminal_unicode=False, all styles fall back to ascii.

Horizontal Rules

Thehr()global generates horizontal rules:

{{ hr() }}                          {# full terminal width #}
{{ hr(40) }}                        {# 40 characters wide #}
{{ hr(60, title="Results") }}       {# -- Results ---------- #}
{{ hr(40, char="=") }}              {# ======== #}

CLI: kida render

Thekida rendercommand renders a template to stdout from the command line. It defaults to terminal mode.

# Basic render with JSON data
kida render template.txt --data context.json

# Inline JSON
kida render template.txt --data-str '{"name": "world"}'

# Override width and color depth
kida render template.txt --data data.json --width 60 --color basic

# Progressive streaming output
kida render template.txt --data data.json --stream --stream-delay 0.05

# HTML mode (disables terminal features)
kida render template.txt --data data.json --mode html

Options

Flag Type Default Description
--data FILE Path -- JSON file providing template context
--data-str JSON String -- Inline JSON context
--mode html / terminal terminal Rendering mode
--width N int Auto Override terminal width
--color DEPTH none/basic/256/truecolor Auto Override color depth
--stream flag off Progressive chunk-by-chunk output
--stream-delay SECS float 0.02 Delay between streamed chunks

The template's parent directory is used as the loader root, so{% from "components.txt" import panel %} works when components.txtis in the same directory.

NO_COLOR / TTY Detection

Kida follows the NO_COLOR convention and performs automatic TTY detection.

Environment Variables

Variable Effect
NO_COLOR Set to any value to disable all color output
FORCE_COLOR Set to any value to force color output (overridesNO_COLORand non-TTY)
COLORTERM=truecolor or 24bit Enables 24-bit RGB color
TERM=*256color* Enables 256-color mode

Detection Order

Color depth is auto-detected in this order:

  1. NO_COLOR set --> "none"(no colors)
  2. FORCE_COLOR set --> "basic"(16 colors)
  3. COLORTERM is truecolor or 24bit --> "truecolor"
  4. TERM contains 256color --> "256"
  5. stdout is a TTY -->"basic"
  6. Otherwise -->"none"

Unicode Detection

Unicode support is detected from theLANG environment variable. If LANG contains UTF(case-insensitive), Unicode box-drawing and icon characters are used. Otherwise, ASCII fallbacks are substituted.

Non-TTY Behavior

When stdout is not a TTY:

  • All colors are disabled (unlessFORCE_COLORis set)
  • LiveRendererappends output instead of overwriting
  • stream_to_terminal()writes immediately with no delay
  • kida renderoutput is safe to pipe or redirect

See Also

  • Streaming --render_stream() and {% flush %}for progressive output
  • Escaping -- How autoescape modes work, including terminal sanitization
  • API Reference -- Full method signatures forEnvironment, Template, and filters
2

Terminal Rendering

Build rich terminal output with colors, components, and live updates

Terminal Rendering

Kida templates can render to the terminal with full ANSI color support, responsive layouts, and real-time animation. The same template language you use for HTML works for CLI tools — with a dedicated set of filters and components designed for terminal output.

Prerequisites

  • Python 3.14+
  • Kida installed
  • A terminal that supports ANSI escape codes (almost all modern terminals)

Your First Terminal Render

The fastest way to get colored terminal output is theterminal_env() factory. It returns a standard Kida Environment pre-configured with autoescape="terminal"and all terminal filters registered.

from kida.terminal import terminal_env

env = terminal_env()
tpl = env.from_string('{{ "Hello" | bold | cyan }} from Kida!')
print(tpl.render())

Run this and you will see "Hello" printed in bold cyan, followed by unstyled text. The bold, cyan, and other style filters wrap their input in the appropriate ANSI escape sequences. When autoescape="terminal"is active, Kida handles the reset codes automatically so styles do not bleed into surrounding text.

You can also use the CLI to render a template file directly. Save a template asgreeting.txt:

{{ name | green | bold }}: {{ message | dim }}

Then render it from the command line:

kida render greeting.txt --mode terminal -v name=World -v message="Welcome to Kida"

The --mode terminal flag tells the CLI to use the terminal rendering environment. You can pass variables with -v key=value.

The core style filters available out of the box are:bold, dim, italic, underline, cyan, green, red, yellow, and bright_cyan. These compose naturally — chain them in any order and Kida merges the escape codes into a single sequence.

Colors and Graceful Degradation

Terminals vary widely in their color support. Kida recognizes four color depth levels and degrades gracefully when a template uses colors that the terminal cannot display.

Level Description Example
none No color output CI logs, piped output
basic 16 ANSI colors Older terminals
256 256-color palette Most modern terminals
truecolor 24-bit RGB iTerm2, Windows Terminal, Kitty

By default,terminal_env()auto-detects the terminal's color depth. You can override this explicitly:

from kida.terminal import terminal_env

# Force basic 16-color mode
env = terminal_env(terminal_color="basic")
tpl = env.from_string('{{ status | fg("#ff6600") | bold }}')
print(tpl.render(status="DEGRADED"))

When this template runs with terminal_color="basic", the hex color #ff6600is automatically mapped to the nearest basic ANSI color (yellow). No error, no missing output — the template still works, just with reduced fidelity.

For full control over custom colors, use thefg() and bg()filters. They accept both named colors and hex values:

env = terminal_env(terminal_color="truecolor")
tpl = env.from_string("""
{{ title | fg("#00d4aa") | bold }}
{{ subtitle | fg("yellow") | bg("#1a1a2e") }}
""")
print(tpl.render(title="Deploy Report", subtitle="v2.1.0 — production"))

Kida also respects the NO_COLOR and FORCE_COLOR environment variables. When NO_COLOR is set, all color output is suppressed regardless of the terminal_color setting. When FORCE_COLORis set, auto-detection is skipped and colors are always emitted. This follows the community convention at no-color.org.

:::note[Testing Tip] Usekida render template.txt --mode terminal --color noneto see exactly what your template renders without any ANSI codes. This is useful for verifying layout and content before adding color. :::

To test how your template looks at different color depths from the CLI:

# See output with only basic colors
kida render dashboard.txt --mode terminal --color basic

# See output with no colors at all
kida render dashboard.txt --mode terminal --color none

Layout and Built-in Components

Terminal output benefits from structure — headers, dividers, aligned key-value pairs, and status indicators. Kida provides filters and built-in globals for all of these.

Thehr() global renders a horizontal rule. The icons object provides common Unicode glyphs. The pad() filter pads a value to a fixed width, and kv()formats label-value pairs with aligned separators:

from kida.terminal import terminal_env

env = terminal_env(terminal_width=80)
tpl = env.from_string("""
{{ "Service Status" | bold | underline }}
{{ hr(80) }}
{% for svc in services %}
{{ svc.name | pad(20) | bold }} {{ svc.status | badge() }}  {{ svc.latency | kv("latency", sep=": ") }}
{% end %}
{{ hr(80) }}
{{ summary | dim }}
""")

services = [
    {"name": "api-gateway", "status": "healthy", "latency": "12ms"},
    {"name": "auth-service", "status": "healthy", "latency": "8ms"},
    {"name": "worker-pool", "status": "degraded", "latency": "340ms"},
]
print(tpl.render(services=services, summary="3 services checked"))

The badge() filter wraps a value in a colored pill-style label — "healthy" renders in green, "degraded" in yellow, "down" in red. The bar()filter draws a progress bar of a given width, useful for displaying percentages or resource usage:

{{ "CPU" | pad(8) }} {{ cpu_pct | bar(40) }} {{ cpu_pct }}%
{{ "Memory" | pad(8) }} {{ mem_pct | bar(40) }} {{ mem_pct }}%

The terminal_width parameter on terminal_env() controls the default width used by hr()and other width-aware components. If not specified, Kida reads the actual terminal width at render time.

Thetable() filter formats a list of dictionaries as an aligned table with headers. The tree() filter renders nested data as a tree with box-drawing characters. And syntax()applies syntax highlighting to code strings:

{{ "Dependencies" | bold }}
{{ deps | tree() }}

{{ "Recent Queries" | bold }}
{{ queries | table() }}

Icons are available through the icons global: icons.check (checkmark), icons.cross (X mark), icons.warn (warning triangle), and icons.gear (gear). Use box.roundfor rounded box-drawing characters. These degrade to ASCII equivalents when the terminal does not support Unicode.

LiveRenderer — Animated Output

For CLI tools that run multi-step operations, static output is not enough.LiveRendererre-renders a template in place, replacing the previous output on each update. This is how you build progress displays, deploy trackers, and monitoring dashboards.

LiveRenderer works as a context manager. Call update()to push new data and trigger a re-render:

import time
from kida.terminal import LiveRenderer, terminal_env

env = terminal_env()
tpl = env.from_string("""
{{ "Deploy Progress" | bold | cyan }}
{{ hr(60) }}
{% for step in steps %}
{% if step.status == "done" %}
  {{ icons.check | green }} {{ step.name | pad(30) }} {{ step.duration }}
{% elif step.status == "running" %}
  {{ spinner() }} {{ step.name | pad(30) | yellow }} ...
{% else %}
  {{ "  " }} {{ step.name | pad(30) | dim }}
{% end %}
{% end %}
{{ hr(60) }}
{{ elapsed | dim }}
""")

steps = [
    {"name": "Pull image", "status": "pending", "duration": ""},
    {"name": "Run migrations", "status": "pending", "duration": ""},
    {"name": "Health check", "status": "pending", "duration": ""},
    {"name": "Switch traffic", "status": "pending", "duration": ""},
]

with LiveRenderer(tpl, refresh_rate=0.1) as live:
    for i, step in enumerate(steps):
        steps[i]["status"] = "running"
        live.update(steps=steps, elapsed=f"{i * 2}s elapsed")

        time.sleep(2)  # Simulate work

        steps[i]["status"] = "done"
        steps[i]["duration"] = "2.0s"
        live.update(steps=steps, elapsed=f"{(i + 1) * 2}s elapsed")

print("Deploy complete!")

The LiveRendererconstructor accepts several options:

  • refresh_rate — minimum seconds between screen updates (default 0.1). Prevents flickering when update()is called rapidly.
  • file — output file object (default sys.stderr). Useful for redirecting live output.
  • transient — if True, the live display is cleared when the context manager exits. Use this for progress indicators that should disappear when done.

Thespinner() global produces an animated spinner character that cycles on each re-render. It works automatically inside LiveRenderer— no extra threading needed.

For operations where you want continuous animation without explicitupdate() calls, use start_auto() and stop_auto():

with LiveRenderer(tpl) as live:
    live.start_auto(status="Connecting...", progress=0)
    # Spinner animates while we wait
    result = long_running_operation()
    live.stop_auto()
    live.update(status="Complete", progress=100, result=result)

LiveRenderer is thread-safe. Each call to update()atomically swaps the render context, so background threads can push updates without locks. The renderer freezes the context snapshot before each render pass to avoid tearing.

Optimize with static_context

When building CLI tools, some template values are known at startup and never change — the application name, version string, feature flags, environment label. Passing these throughstatic_contextlets Kida fold them at compile time, eliminating branches and lookups at render time.

from kida.terminal import terminal_env

env = terminal_env()

TEMPLATE = """
{{ app_name | bold | cyan }} {{ version | dim }}
{{ hr(60) }}
{% if debug %}
{{ "DEBUG MODE" | bold | red }}
{{ hr(60) }}
{% end %}
{% for svc in services %}
  {{ svc.name | pad(20) }} {{ svc.status | badge() }}
{% end %}
"""

# Values known at app startup — fold at compile time
static = {"app_name": "deployer", "version": "2.1.0", "debug": False}

tpl = env.from_string(TEMPLATE, name="dashboard", static_context=static)

# Only pass dynamic data at render time
print(tpl.render(services=[
    {"name": "api", "status": "healthy"},
    {"name": "worker", "status": "degraded"},
]))

:::note[How it works] Withdebug: False in static_context, the entire {% if debug %}block is removed from the compiled template. Zero runtime cost for that branch — it does not exist in the compiled AST. :::

Note thatstatic_context is only available on env.from_string(), not on env.get_template(). This is intentional — file-loaded templates are cached globally, but static context specializes a template for particular values.

Use the--explainflag to inspect what the compiler optimized:

kida render dashboard.txt --mode terminal --explain

The explain output shows the compiled AST and highlights which nodes were constant-folded. This is helpful for verifying that your static values are actually being used by the optimizer.

Good candidates forstatic_context:

  • Application name and version
  • Environment label (production, staging)
  • Feature flags
  • Terminal width (if you want a fixed layout)
  • Configuration that does not change per render

Streaming Output

For templates that produce long output — log formatters, report generators, file listings — streaming renders chunks progressively instead of buffering the entire result.

from kida.terminal import terminal_env, stream_to_terminal

env = terminal_env()
tpl = env.from_string("""
{{ "Build Log" | bold | underline }}
{{ hr(60) }}
{% for entry in log_entries %}
{{ entry.timestamp | dim }} {{ entry.level | badge() }} {{ entry.message }}
{% end %}
""")

context = {"log_entries": build_log}
stream_to_terminal(tpl, context, delay=0.02)

The delay parameter controls the pause between chunks in seconds. Set it to 0for maximum speed, or increase it for a typewriter effect that is easier to follow visually.

From the CLI, use the--streamflag:

kida render build-log.txt --mode terminal --stream --stream-delay 0.05

When should you use streaming versus LiveRenderer? Streaming is for output that grows — each line appears and stays. LiveRenderer is for output that changes in place — the same region is re-rendered with updated data. Use streaming for logs and reports. Use LiveRendererfor dashboards and progress displays.

Complete Example — Service Dashboard

Here is a full working example that ties togetherterminal_env, static_context, LiveRenderer, colors, and components. Create the following file structure:

my-cli/
  dashboard.py
  templates/
    dashboard.txt

templates/dashboard.txt:

{{ app_name | bold | cyan }} {{ version | dim }}  {{ env_label | badge() }}
{{ hr(width) }}

{% for svc in services %}
{% if svc.status == "healthy" %}
  {{ icons.check | green }} {{ svc.name | pad(20) | bold }} {{ svc.latency | kv("latency") }}
{% elif svc.status == "degraded" %}
  {{ icons.warn | yellow }} {{ svc.name | pad(20) | bold }} {{ svc.latency | kv("latency") }}
{% else %}
  {{ icons.cross | red }} {{ svc.name | pad(20) | bold }} {{ svc.latency | kv("latency") }}
{% end %}
{% end %}

{{ hr(width) }}
{{ "CPU" | pad(8) }} {{ cpu | bar(40) }} {{ cpu }}%
{{ "Mem" | pad(8) }} {{ mem | bar(40) }} {{ mem }}%
{{ hr(width) }}
{{ updated_at | dim }}

dashboard.py:

import time
import random
from kida import FileSystemLoader
from kida.terminal import LiveRenderer, terminal_env

env = terminal_env(loader=FileSystemLoader("templates/"), terminal_width=72)

static = {"app_name": "myapp", "version": "2.1.0", "env_label": "production", "width": 72}
tpl = env.from_string(
    open("templates/dashboard.txt").read(),
    name="dashboard",
    static_context=static,
)

services = [
    {"name": "api-gateway", "status": "healthy", "latency": "12ms"},
    {"name": "auth-service", "status": "healthy", "latency": "8ms"},
    {"name": "search-index", "status": "healthy", "latency": "45ms"},
    {"name": "worker-pool", "status": "healthy", "latency": "120ms"},
]

with LiveRenderer(tpl, refresh_rate=0.5, transient=False) as live:
    for tick in range(30):
        # Simulate changing metrics
        for svc in services:
            latency = random.randint(5, 500)
            svc["latency"] = f"{latency}ms"
            svc["status"] = "healthy" if latency < 200 else "degraded" if latency < 400 else "down"

        live.update(
            services=services,
            cpu=random.randint(10, 95),
            mem=random.randint(30, 85),
            updated_at=f"Updated {tick + 1}s ago",
        )
        time.sleep(1)

Run it with python dashboard.pyand watch the service statuses, latencies, and resource bars update in real time.

Next Steps

You now have the tools to build rich terminal output with Kida. Here are some directions to explore:

  • Terminal Rendering — terminal filters, layout components, and rendering patterns
  • Filter Reference — full list of built-in filters
  • The--explainflag for inspecting compiled templates and verifying optimizations
  • examples/terminal_dashboard/ and examples/terminal_deploy/in the Kida repository for more patterns
  • Milo for building fully interactive terminal applications on top of Kida's rendering layer
3

Worker Auto-Tuning

Workload-aware parallelization for free-threaded Python

Worker Auto-Tuning

Kida provides a workload-aware worker pool toolkit for framework authors who need to parallelize template rendering. It is calibrated for free-threaded Python (3.14t) where CPU-bound rendering achieves true parallelism.

from kida.utils.workers import get_optimal_workers, should_parallelize

Quick Start

from concurrent.futures import ThreadPoolExecutor
from kida.utils.workers import get_optimal_workers, should_parallelize

contexts = [{"name": f"User {i}"} for i in range(100)]

if should_parallelize(len(contexts)):
    workers = get_optimal_workers(len(contexts))
    with ThreadPoolExecutor(max_workers=workers) as executor:
        results = list(executor.map(template.render, contexts))
else:
    results = [template.render(**ctx) for ctx in contexts]

Core Functions

Import: Usefrom kida.utils.workers import ... or from kida import get_optimal_workers, should_parallelize, ...

should_parallelize

Determine if parallelization is worthwhile. Thread pool overhead (~1-2ms per task) only pays off above a threshold.

from kida.utils.workers import should_parallelize

should_parallelize(5)    # False — below threshold
should_parallelize(100)  # True — above threshold

# With work size estimate (bytes of template output)
should_parallelize(100, total_work_estimate=500)  # False — too small

get_optimal_workers

Calculate the optimal worker count based on workload type, environment, CPU cores, and free-threading status.

from kida.utils.workers import get_optimal_workers, WorkloadType

# Template rendering (default)
get_optimal_workers(100)  # 4 (local, free-threading)

# Template compilation
get_optimal_workers(100, workload_type=WorkloadType.COMPILE)  # 2

# Override auto-tuning
get_optimal_workers(100, config_override=16)  # 16

# Weight heavy templates higher
get_optimal_workers(50, task_weight=2.0)  # Adjusts for heavy work

Workload Types

Type Use Case Parallelism
WorkloadType.RENDER Template rendering (CPU-bound) High — benefits from free-threading
WorkloadType.COMPILE Template compilation (CPU-bound) Moderate — shared cache limits scaling
WorkloadType.IO_BOUND File loading, network High — threads wait on I/O

Environment Detection

The toolkit auto-detects the execution environment to tune worker counts:

Environment Detection Worker Strategy
CI CI, GITHUB_ACTIONS, etc. Conservative (2 workers max)
Local Default Moderate (up to 4 workers)
Production KIDA_ENV=production Aggressive (up to 8 workers)

Override detection with theKIDA_ENVenvironment variable:

export KIDA_ENV=production  # or "ci" or "local"

Free-Threading Detection

The toolkit detects whether the GIL is disabled and scales worker counts accordingly:

from kida.utils.workers import is_free_threading_enabled

if is_free_threading_enabled():
    print("GIL disabled — true parallelism available")

On free-threaded Python, render workloads get a 1.5x multiplier on the CPU-based worker count.

Template Scheduling

For optimal throughput, schedule heavy templates first to avoid the "straggler effect" where one slow render delays overall completion.

estimate_template_weight

Estimate relative complexity of a template:

from kida.utils.workers import estimate_template_weight

weight = estimate_template_weight(template)
# 1.0 = average, >1 = heavy, <1 = light (capped at 5.0)

Weight factors:

  • Source size: +0.5 per 5KB above 5KB threshold
  • Block count: +0.1 per block above 3
  • Macro count: +0.2 per macro
  • Inheritance: +0.5 if extends another template
  • Includes: +0.15 per include statement

order_by_complexity

Sort templates for optimal parallel execution:

from kida.utils.workers import order_by_complexity

# Heavy templates first (default — best for parallel execution)
ordered = order_by_complexity(templates)

# Light templates first (useful for testing)
ordered = order_by_complexity(templates, descending=False)

Workload Profiles

Inspect the tuning parameters for any workload/environment combination:

from kida.utils.workers import get_profile, WorkloadType

profile = get_profile(WorkloadType.RENDER)
print(profile.parallel_threshold)        # 10
print(profile.max_workers)               # 4
print(profile.free_threading_multiplier) # 1.5

WorkloadProfile Fields

Field Type Description
parallel_threshold int Minimum tasks before parallelizing
min_workers int Floor for worker count
max_workers int Ceiling for worker count
cpu_fraction float Fraction of cores to use (0.0-1.0)
free_threading_multiplier float Extra scaling when GIL is disabled

Complete Example

from concurrent.futures import ThreadPoolExecutor
from kida import Environment, FileSystemLoader
from kida.utils.workers import (
    get_optimal_workers,
    order_by_complexity,
    should_parallelize,
    WorkloadType,
)

env = Environment(loader=FileSystemLoader("templates/"))

# Load and schedule templates (requires a loader; FunctionLoader returns [])
templates = [env.get_template(name) for name in env.loader.list_templates()]
ordered = order_by_complexity(templates)

# Build render tasks
tasks = [(tmpl, {"page": page}) for tmpl, page in zip(ordered, pages, strict=True)]

if should_parallelize(len(tasks)):
    workers = get_optimal_workers(
        len(tasks),
        workload_type=WorkloadType.RENDER,
    )
    with ThreadPoolExecutor(max_workers=workers) as pool:
        results = list(pool.map(lambda t: t[0].render(**t[1]), tasks))
else:
    results = [tmpl.render(**ctx) for tmpl, ctx in tasks]

See Also

✓ Track Complete