docfx/docs/cursor.md
Terminal.Gui provides a unified cursor management system that separates the Terminal Cursor (the visible cursor indicator users see) from the Draw Cursor (internal rendering state).
Key Concepts:
IOutputBuffer.Col/Row) for rendering - NOT related to Terminal CursorTerminal.Gui uses a xref:Terminal.Gui.Drivers.Cursor record class to represent cursor state:
public record Cursor
{
/// <summary>Position in screen coordinates. Null = hidden.</summary>
public Point? Position { get; init; }
/// <summary>Cursor visual style (blinking bar, block, etc.)</summary>
public CursorStyle Style { get; init; } = CursorStyle.Hidden;
/// <summary>True if cursor should be visible</summary>
public bool IsVisible => Position.HasValue && Style != CursorStyle.Hidden;
}
Key characteristics:
record)with expression to modify: cursor with { Position = newPos }The xref:Terminal.Gui.Drivers.CursorStyle enum is based on ANSI/VT DECSCUSR terminal standards:
public enum CursorStyle
{
Default = 0, // Implementation-defined (usually BlinkingBlock)
BlinkingBlock = 1, // █ (blinking)
SteadyBlock = 2, // █ (steady)
BlinkingUnderline = 3, // _ (blinking)
SteadyUnderline = 4, // _ (steady)
BlinkingBar = 5, // | (blinking, common in text editors)
SteadyBar = 6, // | (steady)
Hidden = -1 // No visible cursor
}
Platform Mapping:
Views use the xref:Terminal.Gui.ViewBase.View.Cursor property to manage cursor state:
// Set cursor at column 5, row 0 in viewport - convert to screen coords
Point screenPos = ViewportToScreen (new Point (5, 0));
Cursor = new Cursor { Position = screenPos, Style = CursorStyle.BlinkingBar };
// Hide cursor
Cursor = new Cursor { Position = null };
// or
Cursor = new Cursor { Style = CursorStyle.Hidden };
// Update position keeping same style
Point newScreenPos = ViewportToScreen (new Point (6, 0));
Cursor = Cursor with { Position = newScreenPos };
protected override void OnDrawContent (Rectangle viewport)
{
// Calculate cursor position in content coordinates
int cursorCol = _cursorPosition - _scrollOffset;
// Only set cursor if within viewport
if (cursorCol >= 0 && cursorCol < Viewport.Width && HasFocus)
{
Point screenPos = ViewportToScreen (new Point (cursorCol, 0));
Cursor = new Cursor
{
Position = screenPos,
Style = CursorStyle.BlinkingBar
};
}
else
{
// Cursor outside viewport or no focus - hide it
Cursor = new Cursor { Position = null };
}
// ... drawing code ...
}
CRITICAL: Cursor.Position must ALWAYS be in screen coordinates.
Views have three coordinate systems:
Always convert before setting cursor:
// From content area
Point contentPos = new Point (column, row);
Point screenPos = ContentToScreen (contentPos);
Cursor = new Cursor { Position = screenPos, Style = CursorStyle.BlinkingBar };
// From viewport
Point viewportPos = new Point (column, row);
Point screenPos = ViewportToScreen (viewportPos);
Cursor = new Cursor { Position = screenPos, Style = CursorStyle.BlinkingBar };
The framework automatically hides the cursor when:
view.Enabled == falseview.Visible == false== false== falseViews only need to:
Cursor.Position when they want the cursor visiblenull or CursorStyle.Hidden to hideWhen cursor position changes without requiring a full redraw, use:
public void SetCursorNeedsUpdate ()
{
App?.Driver?.SetCursorNeedsUpdate (true);
}
This signals the driver that cursor position needs updating on next iteration without triggering view redraw.
When to use:
When NOT to use:
private void MoveCursorRight ()
{
_cursorPosition++;
// Calculate new screen position
int viewportCol = _cursorPosition - _scrollOffset;
if (viewportCol >= 0 && viewportCol < Viewport.Width)
{
Point screenPos = ViewportToScreen (new Point (viewportCol, 0));
Cursor = Cursor with { Position = screenPos };
SetCursorNeedsUpdate (); // Efficient - no redraw needed
}
}
The ApplicationNavigation class manages cursor positioning at the application level. Called once per main loop iteration, it:
GetCursorNeedsUpdate() returns false (optimization)TopRunnableView.MostFocusedDriver.SetCursor() with the final cursor stateThis ensures only the deepest focused view's cursor is displayed, and only when visible within the view hierarchy.
Drivers implement cursor control through IDriver.SetCursor(Cursor) which delegates to IOutput.SetCursor(Cursor). The driver also tracks an update flag via GetCursorNeedsUpdate() / SetCursorNeedsUpdate() to avoid redundant cursor updates.
Each driver implements cursor control based on platform capabilities:
Windows supports two modes based on terminal capabilities:
Legacy Console Mode (pre-Windows 10 or conhost compatibility mode):
CONSOLE_CURSOR_INFO structure with P/InvokeModern VT Mode (Windows Terminal, modern ConHost):
All use pure ANSI escape sequences:
CSI ? 25 h/l) for show/hideCSI Ps SP q) for cursor styleCSI row;col H) for positioningOnly emits style change sequences when the style actually changes (optimization).
| Sequence | Description |
|---|---|
CSI ? 25 h | DECTCEM - Show cursor |
CSI ? 25 l | DECTCEM - Hide cursor |
CSI Ps SP q | DECSCUSR - Set cursor style (Ps = 0-6) |
CSI row ; col H | CUP - Set cursor position |
CursorStyle to DECSCUSR Mapping:
| CursorStyle | DECSCUSR Ps | Appearance |
|---|---|---|
Default | 0 | Implementation-defined (usually blinking block) |
BlinkingBlock | 1 | █ blinking |
SteadyBlock | 2 | █ steady |
BlinkingUnderline | 3 | _ blinking |
SteadyUnderline | 4 | _ steady |
BlinkingBar | 5 | | blinking |
SteadyBar | 6 | | steady |
Hidden | N/A | Uses DECTCEM hide instead |
The cursor is updated once per main loop iteration:
┌──────────────────────────────────────────────────────────┐
│ Main Loop Iteration │
├──────────────────────────────────────────────────────────┤
│ 1. Process input events │
│ 2. Execute callbacks │
│ 3. Layout pass (if needed) │
│ 4. Draw pass (if needed) │
│ └─ Cursor hidden during draw to prevent flicker │
│ 5. ApplicationNavigation.UpdateCursor() │
│ └─ Checks GetCursorNeedsUpdate() │
│ └─ Gets MostFocused view's Cursor │
│ └─ Validates position within ancestor viewports │
│ └─ Calls Driver.SetCursor(cursor) │
└──────────────────────────────────────────────────────────┘
Views that display text with a visible insertion point should:
BlinkingBar)Views using highlight-based selection (like xref:Terminal.Gui.Views.ListView) should hide the cursor:
Cursor = new Cursor { Position = null };
The selection is indicated visually through attribute changes, not the terminal cursor.
Before (v1 / early v2): PositionCursor() override returned viewport-relative coordinates.
After (current v2): Set xref:Terminal.Gui.ViewBase.View.Cursor property with screen-absolute coordinates during OnDrawContent().
Key changes:
PositionCursor() overrideTerminal Cursor vs Draw Cursor are completely separate:
| Aspect | Terminal Cursor | Draw Cursor |
|---|---|---|
| Purpose | Show user where input goes | Track where next character renders |
| API | xref:Terminal.Gui.ViewBase.View.Cursor property | IOutputBuffer.Col/Row |
| Affected by | Cursor = new Cursor { ... } | Move(), AddRune(), AddStr() |
| Visibility | User sees blinking cursor | Internal only |
| Coordinates | Screen-absolute | Buffer-relative |
| Management | View sets explicitly | Rendering system updates |
❌ NEVER do this:
// WRONG - Don't use Move() for cursor positioning
Move (cursorCol, cursorRow); // This affects Draw Cursor, not Terminal Cursor
✅ DO this:
// CORRECT - Use Cursor property
Point screenPos = ViewportToScreen (new Point (cursorCol, cursorRow));
Cursor = new Cursor { Position = screenPos, Style = CursorStyle.BlinkingBar };
Cursor.PositionMove() for cursor positioning (it affects Draw Cursor)with expressionCheck:
Cursor.Position set to a valid screen coordinate (not null)?Cursor.Style something other than Hidden?== true?Enabled and Visible?Check:
Check:
SetNeedsDraw() when only cursor moved? (Use SetCursorNeedsUpdate() instead)