-
Notifications
You must be signed in to change notification settings - Fork 0
Core Output
The output layer of Cursorial.Core is everything you need to emit correct VT escape sequences without
hand-rolling byte strings: a byte sink, a Style/Color value-type vocabulary, a family of pure byte-emitting
writers (SGR, cursor, screen, hyperlinks, sized text), and a capability-aware quantizer that down-converts a style
to what the terminal can actually render. Reach for these directly when you want "print red bold text and move the
cursor" without pulling in the cell buffer and renderer above — or use them as the substrate that
the renderer sits on.
The design goal is composability: every writer targets IBufferWriter<byte>, so the same call feeds a live terminal
(via a session's sink), an in-memory ArrayBufferWriter<byte> for tests, or a recorded trace.
IOutputByteSink is the destination for bytes headed to the terminal — the output counterpart to
IInputByteSource. It exposes a single Writer of type System.IO.Pipelines.PipeWriter, used with the standard
pipe protocol (get memory, advance, flush). A TerminalSession hands you one as session.Output.
Two contract rules:
-
Never call
PipeWriter.Completeyourself. Completing the underlying transport is the sink's job and happens throughIAsyncDisposable.DisposeAsync. -
Do call
FlushAsyncwhen bytes must reach the terminal before further work proceeds — most importantly during capability negotiation, where the app waits for a response before continuing.
var frame = new ArrayBufferWriter<byte>();
SgrEncoder.WriteAbsolute(frame, style);
// ... append more sequences into the same scratch buffer ...
await session.Output.Writer.WriteAsync(frame.WrittenMemory);
await session.Output.Writer.FlushAsync();Style is a readonly record struct carrying foreground/background Color, a TextAttributes flag set, an
UnderlineStyle shape, an independent underline Color, and an optional OSC 8 Hyperlink. default(Style) (also
Style.Default) means "no styling" — both colors default, no attributes. Equality is component-wise, so a stable
style produces no diff. Compose fluently with allocation-free With* helpers; each returns a new value:
var style = Style.Default
.WithForeground(Color.FromRgb(64, 224, 208)) // truecolor
.WithBackground(Color.FromPalette(0)) // ANSI palette index
.WithAttributes(TextAttributes.Bold | TextAttributes.Italic);The helper set: WithForeground, WithBackground, WithAttributes (replace the whole set), AddAttributes /
RemoveAttributes (toggle bits), WithUnderlineStyle, WithUnderlineColor, and WithHyperlink (a Hyperlink
overload and a (uri, id?) overload). Style.Transparent is the compositing identity used by the drawing layer
(all colors Color.Transparent).
Color is a value type discriminated by ColorKind:
-
Color.Default— the terminal's default for whatever slot it lands in (no SGR color emitted). -
Color.FromPalette(byte)— an index into the 256-color palette; indices 0–15 are the ANSI base colors. -
Color.FromRgb(r, g, b)— fully opaque 24-bit truecolor. -
Color.FromRgba(r, g, b, a)— truecolor with an explicit alpha channel.
Convenience factories: Color.FromHex("#RRGGBB") (also 3-digit #RGB), Color.FromHsv(h, s, v, a),
Color.Transparent (Rgb with alpha 0). The Colors static class has named constants (Colors.TrueWhite,
Colors.TrueBlack, ANSI palette entries, and more). Read the channels via Red/Green/Blue/PaletteIndex/
Alpha; query with IsDefault/IsOpaque/IsTransparent.
Alpha semantics. Alpha (0 = transparent, 255 = opaque) only takes effect during layered drawing into a cell
buffer, where the active blending mode composites the source over the existing cell. Alpha is meaningful only for
Rgb colors — palette and default colors short-circuit through compositing because they have no known RGB
equivalent to mix against. Terminal output is always opaque: the cell buffer stores the fully composited result
and the renderer emits SGR for it as an opaque color. (Compositing itself lives one layer up in
Cursorial.Rendering.)
TextAttributes is a [Flags] enum — Bold, Faint, Italic, Underline, Blink, Inverse, Hidden,
Strikethrough, Overline — combined with bitwise OR. The presence of an underline is the Underline flag; the
shape is the separate UnderlineStyle enum (Single, Double, Curly, Dotted, Dashed), because the shapes
don't combine. The shape is only emitted when the Underline flag is set, and it maps to the SGR 4:n colon
sub-parameter form for the extended shapes.
SgrEncoder turns a Style into SGR escape-sequence bytes. It is pure — no capability awareness, no
"previously emitted" state — with three operations:
-
WriteReset(writer)— emitCSI 0 m, resetting all SGR state. -
WriteAbsolute(writer, in style)— emitSGR 0plus the parameters needed to express the style, putting the terminal into a known state regardless of what was active. The right call for one-off styled output. -
WriteDelta(writer, in from, in to)— emit only the parameters that differ between two styles (including reset codes for attributes that turn off). Writes zero bytes when the styles are equal. This is what the diff renderer uses when painting many adjacent runs.
var buffer = new ArrayBufferWriter<byte>();
SgrEncoder.WriteAbsolute(buffer, Style.Default.WithForeground(Color.FromPalette(1)));
// emit your glyph bytes here...
SgrEncoder.WriteReset(buffer);SgrEncoder knows the SGR fine print so you don't have to: the shared SGR 22 reset for both Bold and Faint, the
4:n colon form for extended underline shapes, and the SGR 58/59 underline-color extension.
CursorWriter emits cursor positioning, visibility, and shape sequences. Positions are 0-based in the API,
1-based on the wire — the writers translate at the boundary, so you stay in cell coordinates throughout.
-
WriteMoveTo(writer, column, row)(CUP),WriteColumnAbsolute(CHA),WriteRowAbsolute(VPA). - Relative moves
WriteMoveUp/WriteMoveDown/WriteMoveLeft/WriteMoveRight(a count of zero or less is a no-op). -
WriteSavePosition/WriteRestorePosition(DECSC / DECRC). -
WriteHide/WriteShow(DEC mode 25). -
WriteShape(writer, CursorShape)(DECSCUSR) with the seven xterm shapes, includingCursorShape.Default("restore the terminal default") and the blinking/steady block, underline, and bar forms.
ScreenWriter covers screen-level sequences:
- Clear:
WriteClearScreen,WriteClearScreenAfter/Before,WriteClearScreenAndScrollback; and the line variantsWriteClearLine,WriteClearLineAfter/Before. - Alternate screen buffer:
WriteEnterAlternateScreen/WriteLeaveAlternateScreen(DECSET/DECRST 1049 — saves and restores the cursor). Wrap any full-screen UI in these so the user's shell scrollback reappears on exit. - Autowrap:
WriteDisableAutowrap/WriteEnableAutowrap(DECAWM). Cell-grid renderers should disable autowrap for the session — they always position via CUP, and leaving it on invites a deferred-wrap drift bug on some terminals. - Scroll region:
WriteScrollRegion(writer, topRow, bottomRow)(DECSTBM, 0-based inclusive rows on the API),WriteResetScrollRegion, andWriteScrollUp/WriteScrollDown(writer, lines)(SU/SD).
HyperlinkWriter emits OSC 8 hyperlink anchors:
HyperlinkWriter.WriteHyperlink(buffer, "https://example.com", "Open the docs");
// or open/close manually around styled runs:
HyperlinkWriter.WriteOpen(buffer, "https://example.com", id: "doc-1");
// ...emit link text...
HyperlinkWriter.WriteClose(buffer);Gate emission on TextStylingCapabilities.Hyperlinks if you're targeting legacy terminals. The optional id groups
adjacent or wrapped runs into one logical link — give the same id to every anchor that should highlight together.
TextSizingWriter emits the Kitty OSC 66 text-sizing protocol, driven by a TextSizing value:
var sizing = new TextSizing(Scale: 2, Width: 0); // double-tall, auto width
TextSizingWriter.WriteSplit(buffer, sizing, "BIG TITLE"); // chunks long strings on grapheme boundariesTextSizing carries the spec parameters (Scale/Width/Numerator/Denominator plus Vertical/Horizontal
alignment); default / TextSizing.Normal is plain text and emits an empty metadata block. Write emits a single
sequence (caller respects the 4096-byte payload cap); WriteSplit chunks at grapheme-cluster boundaries when the
payload exceeds the cap, so a multi-codepoint glyph is never split mid-sequence. Sized text bypasses the cell grid —
it's drawn at a fixed position rather than encoded as cell contents. A non-supporting terminal renders the text at
normal size and ignores the OSC.
StyleQuantizer adapts a Style down to what a given terminal can render. Construct it once with an
OutputCapabilities and reuse it — it holds no state beyond its capabilities, and Quantize returns an unchanged
copy of any already-renderable style.
var quantizer = new StyleQuantizer(session.Capabilities.Output);
Style safe = quantizer.Quantize(style); // RGB -> palette/16 as needed, drops unsupported attrs
SgrEncoder.WriteAbsolute(buffer, safe);The typical pipeline is style → StyleQuantizer.Quantize → SgrEncoder.Write{Absolute|Delta} — keeping the
encoder pure (it emits exactly what the style describes) and the quantizer the single place capability decisions
live. What it adjusts:
- Color depth — RGB → 256-color (xterm's 6×6×6 cube + grayscale ramp) when truecolor is unavailable; palette > 15 → 16 via channel-on thresholds.
-
Underline shape — extended shapes collapse to
Singlewhen the terminal lacks extended-underline support; colored underline is dropped when unsupported. - Attributes — drops attribute bits the terminal doesn't honor.
The cell-buffer renderer in
Cursorial.Renderingdoes this for you when constructed with anOutputCapabilities— it holds aStyleQuantizerand runs every cell through it before diffing. Quantize by hand only when you're driving the writers directly.
OutputCapabilities is the aggregate describing which output features a terminal honors, broken into categorized
records (each with a None static for defaults). You get a realized instance from negotiation as
session.Capabilities.Output:
-
Color(ColorCapabilities) —Depth(aColorDepthofNoColor/Ansi16/Ansi256/Truecolor, ordered sodepth >= ColorDepth.Ansi256is meaningful),TruecolorVerified(truecolor was empirically round-tripped, not merely advertised),OscPaletteSet, and the queriedDefaultForeground/DefaultBackground/DefaultCursorColor(nullable — useful for light-vs-dark detection and accent-color picking). -
Styling(TextStylingCapabilities) —Italic,Underline,ExtendedUnderline,ColoredUnderline,Strikethrough,Overline,Hyperlinks. -
TextSizing(TextSizingCapabilities) — the OSC 66 sub-features. -
Graphics,Cursor,Window,Protocol— image protocols, cursor shape/color control, window manipulation, and output-side protocols (synchronized output, etc.).
These are realized capabilities — features a terminal claims but doesn't honor are reported unavailable. See Capability negotiation for how the negotiator builds this surface.
-
Capability negotiation — how
OutputCapabilitiesis detected and realized. - Cell buffer & renderer — the diffing layer that composes these writers.
- Text utilities — grapheme widths and ANSI-aware wrapping that pair with styled output.
-
Input — the input counterpart, with the parallel
IInputByteSourceabstraction.
Cursorial.Core
Cursorial.Rendering
Drawing & Animation
Cursorial.UI
- Overview
- Layout & panels
- Controls
- Styling & themes
- Data binding
- Input & focus
- Windowing
- Animation & transitions
Declarative