5.9 KiB
Executable File
Contributing to ferret
Patches are welcome at http://git.dcos.net/dcosnet/ferret.
Development setup
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 for the 5-minute get-started guide and README.md for full architecture documentation.
Code style
- Rust 2021 edition, MSRV 1.75. No features requiring a newer compiler.
cargo fmtbefore committing. No manual formatting.cargo clippy --release -- -D warningsmust be clean. The CI lint script (./scripts/build.sh --lint) enforces this.- All FFI lives in
mpv-bindings. Nounsafeblocks outside that crate except where documented with a SAFETY comment explaining the invariant. - No
async. Thread-per-subsystem withcrossbeamchannels. SeeBLOG.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:
// 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.
// 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:
./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
- Add the
Key→Cmdrow toconst TABLEincrates/player-app/src/keymap.rs. - 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)". - 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.
- Update the Keyboard Shortcuts table in
README.md. - Run
./scripts/build.sh --lintto verify the table compiles clean.
Adding a new mpv property
- Add a
const PROP_<NAME>: EventId = N;tocrates/player-core/src/engine.rs(next available integer). - Add the property to the
observedarray inengine_main. - Add a
(PROP_<NAME>, PropertyValue::Variant(value))arm toapply_property_change. - Add the field to
PlaybackStateincrates/player-core/src/state.rs. - 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 that covers the project.