Skip to content

Getting Started

Mike Strobel edited this page Jun 26, 2026 · 1 revision

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.

Install

Only Cursorial.Core is published to NuGet today; it's the foundation every other layer builds on.

dotnet add package Cursorial.Core

The 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).

Open a session

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:

  • Capabilities is 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 ProcessExit all 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.

Render a frame

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.)

Write takes (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.

Read input

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 CancellationTokenReadAllAsync(ct) — so you can break the loop cleanly from elsewhere.
  • KeyEvent.Text is a ReadOnlyMemory<char>. Match its .Span against the printable text, as above. The Kind distinguishes Down / Up / repeats (repeats arrive as Down with IsRepeat set on terminals that report them).
  • Resizes flow through the same stream, in order with everything else, as ResizeEvent (Columns / Rows in cells). Resize your buffer and re-render.
  • Nothing is silently swallowed — sequences the interpreter doesn't recognize surface as UnknownEvent with the original wire bytes, so you can log or forward them.

Putting it together

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.

Bring your own transports

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.

Going up the stack

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.

See also

Clone this wiki locally