# HotkeyManager

Source: /tuil/docs/reference/packages/hotkeys/api/hotkey-manager
Locale: en

class exported by @mwillbanks/tuil-hotkeys.



{/* Generated by tooling/docs/generate-reference.ts. */}

## class [#class]

Public class exported by `@mwillbanks/tuil-hotkeys`.

```ts
export class HotkeyManager {
  readonly #bindings = new Set<HotkeyBinding>();
  readonly #observers = new Set<(event: HotkeyEvent) => void>();
  readonly #sequence: string[] = [];
  #sequenceTimer?: ReturnType<typeof setTimeout>;
  #pendingExact?: PendingHotkey;
  readonly #deferredErrors: unknown[] = [];

  constructor(readonly sequenceTimeout = 750) {}

  register(binding: HotkeyBinding): () => void {
    if (!binding.keys.trim()) {
      throw new Error("Hotkey notation cannot be empty");
    }
    const registered = Object.freeze({ ...binding });
    this.#bindings.add(registered);
    return () => {
      this.#bindings.delete(registered);
      if (this.#pendingExact?.binding === registered) {
        this.#pendingExact = undefined;
        this.#sequence.length = 0;
        if (this.#sequenceTimer) clearTimeout(this.#sequenceTimer);
      }
    };
  }

  list(): readonly HotkeyBinding[] {
    return [...this.#bindings];
  }

  conflicts(
    platform: NodeJS.Platform = process.platform,
  ): readonly HotkeyConflict[] {
    const groups = new Map<string, HotkeyBinding[]>();
    for (const binding of this.#bindings) {
      const key = normalizeHotkeyNotation(binding.keys, platform);
      const current = groups.get(key) ?? [];
      current.push(binding);
      groups.set(key, current);
    }
    const conflicts: HotkeyConflict[] = [];
    for (const [keys, bindings] of groups) {
      if (bindings.length < 2) continue;
      conflicts.push({
        keys,
        bindings,
        resolved: bindings.toSorted(
          (left, right) => this.#rank(right) - this.#rank(left),
        )[0] as HotkeyBinding,
      });
    }
    return conflicts;
  }

  observe(observer: (event: HotkeyEvent) => void): () => void {
    this.#observers.add(observer);
    return () => this.#observers.delete(observer);
  }

  drainErrors(): readonly unknown[] {
    return this.#deferredErrors.splice(0);
  }

  async dispatch(
    input: string,
    key: TerminalKey = {},
    context: HotkeyDispatchContext = {},
  ): Promise<HotkeyBinding | undefined> {
    const platform = context.platform ?? process.platform;
    const chord = normalizeHotkeyNotation(
      normalizeTerminalKey(input, key, platform),
      platform,
    );
    this.#sequence.push(chord);
    if (this.#sequenceTimer) clearTimeout(this.#sequenceTimer);
    const sequence = this.#sequence.join(" ");
    const eligible = [...this.#bindings].filter((binding) =>
      this.#isEligible(binding, context, platform),
    );
    const exact = eligible
      .filter(
        (binding) =>
          normalizeHotkeyNotation(binding.keys, platform) === sequence,
      )
      .sort((left, right) => this.#rank(right) - this.#rank(left));
    const hasLongerPrefix = eligible.some((candidate) =>
      normalizeHotkeyNotation(candidate.keys, platform).startsWith(
        `${sequence} `,
      ),
    );
    const binding = exact[0];
    if (binding && hasLongerPrefix) {
      this.#pendingExact = {
        binding,
        input,
        sequence: [...this.#sequence],
        context,
        platform,
      };
      this.#sequenceTimer = setTimeout(() => {
        const pending = this.#pendingExact;
        this.#pendingExact = undefined;
        this.#sequence.length = 0;
        if (pending && this.#isPendingEligible(pending)) {
          void this.#executeBinding(
            pending.binding,
            pending.input,
            pending.sequence,
            pending.platform,
          ).catch((error) => {
            this.#handleDeferredError(error, pending.context);
          });
        }
      }, this.sequenceTimeout);
      return undefined;
    }
    if (!binding && hasLongerPrefix) {
      this.#sequenceTimer = setTimeout(() => {
        this.#pendingExact = undefined;
        this.#sequence.length = 0;
      }, this.sequenceTimeout);
      return undefined;
    }
    if (!binding) {
      const pending = this.#pendingExact;
      this.#pendingExact = undefined;
      this.#sequence.length = 0;
      if (pending && this.#isPendingEligible(pending)) {
        await this.#executeBinding(
          pending.binding,
          pending.input,
          pending.sequence,
          pending.platform,
        );
        return this.dispatch(input, key, context);
      }
      return undefined;
    }
    this.#pendingExact = undefined;
    if (this.#sequenceTimer) clearTimeout(this.#sequenceTimer);
    const pressed = [...this.#sequence];
    this.#sequence.length = 0;
    await this.#executeBinding(binding, input, pressed, platform);
    return binding;
  }

  async #executeBinding(
    binding: HotkeyBinding,
    input: string,
    sequence: readonly string[],
    platform: NodeJS.Platform,
  ): Promise<void> {
    const event: HotkeyEvent = {
      keys: normalizeHotkeyNotation(binding.keys, platform),
      input,
      sequence,
      binding,
      defaultPrevented: false,
      preventDefault() {
        this.defaultPrevented = true;
      },
    };
    await binding.handler(event);
    for (const observer of this.#observers) {
      observer(event);
    }
  }

  #isEligible(
    binding: HotkeyBinding,
    context: HotkeyDispatchContext,
    platform: NodeJS.Platform,
  ): boolean {
    if (binding.platforms && !binding.platforms.includes(platform)) {
      return false;
    }
    if (
      binding.enabled === false ||
      (typeof binding.enabled === "function" && !binding.enabled())
    ) {
      return false;
    }
    const scope = binding.scope ?? "application";
    const activeScopes =
      typeof context.activeScopes === "function"
        ? context.activeScopes()
        : context.activeScopes;
    const active = activeScopes?.[scope];
    return (
      (scope === "application" && context.allowApplication !== false) ||
      active === true ||
      (typeof active === "string" && active === binding.scopeId)
    );
  }

  #rank(binding: HotkeyBinding): number {
    return (
      scopePriority[binding.scope ?? "application"] * 1000 +
      (binding.priority ?? 0)
    );
  }

  #isPendingEligible(pending: PendingHotkey): boolean {
    return (
      this.#bindings.has(pending.binding) &&
      this.#isEligible(pending.binding, pending.context, pending.platform)
    );
  }

  #handleDeferredError(error: unknown, context: HotkeyDispatchContext): void {
    if (context.onError) {
      try {
        context.onError(error);
        return;
      } catch (reportError) {
        this.#deferredErrors.push(
          new AggregateError(
            [error, reportError],
            "Hotkey handler and error reporting failed",
          ),
        );
        return;
      }
    }
    this.#deferredErrors.push(error);
  }
}
```

