# How Tabby Supports Multi-Chord Shortcuts: A Technical Deep Dive

> Discover how Tabby's hotkey system supports multi-chord shortcuts. Learn about its ordered sequence matching and centralized keystroke history for efficient custom commands.

- Repository: [Eugene/tabby](https://github.com/Eugeny/tabby)
- Tags: deep-dive
- Published: 2026-03-03

---

**Tabby implements multi-chord shortcuts by treating user input as an ordered sequence of keystroke chords, storing these sequences as nested string arrays in the configuration, and matching them against a rolling history of recent keystrokes centralized in the `HotkeysService`.**

Tabby, the open-source terminal emulator by Eugeny, provides an extensible hotkey framework that handles both simple key combinations and complex multi-chord shortcuts like `Ctrl-K → C`. The system is designed around the concept of *chords*—simultaneous key combinations that can be chained together into sequences. This architecture allows power users to create Emacs-style keybindings while keeping the implementation maintainable in [`tabby-core/src/services/hotkeys.service.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-core/src/services/hotkeys.service.ts).

## Chords vs. Multi-Chord Sequences

In Tabby's terminology, a **chord** represents a single simultaneous key combination (e.g., `Ctrl+K` or `Alt+Shift+T`). A **multi-chord shortcut** is an ordered list of such chords that must be pressed in sequence, such as `Ctrl+K` followed by `P`.

The configuration layer supports three distinct formats for defining these shortcuts:

- **String** – A single chord (e.g., `"Ctrl+C"`).
- **Array of strings** – A multi-chord sequence (e.g., `["Ctrl+K", "P"]`).
- **Array of arrays** – Multiple alternative sequences for the same action (e.g., `[["Ctrl+K", "C"], ["Alt+X", "Y"]]`).

When Tabby initializes, `HotkeysService.getHotkeysConfigRecursive` recursively normalizes these nested objects into a flat, predictable map where every value is an array of possible sequences. This normalization ensures the matching logic can treat single chords and multi-chord sequences uniformly.

## Event Capture and Keystroke History

Every keyboard event flows through `HotkeysService.pushKeyEvent`, which maintains two critical pieces of state:

- `pressedKeys` – A set tracking currently depressed keys.
- `lastKeystrokes` – A rolling history of recently completed chords.

The helper functions in [`tabby-core/src/services/hotkeys.util.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-core/src/services/hotkeys.util.ts) handle canonical naming. `getKeystrokeName` converts raw `keydown`/`keyup` events into normalized strings like `"Ctrl-Shift-A"`, ensuring consistent matching regardless of platform-specific key naming.

On each `keyup` event, the service builds a canonical name for the just-released combination and pushes it onto `lastKeystrokes`. This history preservation is essential for multi-chord detection, as the system must remember that `Ctrl+K` was pressed several seconds before the subsequent `C` key.

## The Matching Algorithm

The core matching logic resides in `HotkeysService.matchActiveHotkey`. This method constructs the **current sequence** by combining historical keystrokes with the active one:

```typescript
const currentSequence = this.getCurrentKeystrokes(); // past keystrokes + current one

```

The algorithm then iterates over every configured hotkey and its associated sequences:

1. It discards sequences longer than the available history.
2. It verifies that the **last element** of the sequence matches `this.pressedKeystroke`.
3. It walks backward through `currentSequence` to confirm earlier chords appear in the correct order (allowing unrelated keystrokes between sequence elements).

When a match is found, the service emits the hotkey ID via the `_hotkey` subject. For multi-chord sequences, `clearCurrentKeystrokes()` resets the history immediately after firing, ensuring the next shortcut starts with a clean slate.

Components subscribe to `hotkey$` (which filters out input fields) or `unfilteredHotkey$` to react to these events, regardless of whether the trigger was a single chord or a five-chord sequence.

## Configuring Multi-Chord Shortcuts

### User Configuration Format

Define multi-chord shortcuts in Tabby's config file as nested arrays:

```json
{
  "hotkeys": {
    "open-devtools": ["Ctrl+Shift+I"],
    "toggle-panel": ["Ctrl+K", "P"],
    "custom-action": [["Ctrl+K", "C"], ["Alt+X", "Y"]]
  }
}

```

After `getHotkeysConfigRecursive` processes this, all entries become arrays of arrays, unifying the internal representation.

### Reacting to Shortcuts in Components

Subscribe to the observable to handle specific multi-chord triggers:

```typescript
import { Component, NgZone } from '@angular/core';
import { HotkeysService } from 'tabby-core';

@Component({ selector: 'app-example' })
export class ExampleComponent {
  constructor(
    private hotkeys: HotkeysService,
    private zone: NgZone,
  ) {
    this.hotkeys.hotkey$.subscribe(id => {
      if (id === 'toggle-panel') {
        this.zone.run(() => this.toggleSidePanel());
      }
    });
  }

  toggleSidePanel() {
    // Implementation here
  }
}

```

### Registering New Actions

Expose new hotkey IDs by implementing `HotkeyProvider` (defined in [`tabby-core/src/api/hotkeyProvider.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-core/src/api/hotkeyProvider.ts)):

```typescript
import { Injectable } from '@angular/core';
import { HotkeyDescription, HotkeyProvider, TranslateService } from 'tabby-core';

@Injectable()
export class MyHotkeyProvider extends HotkeyProvider {
  private hotkeys: HotkeyDescription[] = [{
    id: 'my-multi-chord-action',
    name: this.translate.instant('Execute custom workflow')
  }];

  constructor(private translate: TranslateService) { super(); }

  async provide(): Promise<HotkeyDescription[]> {
    return this.hotkeys;
  }
}

```

Once registered, users can bind `"my-multi-chord-action"` to sequences like `["Ctrl+K", "O"]` through the settings UI, and `HotkeysService` will recognize it automatically.

## Key Source Files

- **[`tabby-core/src/services/hotkeys.service.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-core/src/services/hotkeys.service.ts)** – Core service capturing events, building keystroke history, and matching sequences.
- **[`tabby-core/src/services/hotkeys.util.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-core/src/services/hotkeys.util.ts)** – Utilities for canonical key naming and ordering.
- **[`tabby-core/src/api/hotkeyProvider.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-core/src/api/hotkeyProvider.ts)** – Interface for modules exposing hotkey descriptions.
- **[`tabby-terminal/src/hotkeys.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-terminal/src/hotkeys.ts)** – Example `HotkeyProvider` implementation listing built-in terminal actions.
- **[`tabby-settings/src/components/hotkeySettingsTab.component.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-settings/src/components/hotkeySettingsTab.component.ts)** – UI component for editing hotkeys and parsing user input into the configuration format.

## Summary

- Tabby treats shortcuts as **sequences of chords**, where each chord is a simultaneous key combination.
- Configuration supports **nested arrays** to define multi-chord sequences and alternative bindings, normalized by `getHotkeysConfigRecursive`.
- `HotkeysService` maintains a **rolling keystroke history** via `lastKeystrokes` and matches against it in `matchActiveHotkey`.
- The system allows **arbitrary sequence lengths** and clears the history after successful multi-chord matches to prevent state contamination.
- Components consume shortcuts through **RxJS observables** (`hotkey$`), decoupling input handling from business logic.

## Frequently Asked Questions

### How many chords can a multi-chord shortcut contain?

Tabby imposes no hardcoded limit on sequence length in `matchActiveHotkey`. The algorithm simply checks if the sequence fits within the available history buffer. Practically, sequences longer than three or four chords become difficult for users to remember and execute, but the architecture supports theoretically unlimited depth.

### Does Tabby's multi-chord system require a timeout between chords?

No explicit timeout is enforced in the current implementation. The system maintains the keystroke history indefinitely until a match occurs or a new chord begins. However, `clearCurrentKeystrokes()` resets the buffer immediately after a successful multi-chord match, ensuring subsequent shortcuts start fresh.

### How does this compare to Emacs-style key chords?

Tabby's implementation closely mirrors Emacs' approach, where prefixes like `Ctrl+X` (`C-x`) open a keymap for subsequent chords. The critical difference is that Tabby uses a **flat sequence array** rather than nested keymaps. In [`tabby-core/src/services/hotkeys.service.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-core/src/services/hotkeys.service.ts), the matching logic linearly searches all configured sequences against the history, whereas Emacs uses a tree-based dispatch system.

### Can extensions define default multi-chord shortcuts?

Yes. When creating a `HotkeyProvider` implementation, you can suggest default bindings in your module's configuration schema. However, user settings in `store.hotkeys` always override provider defaults. The `HotkeysService` merges these configurations during initialization, giving users final control over multi-chord assignments.