169 lines
5.9 KiB
Markdown
Executable File
169 lines
5.9 KiB
Markdown
Executable File
# Contributing to ferret
|
|
|
|
Patches are welcome at <http://git.dcos.net/dcosnet/ferret>.
|
|
|
|
## Development setup
|
|
|
|
```bash
|
|
git clone http://git.dcos.net/dcosnet/ferret.git
|
|
cd ferret
|
|
./scripts/setup-libmpv.sh
|
|
source ./mpv-prefix/env.sh
|
|
cargo build --release
|
|
```
|
|
|
|
See [QUICKSTART.md](QUICKSTART.md) for the 5-minute get-started guide and
|
|
[README.md](README.md) for full architecture documentation.
|
|
|
|
## Code style
|
|
|
|
- **Rust 2021 edition, MSRV 1.75.** No features requiring a newer compiler.
|
|
- **`cargo fmt` before committing.** No manual formatting.
|
|
- **`cargo clippy --release -- -D warnings` must be clean.** The CI lint
|
|
script (`./scripts/build.sh --lint`) enforces this.
|
|
- **All FFI lives in `mpv-bindings`.** No `unsafe` blocks outside that crate
|
|
except where documented with a SAFETY comment explaining the invariant.
|
|
- **No `async`.** Thread-per-subsystem with `crossbeam` channels. See
|
|
`BLOG.md` → "Why thread-per-subsystem, not async?" for the rationale.
|
|
- **Table-driven dispatch over branch ladders.** See "Coding standards"
|
|
below.
|
|
|
|
## Coding standards
|
|
|
|
This codebase follows four standards, adapted to Rust where the standard
|
|
is language-specific:
|
|
|
|
| Standard | Application |
|
|
|---|---|
|
|
| PEP 868 | Idiomatic clarity: data over branches, comprehensions over loops, one statement per line |
|
|
| POSIX | Shell scripts: `set -euo pipefail`, double-quoted expansions, `command -v` for tool detection |
|
|
| SEI CERT | Defensive FFI: every `unsafe` block carries a SAFETY comment; no ad-hoc `extern "C"` outside `mpv-bindings::sys` |
|
|
| MISRA | Table-driven control flow; closed-set `match` → `const TABLE` lookup; single exit per logical path |
|
|
|
|
### Table-driven dispatch (PEP 868 + MISRA)
|
|
|
|
When you have a closed-set `match` on an enum or a known string set, use a
|
|
`const TABLE` lookup instead of a `match` ladder. Example:
|
|
|
|
```rust
|
|
// PREFERRED: table-driven
|
|
const TABLE: &[(LoopMode, &str, &str)] = &[
|
|
(LoopMode::Off, "no", "no"),
|
|
(LoopMode::File, "inf", "no"),
|
|
(LoopMode::Playlist, "no", "inf"),
|
|
];
|
|
let (_, file_v, list_v) = TABLE
|
|
.iter()
|
|
.copied()
|
|
.find(|(m, _, _)| *m == mode)
|
|
.expect("LoopMode is exhaustive over TABLE");
|
|
|
|
// AVOID: branch ladder
|
|
match mode {
|
|
LoopMode::Off => { /* ... */ }
|
|
LoopMode::File => { /* ... */ }
|
|
LoopMode::Playlist => { /* ... */ }
|
|
}
|
|
```
|
|
|
|
**Why:** the table is data — it can be inspected, serialized, and extended
|
|
without touching control flow. New variants add one row, not one arm.
|
|
|
|
### Step-down logic (Unix philosophy)
|
|
|
|
When you have a fork of choices (multiple backends, multiple formats,
|
|
multiple fallbacks), use `or_else` chaining — not `if/else` ladders. Each
|
|
backend is a single-purpose function returning `Option<T>`; the entry point
|
|
is a flat chain.
|
|
|
|
```rust
|
|
// PREFERRED: step-down chain
|
|
pub fn pick_file(title: &str, filters: &[(&str, &[&str])]) -> Option<String> {
|
|
pick_file_zenity(title, filters)
|
|
.or_else(|| pick_file_kdialog(title, filters))
|
|
.or_else(|| pick_file_rfd(title, filters))
|
|
}
|
|
|
|
// AVOID: nested if/else
|
|
pub fn pick_file(title: &str, filters: &[(&str, &[&str])]) -> Option<String> {
|
|
if let Some(p) = pick_file_zenity(title, filters) {
|
|
Some(p)
|
|
} else if let Some(p) = pick_file_kdialog(title, filters) {
|
|
Some(p)
|
|
} else {
|
|
pick_file_rfd(title, filters)
|
|
}
|
|
}
|
|
```
|
|
|
|
**Why:** the chain is composable, testable per-backend, and reads top-to-bottom
|
|
as "try this, then this, then this". Adding a new backend is one line.
|
|
|
|
### Decisive language
|
|
|
|
Code comments and docs use present-tense, decisive statements. Avoid
|
|
phrasing that suggests back-and-forth or indecision:
|
|
|
|
- ❌ "falls back to", "restored", "brought back", "kept for compatibility",
|
|
"non-breaking", "previously", "original behavior was"
|
|
- ✅ "steps down to", "routes through", "uses", "selects"
|
|
|
|
Comments should read as design decisions, not as a record of changes.
|
|
|
|
## Pre-commit checks
|
|
|
|
Run before pushing:
|
|
|
|
```bash
|
|
./scripts/build.sh --lint # clippy + bracket audit + deref audit
|
|
./scripts/build.sh --test # cargo test
|
|
./scripts/build.sh --ci # both + build (full CI pass)
|
|
```
|
|
|
|
The bracket audit (`scripts/audit_brackets.py`) is a Rust-aware tokenizer
|
|
that catches unbalanced `()`/`{}`/`[]` in milliseconds — faster than
|
|
waiting for `cargo check` to reach the affected crate.
|
|
|
|
The deref audit (`scripts/audit_deref.py`) flags `match` arms on `&Enum`
|
|
where a captured reference might be forwarded without `*` — the bug class
|
|
that caused the `SetVideoRotate` compile error in §9 of the QA report.
|
|
|
|
## Adding a new hotkey
|
|
|
|
1. Add the `Key` → `Cmd` row to `const TABLE` in
|
|
`crates/player-app/src/keymap.rs`.
|
|
2. Add a menu entry for the hotkey in `crates/player-ui/src/app.rs`
|
|
(File / Playback / Audio / Subtitles / Video / Help menu). Append the
|
|
shortcut key in parentheses to the label: `"Play / Pause (Space)"`.
|
|
3. If the hotkey has no existing control-bar button, consider whether it
|
|
needs one. Every hotkey must have a menu entry and/or a button — no
|
|
orphan hotkeys.
|
|
4. Update the Keyboard Shortcuts table in `README.md`.
|
|
5. Run `./scripts/build.sh --lint` to verify the table compiles clean.
|
|
|
|
## Adding a new mpv property
|
|
|
|
1. Add a `const PROP_<NAME>: EventId = N;` to
|
|
`crates/player-core/src/engine.rs` (next available integer).
|
|
2. Add the property to the `observed` array in `engine_main`.
|
|
3. Add a `(PROP_<NAME>, PropertyValue::Variant(value))` arm to
|
|
`apply_property_change`.
|
|
4. Add the field to `PlaybackState` in `crates/player-core/src/state.rs`.
|
|
5. Expose the state in the UI if user-visible
|
|
(`crates/player-ui/src/app.rs`).
|
|
|
|
## Reporting bugs
|
|
|
|
Please include:
|
|
|
|
- ferret version (`ferret --version`)
|
|
- libmpv version (`pkg-config --modversion mpv`)
|
|
- Desktop environment / window manager
|
|
- Steps to reproduce
|
|
- Log output (`RUST_LOG=info ferret ...`)
|
|
|
|
## License
|
|
|
|
By contributing, you agree that your contributions are licensed under the
|
|
[GPL-2.0-or-later](LICENSE) license that covers the project.
|