Skip to content

feat(desktop): toggle to disable adaptive keyboard-layout mapping#4078

Merged
Kitenite merged 2 commits into
mainfrom
disable-adaptive-layout-m
May 5, 2026
Merged

feat(desktop): toggle to disable adaptive keyboard-layout mapping#4078
Kitenite merged 2 commits into
mainfrom
disable-adaptive-layout-m

Conversation

@Kitenite
Copy link
Copy Markdown
Collaborator

@Kitenite Kitenite commented May 5, 2026

Summary

  • Adds an opt-in "Adaptive layout mapping" preference on the keyboard settings page (defaults off).
  • When off, shortcuts dispatch and render exactly as bound — ⌘Z stays ⌘Z on QWERTZ instead of being remapped to physical KeyY.
  • Resolver index, display hooks (useHotkeyDisplay / useFormatBinding), and imperative getDispatchChord all read the preference and pass null for the layout map when adaptive is off; resolver subscribes so toggling rebuilds the chord index live.

Test plan

  • Open Settings → Keyboard, see new "Adaptive layout mapping" toggle styled like the General page rows
  • With toggle off (default) on QWERTZ: ⌘Z fires the bound handler and renders as ⌘Z (not ⌘Y)
  • Flip toggle on: existing layout-aware behavior returns (display + dispatch follow the OS layout)
  • Reload the app — preference persists via localStorage
  • bun run lint and existing hotkey tests still green

Summary by cubic

Adds an opt-in “Adaptive layout mapping” toggle in Keyboard settings to control layout-aware shortcuts. When off (default), shortcuts dispatch and display exactly as bound (e.g., ⌘Z stays ⌘Z on QWERTZ).

  • New Features

    • Toggle appears in Settings → Keyboard and persists via localStorage (zustand persist).
    • Resolver and display respect the preference: useHotkeyDisplay, useFormatBinding, and getDispatchChord pass a null layout map when disabled.
    • Resolver reindexes on overrides, layout changes, and toggle changes; turning it on restores layout-aware behavior.
  • Refactors

    • Export useKeyboardPreferencesStore from the hotkeys barrel to simplify imports.

Written for commit eb30829. Summary will update on new commits.

Summary by CodeRabbit

  • New Features
    • Added "Adaptive layout mapping" toggle to keyboard shortcuts settings to enable/disable adaptive remapping.
    • Hotkey display, resolution, and behavior now respect the new toggle — when disabled, authored chord mappings are preserved; when enabled, layouts are adaptively applied.

Adds an opt-in "Adaptive layout mapping" preference on the keyboard
settings page. When off (default), shortcuts dispatch and display
exactly as bound regardless of the OS input source — i.e. ⌘Z stays
⌘Z on QWERTZ instead of being remapped to physical KeyY.

The preference is persisted in localStorage and read by the resolver
index, the display hooks (useHotkeyDisplay, useFormatBinding), and
the imperative getDispatchChord, all of which pass null for the
layout map when adaptive is off.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 5, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3c9398ef-0b9e-4b51-81bc-96f2a6372934

📥 Commits

Reviewing files that changed from the base of the PR and between fa9d814 and eb30829.

📒 Files selected for processing (2)
  • apps/desktop/src/renderer/hotkeys/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/settings/keyboard/page.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/desktop/src/renderer/routes/_authenticated/settings/keyboard/page.tsx

📝 Walkthrough

Walkthrough

A new persisted keyboard preference toggles "adaptive layout mapping". Multiple hotkey hooks, utilities, and the settings UI now read this preference and conditionally apply the keyboard layout map (or null) when converting or formatting hotkey bindings; the registered chord index rebuilds when the preference changes.

Changes

Adaptive Layout Mapping

Layer / File(s) Summary
State Management
apps/desktop/src/renderer/hotkeys/stores/keyboardPreferencesStore.ts, apps/desktop/src/renderer/hotkeys/stores/index.ts
Adds useKeyboardPreferencesStore with adaptiveLayoutEnabled: boolean and setAdaptiveLayoutEnabled(enabled: boolean) persisted to localStorage; re-exported from stores barrel.
Preference-aware helper
apps/desktop/src/renderer/hotkeys/hooks/useHotkeyDisplay/useHotkeyDisplay.ts, apps/desktop/src/renderer/hotkeys/utils/resolveHotkeyFromEvent.ts
Introduces useActiveLayoutMap() hook and activeLayoutMap() helper that return the layout map when adaptiveLayoutEnabled is true, otherwise null.
Core Binding Logic
apps/desktop/src/renderer/hotkeys/hooks/useBinding/useBinding.ts
getDispatchChord now reads preference and passes either the layout map or null into bindingToDispatchChord.
Display Integration
apps/desktop/src/renderer/hotkeys/hooks/useHotkeyDisplay/useHotkeyDisplay.ts
useHotkeyDisplay and useFormatBinding derive layoutMap via useActiveLayoutMap() and pass it into bindingToDispatchChord / formatHotkeyDisplay.
Event resolver wiring
apps/desktop/src/renderer/hotkeys/utils/resolveHotkeyFromEvent.ts
registeredAppChords is initialized via activeLayoutMap() and now rebuilds when keyboard preferences change (subscription added), in addition to overrides and layout changes.
Settings UI
apps/desktop/src/renderer/routes/_authenticated/settings/keyboard/page.tsx
Adds "Adaptive layout mapping" Switch and Label, wired to useKeyboardPreferencesStore to toggle and persist the preference.
Public re-exports
apps/desktop/src/renderer/hotkeys/index.ts
Re-exports useKeyboardPreferencesStore from the hotkeys module index.

Sequence Diagram

sequenceDiagram
    actor User
    participant Settings as Settings UI
    participant Store as Keyboard<br/>Preferences Store
    participant Hooks as Hotkey Hooks<br/>(useBinding, useHotkeyDisplay)
    participant Utils as Hotkey Utils<br/>(resolveHotkeyFromEvent)
    participant Display as Hotkey Display

    User->>Settings: Toggle adaptive layout mapping
    Settings->>Store: setAdaptiveLayoutEnabled(enabled)
    Store->>Store: Persist to localStorage

    activate Hooks
    Hooks->>Store: Subscribe to adaptiveLayoutEnabled changes
    Hooks->>Hooks: Derive layoutMap via useActiveLayoutMap()
    deactivate Hooks

    activate Utils
    Utils->>Store: Subscribe to adaptiveLayoutEnabled changes
    Utils->>Utils: Rebuild registeredAppChords index<br/>using activeLayoutMap()
    deactivate Utils

    rect rgba(100, 200, 100, 0.5)
        Note over Hooks,Display: When adaptiveLayoutEnabled = true
        Hooks->>Display: Pass current keyboard layout map
        Display->>Display: Adapt hotkey display to layout
    end

    rect rgba(200, 100, 100, 0.5)
        Note over Hooks,Display: When adaptiveLayoutEnabled = false
        Hooks->>Display: Pass null (no layout map)
        Display->>Display: Preserve authored hotkey chords
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A toggled hop, a keyboard's new song,
Adaptive maps guide chords along.
When turned on, the layout bends each key,
When off, the original chords stay free.
A tiny store makes the choice stay strong.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title directly and accurately describes the main change: adding a toggle to disable adaptive keyboard-layout mapping, which is the primary feature across all modified files.
Description check ✅ Passed The description covers the key changes, provides a clear test plan with checkboxes, and includes both a manual summary and auto-generated details. All major template sections are adequately addressed.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch disable-adaptive-layout-m

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 5, 2026

Greptile Summary

  • Adds an adaptiveLayoutEnabled Zustand store (persisted to localStorage) and wires it through the resolver index, display hooks, and imperative getDispatchChord so toggling the preference rebuilds the chord index and re-renders display glyphs without a page reload.
  • A new Settings → Keyboard toggle exposes the preference; when off (the default), bindings dispatch and display using their authored chord regardless of the active OS input source.
  • Two P2 findings: useActiveLayoutMap subscribes to the layout store even when adaptive is off (unnecessary re-renders on layout change), and page.tsx imports useKeyboardPreferencesStore via a deep file path instead of the renderer/hotkeys/stores barrel.

Confidence Score: 4/5

Safe to merge — no correctness bugs; two P2 style/performance findings only.

