User Guide

This guide covers the core concepts and features of ViewText in detail.

Field Registry

The Field Registry is the foundation of ViewText. It maps field names to getter functions that extract values from context dictionaries.

Creating a Registry

from viewtext import BaseFieldRegistry

registry = BaseFieldRegistry()

Registering Fields

Fields are registered with a name and a callable that takes a context dictionary:

# Simple field getter
registry.register("username", lambda ctx: ctx["user"]["name"])

# More complex getter with default values
registry.register("status", lambda ctx: ctx.get("status", "offline"))

# Computed fields
registry.register("full_name", lambda ctx: f"{ctx['first']} {ctx['last']}")

Checking for Fields

if registry.has_field("username"):
    getter = registry.get("username")
    value = getter(context)

Formatter System

Formatters transform raw values into formatted strings for display. ViewText includes several built-in formatters and supports custom formatters.

ViewText provides built-in formatters for:

  • text - Basic text with prefix/suffix

  • text_uppercase - Convert to uppercase

  • number - Format numbers with separators and decimals

  • price - Currency formatting with symbols

  • datetime - Format timestamps and dates

  • relative_time - Human-readable time differences (e.g., “5m ago”)

  • template - Combine multiple fields with Python format specifications

Quick Example

[[layouts.demo.lines]]
field = "price"
index = 0
formatter = "price"

[layouts.demo.lines.formatter_params]
symbol = "$"
decimals = 2
thousands_sep = ","
Input: 1234.50 → Output: "$1,234.50"

For complete documentation of all formatters, parameters, and examples, see Formatters Reference.

Custom Formatters

You can register custom formatters with the FormatterRegistry:

from viewtext import get_formatter_registry

def format_percentage(value, **kwargs):
    decimals = kwargs.get("decimals", 1)
    return f"{value:.{decimals}f}%"

formatter_registry = get_formatter_registry()
formatter_registry.register("percentage", format_percentage)

Layout Configuration

Layouts are defined in TOML files and specify how fields map to output. ViewText supports two types of layouts: line-based layouts and dictionary-based layouts.

Line-Based Layouts

Line-based layouts map fields to numbered line positions (indices). This is useful for fixed-position text displays like terminal dashboards or e-ink displays.

[layouts.my_layout]
name = "My Layout"

[[layouts.my_layout.lines]]
field = "field_name"
index = 0
formatter = "text"

[layouts.my_layout.lines.formatter_params]
prefix = "Label: "

The build_line_str() method returns a list of strings, one per line:

lines = engine.build_line_str(layout, context)
# Returns: ["Label: value", "Line 2", ...]

Dictionary-Based Layouts

Dictionary-based layouts map fields to named keys, producing key-value pairs. This is useful for JSON APIs, configuration files, or structured data output.

[layouts.my_dict_layout]
name = "My Dictionary Layout"

[[layouts.my_dict_layout.items]]
key = "display_name"
field = "field_name"
formatter = "text"

[layouts.my_dict_layout.items.formatter_params]
prefix = "Label: "

[[layouts.my_dict_layout.items]]
key = "temperature"
field = "temp"
formatter = "number"

[layouts.my_dict_layout.items.formatter_params]
decimals = 1
suffix = "°"

The build_dict_str() method returns a dictionary with formatted string values:

result = engine.build_dict_str(layout, context)
# Returns: {"display_name": "Label: value", "temperature": "72.5°"}

When to Use Each Type

  • Line-based layouts: Terminal UIs, e-ink displays, fixed-position text grids

  • Dictionary-based layouts: JSON APIs, key-value stores, structured data export

CLI Support

The CLI automatically detects layout type:

# List shows layout type
viewtext list

# Render outputs key:value pairs for dict layouts
echo '{"temp": 72.5}' | viewtext render weather_dict

# JSON output returns dict for dict layouts, array for line layouts
echo '{"temp": 72.5}' | viewtext render weather_dict --json

Multiple Layouts

A single TOML file can contain multiple layouts:

[layouts.compact]
name = "Compact View"
# ... lines ...

[layouts.detailed]
name = "Detailed View"
# ... lines ...

Formatter Parameters

Each line can have formatter-specific parameters:

[[layouts.demo.lines]]
field = "price"
index = 0
formatter = "price"

[layouts.demo.lines.formatter_params]
symbol = "$"
decimals = 2
thousands_sep = ","
symbol_position = "prefix"

