# How Ghostty Manages Terminal Modes: DEC Private Modes, DECCKM, and DECSCUSR Explained

> Discover how Ghostty manages terminal modes like DEC private modes, DECCKM, and DECSCUSR. Learn about its efficient bitfield implementation and CSI command handling for cursor styles.

- Repository: [Ghostty/ghostty](https://github.com/ghostty-org/ghostty)
- Tags: internals
- Published: 2026-05-01

---

**Ghostty implements terminal modes as a compile-time generated packed bitfield in `ModeState` (8 bytes), with DECCKM mapped to the `cursor_keys` enum and DECSCUSR handled as a CSI `q` command that sets an internal `CursorStyle` enum.**

Ghostty is a fast, feature-rich terminal emulator written in Zig that achieves VT220 compatibility through a sophisticated type-safe state machine. Understanding how Ghostty manages **DEC private modes**—including DECCKM for application cursor keys and DECSCUSR for cursor styling—reveals an architecture that prioritizes both performance and spec compliance. All mode state lives in a compact 8-byte structure generated at compile time from a central declaration table.

## The Mode State Machine Architecture

Ghostty treats terminal modes as a strongly typed state machine centered in `src/terminal/modes.zig`. Rather than using hashtables or sparse arrays, the emulator generates a dense, packed representation that fits each mode into a single bit.

### Centralized Mode Definitions

All supported DEC and ANSI modes are declared in a single table named `entries` in `src/terminal/modes.zig`. Each **ModeEntry** specifies the mode name, its numeric identifier, default value, and whether it uses ANSI or DEC namespace.

```zig
// Excerpt from src/terminal/modes.zig
const entries: []const ModeEntry = &.{
    // DEC private modes
    .{ .name = "cursor_keys", .value = 1 },  // DECCKM
    .{ .name = "cursor_blinking", .value = 12 },
    // ... additional entries
};

```

### Packed Bitfield Representation

From this table, Ghostty generates three compile-time constructs:

1. **`Mode` enum** – Maps numeric identifiers (e.g., `1` for DECCKM) to typed enum members (e.g., `.cursor_keys`)
2. **`ModePacked`** – A packed struct where each mode occupies one bit (`@sizeOf(ModePacked) == 8`)
3. **`ModeState`** – A wrapper holding current values, saved values, and defaults as `ModePacked` instances

```zig
// From src/terminal/modes.zig
pub const ModeState = struct {
    values: ModePacked = .{},
    saved: ModePacked = .{},
    default: ModePacked = .{},

    pub fn set(self: *ModeState, mode: Mode, value: bool) void { 
        // Implementation sets specific bit
    }
    
    pub fn get(self: *const ModeState, mode: Mode) bool { 
        // Implementation reads specific bit
    }
    
    pub fn save(self: *ModeState, mode: Mode) void { 
        self.saved = self.values; 
    }
    
    pub fn restore(self: *ModeState, mode: Mode) bool { 
        self.values = self.saved; 
        return true;
    }
};

```

Accessing any **Ghostty terminal mode** requires only a single memory read or write to this 8-byte structure, eliminating the overhead of hash lookups or branching dispatch tables.

## DECCKM (Application Cursor Keys) Implementation

DECCKM (DEC Private Mode 1) switches the numeric keypad and arrow keys between normal and application mode. In Ghostty, this maps directly to the `cursor_keys` mode entry.

### CSI Parsing in stream.zig

When the parser encounters `CSI ?1h` (set) or `CSI ?1l` (reset), the dispatch logic in `src/terminal/stream.zig` converts the numeric parameter into the typed `Mode` enum and forwards it to the handler:

```zig
// From src/terminal/stream.zig
case 'h' => { // Set Mode
    const mode = modes.modeFromInt(param, ansi = false).?;
    self.handler.vt(.set_mode, .{ .mode = mode, .enabled = true });
}

```

The `StreamHandler.setMode` implementation then calls `self.terminal.modes.set(mode, enabled)`, updating the packed bitfield in `ModeState`.

### Runtime Usage in Surface.zig

Ghostty consults the `cursor_keys` state when generating arrow key escape sequences. In `src/Surface.zig` (and related input handling), the application checks `t.modes.get(.cursor_keys)` to determine whether to send application sequences (`\x1bOD`) or normal sequences (`\x1b[D`):

```zig
// Conceptual usage in input handling
const left_arrow = if (t.modes.get(.cursor_keys)) "\x1bOD" else "\x1b[D";

```

## DECSCUSR (Select Cursor Style) Handling

Unlike DECCKM, **DECSCUSR is not a mode** but a CSI command that directly manipulates cursor appearance. Ghostty implements this as a `q` subcommand that maps numeric arguments to an internal `ansi.CursorStyle` enum.

### CSI q Parsing

The parser in `src/terminal/stream.zig` handles `CSI ? q` (DECSCUSR) by matching the intermediate character and mapping the parameter to cursor styles:

```zig
// From src/terminal/stream.zig (lines 88-106)
'q' => switch (input.intermediates[0]) {
    ' ' => {
        const style = switch (input.params.len) {
            0 => .default,
            1 => switch (input.params[0]) {
                0 => .default,
                1 => .blinking_block,
                2 => .steady_block,
                3 => .blinking_underline,
                4 => .steady_underline,
                5 => .blinking_bar,
                6 => .steady_bar,
                else => { log.warn("unknown cursor style"); return; },
            },
            else => { log.warn("invalid DECSCUSR params"); return; },
        };
        self.handler.vt(.cursor_style, style);
    },
    // ...
},

```

### Cursor Style and Mode Interaction

DECSCUSR interacts with DEC private mode 12 (cursor blinking), which controls whether the cursor blinks globally. When reporting the current state via DECRQSS, Ghostty combines the active cursor style with the `cursor_blinking` mode bit. In `src/termio/stream_handler.zig`, the response formatter reads both the style and the mode state:

```zig
// From src/termio/stream_handler.zig
const blink = self.terminal.modes.get(.cursor_blinking);
const style: u8 = switch (self.terminal.screens.active.cursor.cursor_style) {
    .block => if (blink) 1 else 2,
    .underline => if (blink) 3 else 4,
    .bar => if (blink) 5 else 6,
    .block_hollow => if (blink) 1 else 2,
};
try writer.print("{d} q", .{style});

```

If the user has configured a default blink preference, `setMode` in `stream_handler.zig` skips DEC mode 12 to preserve the DECSCUSR behavior, ensuring that explicit cursor style commands take precedence.

## Mode Reporting and DECRPM

Ghostty generates **DECRPM** (DEC Report Mode) responses using the `Report.encode` method in `src/terminal/modes.zig`. This encodes the mode's numeric tag and current state (set, reset, permanently set, etc.) according to VT specifications:

```zig
// From src/terminal/modes.zig
try writer.print("\x1B[{s}{};{}$y", .{
    if (self.tag.ansi) "" else "?",
    self.tag.value,
    @intFromEnum(self.state),
});

```

The `StreamHandler.sendModeReport` method wraps this functionality to communicate mode state back to host applications.

## Practical Code Examples

The following Zig snippets demonstrate how to interact with Ghostty's mode system programmatically:

```zig
const std = @import("std");
const terminal = @import("terminal");

// Enable DECCKM (application cursor keys)
pub fn enableCursorKeys(t: *terminal.Terminal) void {
    t.modes.set(.cursor_keys, true);
}

// Query DECCKM state
pub fn isCursorKeysEnabled(t: *terminal.Terminal) bool {
    return t.modes.get(.cursor_keys);
}

// Set cursor style to blinking block (DECSCUSR 1)
pub fn setBlinkingBlock(t: *terminal.Terminal) void {
    t.handler.vt(.cursor_style, .blinking_block);
}

// Reset cursor style to terminal default (DECSCUSR 0)
pub fn resetCursorStyle(t: *terminal.Terminal) void {
    t.handler.vt(.cursor_style, .default);
}

```

## Summary

- **Ghostty stores all DEC/ANSI private modes** in a compile-time generated `ModeState` struct using an 8-byte packed bitfield for O(1) access.
- **DECCKM maps to `cursor_keys`** (mode value 1) and is toggled via CSI `?1h/l`, affecting arrow key escape sequences generated in `src/Surface.zig`.
- **DECSCUSR is a CSI `q` command**, not a mode, that sets an internal `CursorStyle` enum; it interacts with mode 12 (`cursor_blinking`) when reporting state via DECRQSS.
- **Mode reporting** uses `Report.encode` in `src/terminal/modes.zig` to format DECRPM responses with proper ANSI/DEC prefixes and state codes.

## Frequently Asked Questions

### What is the difference between DECCKM and DECSCUSR in Ghostty?

**DECCKM** is a true **DEC private mode** (mode 1) stored in the `ModeState` bitfield that changes the behavior of arrow keys. **DECSCUSR** is a CSI command (CSI `q`) that directly sets cursor appearance without changing mode state, though Ghostty tracks the selected style separately from the `cursor_blinking` mode (mode 12).

### How much memory does Ghostty use to store terminal modes?

Ghostty uses exactly **8 bytes** (`@sizeOf(ModePacked)`) to store the current, saved, and default values for all supported modes combined. This packed representation in `src/terminal/modes.zig` ensures cache-friendly access patterns and minimal memory overhead.

### Can Ghostty save and restore mode states?

Yes. The `ModeState` struct in `src/terminal/modes.zig` provides `save()` and `restore()` methods that copy the current `ModePacked` bitfield to and from a backup field. This implements the DEC save/restore cursor and mode semantics used by applications like tmux and vim.

### How does Ghostty handle the cursor blinking mode (DEC mode 12)?

Ghostty treats mode 12 (`cursor_blinking`) as a global toggle that can be overridden by user configuration. When processing `CSI ?12h/l`, the `setMode` handler in `src/termio/stream_handler.zig` checks user preferences; if the user has disabled blinking globally, the mode change is ignored to preserve consistent cursor behavior across applications.