# Terminal Rendering URL: /docs/usage/terminal-rendering/ Section: usage Tags: usage, terminal, ansi, cli -------------------------------------------------------------------------------- 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. Set autoescape="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. The Styled 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 with terminal_env(), which creates a pre-configured Environment with 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 return Styled strings 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 The fg and bg filters 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 in components.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 the columns 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 The stack 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 The fg() 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_COLOR set, 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 The WidthStrategy dataclass 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_width is not explicitly set: Explicit override: Environment(ambiguous_width=2) or terminal_env(ambiguous_width=2) Terminal probe: Writes a test character and measures cursor movement via ANSI DSR wcwidth library: Uses per-character width data if the optional wcwidth package 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 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 The pad, truncate, center, and wordwrap template filters all use these functions automatically in terminal mode. LiveRenderer LiveRenderer provides 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), use start_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 LiveRenderer context. {{ 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.), LiveRenderer appends 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 The icons global is an IconSet proxy. 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 generic icons.arrow — use a directional variant (arrow_r, arrow_l, arrow_u, arrow_d) or fat_arrow (⇒). If you reference an unknown icon name, Kida raises AttributeError with "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. When terminal_unicode=False, all icons return their ASCII fallback. Box Drawing The box global is a BoxSet proxy 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). When terminal_unicode=False, all styles fall back to ascii. Horizontal Rules The hr() global generates horizontal rules: {{ hr() }} {# full terminal width #} {{ hr(40) }} {# 40 characters wide #} {{ hr(60, title="Results") }} {# -- Results ---------- #} {{ hr(40, char="=") }} {# ======== #} CLI: kida render The kida render command 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.txt is 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 (overrides NO_COLOR and 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_COLOR set --> "none" (no colors) FORCE_COLOR set --> "basic" (16 colors) COLORTERM is truecolor or 24bit --> "truecolor" TERM contains 256color --> "256" stdout is a TTY --> "basic" Otherwise --> "none" Unicode Detection Unicode support is detected from the LANG 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_COLOR is set) LiveRenderer appends output instead of overwriting stream_to_terminal() writes immediately with no delay kida render output 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 -------------------------------------------------------------------------------- Metadata: - Word Count: 2836 - Reading Time: 14 minutes