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.
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 paddingcols(cells, sep=" ")-- multi-column layout from(content, width[, align])tuplesrule(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 borderheader(width=0, style="double")-- double-bordered title barfooter(width=0, style="heavy")-- heavy-bordered summary barbox(title="", width=0, style="round", padding=1)-- bordered box with title in a separate header rowconnector(indent=2)-- vertical pipe between panels for visual continuity
Layout helpers -- content arrangement:
banner(text, width=0, char="=", padding=1)-- full-width centered bannertwo_col(left_width=0, sep=" | ")-- two-column layout split on|||stack(threshold=60, sep=" | ")-- responsive: side-by-side when wide, stacked when narrowdl(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 %}
Header + Panel + Footer Layout
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:
- 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.
- RGB/256 to basic 16: Finds the nearest of the 16 standard ANSI colors by squared Euclidean distance against canonical RGB values.
- 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:
- Explicit override:
Environment(ambiguous_width=2)orterminal_env(ambiguous_width=2) - Terminal probe: Writes a test character and measures cursor movement via ANSI DSR
wcwidthlibrary: Uses per-character width data if the optionalwcwidthpackage is installed- Locale heuristic: CJK locales (
ja,ko,zh) default to 2 - 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 escapesansi_ljust(s, width)-- left-justify to visible widthansi_rjust(s, width)-- right-justify to visible widthansi_center(s, width)-- center within visible widthansi_truncate(s, width)-- truncate at visible width, preserving ANSI codesansi_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
- On
__enter__, the cursor is hidden to prevent flicker - Each
update(**context)re-renders the template and overwrites the previous output using\033[A(cursor up) and\033[2K(erase line) - On
__exit__, the cursor is restored; Ctrl+C is handled gracefully - Context accumulates across
update()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:
NO_COLORset -->"none"(no colors)FORCE_COLORset -->"basic"(16 colors)COLORTERMistruecoloror24bit-->"truecolor"TERMcontains256color-->"256"- stdout is a TTY -->
"basic" - 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 (unless
FORCE_COLORis set) LiveRendererappends output instead of overwritingstream_to_terminal()writes immediately with no delaykida 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 for
Environment,Template, and filters