-
Notifications
You must be signed in to change notification settings - Fork 1
Description
Overview
Build a native WPF terminal control that renders VT100/xterm escape sequences directly, using ConPTY for process management. No WebView2, no xterm.js — fully native.
This is the most complex component of the Windows app. It replaces what SwiftTerm does on macOS.
Architecture
TerminalControl (WPF custom control)
A FrameworkElement subclass that renders terminal content using WPF's DrawingContext (low-level rendered text, not TextBlock/RichTextBox).
public class TerminalControl : FrameworkElement
{
private TerminalBuffer _buffer;
private VtParser _parser;
private ConPtyProcess _process;
protected override void OnRender(DrawingContext dc) {
// Render visible rows from _buffer using FormattedText
for (int row = 0; row < _visibleRows; row++) {
var line = _buffer.GetLine(_scrollOffset + row);
// Render each character run with its attributes (fg, bg, bold, etc.)
}
}
}VtParser — VT100/xterm escape sequence parser
A state machine that processes raw byte output from ConPTY:
Must handle:
- CSI sequences: cursor movement (CUU/CUD/CUF/CUB), erase (ED/EL), SGR (colors, bold, italic, underline), scroll regions (DECSTBM), insert/delete lines
- OSC sequences: window title (OSC 0/2), color queries (OSC 10/11)
- DCS sequences: (can be stubbed initially)
- C0 controls: BEL, BS, HT, LF, CR, SO, SI
- Mode sets: DECCKM (cursor keys), DECAWM (auto-wrap), DECTCEM (cursor visible), alternate screen buffer (1049)
- Mouse reporting: SGR mode (1006) for tmux mouse support
- 256-color (CSI 38;5;N m) and 24-bit color (CSI 38;2;R;G;B m)
Implementation approach:
Use a table-driven state machine (states: Ground, Escape, EscapeIntermediate, CsiEntry, CsiParam, CsiIntermediate, OscString, DcsEntry, etc.). Reference: the VT500 state machine diagram from Paul Flo Williams.
TerminalBuffer — Screen state + scrollback
public class TerminalBuffer
{
private List<TerminalLine> _scrollback; // History
private TerminalLine[] _screen; // Active viewport (rows × cols)
private int _cursorRow, _cursorCol;
private CellAttributes _currentAttributes;
private int _scrollRegionTop, _scrollRegionBottom;
private bool _alternateScreenActive;
private TerminalLine[] _savedScreen; // For alt screen save/restore
public int ScrollbackLength => _scrollback.Count;
public int Rows { get; }
public int Cols { get; }
}
public struct TerminalCell
{
public char Character;
public CellAttributes Attributes; // fg, bg, bold, italic, underline, etc.
}ConPtyProcess — Windows ConPTY wrapper
public class ConPtyProcess : IDisposable
{
// P/Invoke ConPTY APIs:
// CreatePseudoConsole() — create pseudo console with given size
// Pipe stdin/stdout to the console
// Start process (cmd.exe, powershell, or the agent command) attached to ConPTY
public Stream InputStream { get; } // Write to process
public Stream OutputStream { get; } // Read from process
public void Resize(int cols, int rows);
public void Write(string text);
public void Write(byte[] data);
public event Action<byte[]> DataReceived;
public event Action<int> ProcessExited;
}ConPTY P/Invoke functions needed:
CreatePseudoConsole(COORD size, HANDLE hInput, HANDLE hOutput, DWORD dwFlags, out IntPtr phPC)ResizePseudoConsole(IntPtr hPC, COORD size)ClosePseudoConsole(IntPtr hPC)- Process creation with
PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE
Rendering Pipeline
ConPTY output bytes → VtParser (state machine) → TerminalBuffer (screen state)
→ InvalidateVisual() → OnRender() → DrawingContext (WPF rendering)
Performance considerations:
- Batch output processing: accumulate ConPTY data for ~16ms before triggering render (match display refresh rate)
- Only re-render dirty rows (track which rows changed since last render)
- Use
FormattedTextfor text rendering withTypefacefor font selection - Character cell dimensions calculated from font metrics once on font change
- Double-buffered: parse on background thread, swap buffer reference on UI thread
Text Selection
- Mouse click+drag selects text (like a normal terminal)
- Track selection start/end as (row, col) coordinates
- Render selection highlight as inverted colors
- Ctrl+C copies selection to clipboard (when selection active; otherwise sends ^C to process)
- Ctrl+Shift+C always copies
Scrollback
- Mouse wheel scrolls through scrollback history
- Scrollbar (optional, can be hidden)
- When scrolled up and new output arrives: stay at current scroll position, show "scroll to bottom" indicator
- Max scrollback: configurable (default 50000 lines)
Resize Handling
- On control resize: recalculate rows/cols from pixel dimensions and font metrics
- Call
ConPtyProcess.Resize(cols, rows)to inform ConPTY - Reflow existing content (or simply truncate/pad — reflow is complex, start simple)
Alternative: Use Microsoft.Terminal.Control
If building from scratch proves too complex, consider referencing or using components from the Windows Terminal open source project (microsoft/terminal on GitHub). The Microsoft.Terminal.Control and Microsoft.Terminal.TerminalConnection libraries may be embeddable, though they target WinUI 3, not WPF directly.
References
- macOS equivalent: SwiftTerm (
LocalProcessTerminalView) - VT state machine: https://vt100.net/emu/dec_ansi_parser
- ConPTY docs: https://learn.microsoft.com/en-us/windows/console/creating-a-pseudoconsole-session
- Windows Terminal source: https://github.com/microsoft/terminal
Acceptance Criteria
- ConPTY process spawns and attaches correctly
- VT parser handles CSI, OSC, C0 controls, SGR colors (256 + 24-bit)
- Terminal buffer manages screen state + scrollback
- WPF rendering shows text with correct colors, bold, underline
- Keyboard input forwarded to ConPTY process
- Mouse scroll works (scrollback + forwarded to process for tmux)
- Text selection with copy to clipboard
- Resize updates ConPTY and reflows/adjusts display
- Cursor renders correctly (block/beam/underline, blink)
- Alternate screen buffer switches work (for vim, less, tmux)
- Performance: smooth rendering at 60fps during heavy output