This document contains the original specifications for the XenoAtom.Terminal.UI project.
Status: Draft (v0.2 - first round)
All names used in this document are preliminary and subject to change.
This is an early design document kept for historical context. It may not reflect the current implementation or current naming.
The following libraries and resources are relevant to help specify this project:
XenoAtom.Ansi library: C:\code\XenoAtom\XenoAtom.AnsiXenoAtom.Terminal library: C:\code\XenoAtom\XenoAtom.Terminal
XenoAtom.Collections library: C:\code\XenoAtom\XenoAtom.Collections
UnsafeDictionary, UnsafeList) for hot paths.The NuGet packages of these libraries will be used for the XenoAtom.Terminal.UI project.
For inspiration:
XenoUI prototype library: C:\code\StackRift\XenoUI
XenoAtom.Terminal.UI aims to be a modern .NET terminal UI framework by combining:
Terminal.Instance).Span<T> / ReadOnlySpan<T> and pooling.TerminalSize).At runtime a TerminalApp orchestrates:
XenoAtom.Terminal (TerminalEvent stream)The key implementation pillars are:
Dispatcher)Visual)BindingManager)TerminalAppTerminalApp is the main entry point for running a terminal UI.
Responsibilities:
Dispatcher and enforce UI-thread affinity.Visual for the session.FullscreenHost or InlineInteractiveHost).Output ownership rule:
TerminalApp session is active, application code must not write directly to the terminal output stream.
A host is responsible for:
XenoAtom.Terminal efficiently.Two hosts are required:
FullscreenHost: full viewport ownership (alternate screen).InlineInteractiveHost: document flow + live region ownership (inline, interactive).The same Visual system is used for both hosts.
Behavior:
UseAlternateScreen()UseRawMode(TerminalRawModeKind.CBreak)HideCursor()EnableMouseInput(...) when supportedEnableBracketedPasteInput() when supported(0,0) to (Columns-1, Rows-1).TerminalResizeEvent: re-measure, re-arrange, repaint.Flush strategy:
InlineInteractiveHost is used for:
The inline host manages two conceptual areas:
Flow area (append-only)
Live region (updated in place)
The host must support appending arbitrary non-interactive UI blocks while a live region exists.
Conceptual API (names TBD):
TerminalApp.Append(Visual block) (or Append(IRenderable block))TerminalApp.AppendMarkup(string markup) convenienceSemantics:
This is intentionally not limited to “logging” or “markup”; any UI block that renders to lines is valid.
The host may reserve vertical space even when started near the bottom of the terminal:
N lines and fewer than N lines remain below the current cursor position,
the host may emit newlines to scroll the terminal and reserve the required space.Dynamic height:
Exit behavior:
Cursor behavior:
TextBox) and is positioned at the caret.For interactive inline sessions, the host/app should configure terminal input similarly to the XenoAtom.Terminal ReadLine editor:
Terminal.StartInput(...) when needed).Terminal.SetInputEcho(false) scope).TerminalInputOptions.TreatControlCAsInput = trueTerminal.EnableMouseInput(...)Terminal.EnableBracketedPasteInput()The UI runtime is single-threaded.
Dispatcher runs the UI loop and schedules work:
Invoke, BeginInvoke, InvokeAsyncRun(), Exit()DispatcherObject base class provides VerifyAccess() / CheckAccess() semantics.VisualVisual is the base type for all UI components.
Key requirements:
protected int ChildrenCount { get; }protected Visual GetChild(int index)A LayoutVisual (or similar) participates in measure/arrange.
Layout protocol:
Measure(availableSize) returns desired size (cells).Arrange(finalRect) commits bounds and prepares for rendering.The protocol should be closer to SwiftUI in spirit (simple, composable), but implementable in a retained tree.
We want:
Computed<T> (renamed from FuncObservable<T>)Computed<T> represents a value produced by a function (or a constant) that:
Usage examples:
new TextBlock(() => $"Count: {model.Count}")new Button(() => model.IsComplex ? new TextBlock("Complex") : new TextBlock("Simple"))To avoid general diffing, a computed child is managed through a slot:
Visual.Dynamic lists are handled via dedicated controls (ItemsControl/ListBox) rather than arbitrary diffing of children lists.
The binding model tracks dependencies automatically:
User code:
[Bindable]
public partial bool IsActive { get; set; }
The source generator generates a property wrapper that routes reads/writes through a BindingManager.
Generated shape (simplified):
partial class MyComponent : MyComponent.IBindings
{
private bool _isActive;
public bool IsActive
{
get => BindingManager.Current.GetValue(ref _isActive, __IsActive__Accessor.Instance);
set => BindingManager.Current.SetValue(ref _isActive, value, __IsActive__Accessor.Instance);
}
// Access to bindings is provided via a C# extension member (this.Bind).
Binding<bool> IBindings.IsActive => new Binding<bool>(this, __IsActive__Accessor.Instance);
public interface IBindings : BindableObject.IBindings
{
Binding<bool> IsActive { get; }
}
private sealed class __IsActive__Accessor : BindingAccessor<bool>
{
public static __IsActive__Accessor Instance { get; } = new(nameof(IsActive), StaticGetter, StaticSetter);
private static bool StaticGetter(object instance) => ((MyComponent)instance).IsActive;
private static void StaticSetter(object instance, bool value) => ((MyComponent)instance).IsActive = value;
}
}
Notes:
Binding<T> is a lightweight struct used to reference a bindable property without allocations.IBindings interface exposes binding handles (e.g. this.Bind.IsActive) without reflection.BindingAccessor<T> is a generated, cached descriptor for the property (name + getter/setter delegates).BindableObject base (name TBD) which is a DispatcherObject, or they implement IDispatcherObject.BindingManagerBindingManager is attached to a Dispatcher and is accessed via BindingManager.Current (thread-static / dispatcher-local).
Responsibilities:
Tracking is scoped:
BeginTracking(context) starts capturing reads.EndTracking() ends capturing and returns the collected dependency set.At runtime:
get registers a dependency for the current evaluation contextset notifies dependentsDependencies must be tracked per context:
Invalidation rules:
Environment values (SwiftUI-like) allow theming/styling and other contextual values:
Environment associated with visuals.Storage requirements:
XenoAtom.Terminal)The app consumes terminal events:
TerminalKeyEventTerminalTextEventTerminalPasteEvent (bracketed paste)TerminalMouseEventTerminalResizeEventTerminalSignalEventThese events are considered raw and are not delivered directly to components. They are translated by the runtime/host into higher-level routed UI events.
The UI uses routed events for delivering input and control events through the visual tree.
Routing strategies:
Event args:
RoutedEventArgs (name TBD)
OriginalSource (the initial target)Source (current handler target)Handled flag (when set, stops routing depending on strategy and handler options)Event registration and handlers:
RoutedEvent<TEventArgs> (name TBD) represents an event identifier and contains metadata (name, routing strategy).Visual exposes:
AddHandler(RoutedEvent<T>, handler, handledEventsToo: bool = false)RemoveHandler(...)Button.Click).Routed events should be definable via source generator attributes to reduce boilerplate.
Example user code:
[RoutedEvent(RoutingStrategy.Preview | RoutingStrategy.Bubble)]
protected virtual void OnPointerPressed(PointerPressedEventArgs e)
{
}
Generates code similar to:
public static readonly RoutedEvent<PointerPressedEventArgs> PointerPressedEvent =
RoutedEvent.Register<MyVisual, PointerPressedEventArgs>(
nameof(PointerPressed),
static (sender, args) => (sender as MyVisual)?.OnPointerPressed(args),
RoutingStrategy.Preview | RoutingStrategy.Bubble);
public event EventHandler<PointerPressedEventArgs> PointerPressed
{
add => AddHandler(PointerPressedEvent, value);
remove => RemoveHandler(PointerPressedEvent, value);
}
The runtime converts raw terminal events into UI routed events:
TerminalMouseEvent -> pointer events (targeted by hit testing):
PointerMoved, PointerPressed, PointerReleased, PointerWheelChanged (names TBD)TerminalKeyEvent -> key events (targeted at the focused element):
KeyDown / KeyPressed (name TBD)TerminalTextEvent -> TextInput (targeted at focused element)TerminalPasteEvent -> Paste (targeted at focused element)TerminalResizeEvent -> runtime invalidation (measure/arrange/re-render) and optional ViewportChanged notification.TerminalSignalEvent -> by default mapped to a command (e.g. cancel/exit), configurable.Controls may synthesize higher-level events from lower-level routed events:
Button:
PointerPressed + PointerReleased to produce a Click routed event.Click if the release occurs within bounds (policy TBD).Requirements:
IsTabStop, TabIndex, etc.)We need a command system that can be used for:
Key bindings should be based on KeyGesture and support scoping.
All layout/rendering is cell-based.
Initial (MVP) requirements:
TerminalTextUtility for width (GetWidth) and cell-to-index mapping (TryGetIndexAtCell).Known limitations (to refine later):
TerminalTextUtility for consistency in tests.Rendering produces a set of styled characters in a grid (cells), and a cursor state.
We should support two render targets:
Flush strategies:
Clipboard access uses XenoAtom.Terminal:
TerminalInstance.Clipboard / Terminal.ClipboardWe should define a reusable line editor engine usable by:
TextBox (single-line, MVP)XenoAtom.Terminal ReadLine editor (shared/backported to avoid duplication)Core features (MVP subset is acceptable):
TerminalClipboardThe first MVP should enable a compelling demo:
VStack, HStack, BorderTextBlockButton, CheckBox, TextBox (single-line)ListBox (selection + scrolling)ScrollViewer (if not embedded into ListBox)ProgressBar and/or Spinner (common for inline live regions)Control states to support:
The styling system should be environment-driven:
Deterministic tests are a core requirement:
VirtualTerminalBackend / InMemoryTerminalBackend.PushEvent.Diagnostics:
We will provide an incremental Roslyn Source Generator for:
[Bindable] propertiesMVP note:
[Bindable]) if necessary.TerminalApp runtime + inline host with:
VirtualTerminalBackendDocument visual.