Skip to content

[Windows] Native WPF terminal renderer with ConPTY #139

@2witstudios

Description

@2witstudios

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 FormattedText for text rendering with Typeface for 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

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions