Skip to content

fix(hypr): resolve universal clipboard letters from primary layout - #13007

Open
kvnloo wants to merge 4 commits into
omacom:quattrofrom
kvnloo:tip/12570-clipboard-keycodes
Open

kvnloo wants to merge 4 commits into
omacom:quattrofrom
kvnloo:tip/12570-clipboard-keycodes

Conversation

@kvnloo

@kvnloo kvnloo commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fixes #12570.

Universal "SUPER+A/C/V/X" shortcuts currently inject logical letters through Hyprland's "send_key_state".

When a non-Latin XKB group such as Russian is active, Hyprland may fail to resolve the Latin keysym and report:

send_key_state: key not found

Using fixed QWERTY keycodes avoids that lookup, but introduces a different regression: physical key positions do not preserve logical "A/C/V/X" across layouts such as Dvorak and French AZERTY.

This revision resolves each logical clipboard letter against the configured primary XKB layout, then injects the resulting "code:N" keycode.

That gives us both invariants:

  • a non-Latin secondary group cannot make "C/V/X/A" unresolvable;
  • a non-QWERTY primary layout still receives the intended logical shortcut.

Behavior

For a primary QWERTY layout:

A → code:38
C → code:54
V → code:55
X → code:53

For a primary Dvorak layout, the same logical shortcuts resolve to their Dvorak positions instead:

C → code:31
V → code:60
X → code:56

For French AZERTY:

A → code:24

So "SUPER+A" still produces logical "Ctrl+A" instead of the fixed-QWERTY "Ctrl+Q" regression.

Implementation

The shortcut callback reads the current configured XKB inputs:

  • "input.kb_layout"
  • "input.kb_variant"
  • "input.kb_model"
  • "input.kb_options"
  • "input.kb_rules"

It resolves the requested logical letter with:

xkbcli how-to-type

and selects the keycode belonging to the primary configured layout.

Resolved values are cached by:

letter + layout + variant + model + options + rules

so repeated shortcuts do not spawn another resolver process unless the XKB configuration changes.

The fallback deliberately preserves the existing named-key behavior if resolution fails. It does not guess a QWERTY physical position.

Terminal behavior

Current "quattro" uses:

Ctrl+Shift+C
Ctrl+Shift+V

for terminal copy/paste.

This revision preserves those semantics.

Only the logical "C" / "V" position is resolved through the configured primary layout before injection.

It does not revert terminal behavior to the older "Ctrl+Insert" / "Shift+Insert" implementation.

Existing key-state timing

The existing "send_shortcut_once()" down/up lifecycle and its "50 ms" release timer are unchanged.

This PR addresses key resolution only.

The separate held-"SUPER" / synthetic retrigger concerns discussed in #7027 and #10701 are a different invariant and are intentionally not folded into this patch.

Related work / consolidation

This PR consolidates several overlapping attempts so maintainers do not need to reconstruct the history:

If this revision lands, #9130 should no longer need a separate review path for the same underlying fix.

Credits

Thanks @VivusCo for the current #12570 reproduction and for tracing the failure to active-group keysym resolution.

Thanks @nazarenkodima for the earlier #7375 work, and especially @ivankuznetsov for identifying the Dvorak boundary and implementing the primary-layout-aware resolver in "705c7fd". That prior work avoided repeating the same fixed-QWERTY mistake here.

Thanks @Krol-X and @ivantsukev for the additional real-layout testing and for documenting the compatibility requirements around QWERTY, Dvorak, Bulgarian/non-Latin layouts, and application behavior.

Thanks @yashranaway for keeping the fixed-keycode path moving in #9130 as well.

Regression coverage

"hyprland-default-config-test.sh" now exercises the production resolver against real "xkbcli" keymap resolution.

QWERTY + non-Latin secondary layout

us,ru

Requires:

SUPER+A → Ctrl + code:38
SUPER+C → Ctrl + code:54
SUPER+V → Ctrl + code:55
SUPER+X → Ctrl + code:53

Dvorak + non-Latin secondary layout

us,ru
variant: dvorak,

Requires:

SUPER+C → Ctrl + code:31
SUPER+V → Ctrl + code:60
SUPER+X → Ctrl + code:56

and verifies the current terminal semantics remain:

SUPER+C → Ctrl+Shift + logical C position
SUPER+V → Ctrl+Shift + logical V position

French AZERTY

fr,ru

Requires:

