# How Tuicr Handles Input Modes: State Management in the Terminal UI

> Discover how Tuicr manages terminal UI input modes using a state machine. Explore the key action pipeline for seamless navigation, editing, and command contexts.

- Repository: [Almog Gavra/tuicr](https://github.com/agavra/tuicr)
- Tags: internals
- Published: 2026-08-01

---

**Tuicr implements a finite-state machine using an `InputMode` enum to strictly separate navigation, editing, and command contexts, routing every keystroke through a key → action → mode → UI pipeline defined in [`src/app/mod.rs`](https://github.com/agavra/tuicr/blob/main/src/app/mod.rs) and [`src/input/keybindings.rs`](https://github.com/agavra/tuicr/blob/main/src/input/keybindings.rs).**

Tuicr, an open-source terminal UI for code review, manages user interactions through a sophisticated input mode system. The architecture centers on an `InputMode` enum that determines how keyboard events are interpreted and which interface components are rendered. Understanding how Tuicr handles input modes is essential for developers customizing keybindings or extending the application's functionality.

## Tuicr Input Mode Variants

The `InputMode` enum defined in [`src/app/mod.rs`](https://github.com/agavra/tuicr/blob/main/src/app/mod.rs) establishes eight distinct interaction contexts. Each variant controls key interpretation and UI visibility:

- **Normal**: The default navigation state active at startup. Arrow keys, `j`/`k`, and scrolling actions move the cursor or viewport while the diff view, file list, and comment navigator remain visible.
- **Command**: A Vim-style command line triggered by `:`. Users type commands such as `:w`, `:q`, or `:diff`, and a command prompt appears at the bottom of the screen.
- **Comment**: Inline comment entry mode entered by pressing `a` (line comment) or `A` (file-level comment). This overlays a comment panel on the diff where users type text, saving with Ctrl-S and canceling with Ctrl-C.
- **Search**: Pattern entry mode triggered by `/`. A search bar appears at the bottom and matches are highlighted in the diff view.
- **Help**: An overlay displaying key-binding references, activated by pressing `?`.
- **Confirm**: A modal Yes/No dialog for destructive actions (e.g., `:q!`), blocking other input until answered.
- **CommitSelect**: A commit range selector for PR reviews, invoked from the Pull Request tab when choosing "Select commits". The diff rendering adapts to the selected commit range.
- **VisualSelect**: Visual-range selection similar to Vim's visual mode, activated with `v` in Normal mode, allowing highlighted selections to be converted into comments or bulk actions.

## State Transition Architecture

Mode changes follow a strict pipeline that decouples input detection from business logic and rendering.

### Mapping Keys to Actions

In [`src/input/keybindings.rs`](https://github.com/agavra/tuicr/blob/main/src/input/keybindings.rs), raw keyboard events are translated into semantic `Action` variants. This abstraction prevents keybinding conflicts by ensuring physical keys map to intentions rather than direct mode switches:

```rust
// src/input/keybindings.rs
Key::Char('a') => Action::EnterComment,
Key::Char('A') => Action::EnterFileComment,
Key::Char(':') => Action::EnterCommand,
Key::Char('/') => Action::EnterSearch,
Key::Char('?') => Action::EnterHelp,
Key::Char('v') => Action::EnterVisual,

```

### Transitioning Between Modes

The central event loop in [`src/main.rs`](https://github.com/agavra/tuicr/blob/main/src/main.rs) dispatches `Action` variants to `App::handle_action` in [`src/app/mod.rs`](https://github.com/agavra/tuicr/blob/main/src/app/mod.rs), where the `input_mode` field is updated:

```rust
// src/app/mod.rs (simplified)
fn handle_action(&mut self, action: Action) {
    match action {
        Action::EnterCommand => self.input_mode = InputMode::Command,
        Action::EnterComment => self.input_mode = InputMode::Comment,
        Action::EnterSearch => self.input_mode = InputMode::Search,
        Action::EnterHelp => self.input_mode = InputMode::Help,
        Action::EnterVisual => self.input_mode = InputMode::VisualSelect,
        Action::Cancel | Action::Quit => self.input_mode = InputMode::Normal,
        // …other actions…
    }
}

```

Destructive actions requiring confirmation automatically trigger `InputMode::Confirm` before executing.

### Mode-Driven UI Rendering

The `App::render` method selects appropriate UI components by matching against `self.input_mode`. This ensures interface elements only appear when relevant:

- `InputMode::Normal` → `ui::app_layout::render_diff`
- `InputMode::Command` → `ui::status_bar::render_command_prompt`
- `InputMode::Comment` → `ui::comment_panel::render`
- `InputMode::Search` → `ui::status_bar::render_search_bar`
- `InputMode::Help` → `ui::help_popup::render`

## Practical Code Examples

### Switching to Comment Mode

When a user presses `a` in Normal mode, the keybinding layer emits `Action::EnterComment`. The application state updates and the UI layer immediately displays the input panel:

```rust
// Keybinding definition in src/input/keybindings.rs
Key::Char('a') => Action::EnterComment,

// State transition in src/app/mod.rs
fn handle_action(&mut self, action: Action) {
    match action {
        Action::EnterComment => self.input_mode = InputMode::Comment,
        // …
    }
}

// Conditional rendering in src/ui/comment_panel.rs
fn render(&self, area: Rect, app: &App) {
    if app.input_mode == InputMode::Comment {
        // draw input box …
    }
}

```

### Executing Commands

Pressing `:` transitions the interface to Command mode, allowing users to enter Ex-style commands:

```rust
// src/input/keybindings.rs
Key::Char(':') => Action::EnterCommand,

// src/ui/status_bar.rs
if app.input_mode == InputMode::Command {
    // render command input box at bottom
}

```

### Exiting Modes

Universal exit pathways return the application to Normal mode. In Search mode, pressing Enter or Escape triggers `Action::Accept` or `Action::Cancel`, respectively:

```rust
// src/app/mod.rs
match action {
    Action::Cancel | Action::Accept => self.input_mode = InputMode::Normal,
    _ => {}
}

```

## Key Source Files for Input Mode Handling

| File | Role |
|------|------|
| [`src/app/mod.rs`](https://github.com/agavra/tuicr/blob/main/src/app/mod.rs) | Defines the `InputMode` enum and implements `App::handle_action` for state transitions. |
| [`src/input/keybindings.rs`](https://github.com/agavra/tuicr/blob/main/src/input/keybindings.rs) | Maps raw `Key` events to semantic `Action` variants. |
| [`src/main.rs`](https://github.com/agavra/tuicr/blob/main/src/main.rs) | Contains the central event loop that dispatches actions to the application state. |
| [`src/ui/comment_panel.rs`](https://github.com/agavra/tuicr/blob/main/src/ui/comment_panel.rs) | Renders the comment input interface only when `input_mode == InputMode::Comment`. |
| [`src/ui/status_bar.rs`](https://github.com/agavra/tuicr/blob/main/src/ui/status_bar.rs) | Conditionally renders command prompts and search bars based on active mode. |
| [`src/ui/help_popup.rs`](https://github.com/agavra/tuicr/blob/main/src/ui/help_popup.rs) | Displays help overlays when `InputMode::Help` is active. |

## Summary

- Tuicr uses an `InputMode` enum in [`src/app/mod.rs`](https://github.com/agavra/tuicr/blob/main/src/app/mod.rs) to define eight distinct interaction contexts, from Normal navigation to CommitSelect for PR reviews.
- State transitions occur exclusively through `App::handle_action`, which receives semantic `Action` variants from [`src/input/keybindings.rs`](https://github.com/agavra/tuicr/blob/main/src/input/keybindings.rs).
- The architecture enforces a strict separation where keybindings generate actions, actions update modes, and rendering logic in `src/ui/` components queries the current mode to determine visibility.
- Input mode state persists as part of the review session, enabling Tuicr to resume with the previous mode intact.

## Frequently Asked Questions

### What is the default input mode when Tuicr starts?

**Normal** mode is initialized in the `App::new` constructor within [`src/app/mod.rs`](https://github.com/agavra/tuicr/blob/main/src/app/mod.rs). This mode prioritizes navigation commands such as arrow keys, `j`/`k` scrolling, and mode-switching shortcuts like `:` or `/`.

### How does Tuicr prevent keybinding conflicts between different modes?

All keyboard input routes through [`src/input/keybindings.rs`](https://github.com/agavra/tuicr/blob/main/src/input/keybindings.rs), which translates physical keys into abstract `Action` enums. The `App::handle_action` method in [`src/app/mod.rs`](https://github.com/agavra/tuicr/blob/main/src/app/mod.rs) then processes these actions according to the current `InputMode`. This ensures the same physical key performs different functions contextually—pressing `a` triggers `Action::EnterComment` in Normal mode, while in Comment mode it inserts the character "a" into the text buffer.

### Can input modes be nested or stacked in Tuicr?

No, Tuicr maintains a single active mode at any time. The `input_mode` field in the `App` struct holds exactly one `InputMode` variant. Transitions replace the current mode entirely, though Cancel or Quit actions reliably return the user to Normal mode as a safe baseline.

### Where is the input mode state persisted during a review session?

The `input_mode` field is stored within the `App` struct in [`src/app/mod.rs`](https://github.com/agavra/tuicr/blob/main/src/app/mod.rs) and serialized as part of the review session state. When Tuicr resumes a session, it restores the previous `InputMode`, allowing users to continue exactly where they left off, whether they were in the middle of composing a comment or reviewing a specific commit range.