## Members [#members]

| Member                 | Type                                                                                                               | Required | Description                                                                                                                                                        | Related types                                                                                                                                                                                                                       |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `#bindings`            | `Set<HotkeyBinding>`                                                                                               | Yes      | The `#bindings` member uses the `Set<HotkeyBinding>` contract.                                                                                                     | [`HotkeyBinding`](/tuil/docs/reference/packages/hotkeys/api/hotkey-binding)                                                                                                                                                              |
| `#observers`           | `Set<(event: HotkeyEvent) => void>`                                                                                | Yes      | The `#observers` member uses the `Set<(event: HotkeyEvent) => void>` contract.                                                                                     | [`HotkeyEvent`](/tuil/docs/reference/packages/hotkeys/api/hotkey-event)                                                                                                                                                                  |
| `#sequence`            | `string[]`                                                                                                         | Yes      | The `#sequence` member uses the `string[]` contract.                                                                                                               | —                                                                                                                                                                                                                                   |
| `#sequenceTimer`       | `NodeJS.Timeout \| undefined`                                                                                      | No       | The `#sequenceTimer` member uses the `NodeJS.Timeout \| undefined` contract.                                                                                       | —                                                                                                                                                                                                                                   |
| `#pendingExact`        | `PendingHotkey \| undefined`                                                                                       | No       | The `#pendingExact` member uses the `PendingHotkey \| undefined` contract.                                                                                         | —                                                                                                                                                                                                                                   |
| `#deferredErrors`      | `unknown[]`                                                                                                        | Yes      | The `#deferredErrors` member uses the `unknown[]` contract.                                                                                                        | —                                                                                                                                                                                                                                   |
| `__constructor`        | `any`                                                                                                              | Yes      | The `__constructor` member uses the `any` contract.                                                                                                                | —                                                                                                                                                                                                                                   |
| `register`             | `(binding: HotkeyBinding) => () => void`                                                                           | Yes      | The `register` member uses the `(binding: HotkeyBinding) => () => void` contract.                                                                                  | [`HotkeyBinding`](/tuil/docs/reference/packages/hotkeys/api/hotkey-binding)                                                                                                                                                              |
| `list`                 | `() => readonly HotkeyBinding[]`                                                                                   | Yes      | The `list` member uses the `() => readonly HotkeyBinding[]` contract.                                                                                              | [`HotkeyBinding`](/tuil/docs/reference/packages/hotkeys/api/hotkey-binding)                                                                                                                                                              |
| `conflicts`            | `(platform?: NodeJS.Platform) => readonly HotkeyConflict[]`                                                        | Yes      | The `conflicts` member uses the `(platform?: NodeJS.Platform) => readonly HotkeyConflict[]` contract.                                                              | [`HotkeyConflict`](/tuil/docs/reference/packages/hotkeys/api/hotkey-conflict)                                                                                                                                                            |
| `observe`              | `(observer: (event: HotkeyEvent) => void) => () => void`                                                           | Yes      | The `observe` member uses the `(observer: (event: HotkeyEvent) => void) => () => void` contract.                                                                   | [`HotkeyEvent`](/tuil/docs/reference/packages/hotkeys/api/hotkey-event)                                                                                                                                                                  |
| `drainErrors`          | `() => readonly unknown[]`                                                                                         | Yes      | The `drainErrors` member uses the `() => readonly unknown[]` contract.                                                                                             | —                                                                                                                                                                                                                                   |
| `dispatch`             | `(input: string, key?: TerminalKey, context?: HotkeyDispatchContext) => Promise<HotkeyBinding \| undefined>`       | Yes      | The `dispatch` member uses the `(input: string, key?: TerminalKey, context?: HotkeyDispatchContext) => Promise<HotkeyBinding \| undefined>` contract.              | [`HotkeyBinding`](/tuil/docs/reference/packages/hotkeys/api/hotkey-binding), [`HotkeyDispatchContext`](/tuil/docs/reference/packages/hotkeys/api/hotkey-dispatch-context), [`TerminalKey`](/tuil/docs/reference/packages/hotkeys/api/terminal-key) |
| `#executeBinding`      | `(binding: HotkeyBinding, input: string, sequence: readonly string[], platform: NodeJS.Platform) => Promise<void>` | Yes      | The `#executeBinding` member uses the `(binding: HotkeyBinding, input: string, sequence: readonly string[], platform: NodeJS.Platform) => Promise<void>` contract. | [`HotkeyBinding`](/tuil/docs/reference/packages/hotkeys/api/hotkey-binding)                                                                                                                                                              |
| `#isEligible`          | `(binding: HotkeyBinding, context: HotkeyDispatchContext, platform: NodeJS.Platform) => boolean`                   | Yes      | The `#isEligible` member uses the `(binding: HotkeyBinding, context: HotkeyDispatchContext, platform: NodeJS.Platform) => boolean` contract.                       | [`HotkeyBinding`](/tuil/docs/reference/packages/hotkeys/api/hotkey-binding), [`HotkeyDispatchContext`](/tuil/docs/reference/packages/hotkeys/api/hotkey-dispatch-context)                                                                     |
| `#rank`                | `(binding: HotkeyBinding) => number`                                                                               | Yes      | The `#rank` member uses the `(binding: HotkeyBinding) => number` contract.                                                                                         | [`HotkeyBinding`](/tuil/docs/reference/packages/hotkeys/api/hotkey-binding)                                                                                                                                                              |
| `#isPendingEligible`   | `(pending: PendingHotkey) => boolean`                                                                              | Yes      | The `#isPendingEligible` member uses the `(pending: PendingHotkey) => boolean` contract.                                                                           | —                                                                                                                                                                                                                                   |
| `#handleDeferredError` | `(error: unknown, context: HotkeyDispatchContext) => void`                                                         | Yes      | The `#handleDeferredError` member uses the `(error: unknown, context: HotkeyDispatchContext) => void` contract.                                                    | [`HotkeyDispatchContext`](/tuil/docs/reference/packages/hotkeys/api/hotkey-dispatch-context)                                                                                                                                             |

## Parameters [#parameters]

This declaration has no public members.

## Returns [#returns]

This declaration does not return a value.

## Throws [#throws]

No thrown errors are documented for this declaration.

## Related types [#related-types]

* [`Hotkey`](/tuil/docs/reference/packages/hotkeys/api/hotkey)
* [`HotkeyBinding`](/tuil/docs/reference/packages/hotkeys/api/hotkey-binding)
* [`HotkeyConflict`](/tuil/docs/reference/packages/hotkeys/api/hotkey-conflict)
* [`HotkeyDispatchContext`](/tuil/docs/reference/packages/hotkeys/api/hotkey-dispatch-context)
* [`HotkeyEvent`](/tuil/docs/reference/packages/hotkeys/api/hotkey-event)
* [`TerminalKey`](/tuil/docs/reference/packages/hotkeys/api/terminal-key)

## Source [#source]

[View the secondary source reference](https://github.com/mwillbanks/tuil/blob/main/packages/hotkeys/src/index.ts)

## Package [#package]

[@mwillbanks/tuil-hotkeys](/tuil/docs/reference/packages/hotkeys)