SUPER+A → Ctrl + code:24

so select-all cannot regress to the QWERTY physical "A" position.

The regression also pins the existing "50 ms" key-up timer so this change cannot silently absorb the separate dispatch-timing behavior.

Verification status

Rebuilt as one commit on current "quattro":

head: 290d0ed
base: 28ceaae
ahead: 1
behind: 0

The implementation and final diff have been audited against the prior #7375/#9130/#7283 lineage.

The historical layout-aware implementation in "705c7fd" was previously reported with focused QWERTY/Dvorak tests passing; this revision adapts that design to current "quattro", adds "SUPER+A", and preserves today's terminal "Ctrl+Shift+C/V" behavior.

Executable repository tests have not been rerun in this environment.

Final live acceptance should cover:

  1. "us,ru", with Russian active before any successful English shortcut:
    • "SUPER+A/C/V/X" work without "key not found".
  2. primary Dvorak + non-Latin secondary layout:
    • logical copy/paste/cut remain "C/V/X", not QWERTY physical positions.
  3. French AZERTY:
    • "SUPER+A" remains logical select-all.
  4. terminal:
    • "SUPER+C/V" retain "Ctrl+Shift+C/V".
  5. repeated shortcuts:
    • existing down/up behavior remains unchanged.

Fixes #12570.

@llstrk

llstrk commented Sep 23, 2026

Copy link
Copy Markdown

One source-supported regression: fixed QWERTY positions change the injected shortcut on other Latin layouts.

Preserve logical letters on non-QWERTY layouts

The new keycode constants bypass keysym lookup, but a physical keycode does not preserve the letter across layouts. With a single Dvorak layout (kb_layout = "us", kb_variant = "dvorak"), code 54 is J, not C. French AZERTY similarly puts Q at code 38.

Source-traced normal-application chords, not observed application behavior:

Layout / action Before This PR
Dvorak: copy Ctrl+C Ctrl+J
Dvorak: paste Ctrl+V Ctrl+K
Dvorak: cut Ctrl+X Ctrl+Q
French AZERTY: select all Ctrl+A Ctrl+Q

Hyprland 0.56.2 accepts code:N directly, whereas the previous letter names resolve to the correct positions in these single-layout configurations. The terminal branch retains Shift, so Dvorak copy/paste instead become Ctrl+Shift+J/K there.

Impact: these shortcuts can invoke unrelated actions or do nothing instead of copy/paste/cut/select-all. Ctrl+Q can quit applications that bind it; individual client reactions were not tested.

Suggested change: preserve logical letter selection on non-QWERTY layouts. Make the non-Latin fallback aware of the layout used for the injected event, rather than applying US physical positions globally.

Verified: source inspection confirms that code:N bypasses the lookup responsible for key not found. Terminal detection, modifier selection and the existing down/up timer are unchanged. This does not establish end-to-end copy/paste success with Russian active.


Review information

Test scope: conclusions are supported by pinned Omarchy/Hyprland source and XKB layout definitions. No compositor or application-level clipboard testing; proposed corrections are untested.

Community review: Independent automated community review, unaffiliated with the Omarchy team, intended to help prepare PRs for their review.

Automated AI review: Astra Medium initial inspection and synthesis, independent Opus 5.5 High and GPT 6 Sol Xhigh technical reviews, and a fresh Astra Medium editorial check.

@kvnloo
kvnloo force-pushed the tip/12570-clipboard-keycodes branch from c2503ef to 02b3eb8 Compare September 23, 2026 23:49
@kvnloo
kvnloo force-pushed the tip/12570-clipboard-keycodes branch from 02b3eb8 to 290d0ed Compare September 23, 2026 23:50
@kvnloo kvnloo changed the title fix(hypr): send universal clipboard chords by keycode fix(hypr): resolve universal clipboard letters from primary layout Sep 23, 2026
@kvnloo

kvnloo commented Sep 23, 2026

Copy link
Copy Markdown
Contributor Author

Updated at "290d0ede" on current "quattro".

Thanks @VivusCo for the detailed #12570 reproduction, and thanks @ivankuznetsov for catching the Dvorak boundary in #7375 and implementing the layout-aware resolver in "705c7fd". That prior work materially changed this revision — instead of replacing the broken keysym lookup with fixed QWERTY positions, this now resolves the logical clipboard letter from the configured primary XKB layout first.

I also went back through #9130, #7375, and #7283 before updating this head:

The current patch now covers "A/C/V/X", not just "C/V/X".

It preserves current "quattro" terminal behavior ("Ctrl+Shift+C/V") and resolves those logical letters through the same primary-layout mapping instead of reverting to the older "Insert" shortcuts.

Regression coverage now includes:

  • "us,ru" QWERTY;
  • primary Dvorak + "ru";
  • French AZERTY "SUPER+A";
  • terminal "Ctrl+Shift+C/V";
  • preservation of the existing "50 ms" key-up timer.

The branch is one commit ahead / zero behind current "quattro".

Thanks also @nazarenkodima, @Krol-X, @ivantsukev, and @yashranaway for the prior implementations/testing that made the compatibility boundaries much clearer.

Executable repo tests and live compositor/application validation are still pending.

@llstrk

llstrk commented Sep 24, 2026

Copy link
Copy Markdown

Automated AI review

Community review: Independent automated community review, unaffiliated with the Omarchy team, intended to help prepare PRs for their review.

Follow-up on the earlier review, checked at 290d0ede after the author's update.

Outcome: the earlier finding (fixed QWERTY positions change the shortcut on other Latin layouts) is resolved. Two narrower, configuration-specific issues remain in the new resolver, plus a robustness gap when the user's Compose file is broken.

Earlier finding: resolved

Verified: the resolver now asks xkbcli how-to-type which key produces each letter in layout 1 of the configured keymap. Layout 1 is the right reference: Hyprland 0.56.2 sends the injected key with modifier group 0, so applications read it in the first layout whichever layout is active.

Layout / action Previous head This revision
Dvorak (us,ru, dvorak,): copy / paste / cut code 54 / 55 / 53 (Ctrl+J / K / Q) code 31 / 60 / 56 (Ctrl+C / V / X)
French AZERTY (fr,ru): select all code 38 (Ctrl+Q) code 24 (Ctrl+A)
Terminal copy / paste on Dvorak Ctrl+Shift+J / K Ctrl+Shift+C / V

Verified in sandboxed runs with real xkbcli 1.13.2 and XKB data:

  • The PR's parser also resolves A/C/V/X to 38/54/55/53 for the Universal paste (SUPER+V) fails with 'key not found' when active keyboard layout lacks the Latin key symbol (e.g. Russian layout) #12570 setup (gb,ru,ua), Omarchy's default us,ru form, de,ru and us,dk,eu. code:N skips the active-group lookup that raises send_key_state: key not found.
  • xkbcli runs once per letter, then the result is cached. Without xkbcli, the named keys are sent with no Lua error.
  • The repository test passes on this head. With this head's test copied onto the base, it fails at the select-all check; on the previous head, it fails at the Dvorak copy check. So the new checks do guard the earlier finding.

Per-device keymaps and kb_file are ignored

The resolver reads only the global input.kb_* values (clipboard.lua). Hyprland builds each keyboard's keymap from that device's own kb_* overrides or kb_file (InputManager.cpp, IKeyboard.cpp), and sends applications the active keyboard's keymap.

Source-traced example, not run in a compositor:

global kb_layout = "us"; external keyboard: kb_variant = "dvorak"
SUPER+C on that keyboard
  resolver (global us)  -> code:54
  application (Dvorak)  -> reads code 54 as J -> Ctrl+J
  base (named key C)    -> resolved on the active keyboard -> Ctrl+C

A global input.kb_file whose layout moves A/C/V/X behaves the same way.

Impact: on setups with a per-device layout or a custom keymap file, the earlier finding's symptom returns: the shortcut sends a different letter. The default global us,ru setup is not affected.

Suggested change: fall back to the named key when input.kb_file is set, and document or handle per-device overrides. Hyprland's Lua hl.get_config returns global values only, so a full fix may need a Hyprland-side API.

Letter matched at any shift level

The parser takes the first layout-1 line for the uppercase letter at any level. In us(3l), us(3l-cros) and us(3l-emacs), key 30 carries A at level 6 and sorts before the real A key (41). SUPER+A then sends Ctrl+K instead of Ctrl+A; the base sent the correct key. A scan of all 753 layout/variant pairs found only these three where the choice changes a previously working chord.

Impact: select-all sends Ctrl+K on those three variants.

Suggested change: look up the lowercase letter (key:lower()) and accept only lines whose level column is 1, which matches how applications and Hyprland's own named-key lookup read the key.

A broken Compose file silently disables the resolver

