XTerm.NET
1.2.0
See the version list below for details.
dotnet add package XTerm.NET --version 1.2.0
NuGet\Install-Package XTerm.NET -Version 1.2.0
<PackageReference Include="XTerm.NET" Version="1.2.0" />
<PackageVersion Include="XTerm.NET" Version="1.2.0" />
<PackageReference Include="XTerm.NET" />
paket add XTerm.NET --version 1.2.0
#r "nuget: XTerm.NET, 1.2.0"
#:package XTerm.NET@1.2.0
#addin nuget:?package=XTerm.NET&version=1.2.0
#tool nuget:?package=XTerm.NET&version=1.2.0
XTerm.NET
A .NET terminal emulator library inspired by xterm.js. XTerm.NET provides a headless terminal emulator that parses and processes VT100/ANSI escape sequences, making it easy to host console applications in your .NET applications.
Sixel graphics are supported. Images arrive as ordinary cell content rather than as an overlay, so
they are overwritten by text, cleared by ED/EL, scrolled with their lines, and freed when they fall
out of the scrollback — see Sixel Images.
Features
- Full VT100/ANSI Escape Sequence Support — Process colors, cursor movement, text attributes, and more
- Headless Design — No UI dependencies; bring your own renderer (Console, WPF, MAUI, etc.)
- Dual Buffer Support — Normal and alternate screen buffers with scrollback
- Keyboard & Mouse Input Generation — Generate escape sequences for keyboard and mouse events
- Rich Event System — Subscribe to terminal events like title changes, bell, resize, and window manipulation
- 256 and True Color Support — Full RGB and 256-color palette support
- Unicode Support — Proper handling of wide characters and Unicode text
- Sixel Graphics — Decodes Sixel images (
ESC P … q) and stores them on the cells they cover, soimg2sixel,chafa,lsixandtimgwork against a host that renders them
Installation
Install via NuGet Package Manager:
dotnet add package XTerm.NET
Or via the Package Manager Console in Visual Studio:
Install-Package XTerm.NET
Usage
The basic architecture is that the Terminal is a XxY array of Buffer Cell structures which represent each cell of the console screen.
- Incoming text from a hosted process is written to the terminal and the terminal will interpret any ANSI VT Escape codes to change color, underline, position etc.
- The terminal host application calls Terminal.GenerateMouseEvent(), Terminal.GenerateKeyEvent() to send input to the console process.
- Requests for information are modeled as events (GetWindowTitle, SetWindowTitle etc.).
Creating a Terminal
Create a terminal instance with default settings (80 columns × 24 rows):
using XTerm;
var terminal = new Terminal();
Or customize the terminal with TerminalOptions:
using XTerm;
using XTerm.Options;
var terminal = new Terminal(new TerminalOptions
{
Cols = 120, // Number of columns
Rows = 40, // Number of rows
Scrollback = 1000, // Scrollback buffer lines (0 to disable)
CursorStyle = CursorStyle.Block,
CursorBlink = true,
TermName = "xterm" // Terminal type for identification
});
Resizing the Terminal
Resize the terminal dynamically to match your UI or window size:
// Resize to new dimensions
terminal.Resize(cols: 120, rows: 50);
// Query current size
int currentCols = terminal.Cols;
int currentRows = terminal.Rows;
Writing Content to the Terminal
Write text and ANSI escape sequences to the terminal:
// Write text (no automatic newline)
terminal.Write("Hello, ");
// Write a line (adds \r\n)
terminal.WriteLine("XTerm.NET!");
// Write with ANSI colors and styles
terminal.WriteLine("\x1b[31mRed text\x1b[0m");
terminal.WriteLine("\x1b[1;32mBold green text\x1b[0m");
terminal.WriteLine("\x1b[38;2;255;100;200mTrue color (RGB) text\x1b[0m");
// Position the cursor and draw
terminal.Write("\x1b[5;10HText at row 5, column 10");
Access the buffer to read terminal content:
var buffer = terminal.Buffer;
// Get cursor position
int cursorX = buffer.X;
int cursorY = buffer.Y;
// Read a line as a string
string lineContent = terminal.GetLine(0);
// Or access the buffer line directly
var line = buffer.Lines[0];
string content = line?.TranslateToString(trimRight: true) ?? "";
Hooking Up Events
Subscribe to events to integrate the terminal into your application:
// Data sent back from the terminal (e.g., query responses)
terminal.DataReceived += (sender, e) =>
{
// Send e.Data to your connected process/PTY
Console.WriteLine($"Terminal sent: {e.Data}");
};
// Terminal title changed (via OSC escape sequence)
terminal.TitleChanged += (sender, e) =>
{
// Update your window title
Console.WriteLine($"Title: {e.Title}");
};
// Terminal resized
terminal.Resized += (sender, e) =>
{
// Notify your PTY/process of the new size
Console.WriteLine($"Resized to {e.Cols}x{e.Rows}");
};
// Bell character received
terminal.BellRang += (sender, e) =>
{
// Play a sound or flash the window
Console.WriteLine("Bell!");
};
// Line feed occurred (useful for tracking output)
terminal.LineFed += (sender, e) =>
{
// Trigger a render update
};
// Cursor style changed
terminal.CursorStyleChanged += (sender, e) =>
{
// Update cursor rendering
Console.WriteLine($"Cursor: {e.Style}, Blink: {e.Blink}");
};
// Buffer switched (normal ↔ alternate)
terminal.BufferChanged += (sender, e) =>
{
Console.WriteLine($"Switched to {e.BufferType} buffer");
};
Window manipulation events (used by some terminal applications):
terminal.WindowMoved += (sender, e) => Console.WriteLine($"Move to ({e.X}, {e.Y})");
terminal.WindowResized += (sender, e) => Console.WriteLine($"Resize to {e.Width}x{e.Height}");
terminal.WindowMinimized += (sender, e) => Console.WriteLine("Minimize");
terminal.WindowMaximized += (sender, e) => Console.WriteLine("Maximize");
terminal.WindowRestored += (sender, e) => Console.WriteLine("Restore");
Rendering the Buffer
XTerm.NET is headless — you provide the rendering logic for your UI framework (Console, WPF, MAUI, Avalonia, etc.). Walk over the terminal buffer and render each cell according to its content and attributes:
void RenderTerminal(Terminal terminal)
{
var buffer = terminal.Buffer;
for (int row = 0; row < terminal.Rows; row++)
{
var line = buffer.Lines[buffer.YDisp + row];
if (line == null) continue;
for (int col = 0; col < terminal.Cols; col++)
{
BufferCell cell = line[col];
// Skip empty cells or continuation cells (wide character's second cell)
if (cell.Width == 0) continue;
// Get the character content
string character = cell.Content;
// Get foreground/background colors
int fgColor = cell.Attributes.GetFgColor();
int bgColor = cell.Attributes.GetBgColor();
int fgMode = cell.Attributes.GetFgColorMode(); // 0=default, 1=256-color, 2=RGB
int bgMode = cell.Attributes.GetBgColorMode();
// Check text style attributes
bool isBold = cell.Attributes.IsBold();
bool isDim = cell.Attributes.IsDim();
bool isItalic = cell.Attributes.IsItalic();
bool isUnderline = cell.Attributes.IsUnderline();
bool isBlink = cell.Attributes.IsBlink();
bool isInverse = cell.Attributes.IsInverse();
bool isInvisible = cell.Attributes.IsInvisible();
bool isStrikethrough = cell.Attributes.IsStrikethrough();
bool isOverline = cell.Attributes.IsOverline();
// Render the cell at (col, row) with the appropriate styling
// Your rendering code here — e.g., DrawText(col, row, character, fg, bg, styles...)
}
}
// Render the cursor if visible
if (terminal.CursorVisible)
{
int cursorX = buffer.X;
int cursorY = buffer.Y;
CursorStyle style = terminal.Options.CursorStyle; // Block, Underline, or Bar
bool blink = terminal.Options.CursorBlink;
// Draw cursor at (cursorX, cursorY) with the appropriate style
}
}
Color mode values:
0— Default terminal color (use theme foreground/background)1— 256-color palette index (0–255)2— True color RGB (extract withcolor & 0xFFfor each channel)
Handling wide characters:
Wide characters (e.g., CJK ideographs, emoji) have Width = 2. The first cell contains the character, and the second cell has Width = 0 as a placeholder — skip it during rendering but allocate space for the double-width glyph.
Sixel Images
A Sixel image (ESC P … q … ESC \) is decoded and written into the cells it covers. Each covered
cell carries a reference to one shared TerminalImage plus the coordinates of the piece it shows,
so an image behaves like terminal content rather than an overlay: printing over a cell replaces
that part of the picture, ED/EL clear it, scrolling carries it, and the image is freed once the
last cell holding it is gone.
Tell the terminal your cell size. XTerm.NET is headless and cannot measure a font, so it cannot work out how many columns an image covers unless you say. Set these from your renderer's metrics, in device pixels:
terminal.Options.CellWidthPixels = 8;
terminal.Options.CellHeightPixels = 17;
Answer the window queries from these same numbers. An image viewer works out the cell size for
itself, by dividing the pixel size it gets from CSI 14 t by the row and column counts it already
has. So your WindowInfoRequested handler must report the grid, not the control:
case WindowInfoRequest.SizePixels: // CSI 14 t
e.WidthPixels = terminal.Cols * terminal.Options.CellWidthPixels;
e.HeightPixels = terminal.Rows * terminal.Options.CellHeightPixels;
e.Handled = true;
break;
case WindowInfoRequest.CellSizePixels: // CSI 16 t
e.CellWidth = terminal.Options.CellWidthPixels;
e.CellHeight = terminal.Options.CellHeightPixels;
e.Handled = true;
break;
Reporting your control's own size instead is the classic way to get this wrong. It includes the scrollbar, any window chrome, and the strip below the last row — the grid is a truncated division, so up to a whole row of the control's height belongs to no row at all. An application dividing that figure by the row count is told the terminal is taller than it is, sizes a picture to fill it, and the surplus runs off the bottom and scrolls the screen.
Rendering the tiles. Extend the per-cell loop above:
BufferCell cell = line[col];
if (cell.Image is TerminalImage image &&
image.TryGetTileSource(cell.ImageCol, cell.ImageRow, out int sx, out int sy, out int sw, out int sh))
{
// Pixels are BGRA8888 with straight (unpremultiplied) alpha, top row first.
// Cache your framework's bitmap against the image object — a ConditionalWeakTable keyed on
// `image` lets the bitmap die when the image does, with no eviction list to maintain.
var bitmap = _bitmaps.GetOrCreate(image);
// Edge tiles are clipped, so scale the destination to match rather than stretching a partial
// tile over a whole cell.
double destW = cellWidth * sw / (double)image.CellWidth;
double destH = cellHeight * sh / (double)image.CellHeight;
DrawImage(bitmap,
source: (sx, sy, sw, sh),
dest: (col * cellWidth, row * cellHeight, destW, destH));
continue;
}
Adjacent cells sharing the same Image reference and ImageRow with consecutive ImageCol values
are contiguous, so a renderer can coalesce them into a single draw call per row instead of one per
cell. If you cache rendered rows, note that image cells must break a text run: compare Image by
reference as well as comparing Attributes.
Image cells hold " " as their content, so TranslateToString and selection copy yield blanks.
Options:
| Option | Default | Purpose |
|---|---|---|
SixelEnabled |
true |
Decode images, and advertise Sixel in the primary Device Attributes reply |
CellWidthPixels / CellHeightPixels |
10 / 20 |
Cell size images are laid out against |
MaxSixelPixels |
4_000_000 |
Largest single image accepted |
MaxImageBytes |
64 MB |
Budget for image data live in the buffer; oldest are dropped past it |
Images are dropped when the terminal is resized to a different column count, because reflow re-wraps lines by copying ranges of cells and the pieces would reassemble in the wrong places. A change of row count alone keeps them.
License
This project is licensed under the MIT License — see the LICENSE file for details.
Author
Tom Laird-McConnell — Iciclecreek
Links
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net6.0
- Unicode.net (>= 2.0.0)
- Wcwidth (>= 3.0.0)
NuGet packages (4)
Showing the top 4 NuGet packages that depend on XTerm.NET:
| Package | Downloads |
|---|---|
|
AvaloniaTerminal
Terminal Control for Avalonia |
|
|
SvcSystems.UI.Terminal
Terminal Control for Avalonia |
|
|
Iciclecreek.Avalonia.Terminal
Avalonia terminal controls (TerminalControl/TerminalWindow) that provide an XTerm console for hosting console applications in Avalonia, including Sixel graphics. |
|
|
MTerminal.Avalonia.Terminal
Fork of Iciclecreek.Avalonia.Terminal with URL detection, scroll fixes, and ShellReady/OutputReceived events. Provides XTerm console for hosting console applications in Avalonia. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 2.0.0-rc001 | 53 | 8/27/2026 |
| 1.2.0 | 103 | 8/26/2026 |
| 1.1.2 | 87 | 8/25/2026 |
| 1.1.1 | 48 | 8/25/2026 |
| 1.1.0 | 327 | 8/24/2026 |
| 1.0.16 | 666 | 8/22/2026 |
| 1.0.15 | 5,649 | 6/23/2026 |
| 1.0.14 | 5,766 | 5/13/2026 |
| 1.0.12 | 8,574 | 1/23/2026 |
| 1.0.11 | 153 | 1/22/2026 |
| 1.0.10 | 446 | 12/29/2025 |
| 1.0.9 | 136 | 12/29/2025 |
| 1.0.8 | 142 | 12/27/2025 |
| 1.0.7 | 134 | 12/27/2025 |
| 1.0.6 | 169 | 12/26/2025 |
| 1.0.5 | 223 | 12/25/2025 |
| 1.0.4 | 211 | 12/23/2025 |
| 1.0.3 | 216 | 12/23/2025 |
| 1.0.2 | 212 | 12/23/2025 |
| 1.0.0-rc001 | 33 | 8/27/2026 |