MarkdownWidget
Renders markdown source text as a composed terminal widget tree. Supports CommonMark headings, inline formatting, fenced code blocks, GFM tables, lists, block quotes, images, and interactive links.
Basic Usage
Pass a markdown string to ctx.Markdown() inside any layout:
using Hex1b;
await using var terminal = Hex1bTerminal.CreateBuilder()
.WithHex1bApp((app, options) => ctx => ctx.VScrollPanel(
ctx.Markdown("""
# Welcome to Hex1b
Render **rich markdown** content in your terminal UI with full support
for headings, *emphasis*, `inline code`, and more.
## Features
- **Bold** and *italic* text formatting
- Fenced code blocks with line numbers
- Tables, lists, and block quotes
- Interactive links with Tab navigation
- Embedded images via Kitty Graphics Protocol
## Code Example
\`\`\`csharp
var app = new Hex1bApp(ctx =>
ctx.Markdown("# Hello, World!")
);
await app.RunAsync();
\`\`\`
> The MarkdownWidget parses CommonMark-compatible
> markdown and renders it as a composed widget tree.
---
| Feature | Status |
|:---------------|:-------:|
| Headings | ✅ Done |
| Inline styles | ✅ Done |
| Code blocks | ✅ Done |
| Tables | ✅ Done |
| Links | ✅ Done |
""")
))
.Build();
await terminal.RunAsync();dotnet runSupported Syntax
Headings
Four heading levels are supported, rendered with decreasing visual weight:
Text Formatting
Inline styles can be combined freely within paragraphs:
Supported styles:
**bold**— Bold text*italic*— Italic (rendered as dim)***bold italic***— Combined bold and italic`code`— Inline code with distinct background~~strikethrough~~— Strikethrough text
Code Blocks
Fenced code blocks are rendered as read-only editors with line numbers. The language tag is displayed but syntax highlighting is not yet supported:
Lists
Unordered, ordered, and task lists are all supported, including nesting:
Tables
GFM-style tables with column alignment (:---, :---:, ---:):
Block Quotes
Block quotes support inline formatting and can span multiple lines:
Images
Inline images () are rendered using the Kitty Graphics Protocol when an image loader is provided via .OnImageLoad(). When no loader is configured or the loader returns null, the alt text is displayed as a text fallback. See Image Loading for configuration details.
Thematic Breaks
Horizontal rules (---, ***, ___) render as full-width dividers between content sections.
Focusable Links
Enable Tab navigation across links with .Focusable(children: true). Links become keyboard-focusable and visually highlight when focused:
using Hex1b;
var lastActivated = "";
await using var terminal = Hex1bTerminal.CreateBuilder()
.WithHex1bApp((app, options) => ctx => ctx.VStack(v => [
v.VScrollPanel(
v.Markdown("""
# Focusable Links Demo
Use **Tab** and **Shift+Tab** to navigate between links.
Press **Enter** to activate a focused link.
## Navigation
- [Hex1b on GitHub](https://github.com/mitchdenny/hex1b)
- [Getting Started](/guide/getting-started)
- [Widget Documentation](/guide/widgets/)
## Intra-Document Links
Jump to the [Navigation](#navigation) section above,
or go to [Resources](#resources) below.
## Resources
Check out the [API Reference](/reference/) for details.
""")
.Focusable(children: true)
.OnLinkActivated(args =>
{
lastActivated = $"{args.Kind}: {args.Url}";
args.Handled = true;
})
),
v.Text(string.IsNullOrEmpty(lastActivated)
? "Press Tab to focus a link, Enter to activate"
: $"Activated → {lastActivated}")
]))
.Build();
await terminal.RunAsync();dotnet runLink Activation
Handle link clicks with .OnLinkActivated(). The event args include the URL, display text, and a Kind property that classifies the link:
| Kind | Description | Example |
|---|---|---|
External | HTTP/HTTPS URLs | https://example.com |
IntraDocument | Heading anchors | #my-heading |
Custom | Everything else | mailto:, command: |
Set args.Handled = true to suppress default behavior (opening browser, scrolling to heading).
Image Loading
Load and display embedded images with .OnImageLoad(). The callback receives a Uri and alt text, and returns pixel data:
ctx.Markdown(source)
.OnImageLoad(async (uri, altText) =>
{
var bytes = await File.ReadAllBytesAsync(uri.LocalPath);
// Decode image to RGBA32 pixel data
return new MarkdownImageData(rgbaData, width, height);
})2
3
4
5
6
7
Images are rendered using the Kitty Graphics Protocol. When the loader returns null, the image alt text is shown as a text fallback.
Image Rendering Customization
The .OnImageLoad() callback controls data loading but not how the image widget is rendered. To customize image rendering, use .OnBlock<MarkdownDocument.ParagraphBlock>() to intercept image-only paragraphs. A dedicated image rendering hook is planned — see #242.
Document Source
For live-updating markdown (e.g., an editor preview), pass an IHex1bDocument instead of a string. The widget uses IHex1bDocument.Version for efficient change detection—re-parsing only occurs when the version advances:
Custom Block Rendering
Override how specific block types are rendered with .OnBlock<TBlock>(). Handlers form a middleware chain—call ctx.Default(block) to fall through to the next handler:
ctx.Markdown(source)
.OnBlock<MarkdownDocument.BlockQuote>((ctx, block) =>
ctx.RootContext.Border(b =>
[ctx.Default(block)],
title: "Note"
))2
3
4
5
6
Available block types: Heading, Paragraph, FencedCode, BlockQuote, UnorderedList, OrderedList, Table, ThematicBreak, TaskList.