From libxkbcommon 1.11, how-to-type loads the user's Compose file by default. If that file opens but fails to parse (for example, an include pointing to a missing file), it exits with no output. The resolver discards stderr, caches the named-key fallback and reuses it until the Lua config reloads or the input.kb_* values change. This is not a regression: the fallback is the base behavior. A healthy Omarchy Compose setup was not affected in testing.

Impact: on a system with a damaged or stale Compose file, #12570 stays unfixed for that session, with no diagnostic.

Suggested change: pass --disable-compose, since only the direct keymap rows are needed. Versions before 1.11 reject that flag, so older libxkbcommon would need a retry without it.


Review information

Test scope: source inspection of this head, the base and the previous head, Hyprland 0.56.2, libxkbcommon and xkeyboard-config. Sandboxed runs of xkbcli, the PR's parser, a mocked-Hyprland harness loading the real clipboard.lua, and the repository test. No live compositor or application testing: end-to-end copy/paste with Russian active and the per-device case are source-traced, not observed.

AI process: Opus 5.5 Medium coordination and synthesis, independent Opus 5.5 Xhigh and GPT 6 Sol Xhigh technical assessments of the new revision, and an Opus 5.5 Medium editorial check.

Opt out: To stop receiving these reviews, reply to this comment saying so.

@WokoFlipper

Copy link
Copy Markdown

Confirming measurement from a live non-Latin session (per @linyiru request in #12557): primary-layout resolution is the shape that survives a non-QWERTY primary.

Environment: Hyprland 0.56.2, layouts us,ru,us. xkbcli how-to-type on us-dvorak gives C/V/X/A = 31/60/56/38, vs hardcoded QWERTY 54/55/53/38 in #9130 — so #9130 sends CTRL+J/K/Q on a Dvorak primary while this PR resolves correctly. Our ru pack runs the keycode equivalent live under RU secondary + QWERTY primary with no issues, which is consistent: both shapes agree exactly when the primary is QWERTY, and only this one holds otherwise.

One note: this depends on xkbcli being present at runtime — worth a fallback (or a configure-time check) for minimal installs.

omarchybot and others added 3 commits October 3, 2026 02:14
…13007

# Conflicts:
#	default/hypr/bindings/clipboard.lua
…ted letter

A keymap set through input.kb_file overrides the kb_* rules the resolver reads, so it sent the QWERTY position into a keymap that put another letter there: Ctrl+K for paste and Ctrl+Q for cut under a Dvorak kb_file. Fall back to the named key there, which Hyprland resolves against the real keymap.

xkbcli lists a letter at every level it appears on, and in us(3l) an uppercase A on level 6 of another key sorted first, so select-all sent that key. Look up the lowercase letter and take only level 1, as an application reads a Ctrl chord.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The clipboard manager bind on quattro is a { panel = ... } bind, which o.bind turns into hl.dsp.global, so loading clipboard.lua against the fake hl needs it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@omarchybot omarchybot added the verified Omarchy Triage has verified that this issue is ready for final review label Oct 3, 2026
@omarchybot

Copy link
Copy Markdown
Collaborator

Reviewed and tested at 816ab6c9, on a disposable Omarchy worker running Hyprland 0.56.2.

The bug is real and this fixes it. I loaded clipboard.lua into the running compositor with kb_layout = "us,ru" and Russian active, then fired the binds at a foot window that records the control bytes it receives. On quattro, all four of SUPER+A/C/V/X fail with send_key_state: key not found and nothing arrives. On this branch, Ctrl+A, Ctrl+V and Ctrl+X arrive, with a QWERTY, Dvorak (dvorak,) or French (fr,ru) primary layout alike. Ctrl+C couldn't be observed this way on either branch, because a raw terminal swallows it, but it is resolved by the same code.

Pushed to your branch:

  • A merge of quattro. It conflicted only on the clipboard-manager line, which quattro now binds as { panel = "omarchy.clipboard" }. The test's fake hl gained hl.dsp.global, which that bind needs.
  • When input.kb_file is set, the resolver now falls back to the named key. A keymap file overrides the kb_* rules, so the resolver was sending QWERTY positions into whatever the file defines. With a Dvorak kb_file on the worker, this branch sent Ctrl+J/K/Q for copy, paste and cut, and Ctrl+Q quits many apps. quattro and the fixed branch send Ctrl+C/V/X.
  • The resolver now looks up the lowercase letter and accepts only level-1 rows. In us(3l), an uppercase A on level 6 of another key sorted first, and select-all sent Ctrl+K on the worker. It now sends Ctrl+A. Both cases have regression tests.

Tests on the worker: hyprland-default-config-test.sh 20/20, and the nine other shell tests that load the bindings all pass. With the level fix reverted, the new check fails. ./test/cli has one failure, vscode generated theme references current theme file, which fails the same way on quattro and is unrelated.

Still open: keyboards with their own kb_layout in a per-device block. The resolver reads only the global input.kb_*, so a keyboard overridden to AZERTY under a global us would get QWERTY keycodes (SUPER+A sending Ctrl+Q), where quattro resolves the named key against that keyboard's own keymap. I traced this from source rather than reproducing it. It is the same mechanism as the kb_file case above. Hyprland's Lua API has no way to read a keyboard's effective keymap, so this can't be fixed from the config side. Whether to accept the limitation is for the maintainer.

Second opinion: Codex Medium reviewed the branch after these fixes. It raised the per-device case above. It also flagged that the resolver runs xkbcli inside the compositor's key handler. I measured about 3 ms per lookup, and each letter is looked up once and cached, so I don't consider it a problem. Its claim that the test's tab escapes were broken does not hold, since the tests pass. Waiting on the maintainer.

@greptile-apps

greptile-apps Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 4/5

[Medium risk] Adds keyboard layout resolution to clipboard shortcuts.

Resolve keyboard-specific settings before merging so keyboards with layout overrides receive the intended shortcuts.

Findings

  1. P1 Keyboard overrides send wrong letters ▶
  2. P2 First shortcuts block desktop events ▶

Summary

Resolves universal clipboard letters to primary-layout keycodes and caches them by keyboard settings. Adds coverage for QWERTY, Dvorak, French AZERTY, higher-level letters, and custom-keymap fallback.

  • Keyboard-specific layout overrides are not included in resolution.
  • Uncached shortcuts wait for xkbcli inside the compositor callback.
  • kvnloo explicitly described the existing held-SUPER and retrigger concerns as deferred; the 50 ms release lifecycle is unchanged.
  • kvnloo explicitly described named-key fallback on resolution failure as intentional; the custom-keymap test also preserves that behavior.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Universal shortcut] --> B{Custom keymap file?}
  B -->|Yes| F[Use named key]
  B -->|No| C[Read global keyboard settings]
  C --> D{Cached letter?}
  D -->|Yes| G[Use cached key]
  D -->|No| E[Wait for xkbcli]
  E --> H{Primary unshifted letter found?}
  H -->|Yes| I[Cache keycode]
  H -->|No| F
  I --> G
  F --> J[Send key down]
  G --> J
  J --> K[Send same key up after 50 ms]
