Part of the Kiwi Foundation
Kiwify.Kiwi.Renderer is the data-display layer of the Kiwi Foundation - a trio of libraries (Presentation, Renderer, CLI) that together cover the full surface of building a professional .NET command-line tool.
Renderer sits above Presentation: it writes through IOutputWriter and uses TextStyle / OutputTheme from that library. CLI tools use Renderer to display query results, configuration dumps, and structured status output. The dependency flows one way: Renderer → Presentation. The CLI layer depends on both.
Source and installation
GitHub: kiwifylabs/kiwi-foundation-tooling - Kiwify.Kiwi.Renderer
NuGet: Kiwify.Kiwi.Renderer
dotnet add package Kiwify.Kiwi.Renderer
Design philosophy
Renderer is built around a three-layer separation: data shaping, rendering, and output transport. These layers compose independently.
Data shaping is handled by the JSON parsing pipeline or the low-level grid API. The rendering layer - TextGridRenderer, HtmlGridRenderer, RtfGridRenderer, and their object-table counterparts - operates on DataGrid, a format-agnostic column and row model, with no dependency on how that model was populated. Output transport is handled entirely by IOutputWriter from Kiwi Presentation, which decouples the rendering layer from the output destination.
This means the same rendering pipeline can target an ANSI-capable terminal, a redirected console stream (where styling is stripped automatically), an HTML file, an RTF document, or any custom sink that implements IOutputWriter. Format selection is a runtime parameter, not an architectural decision.
The practical implication is that applications can evolve from simple terminal-only tools into multi-format reporting pipelines without restructuring display infrastructure. The rendering configuration stays the same; only the format constant and the output writer change.
Kiwi Presentation and Kiwi Renderer compose through shared abstractions - IOutputWriter, TextStyle, OutputTheme - rather than through shared implementation. Renderer adds structured-data rendering capabilities while preserving the same output pipeline model that Presentation uses for prompts, progress bars, and styled console output. The libraries are intentionally modular: each can be used independently, and they integrate seamlessly when used together.
Intent
Displaying structured data in a terminal involves more mechanics than it first appears. Column widths must accommodate the widest value in any row, not just the header. Alignment and padding must be applied consistently across rows. The same dataset may need to appear in different forms across different contexts - a terminal table during an operator session, an HTML export for an archived report, an RTF document for a stakeholder summary - without duplicating formatting logic. A single JSON object and a JSON array of objects require fundamentally different layouts: key/value vs. multi-column.
Beyond format concerns, the source data is often already in JSON form: API responses, serialized configuration, pipeline outputs, ORM projections. Introducing a deserialization step and an intermediate view model adds friction without adding value when the goal is presentation, not processing.
Renderer addresses these concerns through a unified rendering pipeline. At the highest abstraction level, it accepts a JSON string and produces formatted output in the requested format. At the lowest level, it provides a DataGrid API that accepts programmatic rows without any JSON at all. The boundary between the two is well-defined and explicit - JSON is treated as an input and transport format, not as the rendering model itself.
Architectural benefits
| Benefit | Description |
|---|---|
| JSON-first, not JSON-only | The high-level pipeline accepts string, byte[], or JsonDocument. The low-level grid API operates without JSON, enabling programmatic construction and non-JSON data sources. JSON is the input format; the rendering model is DataGrid. |
| Multi-target rendering | The same rendering configuration produces text, HTML, and RTF output. Switching format is a single string constant. Custom formats can be registered at runtime via GridRendererFactory without modifying the library. |
| Output abstraction | All rendering flows through IOutputWriter from Kiwi Presentation. The output destination - terminal, file, stream, in-memory buffer - is external to the rendering logic and can be substituted freely. |
| Theme isolation |
OutputTheme is never mutated by renderers. Theme objects can be shared across concurrent render calls without defensive copying. |
| Extensible format registry |
GridRendererFactory and ObjectTableRendererFactory back their registrations with ConcurrentDictionary. Registration is thread-safe and can be called from module initializers. |
| Interactive paging |
PagedDataGrid<T> layers keyboard navigation onto any grid without altering the data model or rendering configuration. Falls through silently when stdin is redirected. |
| Modular composition | Renderer and Presentation are independently deployable and share abstractions, not implementation. Applications that already use Presentation can add Renderer without restructuring their output infrastructure. |
Where it fits
Renderer is applicable across a range of tooling and automation scenarios that extend well beyond interactive terminal display.
- CLI and DevOps tooling - display query results, deployment history, configuration state, and pipeline summaries in the terminal; export the same data as HTML or RTF without a separate rendering path.
- Operational reporting - produce structured reports from the same dataset that drives terminal display, with no divergence in data preparation logic.
- CI/CD diagnostics - render test summaries, coverage reports, and dependency audits to terminal and file simultaneously from the same renderer instance.
- Automated exports - scheduled jobs that produce HTML or RTF reports alongside terminal logging, using shared rendering configuration.
- Support and administrative tooling - render infrastructure state, resource inventory, and audit log entries in both terminal and document formats.
- API explorers and REST clients - display JSON responses as readable tables without a schema, view model, or deserialization step.
- AI-generated output review - render structured JSON produced by language model pipelines as formatted terminal tables for operator inspection.
The rendering pipeline is designed to support all of these without requiring format-specific code paths in the application layer.
Comparison with alternatives
vs. ConsoleTables
ConsoleTables provides a clean ASCII table from typed objects. It is well-suited for straightforward terminal display where JSON input, multi-format output, and column schema control are not requirements.
Kiwi Renderer covers a wider surface: JSON input without deserialization, multi-format rendering across text/HTML/RTF, predefined column schemas, key/value layout for single objects, JSON syntax highlighting, interactive paging, and the IOutputWriter output abstraction pipeline.
vs. Spectre.Console
Spectre.Console is a comprehensive terminal rendering framework. It excels at rich interactive console layouts - widgets, live updates, trees, panels, and advanced visual composition. For applications where sophisticated terminal presentation is the primary concern, it is a strong and well-maintained choice.
Kiwi Renderer is optimized for a different set of concerns: structured-data rendering with portability across terminal, HTML, and RTF targets; JSON-first input without intermediate models; and integration with the Kiwi Presentation output pipeline. The two libraries share some surface area - both can render tables - but they address different primary problems and operate at different abstraction levels.
If your application needs live terminal widgets, cursor-positioned updates, or rich interactive console layouts, Spectre.Console is the appropriate tool. If your focus is rendering structured data consistently across output formats within a shared output pipeline - particularly in automation, reporting, and tooling contexts - Kiwi Renderer provides a purpose-built model for that problem.
vs. CsvHelper
CsvHelper handles CSV serialization and deserialization. It has no terminal rendering capability. The two libraries are complementary: CsvHelper for structured file I/O, Kiwi Renderer for formatted display.
vs. Dapper / EF Core
Neither Dapper nor EF Core provides result display. Renderer integrates naturally with database-backed CLI tools: query results can be projected to JSON or passed through the low-level grid API and displayed as formatted tables without an intermediate view layer.
How it is used - detailed walkthrough
JSON array → terminal table
The simplest path - pass a JSON string, chain the builder, call AsTable():
var json = """
[
{ "Id": 1, "Name": "Alice", "Role": "Engineer", "Salary": 95000 },
{ "Id": 2, "Name": "Bob", "Role": "Designer", "Salary": 72000 }
]
""";
RenderEngine.Create(json)
.WithTitle("Team Directory")
.WithLeftMargin(2)
.Build()
.AsTable();
Output:
Team Directory
┌────┬───────┬──────────┬────────┐
│ Id │ Name │ Role │ Salary │
├────┼───────┼──────────┼────────┤
│ 1 │ Alice │ Engineer │ 95000 │
│ 2 │ Bob │ Designer │ 72000 │
└────┴───────┴──────────┴────────┘
Columns are auto-discovered from the first row. Integer values are right-aligned automatically.
Applying a theme
var theme = new OutputTheme
{
Header = new TextStyle(ConsoleColor.DarkCyan) { Bold = true },
Border = new TextStyle(ConsoleColor.DarkGray),
Cell = new TextStyle(ConsoleColor.White)
};
RenderEngine.Create(json)
.WithTitle("Team Directory")
.WithTheme(theme)
.WithLeftMargin(2)
.Build()
.AsTable();
OutputTheme is also used for Warning (no-data messages) and Error (render failures). The same theme object can be reused across multiple RenderEngine.Create() calls - renderers never write back to the theme.
PredefinedColumn - subset, rename, align
var columns = new List<PredefinedColumn>
{
new PredefinedColumn { FieldName = "Name", Title = "Employee", Width = 14 },
new PredefinedColumn { FieldName = "Role", Title = "Dept", Width = 12 },
new PredefinedColumn { FieldName = "Salary", Title = "Salary $", Width = 10, RightAlign = true }
};
RenderEngine.Create(json)
.WithTitle("Salary Report")
.WithTheme(theme)
.WithPredefinedColumns(columns)
.Build()
.AsTable();
FieldName is the JSON property name (case-insensitive). Title is the column header. Width is the minimum width; the column grows if a value is wider.
Single object → key/value table
When the JSON root is an object, AsTable() renders a two-column key/value layout:
var profile = """
{ "Id": 1, "Name": "Alice", "Role": "Engineer", "Location": "London" }
""";
RenderEngine.Create(profile)
.WithTitle("Employee Profile")
.WithTheme(theme)
.Build()
.AsTable();
Employee Profile
┌──────────┬───────────┐
│ Id │ 1 │
├──────────┼───────────┤
│ Name │ Alice │
├──────────┼───────────┤
│ Role │ Engineer │
├──────────┼───────────┤
│ Location │ London │
└──────────┴───────────┘
JSON syntax highlighting
RenderEngine.Create(profile)
.WithLeftMargin(2)
.WithJsonTheme(JsonTheme.DefaultTheme)
.Build()
.AsFormattedJson();
DefaultTheme uses a VS Code-inspired palette. Every token type has its own TextStyle:
| Token | DefaultTheme colour |
|---|---|
| Key | Cyan |
| StringValue | Green |
| NumberValue | Yellow |
| BooleanValue | DarkCyan |
| NullValue | DarkGray |
| Punctuation | DarkGray |
Custom theme:
var jsonTheme = new JsonTheme
{
Key = new TextStyle(ConsoleColor.White) { Bold = true },
StringValue = new TextStyle(ConsoleColor.Green),
NullValue = new TextStyle(ConsoleColor.DarkRed)
};
HTML output
using var writer = new StreamWriter("report.html", append: false, Encoding.UTF8);
RenderEngine.Create(json)
.WithTitle("Sales Report")
.WithWriter(writer)
.Build()
.AsTable(OutputFormat.Html);
With Bootstrap CSS classes
HtmlGridStyleDescriptor attaches CSS framework class names to table elements alongside the renderer's generated CSS classes:
var htmlTheme = new OutputTheme
{
HtmlGridStyle = new HtmlGridStyleDescriptor
{
TableClasses = { "table", "table-bordered", "table-striped" },
HeaderClasses = { "table-dark" },
RowClasses = { "table-row" },
CellClasses = { "text-nowrap" }
}
};
The renderer always emits a <style> block with generated CSS classes for the theme's colours. HtmlGridStyle class names are appended to element class attributes alongside the generated ones - both sources of styling are active simultaneously.
RTF output
RenderEngine.Create(json)
.WithFileName("report.rtf")
.WithTheme(rtfTheme)
.Build()
.AsTable(OutputFormat.Rtf);
The RTF renderer uses a pre-built colour table derived from the theme's TextStyle values. The resulting .rtf file opens correctly in Microsoft Word and LibreOffice.
Auto-formatted column names
When JSON field names follow camelCase or PascalCase conventions, WithProperCasedTitles() converts them to readable column headers via StringExtensions.ToSpacedWords - no WithPredefinedColumns() required:
// orderId → "Order Id", customerName → "Customer Name", totalAmount → "Total Amount"
RenderEngine.Create(json)
.WithProperCasedTitles()
.Build()
.AsTable();
Works for both array (multi-column) and single-object (key/value) rendering. Has no effect when WithPredefinedColumns() is used - predefined Title values take precedence.
Fragment mode
WithWrappingTags(false) suppresses document-level wrappers so the output can be embedded in an existing page or document:
// HTML: emits <style> + <table>, no <!DOCTYPE>/html/head/body wrappers
// RTF: emits table rows only, no {\rtf1…} header or colour table
RenderEngine.Create(json)
.WithTheme(theme)
.WithWriter(writer)
.WithWrappingTags(false)
.Build()
.AsTable(OutputFormat.Html); // or OutputFormat.Rtf
HTML fragments always include the <style> block so generated CSS classes resolve correctly when injected into a parent page. RTF fragments omit the colour table; the receiving document must declare compatible colour entries at matching indices.
Low-level grid API
Use the grid API directly for non-JSON data or when rows need to be constructed programmatically:
var grid = new DataGrid { LeftMargin = 2 };
grid.AddColumn(new Column("ID", 6, rightAlign: true));
grid.AddColumn(new Column("Name", 24));
grid.AddColumn(new Column("Cost", 10, rightAlign: true));
var renderer = new TextGridRenderer(grid, theme: theme);
renderer.RenderTitle("Inventory");
renderer.RenderHeader();
renderer.RenderRow("001", "Widget Pro", "$49.99");
renderer.RenderRow("002", "Gadget Lite", "$39.99");
renderer.RenderFooter();
DataGrid.UpdateColumnWidth(caption, candidateWidth) dynamically grows a column as rows are added.
Interactive paging
var pagedGrid = new PagedDataGrid<Product>(
grid: grid,
rows: products, // IReadOnlyList<Product>
pageSize: 20,
toValues: p => new[] { p.Id.ToString(), p.Name, p.Price.ToString("F2") },
theme: theme);
pagedGrid.Render();
The user navigates with n (next), p (previous), q (quit). When stdin is redirected, the grid renders all rows and exits without prompting.
Sample screens
Auto-formatted column names (WithProperCasedTitles)
camelCase and PascalCase JSON field names are converted to readable column headers automatically:
| JSON field | Column header |
|---|---|
orderId |
Order Id |
customerName |
Customer Name |
orderTotal |
Order Total |
fulfillmentStatus |
Fulfillment Status |
┌──────────┬───────────────┬─────────────┬────────────────────┐
│ Order Id │ Customer Name │ Order Total │ Fulfillment Status │
├──────────┼───────────────┼─────────────┼────────────────────┤
│ 1001 │ Alice Chen │ 249.99 │ Shipped │
│ 1002 │ Bob Müller │ 89.50 │ Pending │
│ 1003 │ Carol Smith │ 412.00 │ Delivered │
└──────────┴───────────────┴─────────────┴────────────────────┘
PredefinedColumn - subset, renamed, and aligned
Three columns selected and renamed from a wider dataset, with Salary $ right-aligned:
Salary Report
┌──────────────┬──────────────┬──────────┐
│ Employee │ Dept │ Salary $ │
├──────────────┼──────────────┼──────────┤
│ Alice Chen │ Engineering │ 95000 │
│ Bob Müller │ Design │ 72000 │
│ Carol Smith │ Engineering │ 108000 │
└──────────────┴──────────────┴──────────┘
JSON syntax highlighting (AsFormattedJson)
Token colours with DefaultTheme (VS Code-inspired palette):
{
"id": 1,
"name": "Alice Chen",
"role": "Engineer",
"active": true,
"salary": null
}
| Token | Colour |
|---|---|
Keys ("id", "name", ...) |
Cyan |
| String values | Green |
| Number values | Yellow |
true / false
|
Dark cyan |
null |
Dark grey |
| Braces, colons, commas | Dark grey |
Interactive paging (PagedDataGrid)
Inventory (showing 1-20 of 74)
┌─────┬──────────────────────────┬──────────┐
│ ID │ Name │ Cost │
├─────┼──────────────────────────┼──────────┤
│ 001 │ Widget Pro │ $49.99 │
│ 002 │ Gadget Lite │ $39.99 │
│ 003 │ Sensor Module X │ $124.00 │
│ 004 │ Relay Board v2 │ $18.50 │
│ ... │ ... │ ... │
└─────┴──────────────────────────┴──────────┘
n - next page p - previous page q - quit
When stdin is redirected (CI, piped output), all rows render without the navigation prompt.
HTML output
The renderer emits a <style> block with generated short-form CSS class names. Bootstrap or Tailwind class names supplied via HtmlGridStyleDescriptor are appended to the same elements alongside the generated ones:
<style>
.k4f7a2 { color: #008b8b; font-weight: bold; }
.k9e3b1 { color: #f8f8f2; }
</style>
<h2>Team Directory</h2>
<table>
<tr>
<th class="k4f7a2">Id</th>
<th class="k4f7a2">Name</th>
<th class="k4f7a2">Role</th>
<th class="k4f7a2">Salary</th>
</tr>
<tr>
<td class="k9e3b1">1</td>
<td class="k9e3b1">Alice</td>
<td class="k9e3b1">Engineer</td>
<td class="k9e3b1" style="text-align:right">95000</td>
</tr>
</table>
In fragment mode (WithWrappingTags(false)), the <!DOCTYPE>, <html>, <head>, and <body> wrappers are omitted; the <style> block and <table> element are emitted as-is for injection into an existing page.
Architectural notes
-
Theme immutability. Renderers store a snapshot of
HtmlGridStyleDescriptorin a private field at construction time. They never write back toOutputTheme. Multiple threads can share a theme object. -
Format registration.
GridRendererFactoryandObjectTableRendererFactoryuseConcurrentDictionary.Register()is safe from any thread including module initializers. -
TextStylevalue equality.TextStyleimplementsIEquatable<TextStyle>with value-based equality. The HTML renderer uses this to deduplicate CSS class assignments - two properties with identical styles produce one CSS class, not two. -
RTF colour safety.
RtfHelper.ApplyTextStyleusesTryGetValue- colours not registered in the document's colour table are silently skipped rather than throwing.
Top comments (0)