docfx/docs/drivers.md
The driver model is the mechanism by which Terminal.Gui supports multiple platforms. Windows, Mac, Linux, and unit test environments are all supported through a modular, component-based architecture.
Terminal.Gui v2 uses a sophisticated driver architecture that separates concerns and enables platform-specific optimizations while maintaining a consistent API. The architecture is based on the Component Factory pattern and uses multi-threading to ensure responsive input handling.
Important: xref:Terminal.Gui.ViewBase.View subclasses should not access xref:Terminal.Gui.App.Application's Driver. Use the View APIs instead:
Move(col, row) for positioningAddRune() and AddStr() for drawingApp.Screen for screen dimensionsTerminal.Gui provides three console driver implementations:
| ansi | dotnet | windows | |
|---|---|---|---|
| Theme | Default driver for Unix/macOS and a showcase driver for all platforms. Pure ANSI implementation. Ideal for testing/CI. Deterministic behavior with virtual time support. | Cross-platform managed .NET driver. Simplest implementation using System.Console API. Works with .NET BCL only. | High-performance Windows-only driver. Native Win32 Console API. Direct access to Windows-specific features. |
| Input Model | Reads raw ANSI sequences, parses to Terminal.Gui events | Reads ConsoleKeyInfo from System.Console, converts to Terminal.Gui events | Reads INPUT_RECORD structures directly, converts to Terminal.Gui events |
| Unix Read APIs | poll(STDIN_FILENO, ...), read(STDIN_FILENO, buffer, len), tcgetattr()/tcsetattr() for raw mode via UnixRawModeHelper | N/A (uses .NET Console.ReadKey() which internally delegates to platform APIs) reads char | N/A (Windows-only) |
| Windows Read APIs | P/Invokes ReadFile() reads char | N/A (uses .NET Console.ReadKey() which internally delegates to platform APIs) | P/Invokes ReadConsoleInputW() reads INPUT_RECORD, GetConsoleMode()/SetConsoleMode() enables mouse input and raw mode |
| Output Model | Pure ANSI escape sequences | Managed .NET + ANSI sequences (when VT mode enabled) | Direct character output via Win32 API with double buffering |
| Unix Write APIs | write() syscall to stdout (fd 1) | N/A (uses .NET Console.Write() which internally delegates to platform APIs) | N/A (Windows-only) |
| Windows Write APIs | P/Invokes WriteFile() | N/A (uses .NET Console.Write() which internally delegates to platform APIs) | P/Invokes WriteConsoleW(), CreateConsoleScreenBuffer()/SetConsoleActiveScreenBuffer() for double buffering, SetConsoleTextAttribute() |
| Screen Model | Configurable via Driver.SizeDetection. Default (AnsiQuery): ANSI CSI 18t query, throttled to 500 ms. Polling: ioctl(TIOCGWINSZ) on Unix, Console API on Windows. | Polling-based re-size: Console.WindowWidth/Console.WindowHeight queried periodically. Falls back to 80x25 on IOException. | Event-based re-size: WINDOW_BUFFER_SIZE_EVENT received in input stream via ReadConsoleInputW(). Immediate resize notification. GetConsoleScreenBufferInfoEx() queries dimensions. |
| Cursor Handling | ANSI sequences: DECTCEM (CSI ? 25 h/l) for show/hide, DECSCUSR (CSI Ps SP q) for style. Full CursorStyle support. | ANSI sequences (same as ansi driver). Falls back to Console.SetCursorPosition() on Windows. | • Legacy mode: Win32 CONSOLE_CURSOR_INFO (size percentage only, no blinking control). |
• Modern VT mode: ANSI sequences (same as ansi driver). Full CursorStyle support. | |||
| Advantages | • Cross-platform (all platforms) | ||
| • Pure, clean implementation | |||
| • Perfect for testing/CI | |||
| • Virtual time support | |||
| • Deterministic behavior | |||
| • Configurable size detection | • Cross-platform (all platforms) | ||
| • Maximum compatibility | |||
| • Simple implementation | |||
| • No P/Invoke; Works with .NET BCL | |||
| • Highest performance on Windows | |||
| • Immediate resize detection | |||
| Disadvantages | • Requires proper ANSI support | • Lower performance (managed overhead) | |
| • Limited feature set | |||
• System.ReadKey has bugs on Windows | |||
| • Polling-based resize | • Windows-only | ||
| • More complex P/Invoke code |
The appropriate driver is automatically selected based on the platform when xref:Terminal.Gui.App.Application's Init() is called:
WindowsDriverAnsiDriverExplicitly specify a driver in several ways:
Method 1: Set ForceDriver using Configuration Manager
{
"Application.ForceDriver": "ansi"
}
Method 2: Pass driver name to Init
// Use type-safe constants from DriverRegistry.Names
Application.Init(driverName: DriverRegistry.Names.ANSI);
Method 3: Set ForceDriver on instance
using Terminal.Gui.Drivers;
using (IApplication app = Application.Create())
{
app.ForceDriver = DriverRegistry.Names.ANSI;
app.Init();
}
The ForceDriver property is a configuration property marked with [ConfigurationProperty], which means:
config.json)ForceDriverChanged event// Subscribe to driver changes
Application.ForceDriverChanged += (_, e) =>
{
Logging.Information($"Driver changed: {e.OldValue} → {e.NewValue}");
};
// Change driver
Application.ForceDriver = DriverRegistry.Names.ANSI;
Terminal.Gui provides several methods to discover available drivers at runtime through the Driver Registry:
// Get driver names (AOT-friendly, no reflection)
IEnumerable<string> driverNames = Application.GetRegisteredDriverNames();
Logging.Information("Available drivers:");
foreach (string name in driverNames)
{
Logging.Information($" - {name}");
}
// Output:
// Available drivers:
// - dotnet
// - windows
// - ansi
For more detailed information about each driver:
// Get driver metadata
foreach (DriverRegistry.DriverDescriptor descriptor in Application.GetRegisteredDrivers())
{
Logging.Information($"{descriptor.DisplayName}");
Logging.Information($" Name: {descriptor.Name}");
Logging.Information($" Description: {descriptor.Description}");
Logging.Information($" Platforms: {string.Join(", ", descriptor.SupportedPlatforms)}");
Logging.Information("");
}
// Output:
// Windows Console Driver
// Name: windows
// Description: Optimized Windows Console API driver with native input handling
// Platforms: Win32NT, Win32S, Win32Windows
//
// .NET Cross-Platform Driver
// Name: dotnet
// Description: Cross-platform driver using System.Console API
// Platforms: Win32NT, Unix, MacOSX
// ...
Validate driver names (useful for CLI argument validation):
string userInput = args[0];
if (Application.IsDriverNameValid(userInput))
{
Application.Init(driverName: userInput);
}
else
{
Logging.Information($"Invalid driver: {userInput}");
Logging.Information($"Valid options: {string.Join(", ", Application.GetRegisteredDriverNames())}");
}
Use type-safe constants in code:
using Terminal.Gui.Drivers;
// Type-safe driver names from DriverRegistry.Names
string driverName = DriverRegistry.Names.ANSI; // "ansi"
app.Init(driverName);
Note: The legacy GetDriverTypes() method is now obsolete. Use GetRegisteredDriverNames() or GetRegisteredDrivers() instead for AOT-friendly, reflection-free driver discovery.
Terminal.Gui v2 uses a Driver Registry pattern for managing available drivers without reflection. The registry provides:
DriverRegistry.Names constantsDriverRegistry.Register()// Access well-known driver name constants
string windowsDriver = DriverRegistry.Names.WINDOWS; // "windows"
string dotnetDriver = DriverRegistry.Names.DOTNET; // "dotnet"
string ansiDriver = DriverRegistry.Names.ANSI; // "ansi"
// Get detailed driver information
if (DriverRegistry.TryGetDriver(DriverRegistry.Names.WINDOWS, out DriverRegistry.DriverDescriptor descriptor))
{
Logging.Information($"Found: {descriptor.DisplayName}");
Logging.Information($"Description: {descriptor.Description}");
// Check if supported on current platform
bool isSupported = descriptor.SupportedPlatforms.Contains(Environment.OSVersion.Platform);
}
// Get drivers supported on current platform
foreach (DriverRegistry.DriverDescriptor driver in DriverRegistry.GetSupportedDrivers())
{
Logging.Information($"{driver.Name} - {driver.DisplayName}");
}
// Get the default driver for current platform
DriverRegistry.DriverDescriptor defaultDriver = DriverRegistry.GetDefaultDriver();
Logging.Information($"Default driver: {defaultDriver.Name}");
The v2 driver architecture uses the Component Factory pattern to create platform-specific components. Each driver has a corresponding factory that implements IComponentFactory<T>:
NetComponentFactory - Creates components for DotNetDriverWindowsComponentFactory - Creates components for WindowsDriverAnsiComponentFactory - Creates components for AnsiDriver (all platforms)Each factory is responsible for:
IInput<T>, IOutput, IInputProcessor, etc.)GetDriverName() (single source of truth for driver identity)DriverRegistry with metadataThe factory pattern ensures proper component creation and initialization while maintaining clean separation of concerns.
Each driver is composed of specialized components, each with a single responsibility:
Reads raw console input events from the terminal on a dedicated input thread. The generic type T represents the platform-specific input record type:
ConsoleKeyInfo for DotNetDriver (from Console.ReadKey())WindowsConsole.InputRecord for WindowsDriver (from ReadConsoleInputW())char for AnsiDriver (raw bytes from read() syscall or ReadFile())Input runs on a separate thread managed by MainLoopCoordinator, continuously reading from the console and queueing events into a thread-safe ConcurrentQueue<T> to avoid blocking the UI thread.
Renders the output buffer to the terminal. Platform-specific implementations:
Console.Write() with ANSI sequences (VT mode on Windows)WriteFile() (Windows) or write() (Unix)Responsibilities include:
Translates raw console input into Terminal.Gui events:
MouseEventArgs for mouse inputIKeyConverter<T> to translate TInputRecord to xref:Terminal.Gui.Input.Key:AnsiKeyConverter - For char input (AnsiDriver)NetKeyConverter - For ConsoleKeyInfo input (DotNetDriver)WindowsKeyConverter - For WindowsConsole.InputRecord input (WindowsDriver)Manages the screen buffer and drawing operations:
Contents array (what should be displayed)AddRune(), AddStr(), Move(), FillRect()Detects terminal size changes and raises SizeChanged events when the terminal is resized.
The concrete implementation that implements IDriver and coordinates all the components. This is what gets assigned to xref:Terminal.Gui.App.Application's Driver.
The driver architecture employs a multi-threaded design for optimal responsiveness:
┌─────────────────────────────────────────────┐
│ IApplication.Init() │
│ Creates MainLoopCoordinator<T> with │
│ ComponentFactory<T> │
└────────────────┬────────────────────────────┘
│
├──────────────────┬───────────────────┐
│ │ │
┌────────▼────────┐ ┌──────▼─────────┐ ┌──────▼──────────┐
│ Input Thread │ │ Main UI Thread│ │ Driver │
│ │ │ │ │ Facade │
│ IInput │ │ ApplicationMain│ │ │
│ reads console │ │ Loop processes │ │ Coordinates all │
│ input async │ │ events, layout,│ │ components │
│ into queue │ │ and rendering │ │ │
└─────────────────┘ └────────────────┘ └─────────────────┘
Input Thread: Started by MainLoopCoordinator, runs IInput.Run() which continuously reads console input and queues it into a thread-safe ConcurrentQueue<T>.
Main UI Thread: Runs ApplicationMainLoop.Iteration() which:
IInputProcessorAnsiRequestScheduler.RunSchedule(...)IOutputThis separation ensures that input is never lost and the UI remains responsive during intensive operations.
When xref:Terminal.Gui.App.Application's Init() is called:
MainLoopCoordinator<T> with the appropriate ComponentFactory<T>IInput<T>IOutputDriverImpl and assigns to xref:Terminal.Gui.App.IApplication's DriverWhen xref:Terminal.Gui.App.IApplication's Shutdown() is called:
KittyKeyboardProtocolDetector.Disable() is sent to restore terminal keyboard modeIOutput is disposedThe main driver interface that the framework uses internally. IDriver is organized into logical regions:
Init(), Refresh(), End() - Core lifecycle methodsGetName(), GetVersionInfo() - Driver identificationSuspend() - Platform-specific suspend supportInputProcessor - Processes input into Terminal.Gui eventsOutputBuffer - Manages screen buffer stateSizeMonitor - Detects terminal size changesClipboard - OS clipboard integrationScreen, Cols, Rows, Left, Top - Screen dimensionsSetScreenSize(), SizeChanged - Size managementAppModel - Whether the driver operates in FullScreen or Inline modeInlinePosition - The terminal cursor position (Point) where the inline region starts. Y is the row discovered via CPR at startup; X is reserved for future use. Only meaningful when AppModel == Inline.SupportsTrueColor - 24-bit color capabilityForce16Colors - Force 16-color modeDefaultAttribute - The terminal's actual default foreground/background colors, detected at startup via OSC 10/11 queries. Used by xref:Terminal.Gui.Drawing.Scheme to resolve xref:Terminal.Gui.Drawing.Color's None during role derivation. null if the terminal didn't respond (e.g., legacy console).ColorCapabilities - The terminal's color capability level (NoColor, Colors16, Colors256, TrueColor), detected from $TERM, $COLORTERM, and other environment variablesThe ANSI driver's terminal-size detection strategy is controlled by Driver.SizeDetection (a [ConfigurationProperty]):
| Mode | Mechanism | When to use |
|---|---|---|
AnsiQuery (default) | Sends CSI 18t, parses ESC[8;h;wt response. Async, ~500 ms throttle. | Most terminals. Works everywhere ANSI is supported. |
Polling | ioctl(TIOCGWINSZ) on Unix, Console.WindowWidth/Height on Windows. Synchronous. | When the ANSI response does not reflect the actual terminal size (e.g., some SSH configurations). |
Set via JSON configuration:
{ "Driver.SizeDetection": "Polling" }
Or programmatically before Init():
Driver.SizeDetection = SizeDetectionMode.Polling;
Contents - Screen buffer arrayClip - Clipping regionClearContents(), ClearedContents - Buffer managementOutputBufferImpl.InlineMode = true causes ClearContents() to initialize cells with IsDirty = false. Only cells explicitly drawn by the app are flushed to the terminal — the rest of the visible terminal stays untouched.When AppModel == Inline, the ANSI driver changes behavior in several ways:
| Aspect | FullScreen | Inline |
|---|---|---|
| Screen buffer | Alternate buffer (CSI ?1049h) | Primary (scrollback) buffer |
ClearContents() | All cells dirty | All cells clean |
| Cursor positioning | Absolute terminal coordinates | Offset by App.Screen.Y |
| Shutdown | Emit CSI ?1049l (restore) | Move cursor below inline region |
Startup gate (AnsiStartupGate): In inline mode, AnsiSizeMonitor sends a CPR query (ESC[6n) to discover the cursor row. The startup gate defers the first LayoutAndDraw until both the CPR response and terminal size query complete. If the terminal never responds, the gate times out and rendering proceeds from row 0.
AnsiOutput changes: SetCursorPositionImpl adds App.Screen.Y to all row coordinates so output lands at the correct terminal rows. On init, CSI ?1049h and CSI 2J are skipped. On shutdown, CSI ?1049l is skipped and the cursor is positioned at the bottom of the inline region.
Col, Row, CurrentAttribute - Drawing stateMove(), AddRune(), AddStr(), FillRect() - Drawing operationsSetAttribute(), GetAttribute() - Attribute managementWriteRaw(), GetSixels() - Raw output and graphicsRefresh(), ToString(), ToAnsi() - Output renderingDrivers implement cursor control through IDriver which delegates to IOutput:
SetCursor(Cursor cursor) - Set cursor position and style atomicallyGetCursor() - Get current cursor stateSetCursorNeedsUpdate(bool) / GetCursorNeedsUpdate() - Optimization flag for cursor updates[!NOTE] The cursor system is managed by
ApplicationNavigation. Drivers implement the low-level cursor control; views use the xref:Terminal.Gui.ViewBase.View'sCursorproperty. See Cursor Management for complete details.
KeyDown - Raised for key press and repeat eventsKeyUp - Raised for key release events (only when the driver supports it — currently the ANSI driver with kitty keyboard protocol)MouseEvent - Raised for mouse input events[!NOTE] For testing, use the input injection API. See Input Injection for details.
The ANSI driver detects and enables the kitty keyboard protocol at startup when the terminal supports it. This provides:
Key.EventTypeKey.IsModifierOnly == true and Key.ModifierKey identifying the specific modifier (e.g. ModifierKey.LeftShift)Detection uses the KittyKeyboardProtocolDetector which queries the terminal via CSI ?u. If supported, the protocol is enabled with flags 1+2. On shutdown, the protocol is disabled to restore normal terminal behavior.
Three ANSI parser patterns handle kitty event types: KittyKeyboardPattern (CSI u), CsiKeyPattern (CSI ~), and CsiCursorPattern (CSI cursor letters). Release events route to KeyUp; press and repeat route to KeyDown.
QueueAnsiRequest() - ANSI request handlingNote: The driver is internal to Terminal.Gui. View classes should not access Driver directly. Instead:
Screen to get screen dimensionsMove() for positioning (with viewport-relative coordinates)AddRune() and AddStr() for drawingTerminal.Gui/ViewBase/) can access Driver when needed for framework implementationThe driver selection logic in ApplicationImpl.Driver.cs uses the Driver Registry to select and instantiate drivers:
Selection Priority Order:
IComponentFactory is explicitly provided to ApplicationImpl, it determines the driver via factory.GetDriverName()driverName parameter passed to Init() is looked up in the registryApplication.ForceDriver property is checked and looked up in the registryDriverRegistry.GetDefaultDriver() selects based on current platform:
WindowsDriverAnsiDriverDotNetDriver (fallback)Driver Creation Process:
// Example of how driver creation works internally
DriverRegistry.DriverDescriptor descriptor;
if (DriverRegistry.TryGetDriver(driverName, out descriptor))
{
// Create factory using descriptor's factory function
IComponentFactory factory = descriptor.CreateFactory();
// Factory creates all driver components
MainLoopCoordinator<TInputRecord> coordinator = new (
timedEvents,
inputQueue,
mainLoop,
factory // Factory knows its driver name via GetDriverName()
);
}
This architecture provides:
DriverRegistry.Names constants instead of stringsFor comprehensive documentation on testing Terminal.Gui applications with input injection, virtual time control, and deterministic testing, see Input Injection.
Quick Summary:
VirtualTimeProvider) for timing controlExample:
VirtualTimeProvider time = new ();
using IApplication app = Application.Create(time);
app.Init(DriverRegistry.Names.ANSI);
// Inject input
app.InjectKey(Key.Enter);
app.InjectMouse(new () { Flags = MouseFlags.LeftButtonPressed, ScreenPosition = new (5, 5) });
// Verify behavior
Assert.True(eventFired);
See Input Injection for: