poltertype is data-driven: each supported keyboard layout is one
TOML file, and each language's dictionary is a plain wordlist. Neither
is compiled into the binary — both are read from disk at run time.
There are two paths to add a language: bundled (committed to
data/, shipped with the app, needs a PR and a rebuild) or user
(dropped into your config directory, no rebuild).
TL;DR — for a one-off custom layout on your own machine, drop a
*.tomlinto<config-dir>/poltertype/layouts/and a matching wordlist into<config-dir>/poltertype/wordlists/. To upstream a language, edit the bundled files and submit a PR.
A layout TOML answers one question per row: "when the user presses physical key X under this layout, which character does the OS produce?"
The file lives at data/layout-mappings/<stem>.toml (bundled) or
<config-dir>/poltertype/layouts/<stem>.toml (user). The schema
is identical.
id = "pl-PL" # BCP-47-style; unique
name = "Polski" # display label (optional)
script = "Latin" # Latin / Cyrillic / Greek / Armenian / Hebrew / Arabic / Other
[keys]
# Win SC Set-1 scancodes → produced character.
# `plain` = unshifted, `shift` = shifted variant (optional).
0x10 = { plain = "q", shift = "Q" }
0x11 = { plain = "w", shift = "W" }
# ... fill in alphanumeric and punctuation rowsCover at least the alphanumeric block plus the punctuation that
serves as a word boundary in your language. The fifteen bundled files
in data/layout-mappings/ are good copy-paste templates — they all
list the same 46 scancodes (number row, three letter rows, and the
key left of Z), plus 0x56 where a layout puts a letter on the
extra 105-key board key, as Bulgarian does. The detector ignores
scancodes you don't list, so omitting 0x29 (backtick) for a layout
that doesn't use it is fine.
On Linux the authoritative answer is already installed. xkbcommon
knows what every layout produces per key, so rather than transcribing
a keyboard picture, compile the keymap and read the levels off it —
xkb_keymap_key_by_name for AD01…AB10, then
xkb_keymap_key_get_syms_by_level for group 1 levels 1 and 2, then
xkb_keysym_to_utf8. XKB key names map onto Set-1 scancodes in three
straight runs: AE01–AE12 → 0x02–0x0D, AD01–AD12 →
0x10–0x1B, AC01–AC11 → 0x1E–0x28, AB01–AB10 →
0x2C–0x35, plus BKSL → 0x2B and LSGT → 0x56.
The nine layouts added in 0.9.0 were generated this way and then hand-reviewed; the generator is small enough to rewrite in an afternoon and is not worth vendoring, but knowing the trick is worth a paragraph.
PolterType tracks the character produced per keystroke, not the
OS-level dead-key state machine. Surface the spacing equivalent
(´, ^, ¨) — that matches what shows up in a mid-correction
buffer the rare time the user actually presses a dead key alone.
Add a TOML, restart PolterType, and watch the log:
loaded user layout layout=pl-PL keys=46 dict=false stem=pl_pl
If keys= is too low, you missed entries. If the layout doesn't
appear, look for a parse error in the log.
On Windows, expect the OS to disagree with you — and win. A bundled or plug-in mapping there is replaced by whatever the installed keyboard actually produces, because a Windows layout names a language and a language is not a keyboard. You will see it happen:
adopted the OS keymap for this keyboard layout=pl-PL variant=00000415 keys=48 replaced=2
That is working as intended, and it means Windows is not the place to
check a hand-written table — the value you typed into the TOML may
never be used. A user TOML in your config dir still outranks the
OS, so that is how you test one deliberately. To see exactly which
keys the OS overruled, run with RUST_LOG=poltertype_core=debug and
read the keys the OS disagreed on line.
Without a wordlist, PolterType falls back to the plausibility detector — which is decent for distinctive scripts (Cyrillic vs Latin) but unreliable inside a single script (German vs English). A real wordlist makes detection trustworthy.
Four filenames per layout, all under either data/wordlists/
(bundled) or <config-dir>/poltertype/wordlists/ (user):
| File | Goes into | Used for |
|---|---|---|
<stem>.txt |
user_overlay (full-length lookup) |
The main dictionary. One lowercase word per line. Bundled languages commit this gzipped, as <stem>.txt.gz. |
<stem>-extras.txt |
same as above | Optional second file for organisation (tech vocab, surnames, …). Merged with <stem>.txt. |
<stem>-stop.txt |
short_stop_words (≤2-letter lookup) |
Hand-curated 1- and 2-letter words. Optional — an absent file yields an empty set, which is graceful at runtime. |
<stem>-weak.txt |
weak |
Marks entries that are technically valid but rare (archaic forms, obscure inflections), so that a strong dictionary hit in the other layout wins over a weak hit in this one. Optional. |
# blank lines and `#` comments ignored
# one lowercase word per line
hello
world
function
Bundled languages source their wordlists from public projects with
non-restrictive licences — see data/wordlists/CREDITS.md. The
cargo xtask wordlists fetch command re-downloads them.
For a user-side language, anything works: a Hunspell stems file
stripped of affix flags, an aspell dump master output, a frequency
list — as long as it's one lowercase word per line.
To bundle a new language so users get it without touching their config directory:
-
Drop the layout TOML into
data/layout-mappings/<stem>.toml. -
Add the dictionary source to
xtask/src/consts.rs::HUNSPELL_SOURCESand runcargo xtask wordlists fetch, which downloads the.dic+.aff, expands the affix rules and writesdata/wordlists/<stem>.txt.gzfor you. (Hand-made lists work too — drop<stem>.txt.gzin yourself and skip the entry.) The optional companions all live in the same directory:<stem>-extras.txt— your tech-vocab extras<stem>-stop.txt— short stop words<stem>-weak.txt— valid-but-rare entries
Watch the reported form count. Anything in the tens of millions means the
.affis combinatorial rather than inflectional and wantsExpandMode::StemsOnly— Hebrew is the worked example, seedata/wordlists/CREDITS.md. -
Add the stem to
crates/poltertype-core/build.rs::LAYOUTS, which is what copies the data into the dist tree:const LAYOUTS: &[(&str, &str)] = &[ ("en_us", "en-US"), // ... ("pl_pl", "pl-PL"), ];
-
Add the same stem to
crates/poltertype-core/src/layouts/consts.rs::BUNDLED_LAYOUT_STEMS, which is the list the runtime actually scans for:pub const BUNDLED_LAYOUT_STEMS: &[&str] = &["en_us", "uk_ua", "ru_ru", /* … */ "pt_br"];
Nothing is baked into the binary — the TOML is read from
<data_dir>/layout-mappings/at run time. The stem is all the runtime needs. -
Add the four files to
installers/wix/main.wxs. The MSI is the one installer that enumerates every data file by hand (<File Id="MapPlPl" …>and friends); the AppImage and the macOS bundlecp -Rthe whole tree and need nothing. Four entries per language — the layout TOML,<stem>.fst,<stem>-stop.txt,<stem>-surface.fst— and WiX errors on aSource=that doesn't exist, so only list files the build actually produces (that is why onlyuk_uahas a-weak.txtentry). Skip this step and the language works everywhere except Windows, where it silently isn't installed. -
Register the layout id with the OS switchers in
poltertype-layout, or the app can name your language but never switch to it:- Linux —
linux/shared.rs, both directions of the XKB table. - macOS —
macos.rs,tis_id_to_bcp47(list every variant you know) andbcp47_to_tis_id(base id only; a wrong guess here targets a keyboard the user doesn't have). - Windows needs nothing — it resolves BCP-47 from the HKL.
- Linux —
-
Optional but recommended: extend
derive_vowelswith the language's vowel set (especially if it uses accented vowels — the plausibility detector counts vowel ratio, and unrecognised vowels score as consonants). -
cargo buildandcargo test --workspace.
Keep (3) and (4) in lock-step. If they drift, the runtime logs a
"missing TOML" warning at startup for the stem it can't find — noisy
rather than silent, but it is not a build failure, so read the first
few log lines after wiring a new language.
every_bundled_stem_resolves_to_a_layout in
crates/poltertype-core/src/layouts/tests.rs fails on that drift, so
in practice cargo test catches it first.
If you want to add a language without touching the source tree (e.g. you're not a Rust dev, or you just want a one-off custom mapping for your odd keyboard):
- Open the tray menu → Open User Layouts Folder… to ensure
<config-dir>/poltertype/layouts/exists. - Drop
<stem>.tomlin there (schema as above). - Open the tray menu → Open User Wordlists Folder… for
<config-dir>/poltertype/wordlists/. - Drop the wordlists there.
- Restart PolterType (the engine snapshots the layout database at start; new layouts need a fresh boot to populate the scancode- translation tables). Adding words to an already-loaded language only needs Reload Settings from the tray menu.
- Optionally edit
config.tomland add your new id to[languages].active.
Same TOML, same wordlist format. If your id matches a bundled
layout, your file wins — handy if you disagree with the bundled
mapping for your physical keyboard.
Adding just a layout TOML without any wordlist is a perfectly valid lightweight mode: the dictionary detector silently abstains, and the plausibility detector picks up via vowel-ratio / consonant-cluster / script-fit signals. This works well for layouts whose script differs from any other active layout (a Greek user with Greek + English, say) — script alone is a strong signal. For two layouts in the same script, you'll want a real wordlist.