EARHORNMUSIC UPnP · Android TV · Phones · Android Auto · GNU/Linux
Release 0.5.0

CONTRIBUTING.md

59 lines · 5.6 KB

Changed in: 0.5.0

1# Contributing
2
3Thanks 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
13This 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
18See [`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
29A 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
421. Start from the newest source zip at [faveat.software/earhorn](https://faveat.software/earhorn/#download) and create a feature branch.
432. Make focused commits with messages that explain the _why_ (the diff already shows the _what_).
443. `./gradlew :desktop:run` and verify the change works.
454. `./gradlew :shared:compileKotlinDesktop :desktop:compileKotlinJvm` — no compile errors, no new warnings if you can help it.
465. Email the change as a patch (`git format-patch`) to [`foundry@faveat.software`](mailto:foundry@faveat.software), saying what it fixes.
476. Expect comments. Iterate.
48
49## Reporting bugs
50
51Email [`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
55Don't report security issues in public. See [`SECURITY.md`](SECURITY.md).
56
57## License
58
59By contributing you agree your contributions are licensed under GPLv3+, the same license as the project. See [`LICENSE`](LICENSE).