-
Notifications
You must be signed in to change notification settings - Fork 0
Getting Started
This page gets you from an empty project to a running terminal app: open a session (raw mode + capability
handshake + signal-safe restore), render a frame, and read input. It uses only Cursorial.Core plus the
in-repo Cursorial.Rendering layer — the lowest two layers of the stack. When you're ready for higher-level
abstractions (scenes, a WPF-style UI framework, XAML), the pointers at the bottom send you
there.
Only Cursorial.Core is published to NuGet today; it's the foundation every other layer builds on.
dotnet add package Cursorial.CoreThe layers above it (Cursorial.Rendering, Cursorial.Drawing, Cursorial.Animation, Cursorial.UI,
Cursorial.UI.Xaml) are built and exercised in-repo but not yet packaged — clone the repository and add a
ProjectReference to use them. This quick-start references Cursorial.Rendering that way.
Requirements: .NET SDK 10.0.0 or later, and a real terminal (the happy-path session throws if standard I/O is a pipe — see the BYO overload for those cases).
TerminalSession.OpenAsync() is the one call that does the unglamorous-but-essential work: it puts stdin/stdout
into raw mode, runs the capability handshake, applies the opt-in protocols the negotiator's policy allows (mouse,
focus events, bracketed paste, Kitty keyboard, …), and registers signal handlers so the terminal is restored even
on Ctrl+C or a kill. Disposal reverses everything in LIFO order.
using Cursorial.Terminal;
await using var session = await TerminalSession.OpenAsync();
var caps = session.Capabilities; // realized capabilities — what the terminal actually honors
// session.Input : IAsyncInputDevice
// session.Output : IOutputByteSink (its .Writer is a System.IO.Pipelines.PipeWriter)await using is what triggers the restore — keep the session alive for the whole run of your app and let the
using dispose it. The session is single-shot: each instance opens, runs, and tears down once.
A few things worth knowing:
-
Capabilitiesis the realized set, not the advertised one. Features a terminal claims but doesn't honor are reported unavailable, so you can branch on the flags directly without second-guessing. See Capability negotiation for the full aggregate. -
The signal net is automatic on this overload. SIGINT / SIGTERM / SIGHUP / SIGQUIT and
ProcessExitall restore terminal mode synchronously before exit; handlers are removed on normal disposal. - It throws when standard I/O isn't a TTY (CI without a pseudo-tty, output piped to a file, …). In those environments, use the bring-your-own-transport overload below.
Cursorial.Rendering gives you a CellBuffer (a grid of styled cells) and a stateful FrameRenderer that diffs
successive buffers and emits the minimal byte stream to update the terminal. Construct the renderer with your
output capabilities so it quantizes colors down to what the terminal can show (a stable frame then produces an
empty delta, even on a 16-color terminal).
using System.Buffers;
using Cursorial.Output;
using Cursorial.Rendering;
var buffer = new CellBuffer(Console.WindowWidth, Console.WindowHeight, caps);
var renderer = new FrameRenderer(caps.Output);
// Write(column, row, text, style) — grapheme-aware, wide-cell consistent.
buffer.Write(0, 0, "Hello, terminal 👋",
Style.Default.WithForeground(Color.FromRgb(64, 224, 208))
.WithAttributes(TextAttributes.Bold));
// Diff the buffer into a scratch writer, then flush the frame to the terminal.
var frame = new ArrayBufferWriter<byte>();
renderer.Render(buffer, frame);
await session.Output.Writer.WriteAsync(frame.WrittenMemory);
await session.Output.Writer.FlushAsync();The first Render (and any frame after a resize) is a full redraw; every frame after that is a per-cell delta. The
renderer is the single owner of SGR and cursor state across frames — don't interleave raw escape sequences that
mutate those, or the next diff will desync. (FrameRenderer.Reset() forgets the previous frame and forces a full
redraw if you ever need to.)
Writetakes(column, row)— column first. It computes grapheme widths and lays down the wide-cell continuation for double-width glyphs automatically, so emoji and CJK text advance the cursor correctly.
session.Input is an IAsyncInputDevice — pull events with ReadAllAsync and await foreach. InputEvent is a
sealed record hierarchy, so you pattern-match on the concrete type. (If you'd rather subscribe to classic events,
the push-style IEventInputDevice is the alternative shape; see Input.)
using Cursorial.Input;
using Cursorial.Input.Events;
await foreach (var evt in session.Input.ReadAllAsync())
{
switch (evt)
{
case KeyEvent { Kind: KeyEventKind.Down, Text.Span: "q" }:
return; // quit on 'q'
case KeyEvent { Kind: KeyEventKind.Down } key:
HandleKey(key);
break;
case MouseEvent mouse:
HandleMouse(mouse);
break;
case ResizeEvent resize:
buffer.Resize(resize.Columns, resize.Rows); // re-render after this
break;
}
}Notes that save debugging time later:
-
Pass a
CancellationToken—ReadAllAsync(ct)— so you can break the loop cleanly from elsewhere. -
KeyEvent.Textis aReadOnlyMemory<char>. Match its.Spanagainst the printable text, as above. TheKinddistinguishesDown/Up/ repeats (repeats arrive asDownwithIsRepeatset on terminals that report them). -
Resizes flow through the same stream, in order with everything else, as
ResizeEvent(Columns/Rowsin cells). Resize your buffer and re-render. -
Nothing is silently swallowed — sequences the interpreter doesn't recognize surface as
UnknownEventwith the original wire bytes, so you can log or forward them.
The shape of a minimal app: open the session, render an initial frame, then loop on input — re-rendering whenever state (or the terminal size) changes.
using System.Buffers;
using Cursorial.Input.Events;
using Cursorial.Output;
using Cursorial.Rendering;
using Cursorial.Terminal;
await using var session = await TerminalSession.OpenAsync();
var caps = session.Capabilities;
var buffer = new CellBuffer(Console.WindowWidth, Console.WindowHeight, caps);
var renderer = new FrameRenderer(caps.Output);
var frame = new ArrayBufferWriter<byte>();
async ValueTask Draw()
{
buffer.Clear();
buffer.Write(2, 1, "Press q to quit.", Style.Default.WithForeground(Color.FromRgb(180, 180, 180)));
frame.Clear();
renderer.Render(buffer, frame);
await session.Output.Writer.WriteAsync(frame.WrittenMemory);
await session.Output.Writer.FlushAsync();
}
await Draw();
await foreach (var evt in session.Input.ReadAllAsync())
{
switch (evt)
{
case KeyEvent { Text.Span: "q" }: return;
case ResizeEvent r: buffer.Resize(r.Columns, r.Rows); await Draw(); break;
}
}That's a complete interactive app: raw mode, capability-aware rendering, ordered input, and a guaranteed terminal restore — in a few dozen lines.
The happy-path overload owns terminal-mode state and the standard streams. When you're embedding Cursorial inside a tool that already manages the terminal — or driving it from a recorded byte trace, or running under a custom pseudo-tty — use the BYO overload instead:
await using var session = await TerminalSession.OpenAsync(byteSource, byteSink);It takes an IInputByteSource and an IOutputByteSink and runs the same negotiation + input pipeline over them.
The trade-off: this overload does not touch termios / Windows console-mode flags and does not register
signal handlers — that's the caller's job (the docs are explicit that BYO callers usually have their own strategy,
and double-registration is loud). It also leaves your supplied transports open on disposal; it only reverses the
protocol opt-ins it applied.
Cursorial.Core + Cursorial.Rendering is the "I'll drive my own loop" layer. Each layer above adds structure you
can opt into:
- Drawing — scenes, brushes, charts — a retained-scene compositor over the cell buffer: cached rasters, brushes & gradients, an auto-junctioning box/pen engine, and charts.
- UI Overview — a WPF/Avalonia-style framework: a dependency-property system, element tree with measure/arrange layout, routed input & focus, CSS-like styling, data binding, a control catalog, and windowing. Reach here when you want widgets and layout instead of hand-placing cells.
- XAML — define that UI declaratively, loaded at runtime or compiled by a Roslyn source generator with typed code-behind and compiled bindings.
If you're deciding which layers your project needs, the Architecture page lays out how they stack and what each one owns.
- Architecture — how the layers fit together and what each one owns.
- Core-Input — the full input model: event types, pull vs push delivery, transforms.
- Core-Capability-Negotiation — what the handshake detects and how to read the realized capability aggregate.
- Rendering-Cell-Buffer-and-Renderer — the cell buffer, diffing renderer, blending, and views in depth.
Cursorial.Core
Cursorial.Rendering
Drawing & Animation
Cursorial.UI
- Overview
- Layout & panels
- Controls
- Styling & themes
- Data binding
- Input & focus
- Windowing
- Animation & transitions
Declarative