sdl-keybridge
The universal Rosetta Stone for SDL keyboards.
A static correspondence table that exposes, for every key press, all of its parallel representations — physical scancode, logical keycode, textual or symbolic glyph, localized label — across all the layout × platform combinations supported by SDL.
No other Rust crate combines layout-awareness (AZERTY/QWERTZ/JCUKEN), i18n of named keys (Escape → Échap/Esc/エスケープ), and a cross-layout binding bridge via scancode as a universal pivot.
Philosophy: Rosette, not Champollion
This crate is a static lookup table (Rosette). It exposes parallel data and individual labels. It does not interpret and does not package presentation conventions.
Anything that interprets — formatting a Ctrl+Shift+A combo,
serializing a binding, rebasing a config from one layout to another — is
Champollion territory: the job of the consuming application. The
Rosette gives you the raw correspondence data; you pick the separator,
the ordering, the storage format.
The four public functions
use ;
let loc = new;
// 1. Forward lookup — every parallel representation in one pass.
let r = resolve;
assert_eq!; // Shift+A on AZERTY → 'Q'
// 2. Reverse lookup — find the scancode for a keycode in a layout.
let sc = scancode_for;
assert_eq!;
// 3. Platform-aware modifier label.
let s = modifier_label;
assert_eq!;
// 4. Parse a textual key name back into a keycode.
assert_eq!;
Cross-layout binding bridge
Two lines of consumer code translate a binding from one layout to another through scancode-as-pivot:
use ;
# let keycode_ru = from;
let loc = new;
let sc = scancode_for.unwrap;
let r = resolve;
// r.glyph_local is what the user sees on their French AZERTY keyboard.
Combo formatting (consumer-side)
There is no format_combo() in this crate — the separator and ordering
are yours. Assemble the individual labels the way your UI expects:
use ;
# let loc = new;
# let layout = "linux/fr-t-k0-azerty";
# let locale = "fr";
# let style = Textual;
let ctrl = modifier_label;
let r = resolve;
let combo = format!; // "Ctrl+q" on AZERTY
SDL2 + SDL3 compatibility
There are no sdl2 / sdl3 feature flags. The API takes primitive types
(u32 for scancodes / keycodes, u16 for the modifier bitmask), whose
numeric values are identical between SDL2 and SDL3 for every constant
exposed here. Use the same crate regardless of your SDL binding.
Locales
26 locales available as individual Cargo features.
| Code | Language | Coverage |
|---|---|---|
en (default) |
English | reference, 110 ids |
fr |
Français | full |
de |
Deutsch | full |
cs |
Čeština | near-full |
es |
Español | near-full |
fi |
Suomi | near-full |
it |
Italiano | near-full |
ja |
日本語 | near-full |
ko |
한국어 | near-full |
nl |
Nederlands | near-full |
pl |
Polski | near-full |
pt |
Português | near-full |
ru |
Русский | near-full |
sk |
Slovenčina | near-full |
sv |
Svenska | near-full |
tr |
Türkçe | near-full |
zh-hans |
简体中文 | near-full |
zh-hant |
繁體中文 | near-full |
ar |
العربية | partial (essentials; falls back to English) |
bn |
বাংলা | partial |
hi |
हिन्दी | partial |
id |
Bahasa Indonesia | partial |
sw |
Kiswahili | partial |
th |
ภาษาไทย | partial |
ur |
اردو | partial |
vi |
Tiếng Việt | partial |
Enable only what you need; use the aggregate all-locales feature to
pull them all in.
[]
= { = "0.3", = ["fr", "de", "ja"] }
🙏 Call for translators
The partial locales above only cover the most common keys — F-keys, keypad labels, left/right modifier variants, and platform-aware modifier names all fall back to English for these 8 languages. We kept them partial rather than risk inaccurate translations in scripts we cannot independently verify (Arabic, Devanagari, Bengali, Thai, etc.).
If you are a native speaker, opening a PR to complete any of these
locales is the single highest-leverage contribution you can make.
The recipe is in CONTRIBUTING.md — in short:
cp src/locales/en.rs src/locales/<code>.rs, translate the strings,
send a PR. Each locale is pure Rust (~120 lines, one match expression,
no TOML / no codegen).
The same offer stands for any locale not yet listed (Greek, Hebrew, Icelandic, Kazakh, …). Adding a brand-new locale is ~5 lines of wiring on top of the translated file.
Layouts
v0.3 ships 601 layouts, all sourced from Unicode CLDR 43 at build time — no hand-authored layout data in the crate:
| Platform | Count |
|---|---|
| Android | 175 |
| ChromeOS | 55 |
| macOS | 137 |
| Windows | 209 |
Platform-neutral (und) |
25 |
| Total | 601 |
Every glyph served by resolve() / scancode_for() comes from a
CLDR-maintained XML with the full chain of Unicode Consortium / vendor
review behind it. If you find a character that looks wrong, that's a
CLDR upstream issue — fix it there and refresh data/cldr-43/ to get
the corrected version.
Layout ids are the exact CLDR locale tags prefixed by the platform dir:
windows/en-t-k0-windows— US QWERTY, Windowswindows/en-GB-t-k0-windows— UK QWERTYwindows/fr-t-k0-windows— French AZERTYwindows/de-t-k0-windows— German QWERTZwindows/ru-t-k0-windows— Russian ЙЦУКЕНwindows/ja-t-k0-windows— Japanese JISosx/en-t-k0-osx-colemak— Colemak (macOS)windows/en-t-k0-windows-dvorak— Dvorak (Windows)android/*-t-k0-android*— touch keyboards with long-press ignored (we expose primary glyph)- etc.
CLDR 44+ migrated to a new 3.0 format currently covering only ~8 niche layouts; CLDR 43 remains the canonical legacy source until 3.0 catches up.
Run cargo run --example inspect --all-features -- ? scancode 0 for
the full list of layout ids in this build.
Regenerating from fresh CLDR data
The build script reads data/cldr-43/keyboards/**/*.xml. To switch
source (e.g. drop in a newer CLDR release), replace that directory and
rebuild — the generated output regenerates automatically. Parser lives
in build.rs; ISO-to-scancode mapping is the ISO_MAP table at the
top of that file.
What this crate will not do (non-goals)
- Detect the current OS layout — the caller provides the BCP 47 id. No Rust solution reliably covers all five SDL platforms.
- Dead keys / text composition — a scancode + modifiers resolves to
one glyph. Composition (e.g.
^+e→ê) is the OS/IME's job, triggered bySDL_StartTextInput, not by us. - Package a
format_combo()— presentation conventions vary; the consumer assembles the labels it receives. - Package a binding serializer — config format is yours (JSON, INI, RON, binary, …).
License
Dual-licensed under either of
- Apache License, Version 2.0, (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.