Formatter Presets

Define reusable formatter configurations (presets) to promote consistency and reduce duplication:

[formatters.usd_price]
type = "price"
symbol = "$"
decimals = 2
thousands_sep = ","

[formatters.time_only]
type = "datetime"
format = "%H:%M"

Presets can be referenced by name directly in layouts:

[layouts.product]
name = "Product Display"

[[layouts.product.lines]]
field = "price"
index = 0
formatter = "usd_price"  # References preset

[[layouts.product.lines]]
field = "created_at"
index = 1
formatter = "time_only"  # References preset

Presets can also be used in template formatter field_formatters:

[[layouts.crypto.lines]]
field = "ticker"
index = 0
formatter = "template"

[layouts.crypto.lines.formatter_params]
template = "{symbol}: {price}"
fields = ["symbol", "price"]
field_formatters = { "price": "usd_price" }

For more details on formatter presets, see Formatters Reference.

Layout Engine

The Layout Engine combines field registries, formatters, and layout configurations to generate formatted output.

Creating an Engine

from viewtext import LayoutEngine

# Without field registry (uses context directly)
engine = LayoutEngine()

# With field registry
engine = LayoutEngine(field_registry=registry)

Building Output

ViewText provides two methods for building output, depending on your layout type:

Line-Based Layouts

context = {
    "temp": 72.5,
    "humidity": 65,
    "city": "San Francisco"
}

lines = engine.build_line_str(layout, context)

# lines is a list of strings, one per line
for i, line in enumerate(lines):
    print(f"Line {i}: {line}")

Dictionary-Based Layouts

context = {
    "temp": 72.5,
    "price": 19.99,
    "message": "Hello"
}

result = engine.build_dict_str(layout, context)

# result is a dict with string values
print(result["temp"])       # "72.5°"
print(result["price"])      # "$19.99"
print(result["message"])    # "Hello"

# Export as JSON
import json
print(json.dumps(result, indent=2))

Field Resolution

The engine resolves fields in this order:

  1. Check field registry (if provided)

  2. Check context dictionary directly

  3. Return None if not found

This allows mixing registered fields with direct context values.

For detailed information on field types and definitions, see Fields Reference.

Field Validation

ViewText provides comprehensive field validation to ensure data quality and type safety. Validation rules are defined declaratively in TOML configuration files.

Quick Example

[fields.user_age]
context_key = "age"
type = "int"
min_value = 0
max_value = 120
on_validation_error = "use_default"
default = 0

This field definition ensures that user_age is:

  • An integer value

  • Between 0 and 120

  • Falls back to 0 if validation fails

Validation Features

ViewText supports:

  • Type Checking: Ensure values are the correct type (str, int, float, bool, list, dict)

  • Constraint Validation: Enforce numeric ranges, string lengths, patterns, and allowed values

  • Error Handling: Control what happens when validation fails (use_default, raise, skip, coerce)

  • Type Coercion: Automatically convert compatible types

For complete documentation of validation parameters, error handling strategies, and examples, see Field Validation Reference.

Computed Fields

Computed fields allow you to perform calculations on source data without writing Python code. All operations are defined in TOML configuration files and are compiled at load time.

Available Operations

Temperature Conversions

  • celsius_to_fahrenheit - Convert Celsius to Fahrenheit

  • fahrenheit_to_celsius - Convert Fahrenheit to Celsius

Arithmetic Operations

  • multiply - Multiply two or more values

  • divide - Divide two values (safe with divide-by-zero handling)

  • add - Sum multiple values

  • subtract - Subtract two values

  • modulo - Modulo operation (remainder after division)

Aggregate Operations

  • average - Calculate average of multiple values

  • min - Find minimum of multiple values

  • max - Find maximum of multiple values

Mathematical Operations

  • abs - Absolute value

  • round - Round to nearest integer (optionally specify decimals)

  • ceil - Round up to nearest integer

  • floor - Round down to nearest integer

  • linear_transform - Apply formula: (value * multiply / divide) + add

String Operations

  • concat - Join multiple strings with a separator

  • split - Split a string by separator and take a specific index

  • substring - Extract substring from start to end position

Conditional Operations

  • conditional - Return different values based on field equality condition (condition, if_true, if_false)

Defining Computed Fields

Computed fields are defined in the [fields] section of your TOML configuration:

# Temperature conversion
[fields.temp_f]
operation = "celsius_to_fahrenheit"
sources = ["temp_c"]
default = 0.0

# Price calculation
[fields.total_price]
operation = "multiply"
sources = ["price", "quantity"]
default = 0.0

# Discount calculation
[fields.discount_price]
operation = "linear_transform"
sources = ["price"]
multiply = 0.8
default = 0.0

# Average score
[fields.average_score]
operation = "average"
sources = ["score1", "score2", "score3"]
default = 0.0

Operation Parameters

Each computed field requires:

  • operation - Name of the operation to perform

  • sources - List of field names from context to use as inputs

  • default - Value to return if operation fails or sources are missing

Some operations support additional parameters:

Linear Transform Parameters

  • multiply - Multiplier for the value (default: 1)

  • divide - Divisor for the value (default: 1)

  • add - Addend for the value (default: 0)

Formula: (value * multiply / divide) + add

# Convert km/h to mph
[fields.speed_mph]
operation = "linear_transform"
sources = ["speed_kmh"]
multiply = 0.621371
default = 0.0

# Apply 20% discount and add $5 handling fee
[fields.discounted_price]
operation = "linear_transform"
sources = ["price"]
multiply = 0.8
add = 5.0
default = 0.0

Round Operations

# Scale mempool size and round up
[fields.vsize_scaled]
operation = "linear_transform"
context_key = "mempool.vsize"
divide = 1000000
default = 0

[fields.vsize_mb]
operation = "ceil"
sources = ["vsize_scaled"]
default = 0

String Operations Parameters

  • separator - Separator for concat/split operations (default: empty string for concat, space for split)

  • index - Index for split operation (which part to extract)

  • start - Start position for substring operation

  • end - End position for substring operation (optional)

# Concatenate first and last name
[fields.full_name]
operation = "concat"
sources = ["first_name", "last_name"]
separator = " "
default = ""

# Extract domain from email
[fields.domain]
operation = "split"
sources = ["email"]
separator = "@"
index = 1
default = ""

# Extract year from date string
[fields.year]
operation = "substring"
sources = ["date"]
start = 0
end = 4
default = ""

# Get last 3 characters
[fields.suffix]
operation = "substring"
sources = ["text"]
start = -3
default = ""

Modulo Operation

# Check if number is even/odd (result will be 0 or 1)
[fields.remainder]
operation = "modulo"
sources = ["number", "divisor"]
default = 0

Conditional Operations

# Display price with currency formatting
[fields.price_display]
operation = "conditional"
condition = { field = "currency", equals = "USD" }
if_true = "$~amount~"
if_false = "~amount~ ~currency~"
default = ""

The ~field_name~ syntax in if_true and if_false allows embedding other field values.

Error Handling

Computed fields include automatic error handling:

  • Missing source values return the default

  • Non-numeric values return the default (for numeric operations)

  • Division by zero returns the default

  • Modulo by zero returns the default

  • Out-of-bounds string indices return the default

  • Invalid operations raise ValueError at configuration load time

Example Use Cases

Unit Conversions

[fields.temp_f]
operation = "celsius_to_fahrenheit"
sources = ["temp_c"]
default = 0.0

[fields.meters_to_feet]
operation = "linear_transform"
sources = ["meters"]
multiply = 3.28084
default = 0.0

E-commerce Calculations

# Line item total
[fields.line_total]
operation = "multiply"
sources = ["price", "quantity"]
default = 0.0

# Discounted price
[fields.sale_price]
operation = "linear_transform"
sources = ["price"]
multiply = 0.85
default = 0.0

Data Aggregation

# Daily temperature range
[fields.temp_min]
operation = "min"
sources = ["temp_morning", "temp_noon", "temp_evening"]
default = 0.0

[fields.temp_max]
operation = "max"
sources = ["temp_morning", "temp_noon", "temp_evening"]
default = 0.0

[fields.temp_avg]
operation = "average"
sources = ["temp_morning", "temp_noon", "temp_evening"]
default = 0.0

Benefits

  1. Declarative - Define calculations in configuration, not code

  2. Reusable - Same operations work across different layouts

  3. Safe - Built-in error handling prevents crashes

  4. Maintainable - Easy to understand and modify

  5. Fast - Compiled at configuration load time

See examples/computed_fields.toml and examples/demo_computed_fields.py for complete examples.

Layout Loader

The LayoutLoader handles loading and parsing TOML configuration files.

Loading Layouts

from viewtext import LayoutLoader

# Load from specific file
loader = LayoutLoader("config/layouts.toml")

# Load from default location (./layouts.toml)
loader = LayoutLoader()

# Get a specific layout
layout = loader.get_layout("weather")

Split Configuration Files

For large projects, you can split your configuration into separate files for better organization and maintainability:

from viewtext import LayoutLoader

# Method 1: Using constructor parameters
loader = LayoutLoader(
    config_path="layouts.toml",
    formatters_path="formatters.toml",
    fields_path="fields.toml"
)
config = loader.load()

# Method 2: Using static method
config = LayoutLoader.load_from_files(
    layouts_path="layouts.toml",
    formatters_path="formatters.toml",
    fields_path="fields.toml"
)

Example: Separate Formatters File

formatters.toml:

[formatters.price_usd]
type = "price"
symbol = "$"
decimals = 2

[formatters.price_eur]
type = "price"
symbol = "€"
decimals = 2

layouts.toml:

[layouts.product]
name = "Product Display"

[[layouts.product.lines]]
field = "price"
index = 0
formatter = "price_usd"

Example: Separate Fields File

fields.toml:

[fields.temperature]
context_key = "temp"
default = 0

[fields.city]
context_key = "location.city"
default = "Unknown"

[fields.first_tag]
context_key = "tags.0"
default = ""

CLI Usage with Split Files

# Use --formatters and --fields flags
viewtext --config layouts.toml \\
         --formatters formatters.toml \\
         --fields fields.toml \\
         list

viewtext -c layouts.toml -f formatters.toml -F fields.toml render weather

Benefits of Split Files

  1. Modularity: Separate concerns into different files

  2. Reusability: Share formatters and fields across multiple layout files

  3. Team Collaboration: Different team members can work on different files

  4. Maintainability: Easier to find and update specific configurations

Merging Behavior

When multiple files are provided:

  • Fields from fields.toml are merged into the base configuration

  • Formatters from formatters.toml are merged into the base configuration

  • If the same key exists in multiple files, values from separate files take precedence

  • All separate files are optional

Getting Formatter Parameters

# Get global formatter configuration
params = loader.get_formatter_params("usd_price")

Error Handling

ViewText raises specific exceptions for common errors:

from viewtext import LayoutLoader, BaseFieldRegistry

# FileNotFoundError
try:
    loader = LayoutLoader("missing.toml")
    loader.load()
except FileNotFoundError as e:
    print(f"Config file not found: {e}")

# ValueError for unknown layout
try:
    layout = loader.get_layout("nonexistent")
except ValueError as e:
    print(f"Layout error: {e}")

# ValueError for unknown field
registry = BaseFieldRegistry()
try:
    getter = registry.get("unknown_field")
except ValueError as e:
    print(f"Field error: {e}")

Best Practices

  1. Separate concerns: Keep field logic in the registry, formatting in formatters, and layout structure in TOML files

  2. Use meaningful names: Choose descriptive field and layout names

  3. Provide defaults: Use .get() with defaults in field getters for optional data

  4. Validate data: Formatters should handle None and invalid values gracefully

  5. Reuse formatters: Define global formatter configurations for consistency

  6. Test layouts: Verify layouts with sample data before deployment

Command Line Interface

ViewText includes a CLI for inspecting and testing layouts.

Basic Commands

# List all available layouts
viewtext list

# Show specific layout configuration
viewtext show weather

# Show field mappings from config
viewtext fields

# Show all available formatters
viewtext formatters

# Show all template formatters in layouts
viewtext templates

# Render a layout with mock data
viewtext render weather

# Render a layout with JSON input from stdin (autodetected)
echo '{"temp": 72.5}' | viewtext render weather

# Render a layout with JSON input and JSON output
echo '{"temp": 72.5}' | viewtext render weather --json

# Show configuration info
viewtext info

Global Config Option

Use the --config or -c option to specify a custom configuration file:

# Global option can be placed before any command
viewtext -c examples/layouts.toml list
viewtext --config my_layouts.toml show weather
viewtext -c custom.toml render crypto_ticker

The default config file is layouts.toml in the current directory.

CLI Output

The CLI provides rich formatted output with tables and colors:

$ viewtext list

Configuration File: layouts.toml

┌────────────────┬─────────────────────┬───────┐
│ Layout Name     Display Name         Lines │
├────────────────┼─────────────────────┼───────┤
│ weather         Weather Display          6 │
│ crypto_ticker   Crypto Ticker            5 │
└────────────────┴─────────────────────┴───────┘

Total layouts: 2

JSON Input and Output

The render command automatically detects JSON input from stdin and can output results as JSON arrays:

# JSON input is autodetected from stdin, output is rich formatted text
$ echo '{"demo1": "Line 1", "demo2": "Line 2"}' | viewtext render demo

# Use --json flag to output as JSON array instead of formatted text
$ echo '{"demo1": "Line 1", "demo2": "Line 2"}' | viewtext render demo --json
[
  "Line 1",
  "Line 2"
]

The --json flag controls the output format only. Input JSON is automatically detected when available on stdin.

Template Formatters Command

The templates command shows all layouts using template formatters:

$ viewtext -c examples/demo_template_formatter.toml templates

Configuration File: examples/demo_template_formatter.toml

┌────────────────────────┬──────────────┬─────────────────────────┬──────────────┐
│ Layout                  Field         Template                 Fields Used  │
├────────────────────────┼──────────────┼─────────────────────────┼──────────────┤
│ crypto_composite_price  current_price│ {fiat} - ${usd} - {sat}  price.fiat,  │
│ (Crypto Price Display)                                         price.usd,   │
│                                                                price.sat    │
└────────────────────────┴──────────────┴─────────────────────────┴──────────────┘

Total template formatters: 1

Advanced Usage

Singleton Pattern

ViewText provides singleton accessors for global instances:

from viewtext import (
    get_layout_engine,
    get_formatter_registry,
    get_layout_loader
)

# These return global singleton instances
engine = get_layout_engine(field_registry=registry)
formatters = get_formatter_registry()
loader = get_layout_loader("layouts.toml")

Dynamic Layouts

Build layouts dynamically from data:

def create_dynamic_layout(fields):
    layout = {
        "name": "Dynamic Layout",
        "lines": []
    }

    for i, field in enumerate(fields):
        layout["lines"].append({
            "field": field,
            "index": i,
            "formatter": "text"
        })

    return layout

# Use the dynamic layout
layout = create_dynamic_layout(["temp", "humidity", "pressure"])
lines = engine.build_line_str(layout, context)

Context Factories

Create reusable context builders:

class WeatherContext:
    def __init__(self, api_data):
        self.data = api_data

    def to_context(self):
        return {
            "temp": self.data["main"]["temp"],
            "humidity": self.data["main"]["humidity"],
            "city": self.data["name"],
            "timestamp": self.data["dt"]
        }

weather = WeatherContext(api_response)
lines = engine.build_line_str(layout, weather.to_context())

TOML Schema Validation

ViewText provides a JSON Schema for validating TOML configuration files with editor support.

Editor Support

The schema enables validation and autocomplete in editors that support Taplo:

VS Code

Install the Even Better TOML extension for:

  • Syntax validation

  • Property autocomplete

  • Hover documentation

  • Format on save

Neovim

Use the taplo LSP for validation and completion.

Other Editors

Any editor with Taplo LSP support will work.

Schema Features

The schema validates:

  • Field definitions - Ensures proper structure with required properties

  • Formatter configurations - Validates types and parameters

  • Layout definitions - Validates layout structure

  • Computed operations - Validates operation names and parameters

  • Validation rules - Validates type constraints and error handling

Example with autocomplete:

[fields.user_age]
context_key = "age"
type = "int"                    # Autocomplete suggests: str, int, float, bool, list, dict
min_value = 0
max_value = 120
on_validation_error = "use_default"  # Autocomplete suggests: use_default, raise, skip, coerce

[fields.temp_f]
operation = "celsius_to_fahrenheit"  # Autocomplete suggests all operations

Validation Command

Check TOML files manually using the taplo command:

# Check a single file
taplo check layouts.toml

# Format and check
taplo format layouts.toml

Configuration

The schema is configured in .taplo.toml and automatically applies to:

  • **/layouts*.toml - Layout configuration files

  • **/fields.toml - Field-only configuration files

  • **/formatters.toml - Formatter-only configuration files

See .taplo/README.md for more information on the schema.