Loading

Reviews (1) · Last reviewed commit: "Stub hl.dsp.global in the clipboard shor..."

Comment on lines +55 to +60
local config = {
layout = tostring(hl.get_config("input.kb_layout") or "us"),
variant = tostring(hl.get_config("input.kb_variant") or ""),
model = tostring(hl.get_config("input.kb_model") or ""),
options = tostring(hl.get_config("input.kb_options") or ""),
rules = tostring(hl.get_config("input.kb_rules") or ""),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Keyboard overrides send wrong letters

resolve_primary_keycode reads only global input.kb_* settings. If a keyboard has its own Dvorak layout while the global layout is us, copy resolves to code:54 instead of Dvorak's code:31. The focused application receives the wrong shortcut.

Resolve against the keyboard used for the injected event, including its overrides, and include those settings in the cache key.

Comment on lines +84 to +91
local pipe = io.popen(table.concat(command, " ") .. " 2>/dev/null")
if not pipe then
keycode_cache[cache_key] = key
return key
end

local output = pipe:read("*a") or ""
pipe:close()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 First shortcuts block desktop events

Each uncached shortcut runs xkbcli and waits for all its output inside the Hyprland callback. The compositor waits for a keymap build before sending the shortcut, once for each letter and again after settings change. A slow resolver also delays other desktop events.

Move this work out of the keypress callback, or resolve asynchronously and cache the result.

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

Labels

bug Something isn't working verified Omarchy Triage has verified that this issue is ready for final review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Universal paste (SUPER+V) fails with 'key not found' when active keyboard layout lacks the Latin key symbol (e.g. Russian layout)

4 participants