All three code paths (resolver index, display hooks, imperative dispatch) consistently gate layout translation on the preference, and the store subscription correctly rebuilds state on toggle. Only P2 findings: unnecessary layout-store subscription when adaptive is off, and a deep import path in page.tsx.

apps/desktop/src/renderer/hotkeys/hooks/useHotkeyDisplay/useHotkeyDisplay.ts — layout store subscription is active even when adaptive is disabled.

Important Files Changed

Filename Overview
apps/desktop/src/renderer/hotkeys/stores/keyboardPreferencesStore.ts New Zustand store with persist middleware, correctly partializing to only persist adaptiveLayoutEnabled (excludes the setter). Clean implementation.
apps/desktop/src/renderer/hotkeys/hooks/useHotkeyDisplay/useHotkeyDisplay.ts Adds useActiveLayoutMap to gate layout translation on the preference; both hooks updated correctly, but the layout store is subscribed unconditionally causing unnecessary re-renders when adaptive is off.
apps/desktop/src/renderer/hotkeys/utils/resolveHotkeyFromEvent.ts Adds a useKeyboardPreferencesStore.subscribe listener to rebuild registeredAppChords on preference toggle; logic is symmetric with the existing overrides and layout subscribers.
apps/desktop/src/renderer/hotkeys/hooks/useBinding/useBinding.ts getDispatchChord correctly reads the preference and passes null as the layout map when adaptive is off.
apps/desktop/src/renderer/hotkeys/stores/index.ts Adds useKeyboardPreferencesStore to the stores barrel export — straightforward and correct.
apps/desktop/src/renderer/routes/_authenticated/settings/keyboard/page.tsx Adds the preferences toggle UI and wires it to the store; imports useKeyboardPreferencesStore via a deep file path instead of the available renderer/hotkeys/stores barrel.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    Pref[KeyboardPreferencesStore\nadaptiveLayoutEnabled] -->|subscribe| Resolver
    Pref -->|useKeyboardPreferencesStore| UseActiveLayoutMap

    LayoutStore[KeyboardLayoutStore\nmap] -->|subscribe| Resolver
    LayoutStore -->|useKeyboardLayoutStore| UseActiveLayoutMap

    OverridesStore[HotkeyOverridesStore\noverrides] -->|subscribe| Resolver

    UseActiveLayoutMap{adaptive?\nlayoutMap : null} -->|layoutMap| UseHotkeyDisplay
    UseActiveLayoutMap -->|layoutMap| UseFormatBinding

    Resolver[resolveHotkeyFromEvent\nregisteredAppChords] -->|rebuild on any store change| ChordIndex[(chord → HotkeyId)]

    GetDispatchChord[getDispatchChord\nimperative] -->|reads getState| Pref
    GetDispatchChord -->|reads getState| LayoutStore
Loading
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
apps/desktop/src/renderer/hotkeys/hooks/useHotkeyDisplay/useHotkeyDisplay.ts:12-16
**Unnecessary layout-store subscription when adaptive is off**

`useActiveLayoutMap` always subscribes to `useKeyboardLayoutStore`, so every keyboard-layout change (e.g. user switching input sources) triggers a re-render of every `useHotkeyDisplay`/`useFormatBinding` consumer even when adaptive layout is disabled and the return value is always `null`. Moving the selector condition inside the store subscription avoids these phantom re-renders.

```suggestion
function useActiveLayoutMap(): ReadonlyMap<string, string> | null {
	const adaptive = useKeyboardPreferencesStore((s) => s.adaptiveLayoutEnabled);
	const layoutMap = useKeyboardLayoutStore((s) => (adaptive ? s.map : null));
	return layoutMap;
}
```

### Issue 2 of 2
apps/desktop/src/renderer/routes/_authenticated/settings/keyboard/page.tsx:29
**Direct deep import instead of stores barrel**

`useKeyboardPreferencesStore` is now re-exported from `renderer/hotkeys/stores` (via the updated `stores/index.ts`), but the import here bypasses that barrel and reaches directly into the file. The rest of this file uses the top-level `renderer/hotkeys` barrel for related stores — using `renderer/hotkeys/stores` here would be more consistent and keeps internal file layout opaque to consumers.

```suggestion
import { useKeyboardPreferencesStore } from "renderer/hotkeys/stores";
```

Reviews (1): Last reviewed commit: "feat(desktop): add toggle to disable ada..." | Re-trigger Greptile

Comment on lines +12 to +16
function useActiveLayoutMap(): ReadonlyMap<string, string> | null {
const layoutMap = useKeyboardLayoutStore((s) => s.map);
const adaptive = useKeyboardPreferencesStore((s) => s.adaptiveLayoutEnabled);
return adaptive ? layoutMap : null;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Unnecessary layout-store subscription when adaptive is off

useActiveLayoutMap always subscribes to useKeyboardLayoutStore, so every keyboard-layout change (e.g. user switching input sources) triggers a re-render of every useHotkeyDisplay/useFormatBinding consumer even when adaptive layout is disabled and the return value is always null. Moving the selector condition inside the store subscription avoids these phantom re-renders.

Suggested change
function useActiveLayoutMap(): ReadonlyMap<string, string> | null {
const layoutMap = useKeyboardLayoutStore((s) => s.map);
const adaptive = useKeyboardPreferencesStore((s) => s.adaptiveLayoutEnabled);
return adaptive ? layoutMap : null;
}
function useActiveLayoutMap(): ReadonlyMap<string, string> | null {
const adaptive = useKeyboardPreferencesStore((s) => s.adaptiveLayoutEnabled);
const layoutMap = useKeyboardLayoutStore((s) => (adaptive ? s.map : null));
return layoutMap;
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/hotkeys/hooks/useHotkeyDisplay/useHotkeyDisplay.ts
Line: 12-16

Comment:
**Unnecessary layout-store subscription when adaptive is off**

`useActiveLayoutMap` always subscribes to `useKeyboardLayoutStore`, so every keyboard-layout change (e.g. user switching input sources) triggers a re-render of every `useHotkeyDisplay`/`useFormatBinding` consumer even when adaptive layout is disabled and the return value is always `null`. Moving the selector condition inside the store subscription avoids these phantom re-renders.

```suggestion
function useActiveLayoutMap(): ReadonlyMap<string, string> | null {
	const adaptive = useKeyboardPreferencesStore((s) => s.adaptiveLayoutEnabled);
	const layoutMap = useKeyboardLayoutStore((s) => (adaptive ? s.map : null));
	return layoutMap;
}
```

How can I resolve this? If you propose a fix, please make it concise.

useHotkeyOverridesStore,
useRecordHotkeys,
} from "renderer/hotkeys";
import { useKeyboardPreferencesStore } from "renderer/hotkeys/stores/keyboardPreferencesStore";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Direct deep import instead of stores barrel

useKeyboardPreferencesStore is now re-exported from renderer/hotkeys/stores (via the updated stores/index.ts), but the import here bypasses that barrel and reaches directly into the file. The rest of this file uses the top-level renderer/hotkeys barrel for related stores — using renderer/hotkeys/stores here would be more consistent and keeps internal file layout opaque to consumers.

Suggested change
import { useKeyboardPreferencesStore } from "renderer/hotkeys/stores/keyboardPreferencesStore";
import { useKeyboardPreferencesStore } from "renderer/hotkeys/stores";
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/routes/_authenticated/settings/keyboard/page.tsx
Line: 29

Comment:
**Direct deep import instead of stores barrel**

`useKeyboardPreferencesStore` is now re-exported from `renderer/hotkeys/stores` (via the updated `stores/index.ts`), but the import here bypasses that barrel and reaches directly into the file. The rest of this file uses the top-level `renderer/hotkeys` barrel for related stores — using `renderer/hotkeys/stores` here would be more consistent and keeps internal file layout opaque to consumers.

```suggestion
import { useKeyboardPreferencesStore } from "renderer/hotkeys/stores";
```

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

…barrel

Match how useHotkeyOverridesStore is exposed so the keyboard settings
page can import both stores from the same module.
@Kitenite Kitenite merged commit 7971e58 into main May 5, 2026
10 checks passed
@Kitenite Kitenite deleted the disable-adaptive-layout-m branch May 5, 2026 06:21
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ✅ Neon database branch

Thank you for your contribution! 🎉

Kitenite added a commit that referenced this pull request May 5, 2026
…h consumer (#4091)

The "Adaptive layout mapping" toggle in #4078 was wired into the resolver
index, the display hooks, and `getDispatchChord`, but missed the actual
hotkey registration in `useHotkey` and the recorder's conflict detector.
Worse, every shipped registry default was a bare-string (physical) chord,
which `bindingToDispatchChord` short-circuits — so flipping the toggle did
nothing for the 75 built-in shortcuts regardless of any other fix. Net
result: a Dvorak / QWERTZ user pressing the labeled-T key for `⌘T` saw
the menu update but no hotkey fire.

- Route every layout-map consumer through a single
  `useEffectiveLayoutMap` / `getEffectiveLayoutMap` chokepoint that gates
  by `adaptiveLayoutEnabled`. Adds a banner on `keyboardLayoutStore`
  warning future contributors not to read the raw store.
- Re-author printable defaults in `registry.ts` as `mode: "logical"`
  via an `L()` helper so the toggle actually moves them on non-US
  layouts. Named-key chords (arrows, Enter, Backspace, Tab) stay as
  bare strings since they're layout-stable.
- Flip `adaptiveLayoutEnabled` to default `true` to match macOS / VS
  Code / Cursor convention. Existing users who explicitly persisted a
  value keep it (zustand persist is write-on-set).
- Update the keyboard settings copy to describe the toggle in
  user-facing terms ("match shortcuts to printed key labels" vs
  "anchor to physical positions").
- Add `registry.test.ts` locking in the logical-default authoring
  convention so a future PR can't silently revert printable chords to
  bare strings.
- `apps/desktop/plans/20260505-hotkey-adaptive-toggle-smoke-test.md`
  documents the manual smoke test for non-US layouts.
saddlepaddle pushed a commit that referenced this pull request May 6, 2026
)

* feat(desktop): add toggle to disable adaptive keyboard-layout mapping

Adds an opt-in "Adaptive layout mapping" preference on the keyboard
settings page. When off (default), shortcuts dispatch and display
exactly as bound regardless of the OS input source — i.e. ⌘Z stays
⌘Z on QWERTZ instead of being remapped to physical KeyY.

The preference is persisted in localStorage and read by the resolver
index, the display hooks (useHotkeyDisplay, useFormatBinding), and
the imperative getDispatchChord, all of which pass null for the
layout map when adaptive is off.

* refactor(hotkeys): export useKeyboardPreferencesStore from top-level barrel

Match how useHotkeyOverridesStore is exposed so the keyboard settings
page can import both stores from the same module.
saddlepaddle pushed a commit that referenced this pull request May 6, 2026
…h consumer (#4091)

The "Adaptive layout mapping" toggle in #4078 was wired into the resolver
index, the display hooks, and `getDispatchChord`, but missed the actual
hotkey registration in `useHotkey` and the recorder's conflict detector.
Worse, every shipped registry default was a bare-string (physical) chord,
which `bindingToDispatchChord` short-circuits — so flipping the toggle did
nothing for the 75 built-in shortcuts regardless of any other fix. Net
result: a Dvorak / QWERTZ user pressing the labeled-T key for `⌘T` saw
the menu update but no hotkey fire.

- Route every layout-map consumer through a single
  `useEffectiveLayoutMap` / `getEffectiveLayoutMap` chokepoint that gates
  by `adaptiveLayoutEnabled`. Adds a banner on `keyboardLayoutStore`
  warning future contributors not to read the raw store.
- Re-author printable defaults in `registry.ts` as `mode: "logical"`
  via an `L()` helper so the toggle actually moves them on non-US
  layouts. Named-key chords (arrows, Enter, Backspace, Tab) stay as
  bare strings since they're layout-stable.
- Flip `adaptiveLayoutEnabled` to default `true` to match macOS / VS
  Code / Cursor convention. Existing users who explicitly persisted a
  value keep it (zustand persist is write-on-set).
- Update the keyboard settings copy to describe the toggle in
  user-facing terms ("match shortcuts to printed key labels" vs
  "anchor to physical positions").
- Add `registry.test.ts` locking in the logical-default authoring
  convention so a future PR can't silently revert printable chords to
  bare strings.
- `apps/desktop/plans/20260505-hotkey-adaptive-toggle-smoke-test.md`
  documents the manual smoke test for non-US layouts.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant