Skip to content

Contributing

github-actions[bot] edited this page Sep 15, 2026 · 4 revisions

WM Keyboard has CI but no formal contribution process, issue templates or CODEOWNERS yet. Mostly there's just a codebase to fit into cleanly. This page covers how the repository is organized and styled, and where a first change is easiest to land, so your pull request needs as little back-and-forth as possible.

Finding your way around the modules

The repository is a single Gradle build of 20 Android library modules plus :app: 17 under :core:* (config, common, language, input, keyman, prediction, emoji, theme, icons, tools, content, addons, voice, settings, feedback, plugins, intelligence) and 3 under :feature:* (tools, addons, ime). Two more sit outside that count: the host-side :tools:dictc (see below), and :feature:llm, a dynamic feature module settings.gradle.kts only includes when the Play channel flag is on. A class's module is visible from its file path, not its package. Everything still lives under com.wasimaster.wmkeyboard.*.

Dependencies only point downward, and that's a real Gradle project graph rather than a convention people remember to follow:

  • :core:config sits at the very bottom. It carries the build flags and API keys every other library reads.
  • :core:common builds on :core:config for shared utilities every module needs.
  • The rest of :core:* is one engine or store per feature area, each depending on only the lower modules it actually needs. :core:settings depends on eleven other :core:* modules because the settings screens surface controls for all of them, while :core:feedback needs just :core:settings and :core:common.
  • :feature:* sits above :core:settings, since the feature modules read the settings repository. :feature:ime (the keyboard runtime itself) depends on all 17 :core:* modules plus :feature:tools.
  • :app sits on top of everything: the settings activity, manifest, and assets, wired to all 20 library modules.

No module declares a dependency on anything above it in that list. There's no dedicated boundary-checking task either, because the Gradle project graph is the constraint. If a dependency direction ever looks surprising, read :app/build.gradle.kts and the module's own build.gradle.kts.

One module breaks the pattern deliberately. :tools:dictc, the host-side dictionary compiler (pure JVM, no Android), doesn't depend on :core:prediction as a project. Its build.gradle.kts adds that module's source directory as an extra Kotlin source directory and filters it to the seven files it needs (Trie.kt, TrieWalker.kt, TrieCompleter.kt, PackedTrie.kt, PackedTrieCodec.kt, RankFloorCache.kt, DictionaryLoader.kt), alongside its own Main.kt. The compiler and the reader share one literal implementation, so they can never drift apart.

  • Architecture The full module-layering diagram, plus the design decisions behind the IME, prediction, and persistence.

Matching the code style

No autoformatter runs over this codebase. .editorconfig at the repo root explains why: a mass reformat of a codebase this size would bury every real finding in diff noise. So it documents the style the code already follows instead of enforcing one. The values that matter most are a 4-space indent, a 140-character line length, Android Studio's ktlint code style, and unlimited star-import thresholds (star imports are allowed). New code should look like the code around it.

Static analysis runs in two layers, both configured under config/:

  • detekt (config/detekt/detekt.yml), layered on detekt's own defaults. maxIssues is 0 and excludeCorrectable is false, but the rule weights are deliberately uneven. Complexity, comment and deprecation rules are turned down to zero weight, while correctness rules stay at full weight, because those are the ones that catch actual bugs. The input-dispatch and layout-resolution code is genuinely branchy, so style metrics like cyclomatic complexity aren't a useful signal there.
  • Android Lint (config/lint/lint.xml). The app module turns on checkAllWarnings (roughly 200 checks that are off by default) and abortOnError, and escalates specific checks (NewApi, WrongThread, Recycle, StaticFieldLeak, HandlerLeak, MissingPermission, and others) to error, each with an inline comment explaining why.

One task runs everything:

./gradlew staticAnalysis

That runs Android Lint against the app module's full flavor, detekt with full type resolution against both the full and lite flavors of the app module plus its unit tests, and per-module detekt across 19 of the 20 library modules. (:core:config has no Kotlin sources to check, and :tools:dictc is skipped deliberately. See below.) CI doesn't run this task, so run it yourself before you ask for review.

Picking a first contribution

  • Dictionaries The two bundled seed word lists live in this repo; hundreds more live in the companion data repository.
  • Emoji keyword packs 125 downloadable language packs, generated by scripts under tools/emoji/ and served from the data repository.
  • Themes Built-in themes are Kotlin, not JSON: a shareable theme needs no code change at all.

A few things worth knowing before you start on any of these:

  • Dictionaries are mostly not in this repo. app/dictionaries-src/ bundles exactly two seed lists (en.txt and bn.txt, each a plain word count line per entry) compiled into .wmdict assets at build time. The much larger catalog of downloadable word lists is fetched at runtime from a separate repository, wasi-master/wmkeyboard-data, per DictionaryCatalog.kt. A new or improved word list for an existing language almost always means a pull request to that companion repository rather than this one. The dictionary pipeline page covers the format.
  • Emoji keyword packs follow the same split. The 125 downloadable packs (per EmojiDictCatalog.kt) also come from wmkeyboard-data. What lives in this repo is the generation tooling under tools/emoji/ (generate_gemoji.py, export_keyword_pack.py, generate_catalog.py, add_names.py, generate_dict_catalog.py, generate_animated.py).
  • Built-in themes are Kotlin, not data files. There are eleven built-in theme families, 28 themes once the colour variants nested inside them are counted. Most are themeFromSeed(...).copy(...) calls in the BuiltInThemes list in core/theme/.../ThemeSpec.kt. Six more sit in core/theme/.../PaletteThemes.kt. Five are ports of well-known editor palettes (Dracula, Nord, Solarized, Catppuccin, Tokyo Night) that carry their own attribution rules. The sixth is the original Cyberpunk palette, which has no upstream to attribute. Either way, contributing one means a Kotlin change to :core:theme. The .wmtheme.json format you may have seen is the export format a user gets from sharing a theme they built in the app, and it requires no change to this codebase at all. See sharing themes and, for distributing one more widely, addon repositories.
  • Docs are this site. Everything under docs/src/content/docs/ is an Astro/Starlight site with its own house rules, written down in the docs project's own CONTENT_GUIDE.md: page anatomy, component usage, and the "verify every claim in code" ground rule. Read that first if you're adding or editing a page.

Opening a pull request

Commits in this repo follow Conventional Commits: type(scope): summary, lowercase, present or imperative tense, for example fix(addons): validate repo URL scheme or docs(tools): rewrite the search page. The common types are feat, fix, docs, perf, refactor, test, build, chore, ci and style. Scopes generally track a module or doc section name (addons, plugins, tools, icons, emoji, typing, lint, about, and so on). Repo-wide changes drop the scope entirely (docs: ..., feat: ...).

Before you open a pull request, run the checks a reviewer would otherwise have to run for you:

./gradlew staticAnalysis
./gradlew testFullDebugUnitTest

See Testing for what that second task covers and where its tests live.

Note

GitHub Actions runs the unit tests, a full and lite debug assemble, the advisory evals and a docs link-check on every pull request (.github/workflows/ci.yml). staticAnalysis is not in CI, and there's no pull request template or CODEOWNERS file, so the static-analysis half of the checklist above is still a recommendation rather than a gate.

Issues and pull requests belong on github.com/wasi-master/wmkeyboard, the repository the app's own About screen points at. Changes to word lists or emoji keyword packs belong on wasi-master/wmkeyboard-data instead, per the "Picking a first contribution" section above.

Details & edge cases

  • Lite-flavor detekt only runs where lite actually differs. staticAnalysis runs a separate lite-flavor detekt pass for just :core:voice, :core:intelligence and :feature:ime, the only modules whose src/lite source set replaces real implementations (ML Kit, LiteRT) with stubs. Every other module's full and lite sources are identical, so a second pass over them would repeat the first.
  • :tools:dictc is excluded from staticAnalysis on purpose. Its source set adds :core:prediction's source directory, so analyzing it would report those same seven files a second time under a different module name.
  • A stricter local pass exists, but it's Kotlin-only. :app and all 20 :core:*/:feature:* library modules gate allWarningsAsErrors in their build.gradle.kts on a warningsAsErrors Gradle project property (./gradlew build -PwarningsAsErrors=true), off by default. The two modules outside that set, :tools:dictc and the Play-only :feature:llm, don't read it. Android Lint's own warningsAsErrors flag is hardcoded to false in app/build.gradle.kts and doesn't read that property. The two are separate switches that happen to share a name.
  • Third-party licenses and attribution for bundled dependencies, data-pack sources, and online services the app calls are listed on the in-app About & licenses screen. Check there before adding a new third-party dependency or data source, since it needs an entry too.
  • One repo URL is cased differently in the app. SOURCE_URL in core/common/.../Support.kt is github.com/wasi-master/WMKeyboard, while the docs site and the F-Droid recipe use the canonical lowercase github.com/wasi-master/wmkeyboard. Both resolve, but the lowercase form is the canonical one.

Clone this wiki locally