Skip to main content

Hosting and options

AddArlecchino

builder.Services.AddArlecchino(options =>
{
options.MinimumWidth = 60;
options.MinimumHeight = 16;
});

One call registers everything and returns a ArlecchinoBuilder for the rest of the setup. The services it puts in the container are singletons:

ServiceRole
ArlecchinoOptionsThe configured options; resolving it also installs the theme palette
IArlecchinoTerminalSystemTerminal unless replaced
SurfaceThe renderer, with padding taken from the options
KeyTextCharacter resolution for the configured input mode
ArlecchinoStateOutput line, modal, file picker request
RepaintThe "this frame is stale" signal the render loop waits on
Navigator, ViewResolver, IArlecchinoViewFactoryRouting and view construction
CommandRegistryRegistered commands
ScreenFrame composition
InputRouterKey dispatch
ArlecchinoKeymap, ArlecchinoStringsThe keymap and the wording, for widgets and stores built by the container
TickerWork on a clock, run between frames
NotificationsWhat the application has said lately, behind the output row
TimeProviderWhere the ticker and the notifications read the time; a test host replaces it

IArlecchinoTerminal is registered with TryAdd, so registering your own before AddArlecchino also wins.

Options

OptionDefaultEffect
TargetFramesPerSecond60Frame rate of the render loop
MinimumWidth / MinimumHeight100 / 30Below this the frame is replaced by a size notice
HorizontalPadding / VerticalPadding2 / 1Gutters applied by the surface
UseAlternateScreentrueEnter the alternate screen buffer and hide the cursor while running
HintsHintsShown.AlwaysDraw the Keys box from whatever holds the focus. WhileWaiting draws it only while a chord is half typed, Never not at all
ShowOutputLinetrueDraw ArlecchinoState.Output on the last row
CommandPaletteKey':'Key that opens the palette
TextInputNativeHow typed characters are resolved
MouseInputfalseReport clicks, drags and the wheel to views
BracketedPastetruePasted text arrives as one block instead of a burst of keys
EscapeTimeout25 msHow long the reader waits for the rest of an escape sequence
Keymapnew ArlecchinoKeymap()Keys the framework itself reacts to
ThemeThemePalette.ArlecchinoColor roles
GraphSymbolsBrailleWhich characters charts are drawn with
ImageProtocolAutoHow a Picture reaches the terminal
AskTerminaltrueAsk the terminal what it can draw before the first frame
TerminalAnswer120 msHow long to wait for that answer before giving up on it
CellWidth / CellHeight10 / 20Pixels a cell is taken to be when the terminal will not say
Stringsnew ArlecchinoStrings()User-visible text
StartRouteViewRoute.NoneRoute shown on the first frame
InputPollInterval8 msSleep between key polls when the input queue is empty
NotificationTimeout5 sHow long a message holds the output row
NotificationLifetime10 minHow long it stays readable on the notifications screen

Builder API

CallEffect
AddView<T>(route)Registers a view resolved through the container
AddView(route, factory)Registers a view built by your own factory delegate
AddViewFactory<T>()Adds an IArlecchinoViewFactory — this is what AddGeneratedViews() does
AddStore<T>()Registers one store by hand — singleton, or scoped when it implements IArlecchinoScopedStore
AddGeneratedStores()Generated: registers every IArlecchinoStore in the project, singleton or scoped — see Source generator
AddGeneratedCommands()Generated: registers every IArlecchinoCommand in the project as a singleton — see Source generator
AddGeneratedWidgets()Generated: registers every IArlecchinoWidget of the project as a singleton — see Source generator
AddWidget<T>()Registers one widget by hand as a singleton; an alternative to AddGeneratedWidgets(), not a layer on top
AddCommand<T>()Registers one IArlecchinoCommand by hand; an alternative to AddGeneratedCommands(), not a layer on top
AddStartup<T>()Registers an IArlecchinoStartup
StartAt(route)Sets StartRoute; also takes a plain string
UseTextInput(mode), UseKeysByPosition()Keyboard layout handling
UseKeymap(keymap)Replaces the key bindings
UseNotifications(key, timeout, lifetime)Turns the output row on, sets both timeouts and the key that opens the notifications screen
WithoutNotifications()Leaves the output row off
UseMouse()Turns on mouse reporting
UseTheme(palette)Replaces the color palette
UseStrings(strings)Replaces user-visible text
UseTerminal<T>()Replaces IArlecchinoTerminal
WithoutHostedService()Drops the render loop, leaving the services
Services, OptionsThe underlying collection and options, for anything not covered above

