# Keyboard Shortcuts

How keyboard input flows through TUIkit: from raw terminal bytes to your view handlers.

## Overview

TUIkit uses a layered event dispatch system. When a key is pressed, it passes through up to five layers. The first layer that consumes the event wins: remaining layers are skipped. Layer 0 (text input) and Layer 3 (focus system) are mutually exclusive: when a text input element is focused, Layer 0 runs and Layer 3 is skipped.

@Image(source: "keyboard-event-dispatch.png", alt: "Flowchart showing keyboard event dispatch through five layers: a hasTextInputFocus check gates Layer 0 (Text Input via focusManager.dispatchKeyEvent for TextField/SecureField). Layer 1: Status Bar Items (shortcut-triggered actions). Layer 2: View Handlers (.onKeyPress modifiers, deepest view first). A second hasTextInputFocus check skips Layer 3 if text input was focused. Layer 3: Focus System (focused element delegation, Tab/Shift+Tab, arrow key fallback). Layer 4: Default Bindings (quit, theme, appearance). Unmatched events are dropped.")

Additionally, `Ctrl+C` (SIGINT) is handled at the OS signal level **before** any of these layers: it always terminates the application.

## Available Keys

The ``Key`` enum defines all keys that TUIkit can recognize from terminal input:

### Character Keys

Any printable character is represented as `.character(Character)`:

```swift
.onKeyPress(Key.from("x")) {
    // handle "x" key
}
```

Uppercase detection: when a capital letter is typed, the resulting ``KeyEvent`` has `shift: true` set automatically.

### Special Keys

| Key | Description |
|-----|-------------|
| `.escape` | Escape key |
| `.enter` | Enter / Return |
| `.tab` | Tab |
| `.backspace` | Backspace / Delete backward |
| `.delete` | Forward delete |

### Arrow Keys

| Key | Description |
|-----|-------------|
| `.up` | Arrow up |
| `.down` | Arrow down |
| `.left` | Arrow left |
| `.right` | Arrow right |

### Navigation Keys

| Key | Description |
|-----|-------------|
| `.home` | Home |
| `.end` | End |
| `.pageUp` | Page Up |
| `.pageDown` | Page Down |

## Key Events and Modifiers

A ``KeyEvent`` combines a ``Key`` with modifier flags:

```swift
public struct KeyEvent {
    public let key: Key
    public let ctrl: Bool     // Ctrl modifier
    public let alt: Bool      // Alt / Option modifier
    public let shift: Bool    // Shift modifier
}
```

The terminal encodes modifiers differently from GUI frameworks:

- **Ctrl+letter**: Detected from ASCII control codes (0x01–0x1A). For example, `Ctrl+C` produces byte `0x03`.
- **Alt+key**: Detected from ESC prefix sequences (`ESC` followed by the key byte).
- **Shift**: Only auto-detected for uppercase letters. The terminal does not send distinct shift codes for most keys.

## Registering Key Handlers

Use the `.onKeyPress()` modifier to handle keyboard events in your views:

### Handle All Keys

```swift
Text("Press any key")
    .onKeyPress { event in
        if event.key == .enter {
            doSomething()
            return true   // consumed: stops propagation
        }
        return false      // not consumed: passes to next handler
    }
```

### Handle Specific Keys

```swift
Text("Use arrow keys")
    .onKeyPress(keys: [.up, .down]) { event in
        if event.key == .up { moveUp() }
        else { moveDown() }
        return true
    }
```

### Handle a Single Key

The single-key variant always consumes the event:

```swift
Text("Press Enter to continue")
    .onKeyPress(.enter) {
        continueAction()
    }
```

### Handler Priority

Handlers are dispatched in **reverse registration order**: the deepest view in the tree (most recently registered) gets the event first. This means inner views can intercept events before outer views see them.

```swift
VStack {
    Text("Outer")
        .onKeyPress(.enter) {
            // Only reached if inner handler did NOT consume it
            print("outer enter")
        }
    Text("Inner")
        .onKeyPress(.enter) {
            // Gets the event FIRST
            print("inner enter")
        }
}
```

> Important: All key handlers are re-registered every render frame. If a view is not rendered (e.g. behind a conditional), its handlers are not active.

## Focus Navigation

The `FocusManager` dispatches key events in three steps:

1. **Focused element delegation**: the focused element's `handleKeyEvent(_:)` is called first. If it consumes the event, dispatch stops.
2. **Tab / Shift+Tab**: cycles focus between elements (wraps around).
3. **Arrow key fallback**: Up/Left move to the previous element in the section, Down/Right move to the next. This only triggers if the focused element did not consume the arrow key in step 1.

| Key | Action |
|-----|--------|
| Tab | Move focus to the next focusable element (wraps around) |
| Shift+Tab | Move focus to the previous focusable element (wraps around) |
| Up / Left | Move to previous element in section (fallback) |
| Down / Right | Move to next element in section (fallback) |

For more details, see <doc:FocusSystem>.

## Default Bindings

Layer 4 provides three built-in key bindings that work without any configuration:

| Key | Action | Condition |
|-----|--------|-----------|
| `q` / `Q` | Quit application | Only when ``QuitBehavior`` allows it |
| `t` / `T` | Cycle to next color theme | Only when theme cycling is enabled |
| `a` / `A` | Cycle to next appearance | Always active |

### Quit Behavior

The ``QuitBehavior`` enum controls when `q` is allowed to quit:

| Value | Behavior |
|-------|----------|
| `.always` | `q` quits from any screen (default) |
| `.rootOnly` | `q` only quits when no status bar context is pushed |

`.rootOnly` is useful for modal dialogs: push a status bar context for the dialog, and `q` will be blocked until the user dismisses it:

```swift
Dialog(title: "Confirm") {
    Text("Are you sure?")
} footer: {
    ButtonRow {
        Button("Yes") { confirm() }
        Button("No") { cancel() }
    }
}
.statusBarItems(context: "confirm-dialog") {
    StatusBarItem(shortcut: "y", label: "yes")
    StatusBarItem(shortcut: "n", label: "no")
}
```

### Changing the built-in quit key

The default quit binding is configurable through ``StatusBarState/quitShortcut``:

```swift
@Environment(\.statusBar) private var statusBar

statusBar.quitShortcut = .escape
statusBar.quitShortcut = .ctrlQ
statusBar.quitShortcut = QuitShortcut(
    key: .f12,
    shortcutSymbol: Shortcut.f12,
    label: "exit"
)
```

This changes both the Layer 4 quit binding and the displayed system item.

### Overriding `q` in a local view context

A user-defined status bar item with shortcut `q` and an action intercepts the key before the default quit binding runs:

```swift
EditorView()
    .statusBarItems {
        StatusBarItem(shortcut: "q", label: "close") {
            dismissEditor()
        }
    }
```

This is the correct way to repurpose `q` for a specific screen, dialog, or focus section. The matching user item handles the event in Layer 1, so Layer 4's built-in quit binding is never reached.

## Status Bar Shortcuts

The status bar displays available shortcuts to the user. Use the ``Shortcut`` namespace for consistent, platform-standard symbols:

### Display Symbols

```swift
.statusBarItems {
    StatusBarItem(shortcut: Shortcut.arrowsUpDown, label: "nav")
    StatusBarItem(shortcut: Shortcut.enter, label: "select", key: .enter)
    StatusBarItem(shortcut: Shortcut.escape, label: "back")
}
```

### Common Symbols

| Symbol | Constant | Description |
|--------|----------|-------------|
| `⎋` | `Shortcut.escape` | Escape |
| `↵` | `Shortcut.enter` | Enter |
| `⇥` | `Shortcut.tab` | Tab |
| `⇤` | `Shortcut.shiftTab` | Shift+Tab |
| `⌫` | `Shortcut.backspace` | Backspace |
| `⌦` | `Shortcut.delete` | Delete |
| `␣` | `Shortcut.space` | Space |
| `↑` | `Shortcut.arrowUp` | Arrow up |
| `↓` | `Shortcut.arrowDown` | Arrow down |
| `←` | `Shortcut.arrowLeft` | Arrow left |
| `→` | `Shortcut.arrowRight` | Arrow right |
| `↑↓` | `Shortcut.arrowsUpDown` | Vertical arrows |
| `←→` | `Shortcut.arrowsLeftRight` | Horizontal arrows |
| `↑↓←→` | `Shortcut.arrowsAll` | All arrows |
| `⌃` | `Shortcut.control` | Control modifier |
| `⇧` | `Shortcut.shift` | Shift modifier |
| `⌥` | `Shortcut.option` | Option / Alt |

### Shortcut Helpers

```swift
// Combine modifier + key: "⌃c"
Shortcut.combine(.control, "c")

// Ctrl+key display: "^c"
Shortcut.ctrl("c")

// Range display: "1-9"
Shortcut.range("1", "9")
```

### Common Shortcut Letters

| Constant | Value |
|----------|-------|
| `Shortcut.quit` | `q` |
| `Shortcut.yes` | `y` |
| `Shortcut.no` | `n` |
| `Shortcut.cancel` | `c` |
| `Shortcut.ok` | `o` |

### Automatic Key Matching

``StatusBarItem`` automatically derives the trigger key from the shortcut string when no explicit `key:` parameter is given:

- Symbol shortcuts (`⎋`, `↵`, `⇥`) map to their corresponding ``Key`` values
- Single-character shortcuts (`"q"`, `"y"`) map to `.character(thatChar)`
- Arrow combinations (`"↑↓"`) match **both** individual arrow keys
- Multi-character non-symbol strings are informational only (no trigger key)

```swift
// Automatic: shortcut "q" triggers on Key.character("q")
StatusBarItem(shortcut: "q", label: "quit") { quit() }

// Explicit: override the trigger key
StatusBarItem(shortcut: Shortcut.enter, label: "select", key: .enter) { select() }

// Informational: no action, no trigger
StatusBarItem(shortcut: Shortcut.arrowsUpDown, label: "nav")
```

## Status Bar Context Stack

The status bar supports a context stack for temporary shortcut overrides: useful for modals and nested navigation:

```swift
// Set global items
.statusBarItems {
    StatusBarItem(shortcut: Shortcut.arrowsUpDown, label: "nav")
    StatusBarItem(shortcut: Shortcut.enter, label: "select", key: .enter)
}

// Push context-specific items (e.g. for a dialog)
.statusBarItems(context: "my-dialog") {
    StatusBarItem(shortcut: "y", label: "yes") { confirm() }
    StatusBarItem(shortcut: "n", label: "no") { cancel() }
}
```

When a context is pushed, its items replace the global items in the display. System items (quit, theme, appearance) remain visible unless a user item uses the same shortcut string.

### System Items

Three system items are shown by default:

| Shortcut | Label | Order | Description |
|----------|-------|-------|-------------|
| `q` | quit | 900 | Quit the application |
| `a` | appearance | 910 | Cycle border appearance |
| `t` | theme | 920 | Cycle color theme |

System items appear on the right side of the status bar. They can be disabled individually:

```swift
// In your App
statusBar.showSystemItems = false       // Hide all system items
statusBar.showThemeItem = false         // Hide only theme cycling
statusBar.showAppearanceItem = false    // Hide only appearance cycling
```

When all system items are hidden and there are no active user items, the status bar is hidden completely.

## Topics

### Input Types

- ``Key``
- ``KeyEvent``
- ``QuitBehavior``

### Focus

- ``FocusState``
- ``Focusable``

### Status Bar

- ``StatusBar``
- ``StatusBarItem``
- ``StatusBarItemProtocol``
- ``Shortcut``
