Skip to content
WM KeyboardWM Keyboard
Accessibility

File formats

Every file format WM Keyboard reads or writes: extensions, structure, and links to the full spec.

WM Keyboard writes several file formats: themes, layouts, snippet packs, sticker and icon packs, plugins, and backups. Every one of them is plain JSON or a ZIP, never a proprietary blob. This page is the map: what each extension is, where it comes from, and what happens when you open one.

Screenshot pending
Opening a WM Keyboard file from a file manager: one confirm dialog, then the import.
ExtensionHoldsExported from
.wmtheme.jsonOne themeTheme gallery → export
.wmlayout.jsonOne custom keyboard layoutLayout editor → export
.wmsnippets.jsonA pack of text snippetsSnippets screen → export
.wmstickersA sticker pack (ZIP)Stickers screen → export
.wmiconsAn icon pack (ZIP)Icons screen → export
.wmpluginA Lua tool plugin (ZIP)Not produced by the app; built by plugin authors
.wmconfig.jsonA full settings-and-data backupData / Backup & restore → export
.wmsettings.jsonA settings-only backup (legacy)No export path anymore; still accepted on import
.wmdictA compiled binary dictionaryNever exported; internal to the app, see below
.txt word lists, .tsv emoji keywordsCustom dictionaries and keyword packsImport-only; not produced by the app
.flexA FlorisBoard theme extensionAnother keyboard’s format, read and converted — see Importing from other keyboards

Every format except .wmdict is registered as a file association, so tapping any of these in a file manager, downloads app, or chat attachment opens WM Keyboard’s import dialog directly. See Opening a file.

A tap on any recognized file (from a file manager, a browser download, or a chat attachment) launches a small dedicated activity, not the settings app itself: opening a theme from your downloads shouldn’t drop you into a tour of the settings screens. It shows exactly one confirm dialog, does the import if you accept, and closes.

Behind that dialog, the app has to figure out what kind of file it’s looking at:

  • If the file starts with a ZIP signature, the app opens it as an archive and scans up to 400 entries (capped at 64 KB of manifest content) for a pack.json, plugin.json, or extension.json. The format tag inside tells sticker packs, icon packs, and plugins apart: they can’t be told apart by file structure alone, since sticker and icon packs both use a pack.json manifest. An extension.json tagged as a FlorisBoard theme is converted rather than installed — see Importing from other keyboards.
  • Otherwise, the app reads the file as text and tries each tagged format’s decoder in order: full config backup, then legacy settings backup, then layout, then snippets, stopping at the first one whose format field matches.
  • A theme file has no format tag at all (every ThemeSpec field has a default, so any JSON object decodes successfully as an all-defaults theme). The .wmtheme.json extension on the file name is the only signal that a file was meant to be a theme, so it’s checked last and only by name.

Layout files are deliberately tolerant past their format tag: a hand-edited .wmlayout.json with a typo doesn’t get refused, it gets repaired, and the confirm dialog lists what changed under “Changed on the way in.” Plugins take the opposite approach: nothing about a plugin is auto-repaired, and any problem is a refusal with a reason, because a plugin is code running on assumptions nobody checked, not data that can be patched up. Importing a .wmsettings.json or .wmconfig.json backup is rollback-safe: the app re-reads the settings it just wrote, and if they don’t come back correctly it reverts the whole settings section rather than leaving you half-migrated.

Every export (theme, layout, snippet pack, sticker pack, icon pack, or config backup) uses Android’s system “Save As” picker. There’s no fixed export folder; you choose the destination each time, the same way you’d save a photo or a PDF. The app suggests a starting file name (the theme’s own name for a theme, the layout’s name for a layout, and so on), but you can rename it in the save dialog.

.wmtheme.json is the bare ThemeSpec object, encoded with every field written out: colors are ARGB integers written as 0xAARRGGBB longs. Because it has no format tag, any well-formed JSON object “imports” as a theme; the file extension is what marks it as one. When a theme travels as a file (export, or as part of a .wmconfig.json backup), its images travel with it: local file paths are stripped out and the bytes are embedded as base64 instead, so the file is self-contained and doesn’t reference a path that only exists on the phone that made it. The two background images have dedicated base64 fields; everything newer (key textures, the popup texture, sticker decals, press-effect images) rides a generic assets map keyed by slot name (keyTexture, popupTexture, decal:<id>, effectImage:<n>, …). See Sharing themes for the in-app side of this.

The format stays plain JSON with unknown-key tolerance on purpose: a theme from a newer app version imports on an older one as a working color theme, minus whatever decorations that version doesn’t know. Fonts and sounds are the deliberate exception — a theme names them by id and never embeds them, so they stay reusable as their own add-ons and a missing one falls back to the user’s global choice.

.wmlayout.json wraps the layout in a small envelope (format, version, and the layout data) specifically so a corrupted or unrelated file can be told apart from a real layout before anything is applied. See Custom layouts & the layout editor for editing and sharing layouts from the app.

Both formats are plain application/json, not a custom MIME type, since a vendor-specific MIME type would stop most file managers and chat apps from offering the file for sharing at all.

.wmsnippets.json is a tagged JSON envelope listing your snippets; snippet IDs in the file are never trusted on import, so importing the same pack twice creates two independent sets rather than overwriting the first. Each snippet may carry a trigger (one exact word) or a triggerPattern with triggerWords (a regular expression over the last few words before the cursor, whose captures reach the text as $1$9), plus confirm to make that trigger offer itself on the suggestion bar rather than expand on its own; those three fields are written only when they’re set, so a file of plain snippets is byte-for-byte what it always was. A pattern that won’t compile is dropped on import while its snippet is kept. See Snippets.

.wmstickers and .wmicons are both ZIP archives holding a pack.json manifest plus the pack’s image files: up to 500 entries / 64 MB for a sticker pack, and 400 entries / 8 MB for an icon pack. An installed device can hold up to 50 sticker packs and 20 icon packs at a time. In both formats, entry names inside the ZIP are never used as file paths directly, so a maliciously named entry can’t write outside the pack’s own folder. The full manifest shape, slot-id tables, and supported SVG subset for icon packs are documented once, in the addon repository format’s Sticker packs and Icon packs sections; the same files these export buttons produce are what a repository serves, so there’s no separate packaging step to publish one. See Emoji & stickers and Icon packs for the in-app side.

.wmplugin is a ZIP containing a plugin.json manifest and a Lua script, the only format here that’s code rather than data. The script is capped at 256 KB, and the archive as a whole at 16 entries / 1 MB; precompiled Lua bytecode is rejected outright, source only. A device can hold up to 50 installed plugins. Nothing about a plugin is silently repaired on import. See Opening a file above.

One distinction worth knowing: an addon repository’s index requires a sha256 hash for every plugin entry, and the app verifies it before installing from a repository. Opening a .wmplugin file directly (from a file manager or a chat attachment) skips that check entirely; only the manifest and script validation above apply. See the plugin developer guide and its security model for what a plugin can and can’t do once installed.

WM Keyboard has two backup formats, and only one of them is still current:

  • .wmconfig.json is the format the app writes today. It bundles several independent, opt-in sections (settings, themes, the learned dictionary, clipboard history, snippets, sticker packs, icon packs, imported word lists, and your list of addon repositories), each one embedded as the same JSON its own store already produces. A section you leave unchecked simply isn’t in the file. The default export file name is timestamped, like wmkeyboard-backup-20260728-143012.wmconfig.json.
  • .wmsettings.json is the older, settings-only format it replaced: a generic dump of every stored preference, with no dedicated export button left in the app. It’s still accepted on import, so an old file keeps working, but new backups are always .wmconfig.json.

Both formats exclude API keys and access tokens (translate, GIF, search, AI provider, and Hugging Face) by default, since a settings file is the kind of thing people email themselves or drop in a shared folder; turn on “Include API keys” explicitly if you want them in that export.

WM KeyboardDataBackup & restore

See Backup & restore for the full export/import flow and what each section covers, and Backup & restore settings for the settings screen itself.

Two plain-text formats exist purely to be imported, never produced by the app:

  • A custom dictionary is a .txt file, one entry per line as word frequency (frequency is optional and defaults to 1). Lines starting with # are comments; anything else is read permissively: a line with no parseable frequency just becomes a word at frequency 1 rather than being rejected, so most word lists found in the wild import as-is. Imported lists stack: several can sit under one language at once, capped at 32 MB per file. See Downloadable dictionaries for the app’s own dictionary downloads (a similar word-frequency format, but fetched and filtered through its own stricter parser rather than this loader).
  • An emoji keyword pack is a .tsv file, one row per emoji: the emoji itself, a comma-separated list of keywords, and an optional display name (tab-separated, capped at 8 MB). A file that instead starts with [ is read as the data repository’s JSON dictionary format rather than TSV, so a .json file pulled straight from that repository also imports without conversion. See Emoji customization for keyword pack downloads and language coverage.

An addon repository is a wmkeyboard-repo.json manifest plus the addon files it indexes, served from any https:// host, no build step or signing required. It can point at 11 addon types in all: eight are the formats already on this page (themes, layouts, dictionaries, emoji keywords, snippets, sticker packs, icon packs, plugins), plus fonts, emoji fonts, and key sounds, which use standard file types (.ttf/.otf, .mp3) rather than a WM Keyboard-specific format. A repository is an index over files the app already knows how to use: the same exports the app produces work as repo payloads with no separate packaging step. The full manifest schema, versioning rules, and a JSON Schema for validation live at Addon repository format. See Installing addons for adding a repository from the app.

  • No .wmdict file association. Bundled and downloaded dictionaries never appear as an openable file: they’re written straight to app storage by the download pipeline or extracted from the APK, and there’s no in-app export for them. Opening a stray .wmdict file from a file manager does nothing special; it isn’t on the recognized-extension list.
  • Compound extensions, on purpose. Every JSON format uses a two-part extension (.wmtheme.json, not .wmtheme) so the file association can match on file name without claiming every plain .json file on the device. The underlying Android pattern matching also can’t backtrack, so a file name with extra dots in it (my.theme.wmtheme.json) needs its own matching pattern; the manifest carries a few redundant patterns per extension specifically to cover that.
  • Legacy dictionary folders. Custom word lists were once stored under each language’s old display name (ENGLISH, BANGLA, and so on) rather than its short language ID (en, bn). The app migrates those folders to the new naming once, automatically, so lists imported years ago aren’t orphaned by a later update.
  • Zip-bomb guards, in one place. Every archive format (.wmstickers, .wmicons, .wmplugin) enforces both an entry-count cap and a total-size cap before extracting anything, sized well above what a legitimate pack would ever need: a sticker pack tops out around 500 entries / 64 MB, an icon pack around 400 entries / 8 MB (a full pack is roughly 90 small SVGs), and a plugin around 16 entries / 1 MB.

.wmdict is the app’s internal binary dictionary format: the compiled form of a bundled or downloaded word list, memory-mapped directly so the keyboard can look up completions without parsing anything at load time. It’s included here for completeness, but it isn’t a user-facing format the way the others are: there’s no export button, no file association, and no reason to ever see one on disk unless you go looking in the app’s own storage.

The format itself is a fixed big-endian header (a WMDC magic number, a version, and word/node/edge counts) followed by six 4-byte-aligned arrays that together encode a compressed trie: the same structure PackedTrie uses in memory, laid out so it can be traversed straight off the mapped file. Bundled dictionaries (English and Bengali) are inflated once out of the APK into app storage the first time they’re needed; downloaded dictionaries are converted and written the same way, atomically, so a partial download never leaves a broken file behind. For the dictionary experience itself (what’s built in, what you can download, and in what sizes), see Downloadable dictionaries.