Startup routes

StartAt is a constant. When the first route depends on runtime state — a missing config file sending the user to a setup view, say — implement IArlecchinoStartup:

public sealed class ChooseStartView : IArlecchinoStartup
{
private readonly Settings _settings;

public ChooseStartView(Settings settings) => _settings = settings;

public ViewRoute Start() => _settings.Exists ? ViewKind.Default : ViewKind.Setup;
}

Register with .AddStartup<ChooseStartView>(). Every startup runs when the hosted service begins, in registration order, each one applied to the navigator.

Every ArlecchinoAsyncStore is started at the same moment, with the token canceled when the host stops — started, not awaited: the first frame is drawn while they load, and each store says where it got to.

The two loops

The hosted service runs two of them at once. One reads the terminal — a blocking, timing-sensitive job, because the rest of an escape sequence arrives a few milliseconds after its Esc. The other draws at the configured rate, and only when a frame is owed.

They do not share state: the reader queues what it read, and the frame loop drains the queue at the top of every turn, before the ticker and before drawing. The frame loop is that side in full, Ticker included.

Failures and shutdown

A terminal application that dies mid-frame leaves the user in the alternate screen with a hidden cursor and no prompt, so the hosted service treats that as its job:

  • Ctrl+C is intercepted (Console.CancelKeyPress) and turned into IHostApplicationLifetime.StopApplication, so the normal shutdown path runs instead of the process being torn down.
  • The terminal is restored on every exit — normal stop, cancellation, an unhandled error in the loop, ProcessExit, or AppDomain.UnhandledException.
  • An exception thrown by a view's Draw is logged through ILogger and reported on the output line via ArlecchinoStrings.ViewFailed; the frame still renders and the application keeps running.
  • The same applies to Handle and to modal callbacks: InputRouter catches, logs and reports rather than letting one bad key kill the process.
  • POSIX signals are answered too. SIGTERM and SIGHUP give the screen back before the process goes, SIGTSTP (Ctrl+Z) restores the terminal before the shell suspends the process, and SIGCONT puts the modes back and repaints from scratch when it is resumed. On Windows only SIGTERM exists, and signals the platform does not have are skipped rather than throwing.

Screen.RedrawEverything() is what the resume path uses, and it is public for the same reason: when something outside the framework has written over the screen, the next frame has to be a full paint rather than a difference against a picture that is no longer there.

AddArlecchino calls AddLogging(), so ILogger is always resolvable — and registers a logger provider of its own, because a console logger would write into the middle of a frame. Where those lines go, and what to attach to a bug report, is on Diagnostics.

Running without the hosted service

WithoutHostedService() leaves every service in place but removes the loop, which is how a single frame is rendered headlessly — for screenshots, layout checks or tests:

var services = new ServiceCollection();
services.AddArlecchino().AddGeneratedViews().AddGeneratedStores().WithoutHostedService();
services.AddSingleton<IHostApplicationLifetime, NullLifetime>();

using var provider = services.BuildServiceProvider();

provider.GetRequiredService<Surface>().SetFixedSize(130, 30);
provider.GetRequiredService<Navigator>().Apply(ViewKind.Default);
provider.GetRequiredService<Screen>().DrawOnce();

SetFixedSize pins the frame so nothing asks the real terminal for its size, and DrawOnce composes exactly one frame to stdout. IHostApplicationLifetime only needs a stand-in when your commands take it. The sample wires this up behind --frame — see Getting started.