Skip to content

Keyboard shortcuts do not fire on non-latin layouts: key matching compares the layout character, not the physical key #3967

Description

@irium

Summary

On Windows with a Russian (ЙЦУКЕН) keyboard layout active, none of BB's web-layer keyboard shortcuts fire. Pressing Ctrl+K does not open the command palette, Ctrl+Shift+[ / ] do not switch threads, and plugin shortcuts are dead. Switching the OS layout back to English makes every one of them work again. I expected shortcuts to be bound to the physical key position, as they are in Chrome, VS Code and the six shortcuts BB routes through its native Electron menu.

The cause is that normalizeAppShortcutInputKey compares the layout-dependent KeyboardEvent.key against the binding's latin shortcut.key. On a Cyrillic layout the K position reports "л", and "л" !== "k".

Versions and environment

  • bb 0.43.3, desktop app built from source at main. The cited code is unchanged at current upstream main (370029d), and the unit repro below fails on a clean main checkout.
  • Windows 11 Pro 10.0.26200
  • Node v24.18.0
  • Keyboard layout: Russian (ЙЦУКЕН), the stock Windows layout. No provider involved — this is entirely in the renderer's key matching.

Steps to reproduce

In the app:

  1. On Windows, add the Russian keyboard layout and make it active (Win+Space).
  2. Open the BB desktop app, focus the thread view.
  3. Press Ctrl+K — nothing happens.
  4. Press Ctrl+Shift+[ — nothing happens.
  5. Switch the layout back to English and repeat steps 3–4 — both work.

As a unit test (fails on main at 370029d, no app or layout needed):

// packages/domain/test/layout-repro.test.ts
import { describe, expect, it } from "vitest";
import { matchesAppShortcut } from "../src/app-keybindings";

describe("cyrillic layout", () => {
  it("matches Ctrl+K pressed on a Russian layout", () => {
    expect(
      matchesAppShortcut(
        // Windows/Chromium reports the layout character in `key`
        { key: "л", code: "KeyK", metaKey: false, ctrlKey: true, altKey: false, shiftKey: false },
        { key: "k", mod: true, meta: false, control: false, alt: false, shift: false },
        false,
      ),
    ).toBe(true);
  });
});
pnpm exec turbo run test --filter=@bb/domain -- layout-repro

Did NOT reproduce — these keep working on the Russian layout, which is what narrowed it down:

  • Ctrl+N, Ctrl+T, Ctrl+W, Ctrl+Shift+N, Ctrl+Shift+T, Ctrl+, — the six commands routed through the native Electron application menu. Chromium matches those accelerators by VK code, so they are layout-independent.
  • Ctrl+\ (sidebar.toggle) — the Backslash position emits \ on both layouts.
  • Ctrl+1Ctrl+9 (thread jumps) — the digit row is identical on both layouts.

That split is the evidence that this is a BB bug in the web layer rather than an Electron or Chromium limitation: the same modifier chord works when Electron matches it and fails when BB matches it.

Expected vs actual

Physical key: K position, Ctrl held, Russian layout active

Actual   — no command runs. The KeyboardEvent is:
             { key: "л", code: "KeyK", ctrlKey: true, altKey: false, shiftKey: false }
           normalizeAppShortcutInputKey() returns "л"
           matchesAppShortcut() compares "л" === "k" -> false

Expected — palette.open fires, exactly as it does on the English layout,
           where the same physical key reports { key: "k", code: "KeyK" }.

The unit test above fails with:

AssertionError: expected false to be true

Evidence

Permalinks at upstream main 370029d7280d2f0a46c0c13272ecdc7e64d5f13e:

  • normalizeAppShortcutInputKey — the code fallback is gated behind input.altKey, which was written for macOS Option compositions. Without Alt held, the raw layout key is returned.
  • matchesAppShortcut — compares that value to the binding key.
  • baseKeyFromCode — knows only Key* and Digit* positions, so punctuation bindings stay unreachable even once the fallback does run.

Everything downstream of that comparison is affected:

There is a second, quieter consequence. appShortcutFromInput normalizes through the same function when recording a custom shortcut, and the result is persisted. Rebinding a command while a Cyrillic layout is active stores { key: "л", mod: true } — the override then works only on that layout, renders as "Ctrl + Л" in settings, and the conflict detector cannot tell it apart from a binding on the same physical key. appShortcutSchema accepts any string, so nothing rejects it.

Also unreachable because baseKeyFromCode returns null for their positions: the [ / ] thread-navigation defaults at app-keybindings.ts#L154-L180.

What you ruled out

Suggested priority and effort

Hits every user whose daily driver is a non-latin layout (Russian, Ukrainian, Greek, Hebrew, Arabic, …) — they lose essentially the whole shortcut surface, and the only workaround is switching layout before every chord. No data loss. The fix is small and contained to one function plus a lookup table.

A fix is ready

A fix branched off main is linked below as a prototype per CONTRIBUTING.md; happy to open a PR if a maintainer would like one.

https://github.com/irium/bb/tree/fix/non-latin-keyboard-shortcuts — single commit d340b06, based on main at c1a64f4.

What it does:

  1. Falls back to KeyboardEvent.code whenever the reported key is a single non-ASCII character, not only when Alt is held.
  2. Teaches baseKeyFromCode the punctuation positions (Comma, Period, Slash, Semicolon, Quote, BracketLeft/Right, Backslash, Backquote, Minus, Equal) so the [ / ] and , bindings resolve too.

The non-ASCII condition is deliberate: layouts that report ASCII keep matching by character, so AZERTY still binds its own letters rather than the QWERTY positions. The existing test at app-keybindings.test.ts#L175 pins that behavior and still passes.

Because shortcut recording shares the same normalization, the persisted-override problem described above is fixed by the same change. No schema validation or migration was added for already-stored non-latin overrides — happy to add either if preferred.

Tests: 6 added (Cyrillic Ctrl+K/Ctrl+P/Ctrl+F, Ctrl+Shift+[, Ctrl+,, a negative case, and one for shortcut recording). @bb/domain 215/215, @bb/app keybinding suites 28/28; typecheck and lint clean on domain, app, desktop, server.

One behavior change worth flagging in review: on a non-latin layout the settings UI keeps displaying the latin key (for example "Ctrl + ,") while the key that physically fires it prints something else. Matching by position but labelling by the latin base seemed the right trade-off versus wiring up navigator.keyboard.getLayoutMap(), but it is a product call.

AGENT GENERATED

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions