XenoAtom.Terminal.UI is designed to feel integrated with XenoAtom.Terminal:
Terminal.Write(Visual) to render a visual once.Terminal.Live(Visual, Func<TerminalLoopResult>) for inline live regions.Terminal.Run(Visual, Func<TerminalLoopResult>) for fullscreen applications.All of these APIs are exposed via C# 14 extension members in src/XenoAtom.Terminal.UI/TerminalExtensions.cs.
Terminal.WriteTerminal.Write(visual) measures, arranges, renders, and writes the final output once.
This is useful for:
Markup)Terminal.LiveTerminal.Live(visual, onUpdate) repeatedly:
onUpdate.The update callback returns a TerminalLoopResult:
Continue: keep running.Stop: stop and remove the live region (cursor restored to where it was before the live region).StopAndKeepVisual: stop and keep the final frame (cursor placed after the live region).You can also use the overload that receives a TerminalRunningContext to access the host kind and terminal instance.
By default, inline live regions do not enable terminal mouse reporting. This preserves the terminal emulator's default mouse behavior (for example: selecting/copying text with the mouse).
If you want mouse interactions (hover, clicks, scroll wheel) for controls hosted in Terminal.Live, enable it:
using XenoAtom.Terminal;
using XenoAtom.Terminal.UI;
using XenoAtom.Terminal.UI.Controls;
Terminal.Live(
new VStack(new Button("Click me")).Padding(1),
onUpdate: () => TerminalLoopResult.Continue,
options: new TerminalLiveOptions { EnableMouse = true, MouseMode = TerminalMouseMode.Move });
Terminal.Live(...) and Terminal.Run(...) now default to LoopMode = Auto.
In auto mode, the host loop is deadline/event-driven:
Post(...), render invalidation, animation work, or async update completion wakes them;15 ms (~66.7 Hz) instead of a fixed Sleep(1) loop;onUpdate, layout, and rendering counts against that budget instead of being added on top of it;This gives better responsiveness and more stable animation pacing than the old fixed 1ms polling loop while still keeping UI work on the calling thread.
UpdateWaitDurationUpdateWaitDuration is no longer the default frame cadence control.
It is now the maximum coarse wait slice used by LoopMode = Polling, which preserves the older periodic re-evaluation
behavior when you explicitly opt into it:
Terminal.Live(
visual,
onUpdate: () => TerminalLoopResult.Continue,
options: new TerminalLiveOptions
{
LoopMode = TerminalLoopMode.Polling,
UpdateWaitDuration = TimeSpan.FromMilliseconds(20)
});
Terminal.Run(
visual,
onUpdate: () => TerminalLoopResult.Continue,
options: new TerminalRunOptions
{
LoopMode = TerminalLoopMode.Polling,
UpdateWaitDuration = TimeSpan.FromMilliseconds(20)
});
Use polling mode only when you specifically want legacy periodic wake-ups.
Larger UpdateWaitDuration values reduce wake frequency, but they also make updates less responsive.
In LoopMode = Auto, changing UpdateWaitDuration does not change the active animation cadence.
Inline live regions measure with an "infinite" height by default, so a simple visual like a VStack("Hello") will
typically only reserve a single row.
If you want the live region to reserve the full visible terminal height (useful for backgrounds, canvases, and layouts
that need vertical space), set the root visual to VerticalAlignment(Align.Stretch):
using XenoAtom.Terminal;
using XenoAtom.Terminal.UI.Controls;
using XenoAtom.Terminal.UI.Layout;
Terminal.Live(
new VStack("Hello").VerticalAlignment(Align.Stretch),
onUpdate: () => TerminalLoopResult.Continue);
During onUpdate, you can write regular output via Terminal.WriteLine(...) / Terminal.Write(...).
That output is placed above the live region. The live region is then re-rendered below.
Terminal UI provides LiveAsync / RunAsync overloads that accept an asynchronous update callback.
Important behavioral notes:
LiveAsync / RunAsync still run the UI loop on the calling thread; the async aspect is the update callback and the ability to await it naturally.onUpdate) is async. Routed event handlers remain synchronous (OnKeyDown, OnPointer*, etc.).Use async updates when you need to integrate with asynchronous APIs (I/O, timers, subprocesses) without blocking the UI loop.
Example: await a timer-like operation and then stop:
using XenoAtom.Terminal;
using XenoAtom.Terminal.UI;
using XenoAtom.Terminal.UI.Controls;
var progress = new State<double>(0);
await Terminal.LiveAsync(
new ProgressBar().Value(progress),
async _ =>
{
await Task.Delay(50);
progress.Value = Math.Min(1.0, progress.Value + 0.05);
return progress.Value >= 1.0
? TerminalLoopResult.StopAndKeepVisual
: TerminalLoopResult.Continue;
});
await, avoid ConfigureAwait(false) in the update callback unless you explicitly marshal back to the UI thread.
context.App.Dispatcher.InvokeAsync(...) to update UI state from a background continuation.Terminal.RunFullscreen runs a UI loop on the main thread:
The onUpdate callback can be used to drive animations (e.g. spinner/progress) or background state updates.
Fullscreen Terminal.Run(...) exits when the configured exit gesture is triggered.
Ctrl+QTerminalRunOptions.ExitGestureSee also: