# Contributing

> Module boundaries, code style, and commit conventions to match, plus a few good places to start.

URL: https://wmkeyboard.pages.dev/development/contributing/
Section: Development

WM Keyboard has CI but no formal contribution process, issue templates or
`CODEOWNERS` yet. Mostly there's 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` and
`:feature:translate`, dynamic feature modules `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 only
  `: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. — /development/architecture/

## 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 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:

```sh
./gradlew staticAnalysis
```

That runs Android Lint against the app module's `intl` debug variant, detekt
with full type resolution against 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. — /development/dictionaries/
	Emoji keyword packs — 126 downloadable language packs, generated by scripts under tools/emoji/ and served from the data repository. — /emoji/customization/
	Themes — Built-in themes are Kotlin, not JSON: a shareable theme needs no code change at all. — /themes/sharing/

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](https://github.com/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](https://wmkeyboard.pages.dev/development/dictionaries/)
  page covers the format.
- **Emoji keyword packs follow the same split.** The 126 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, 29 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 with Alucard, 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](https://wmkeyboard.pages.dev/themes/sharing/) and, for distributing
  one more widely, [addon repositories](https://wmkeyboard.pages.dev/development/addon-repos/).
- **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](https://www.conventionalcommits.org/):
`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:

```sh
./gradlew staticAnalysis
./gradlew unitTests
```

See [Testing](https://wmkeyboard.pages.dev/development/testing/) for what that second task covers and
where its tests live.

GitHub Actions runs the unit tests, debug assembles of the standard and F-Droid builds, 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](https://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](https://github.com/wasi-master/wmkeyboard-data)
instead, per the "Picking a first contribution" section above.

## Details & edge cases

- **F-Droid's stand-ins need their own pass.** `src/nogoogle` in `:app`,
  `:core:content`, `:core:intelligence` and `:feature:ime` replaces the code
  that calls ML Kit directly, and only a build with `-Pwmkb.enableFdroid=true`
  compiles it. `./gradlew staticAnalysis -Pwmkb.enableFdroid=true` analyses
  those files; the plain run does not.
- **`: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` and
  `:feature:translate`, 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](https://wmkeyboard.pages.dev/reference/settings/about/). 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.
