CONTRIBUTING.md
59 lines · 5.6 KB
Changed in: 0.5.0
| 1 | # Contributing |
| 2 | |
| 3 | Thanks for the interest. Earhorn is a Faveat Software project, GPLv3+, and welcomes contributions in proportion to the time it takes to review them. |
| 4 | |
| 5 | ## Before you start |
| 6 | |
| 7 | - **Open an issue first** for anything non-trivial. A 30-second sanity check from a maintainer saves you from a 2-hour PR that gets closed. Bug fixes with clear repro steps don't need this. |
| 8 | - **Keep PRs focused.** One concern per PR. "Drive-by fixes" in a feature PR make review harder; open them as separate PRs instead. |
| 9 | - **No force pushes to long-lived branches** (`main`, `release/*`). Force-push to your own branches all you want. |
| 10 | |
| 11 | ## Architecture overview |
| 12 | |
| 13 | This is a Kotlin Multiplatform project. The split: |
| 14 | |
| 15 | - `shared/` — KMP library. Domain model, UPnP layer, ViewModels, theme tokens, lyrics, playlist persistence. Targets `android` and `desktop` (jvm). **Code here is the same code that powers the Android Earhorn build.** If you touch this module, the Android app (`android/`, next to this folder) needs to incorporate the change too — say so when you send the patch. |
| 16 | - `desktop/` — Compose Multiplatform desktop application. Screens, window setup, AWT menu bar, keyboard shortcuts, packaging Gradle tasks. Desktop-only. The Android sibling has its own equivalent in its own repo. |
| 17 | |
| 18 | See [`DESIGN.md`](DESIGN.md) for the detailed write-up of why the boundary is where it is. |
| 19 | |
| 20 | ## Code style |
| 21 | |
| 22 | - **Kotlin official style.** `./gradlew ktlintCheck` if you have ktlint set up; otherwise just match what's already there. |
| 23 | - **Comment _why_, not _what_.** The code says what it does; comments say why it does it that way. Past bugs that motivated a non-obvious choice are exactly the kind of context worth preserving in a comment. Examples throughout this codebase: search for any block comment starting with "Without this," — those are paid-for lessons. |
| 24 | - **No abbreviation in identifiers** unless it's already a term of art (`Mrl`, `Uri`, `Udn`, `Mrl`). `track`, not `trk`. `currentIndex`, not `curIdx`. |
| 25 | - **Imports** — wildcard imports are fine for `androidx.compose.material.icons.filled.*` if you're using more than three glyphs; otherwise enumerate. |
| 26 | |
| 27 | ## Project-specific conventions |
| 28 | |
| 29 | A few hard-learned rules. Skim them before opening a PR or you'll hit them anyway during review: |
| 30 | |
| 31 | - **Don't use Kotlin's `buildList { }` for lists accessed by index.** Its backing class is `kotlin.collections.builders.ListBuilder`, which uses Kotlin's lowercase `"index: N, size: N"` IOOB format — indistinguishable from Compose `SnapshotStateList` / kotlinx persistent collections in an error dialog. Use `ArrayList<T>(initialCapacity).apply { … }` instead — Java's capital `"Index: N, Size: N"` makes the throw source obvious. See `queueKeys` in `NowPlayingPane.kt` for the canonical pattern. |
| 32 | - **Don't put `project.file()` or `layout.buildDirectory.dir(…).get().asFile` inside Gradle `doLast { }`.** Configuration cache forbids it. Hoist the `File` to the outer task scope (`val foo = project.file(…)`) and capture it into the action. |
| 33 | - **Don't use Kotlin's `File.copyTo(overwrite = true)` when source might equal target.** It deletes the target first, then fails to read the now-missing source. JDK 21's `jpackage` sometimes emits files exactly where we'd otherwise want to copy them; check `exists()` first. |
| 34 | - **Don't use `KeyShortcut(Key.Spacebar)` on any menu item.** It bypasses Compose Desktop's AWT dispatcher and triggers play/pause while the user is typing in a text field. Wire spacebar through `rememberTextInputGuard()` instead — see `shortcuts/Keybindings.kt`. |
| 35 | - **Don't use Material3 `onFocusChanged` for text-input gating.** It fires inconsistently across nested composables. Use `interactionSource` via `rememberTextInputGuard()`. |
| 36 | - **Never fabricate library versions.** If you're not sure a version exists on Maven Central, web-search it first. Maven Central will return 404 for invented versions and Gradle's error message will be misleading about the cause. |
| 37 | - **ViewModels launch coroutines via `safeLaunch { … }`,** not `viewModelScope.launch { … }`. The helper in `shared/src/jvmCommonMain/kotlin/dev/stave/ui/SafeLaunch.kt` installs a `CoroutineExceptionHandler` so an unhandled throw logs to stderr instead of bubbling to the JVM uncaught handler (which Swing surfaces as an opaque "Error" dialog). |
| 38 | - **`return@launch` doesn't exist after `safeLaunch`** — use `return@safeLaunch` (or just `return` in any pre-existing function). `sed` won't catch this; the compiler will. |
| 39 | |
| 40 | ## Pull request flow |
| 41 | |
| 42 | 1. Start from the newest source zip at [faveat.software/earhorn](https://faveat.software/earhorn/#download) and create a feature branch. |
| 43 | 2. Make focused commits with messages that explain the _why_ (the diff already shows the _what_). |
| 44 | 3. `./gradlew :desktop:run` and verify the change works. |
| 45 | 4. `./gradlew :shared:compileKotlinDesktop :desktop:compileKotlinJvm` — no compile errors, no new warnings if you can help it. |
| 46 | 5. Email the change as a patch (`git format-patch`) to [`foundry@faveat.software`](mailto:foundry@faveat.software), saying what it fixes. |
| 47 | 6. Expect comments. Iterate. |
| 48 | |
| 49 | ## Reporting bugs |
| 50 | |
| 51 | Email [`foundry@faveat.software`](mailto:foundry@faveat.software). Include the info listed in the [`README.md`](README.md#reporting-bugs) — distro, Java version, server, repro steps, stack trace from stderr if there is one. |
| 52 | |
| 53 | ## Security |
| 54 | |
| 55 | Don't report security issues in public. See [`SECURITY.md`](SECURITY.md). |
| 56 | |
| 57 | ## License |
| 58 | |
| 59 | By contributing you agree your contributions are licensed under GPLv3+, the same license as the project. See [`LICENSE`](LICENSE). |


