Doc: update CONTRIBUTING.md [ci skip]

This commit is contained in:
Carter Li
2026-09-02 10:44:36 +08:00
parent 8c98bf1953
commit e9f719f37a
+144 -71
View File
@@ -2,15 +2,15 @@
<details>
<summary>
Note
Disclaimer
</summary>
This document is generated by AI, and mainly for AI use. It is not a substitute for human review, and may contain errors or omissions. Please verify any information before relying on it.
This document is generated by AI and reviewed by humans. It is primarily intended for AI-assisted development, while remaining useful to human developers.
</details>
Thanks for your interest in fastfetch. This document covers building, architecture, how to add a module or a logo, code style, commit conventions, and the pull request workflow.
Thank you for your interest in fastfetch. This document covers building, architecture, how to add a module or a logo, code style, commit conventions, and the pull request workflow.
fastfetch is a system information tool written in C23, supporting Linux, macOS, Windows, the BSDs, Solaris, Haiku and Android. The project is borderline obsessive about **startup time** and **optional dependencies** many design decisions only make sense under that constraint, and this document keeps coming back to it.
fastfetch is a system information tool written in C23, supporting Linux, macOS, Windows, the BSDs, Solaris, Haiku and Android. The project places strong emphasis on **startup time** and on keeping **dependencies optional**; many design decisions only make sense under that constraint, and this document returns to it repeatedly.
---
@@ -29,7 +29,7 @@ fastfetch is a system information tool written in C23, supporting Linux, macOS,
- [Changelog](#changelog)
- [Tests](#tests)
- [Pull requests](#pull-requests)
- [Gotchas](#gotchas)
- [Pitfalls](#pitfalls)
---
@@ -38,14 +38,14 @@ fastfetch is a system information tool written in C23, supporting Linux, macOS,
| Task | Start here | Verify with |
|---|---|---|
| Build and run fastfetch | `run.sh`, `CMakeLists.txt` | `./run.sh` |
| Add a module | `src/modules/<name>/`, `src/detection/<name>/`, `src/modules/modules.c` | `cmake -B build && cmake --build build -j` |
| Add a platform implementation | `src/detection/<name>/<name>_<platform>.c` | Build on the target platform or CI |
| Add a module | `src/modules/<name>/`, `src/detection/<name>/`, `src/modules/modules.c`, `CMakeLists.txt` | `cmake -B build && cmake --build build -j` |
| Add a platform implementation | `src/detection/<name>/<name>_<platform>.c`, the matching platform block in `CMakeLists.txt` | Build on the target platform or CI |
| Add an ASCII logo | `src/logo/ascii/<letter>/<name>.txt`, matching `<letter>.inc` | `./build/fastfetch --logo <name>` |
| Change formatting or JSON output | `src/modules/<name>/<name>.c` | `./build/fastfetch -s <name> --format json` |
| Change shared formatting or containers | `src/common/format.h`, `src/common/color.h`, `src/common/FFstrbuf.h` | Build and run the matching test |
| Run the test suite | `tests/`, `build/` | `cd build && ctest --output-on-failure` |
For a normal code change, the shortest useful loop is: configure, build the `fastfetch` target, run the affected module, then run the tests. If you add a directory or a generated input, re-run `cmake -B build` so CMake refreshes its file globs.
For a typical code change, the shortest useful iteration is: configure, build the `fastfetch` target, run the affected module, then run the tests. If you add a directory or a generated input, re-run `cmake -B build` so that CMake refreshes its file globs.
---
@@ -53,7 +53,7 @@ For a normal code change, the shortest useful loop is: configure, build the `fas
### Building
The quickest way is `run.sh` in the repository root it creates `build/`, configures, compiles and runs the binary:
The quickest way is `run.sh` in the repository root: it creates `build/`, configures, compiles and runs the binary.
```sh
./run.sh # build and run
@@ -68,7 +68,7 @@ cmake --build build --target fastfetch -j$(nproc)
./build/fastfetch
```
The default build type is `RelWithDebInfo`. LTO is enabled whenever `ENABLE_LTO=ON` **and** `CMAKE_BUILD_TYPE != Debug` so the default already has it, and only `Debug` builds lack it. This matters because LTO here is not merely an optimization: it is what strips the code of disabled modules. See [Gotchas](#gotchas).
The default build type is `RelWithDebInfo`. LTO is enabled whenever `ENABLE_LTO=ON` **and** `CMAKE_BUILD_TYPE != Debug`, so the default configuration enables it and only `Debug` builds omit it. This is significant because LTO here is not merely an optimization: it is what removes the code of disabled modules. See [Pitfalls](#pitfalls).
### Common build options
@@ -123,12 +123,12 @@ fastfetch.c → options/ (global configuration)
→ common/ (infrastructure)
```
The number of module directories and detection directories does not match, and that is expected. The following relationships are structural rather than a fixed inventory:
The number of module directories does not match the number of detection directories, which is expected. The following relationships are structural rather than a fixed inventory:
- **13 modules have no detection directory** — they are either pure layout (`break`, `separator`, `colors`, `title`, `logo`) or reuse someone else's detection result (`display`, `monitor`, `kernel`, `shell`, `terminal`, `player`, `custom`, `datetime`).
- **13 modules have no detection directory** — they are either pure layout (`break`, `separator`, `colors`, `title`, `logo`) or reuse another module's detection result (`display`, `monitor`, `kernel`, `shell`, `terminal`, `player`, `custom`, `datetime`).
- **4 detection directories serve modules with different names** — `displayserver` (→ `display`, `monitor`), `gtk_qt` (→ `theme`, `icons`, `font`, `cursor`), `terminalshell` (→ `terminal`, `shell`) and `libc`.
So do not assume `modules/<x>/` always has a matching `detection/<x>/`.
Therefore, do not assume that `modules/<x>/` always has a matching `detection/<x>/`.
### Layer boundaries
@@ -139,13 +139,13 @@ So do not assume `modules/<x>/` always has a matching `detection/<x>/`.
| `detection/` | cross-platform data retrieval, clean result structs | print anything, read display config |
| `common/` | general-purpose utilities, no business logic | depend on a specific module |
The test is simple: **no file under `detection/` should contain `printf` or read `instance.config`.** Symmetrically, no file under `modules/` should contain `#ifdef __linux__`.
A simple check: **no file under `detection/` should contain `printf` or read `instance.config`.** Symmetrically, no file under `modules/` should contain `#ifdef __linux__`.
---
## Core architecture: modules vs. detection
This is the single most important convention in the codebase. Every feature is a fixed set of files using CPU as the example:
This is the most important convention in the codebase. Every feature consists of a fixed set of files; using CPU as the example:
```
modules/cpu/option.h module option struct
@@ -158,7 +158,7 @@ detection/cpu/cpu_bsd.c │
detection/cpu/cpu_nosupport.c ┘
```
The interface is deliberately minimal — usually just two things:
The interface is deliberately minimal, typically consisting of only two declarations:
```c
typedef struct FFCPUResult { ... } FFCPUResult; // result struct
@@ -169,7 +169,7 @@ The `const char*` return value is an **error string**; `nullptr` means success.
Helper functions are only exposed when they are genuinely shared between platform implementations (as in `cpu.h`: `ffCPUAppleCodeToName`, `ffCPUDetectByCpuid`).
**The payoff:** adding a platform never touches `modules/`; fixing output formatting never touches `detection/`.
**The benefit of this separation:** adding a platform never touches `modules/`; fixing output formatting never touches `detection/`.
---
@@ -197,7 +197,7 @@ typedef struct FFModuleBaseInfo {
} FFModuleBaseInfo;
```
The source comment openly admits this is UB — `void*` is not compatible with `FF*Options*`. It is a pragmatic compromise to get polymorphism in C. Don't try to "fix" it.
The source comment acknowledges that this is undefined behavior, since `void*` is not compatible with `FF*Options*`. It is a pragmatic compromise to obtain polymorphism in C; do not attempt to "fix" it.
### The registry: a first-letter hash bucket
@@ -210,7 +210,7 @@ for (; *modules; ++modules) { // linear s
}
```
The registry is small enough that a subtraction plus a few string comparisons is preferable to a general-purpose hash table. When adding a module, place its descriptor in the bucket matching the first letter of `.name`; keep the existing `nullptr` terminator at the end.
Because the registry is small, a single arithmetic bucket lookup followed by a few string comparisons is preferable to a general-purpose hash table. When adding a module, place its descriptor in the bucket matching the first letter of `.name`, and keep the existing `nullptr` terminator at the end.
### The calling convention: zero heap allocation
@@ -230,7 +230,7 @@ baseInfo->destroyOptions(optionBuf);
static_assert(sizeof(FFCPUOptions) <= FF_OPTION_MAX_SIZE, "FFCPUOptions size exceeds maximum allowed size");
```
**Meaning: no module option struct may exceed 256 bytes.** Hard constraint, enforced at compile time.
**In other words, no module option struct may exceed 256 bytes.** This is a hard constraint, enforced at compile time.
The three dispatch entry points:
@@ -238,20 +238,49 @@ The three dispatch entry points:
|---|---|---|
| `parseModuleJsonObject` | `common/impl/jsonconfig.c:90` | the `modules[]` array in a JSONC config |
| `parseStructureCommand` | `common/impl/commandoption.c:181` | the colon-separated structure string |
| `ffParseModuleOptions` | `common/impl/commandoption.c:13` | **deprecated** — see [Gotchas](#4-cli-module-options-are-removed) |
### Build-time module discovery
`CMakeLists.txt:131` globs `src/modules/*/` and generates a `MODULE_DISABLE_<UPPER>` option per directory:
`CMakeLists.txt:134` globs `src/modules/*/*.c`, keeps only the entries whose file name matches their directory name, and generates a `MODULE_DISABLE_<UPPER>` option for each:
```cmake
file(GLOB FF_MODULE_DIRS RELATIVE "..." ".../src/modules/*/")
file(GLOB FF_MODULE_SRCS CONFIGURE_DEPENDS RELATIVE "..." ".../src/modules/*/*.c")
set(FF_MODULE_DIRS "")
foreach(FF_MODULE_SRC ${FF_MODULE_SRCS})
get_filename_component(FF_MODULE_DIR "${FF_MODULE_SRC}" DIRECTORY)
get_filename_component(FF_MODULE_NAME "${FF_MODULE_SRC}" NAME_WE)
if("${FF_MODULE_DIR}" STREQUAL "${FF_MODULE_NAME}")
list(APPEND FF_MODULE_DIRS "${FF_MODULE_DIR}")
endif()
endforeach()
foreach(FF_MODULE_DIR ${FF_MODULE_DIRS})
string(TOUPPER "${FF_MODULE_DIR}" FF_MODULE_UPPER)
option(MODULE_DISABLE_${FF_MODULE_UPPER} "Disable module ${FF_MODULE_DIR}" OFF)
endforeach()
```
**Adding a module requires no CMake change.** The same trick generates the package manager switches (`:146` regex-scans `src/modules/packages/option.h` for `FF_PACKAGES_FLAG_*_BIT`).
Because the glob matches sources rather than directories, a directory counts as a module only when `src/modules/<name>/<name>.c` exists. Empty directories — which git does not track, so they are easy to leave behind — and stray files are ignored instead of breaking the configure step with `Cannot find source file`.
### Module sources are discovered automatically
`FF_MODULE_DIRS` also drives the source list (`CMakeLists.txt:516`):
```cmake
foreach(FF_MODULE_DIR ${FF_MODULE_DIRS})
list(APPEND LIBFASTFETCH_SRC
src/modules/${FF_MODULE_DIR}/${FF_MODULE_DIR}.c
)
endforeach()
```
Consequences:
- `src/modules/<name>/<name>.c` is compiled as soon as the file exists — **there is no source list to edit for the module layer**.
- The file name must match the directory name. A directory whose `.c` file is named differently, or has none at all, is silently not a module: a typo therefore shows up as a module missing from `fastfetch --list-modules`, not as a build error.
- The glob uses `CONFIGURE_DEPENDS`, so with the Makefile and Ninja generators the build re-evaluates it and re-runs CMake when a module source is added or removed. With other generators, or to be safe, re-run `cmake -B build` explicitly.
Only the module layer is automated. Sources under `src/detection/` and `src/common/impl/` are **not** globbed and must still be listed by hand — see [step 5](#5-add-the-platform-sources-to-cmakeliststxt).
---
@@ -276,9 +305,7 @@ typedef struct FFFooOptions {
static_assert(sizeof(FFFooOptions) <= FF_OPTION_MAX_SIZE, "FFFooOptions size exceeds maximum allowed size");
```
`FFModuleArgs` must come first. It provides `key`, `format`, `outputColor`, `keyColor`, `keyIcon` and `keyWidth`; because it is the first field, `ffJsonConfigParseModuleArgs()` can handle these generically and you get them **for free — no parsing code to write**.
Note these fields can only be set through the JSON config; CLI module options have been removed (see [Gotchas](#4-cli-module-options-are-removed)).
`FFModuleArgs` must come first. It provides `key`, `format`, `outputColor`, `keyColor`, `keyIcon` and `keyWidth`; because it is the first field, `ffJsonConfigParseModuleArgs()` handles these generically, **so no parsing code is required**.
### 2. Implement detection
@@ -307,7 +334,7 @@ const char* ffDetectFoo(const FFFooOptions* options, FFFooResult* result) {
}
```
**Write one file per target platform, and a `foo_nosupport.c` fallback for the rest.** There are already 45 `*_nosupport.c` files doing exactly this.
**Write one file per target platform, plus a `foo_nosupport.c` fallback for the remaining platforms.** The repository currently contains 45 `*_nosupport.c` files serving this purpose.
### 3. Implement the module
@@ -339,11 +366,11 @@ FFModuleBaseInfo ffFooModuleInfo = {
Points to note:
- **`displayName`** is a literal block of all 20 language fields there is no shortcut macro, so copy the layout from a neighboring module: **`en`, `ar`, `cs`, `de`, `es`, `fr`, `gl`, `he`, `id`, `it`, `ja`, `ko`, `pl`, `pt`, `ru`, `tr`, `uk`, `vi`, `zh_CN`, `zh_TW`**.
**Fill in all 20.** The struct is read by byte offset (see [Gotchas](#6-localization-uses-byte-offsets-not-enums)) and there is **no fallback** — a missing field means the key prints empty in that language.
- **`formatArgs`** must be exhaustive. It drives the placeholder list printed by `fastfetch -h foo-format`; **anything you omit is invisible and unusable to the user.**
- **`displayName`** is a literal block of all 20 language fields; there is no shortcut macro, so copy the layout from an existing module: **`en`, `ar`, `cs`, `de`, `es`, `fr`, `gl`, `he`, `id`, `it`, `ja`, `ko`, `pl`, `pt`, `ru`, `tr`, `uk`, `vi`, `zh_CN`, `zh_TW`**.
**Fill in all 20.** The struct is read by byte offset (see [Pitfalls](#6-localization-uses-byte-offsets-not-enums)) and there is **no fallback** — a missing field means the key prints empty in that language.
- **`formatArgs`** must be exhaustive. It drives the placeholder list printed by `fastfetch -h foo-format`; **any placeholder omitted here is neither listed nor usable by the user.**
- The `moduleFormat` section in `doc/json_schema.json` is generated by `fastfetch -h format-json`. Its metadata comes from `FFModuleBaseInfo::formatArgs`. To keep the schema and module descriptors consistent, do not edit the `moduleFormat` section in `doc/json_schema.json` by hand; update the module metadata and regenerate it instead.
- **`defaultOrder`**: use the current maximum plus 1. Search the existing descriptors for `.defaultOrder =` before choosing a value; do not copy a hard-coded value from this document. Leaving it out, or setting it to `0`, makes the module **disappear** from the interactive `--gen-config` picker. Only `logo`, `command` and `custom` do this deliberately, because they need user arguments or are invoked directly by the display layer.
- **`defaultOrder`**: use the current maximum plus 1. Search the existing descriptors for `.defaultOrder =` before choosing a value; do not copy a hard-coded value from this document. Leaving it out, or setting it to `0`, excludes the module from the interactive `--gen-config` picker. Only `logo`, `command` and `custom` rely on this behavior, because they require user arguments or are invoked directly by the display layer.
### 4. Register it
@@ -358,11 +385,46 @@ Points to note:
`FF_MODULE_DISABLE_FOO` is generated by CMake — you do not define it yourself.
### 5. Modules that need warm-up
### 5. Add the platform sources to `CMakeLists.txt`
If your module needs a sampling interval (CPU usage) or a network round-trip (public IP, weather), also implement `ffPrepareFoo()` and register it in the switch inside `ffPrepareCommandOption()` in `common/impl/commandoption.c`, under the right first-letter `case`. Six modules do this today: CPUUsage, DiskIO, NetIO, PublicIP, Top and Weather.
The module layer is discovered by glob (see [Build-time module discovery](#module-sources-are-discovered-automatically)), **but `src/detection/**` and `src/common/impl/**` are not**. `CMakeLists.txt` lists them explicitly, inside one mutually exclusive chain of platform blocks:
### 6. Verify
| Block | Line | Covers |
|---|---|---|
| `if(LINUX)` | `CMakeLists.txt:522` | Linux |
| `elseif(ANDROID)` | `CMakeLists.txt:608` | Android (Termux) |
| `elseif(FreeBSD)` | `CMakeLists.txt:691` | FreeBSD, MidnightBSD, DragonFly |
| `elseif(NetBSD)` | `CMakeLists.txt:788` | NetBSD |
| `elseif(OpenBSD)` | `CMakeLists.txt:872` | OpenBSD |
| `elseif(APPLE)` | `CMakeLists.txt:959` | macOS / iOS |
| `elseif(WIN32)` | `CMakeLists.txt:1046` | Windows |
| `elseif(SunOS)` | `CMakeLists.txt:1121` | Solaris / illumos |
| `elseif(Haiku)` | `CMakeLists.txt:1204` | Haiku |
| `elseif(GNU)` | `CMakeLists.txt:1282` | GNU/Hurd |
Add the platform implementation to every block whose platform it supports, and `foo_nosupport.c` to every remaining block, so that all ten platforms still link:
```cmake
elseif(FreeBSD)
list(APPEND LIBFASTFETCH_SRC
...
src/detection/foo/foo_bsd.c
)
```
Points to note:
- Exactly one block is compiled per build, so a file omitted from a block does not exist for that platform, and the link fails with an undefined reference to `ffDetectFoo()` — **on that platform only**. A missing entry therefore builds fine locally and fails in CI; this is why every block must be covered.
- A platform may reuse another platform's implementation instead of a stub. `src/common/impl/networking_linux.c`, for example, is listed in nine of the ten blocks (all but `WIN32`). Check where the closest sibling module points before adding a new file.
- `DragonFly` is handled by the inner `if(DragonFly)` sub-block inside the FreeBSD block (`CMakeLists.txt:773`); add the variant there, as `processes`, `top` and `wifi` do.
- New helpers under `src/common/impl/` follow the same rule: they are not globbed, and each block that needs one must list it.
- A few files are appended outside the platform chain because they depend on an option or on a specific feature — for example the proprietary GPU backends (`CMakeLists.txt:1379`) and `src/common/impl/wcwidth.c` (`CMakeLists.txt:1408`). Those are written by hand as well.
### 6. Modules that need warm-up
If your module needs a sampling interval (CPU usage) or a network round-trip (public IP, weather), also implement `ffPrepareFoo()` and register it in the switch inside `ffPrepareCommandOption()` in `common/impl/commandoption.c`, under the matching first-letter `case`. Six modules currently do this: CPUUsage, DiskIO, NetIO, PublicIP, Top and Weather.
### 7. Verify
```sh
cmake -B build && cmake --build build -j
@@ -371,7 +433,9 @@ cmake -B build && cmake --build build -j
./build/fastfetch --gen-config # confirm it appears in the picker
```
Note the `-format` suffix on the help flag: `fastfetch -h foo` does not work, only `fastfetch -h foo-format`.
Note the `-format` suffix on the help flag: `fastfetch -h foo` is not supported; only `fastfetch -h foo-format` works.
If you added detection sources, re-check [step 5](#5-add-the-platform-sources-to-cmakeliststxt) before pushing: a platform block you missed compiles fine locally and fails only when that platform is built.
---
@@ -379,13 +443,13 @@ Note the `-format` suffix on the help flag: `fastfetch -h foo` does not work, on
A new logo **must** have a corresponding "Logo Request" issue, linked from the PR with `Closes #1234`. Logo PRs without a linked issue are not accepted.
### 1. Drop in the ASCII file
### 1. Add the ASCII file
```
src/logo/ascii/<first-letter>/<distro>.txt
```
For example `src/logo/ascii/o/omarchy.txt`. Directories are already split by first letter (`a/``z/`, plus `_/`).
For example `src/logo/ascii/d/distro.txt`. Directories are already split by first letter (`a/` … `z/`, plus `_/`).
The file is plain ASCII art with `$1` … `$9` as color placeholders:
@@ -395,26 +459,29 @@ $3 /\\\///
$2refined.$1 /\\\\//
```
- `$1``$9` map to palette slots 19; at most 9 (`FASTFETCH_LOGO_MAX_COLORS = 9`)
- `$1` … `$9` map to palette slots 19; at most 9 are supported (`FASTFETCH_LOGO_MAX_COLORS = 9`)
- `$$` is a literal `$`
- Characters without a placeholder inherit the current color. Of the 530 existing logos, 239 use no placeholders at all (monochrome) and 291 do **new logos should use placeholders**; monochrome is a legacy style
- Characters without a placeholder inherit the current color. Of the 530 existing logos, 239 use no placeholders at all (monochrome) and 291 do; **new logos should use placeholders**, as monochrome is a legacy style
- Tabs are expanded to 4 spaces
- Colors can be overridden with `--logo-color-1` … `--logo-color-9`
### 2. Register it in the `.inc`
CMake turns each `.txt` into a `FASTFETCH_DATATEXT_LOGO_<UPPERCASE>` macro (`CMakeLists.txt:418`), but **the registry itself is maintained by hand**. Edit `src/logo/ascii/<first-letter>.inc`:
CMake turns each `.txt` file into a `FASTFETCH_DATATEXT_LOGO_<UPPERCASE>` macro (`CMakeLists.txt:418`), but **the registry itself is maintained by hand**. Edit `src/logo/ascii/<first-letter>.inc`:
```c
#ifdef FASTFETCH_DATATEXT_LOGO_OMARCHY
// Omarchy
// src/logo/ascii/d.inc
#ifdef FASTFETCH_DATATEXT_LOGO_DISTRO
// Distro
{
.names = { "omarchy", "Omarchy" },
.lines = FASTFETCH_DATATEXT_LOGO_OMARCHY,
.names = { "Distro" }, // ID (preferred) or NAME from /etc/os-release, do NOT add both
.lines = FASTFETCH_DATATEXT_LOGO_DISTRO,
.colors = {
FF_COLOR_FG_BLUE,
FF_COLOR_FG_WHITE,
FF_COLOR_FG_CYAN,
FF_COLOR_FG_PRIMARY, // recommended for using as WHITE replacement, light-theme terminal friendly
FF_COLOR_FG_BLUE, // preferred
FF_COLOR_FG_256 "34",
FF_COLOR_FG_RGB "0;255;0", // not recommended because of bad compatibility with raw TTY
},
},
#endif
@@ -431,13 +498,13 @@ If the same OS has multiple logo variants, mark the variant explicitly with `.ty
.type = FF_LOGO_LINE_TYPE_SMALL_BIT,
```
Use `FF_LOGO_LINE_TYPE_ALTER_BIT` for an alternate logo, `FF_LOGO_LINE_TYPE_SMALL_BIT` for a small logo, or combine the flags when both apply. This is also a lookup optimization: a logo marked `FF_LOGO_LINE_TYPE_SMALL_BIT` is considered only for `type = small`, while a logo marked `FF_LOGO_LINE_TYPE_ALTER_BIT` is never selected by automatic detection. Alternate logos are available only when explicitly requested through `--logo <source>`.
Use `FF_LOGO_LINE_TYPE_ALTER_BIT` for an alternate logo, `FF_LOGO_LINE_TYPE_SMALL_BIT` for a small logo, or combine the flags when both apply. This is also a lookup optimization: a logo marked `FF_LOGO_LINE_TYPE_SMALL_BIT` is considered only for `type = small`, while a logo marked `FF_LOGO_LINE_TYPE_ALTER_BIT` is never selected by automatic detection. Alternate logos are available only when explicitly requested through `-l <source>`.
### 3. Verify
```sh
./build/fastfetch --logo omarchy
./build/fastfetch --list-logos | grep -i omarchy
./build/fastfetch -l distro
./build/fastfetch --list-logos | grep -i distro
```
---
@@ -474,9 +541,9 @@ Current distribution of platform files in `detection/`:
(These are repository inventory figures, counting `.c`, `.m`, `.cpp`, and `.h`; they may change as platforms and modules are added.)
**Don't pile `#ifdef __linux__` into one file.** When adding platform support, copy the closest existing implementation and change the suffix.
**Do not accumulate `#ifdef __linux__` blocks in a single file.** When adding platform support, copy the closest existing implementation and change the suffix.
`common/` uses the same idea: `common/impl/` contains `io_unix.c` / `io_windows.c`, `netif_linux.c` / `netif_apple.c` / `netif_bsd.c` and so on, with `common/apple/`, `common/windows/` and `common/haiku/` holding platform-specific helpers.
`common/` follows the same convention: `common/impl/` contains `io_unix.c` / `io_windows.c`, `netif_linux.c` / `netif_apple.c` / `netif_bsd.c` and similar files, with `common/apple/`, `common/windows/` and `common/haiku/` holding platform-specific helpers.
When several platform suffixes could apply, use the most specific implementation supported by the build system (for example, `_nbsd.c` instead of the generic `_bsd.c` on NetBSD). Keep the generic file as the fallback for platforms that share its conventions.
@@ -507,7 +574,7 @@ clang-format -i src/modules/foo/*.c src/modules/foo/*.h
`src/3rdparty/**`, `build/**` and `src/logo/builtin.c` are listed in `.clang-format-ignore` — **do not reformat them.**
`.editorconfig` adds LF line endings, 4-space indent, a final newline, and trailing whitespace trimmed (except in Markdown).
`.editorconfig` specifies LF line endings, a 4-space indent, a final newline, and trailing whitespace trimmed (except in Markdown).
### Naming
@@ -522,7 +589,7 @@ clang-format -i src/modules/foo/*.c src/modules/foo/*.h
### Spelling
CI runs codespell (`.codespellrc`). Known false positives live in `ignore-words-list` (`iterm`, `compiletime`, and various non-English distro words). Add new words there rather than changing the code.
CI runs codespell (`.codespellrc`). Known false positives are listed in `ignore-words-list` (`iterm`, `compiletime`, and various non-English distro words). Add new words there rather than changing the code.
### Compiler warnings
@@ -566,7 +633,7 @@ Common verbs: `adds`, `removes`, `fixes`, `improves`, `updates`, `corrects`, `pr
Scopes in use: `Top`, `Processes`, `Memory`, `Logo (Builtin)`, `CI`, `Doc`, `Presets`, `Global`, plus individual module names.
Documentation-only changes get a `[ci skip]` suffix.
Documentation-only changes use a `[ci skip]` suffix.
---
@@ -617,21 +684,21 @@ cmake --build build
cd build && ctest --output-on-failure
```
Coverage focuses on the core data structures and the formatting engine in `common/`. The `detection/` layer has no automated tests (it depends too heavily on a real system) and is instead covered by the CI matrix — 20 workflows under `.github/workflows/` spanning Linux (including musl, loong64, armv7l, i686), macOS, Windows, FreeBSD, NetBSD, OpenBSD, DragonFly, Solaris, OmniOS and Haiku, plus spellcheck and benchmark jobs.
Coverage focuses on the core data structures and the formatting engine in `common/`. The `detection/` layer has no automated tests, because it depends on the state of a running system; it is instead covered by the CI matrix — 20 workflows under `.github/workflows/` spanning Linux (including musl, loong64, armv7l, i686), macOS, Windows, FreeBSD, NetBSD, OpenBSD, DragonFly, Solaris, OmniOS and Haiku, plus spellcheck and benchmark jobs.
**If you touch `common/FFstrbuf.h`, `common/format.h` or `common/color.h`, please extend the matching test.**
**If you modify `common/FFstrbuf.h`, `common/format.h` or `common/color.h`, extend the corresponding test.**
---
## Pull requests
1. **Open an issue first** (feature request / bug report / logo request) so you don't waste effort. Templates are in `.github/ISSUE_TEMPLATE/`.
1. **Open an issue first** (feature request / bug report / logo request) to confirm that the change is wanted before investing effort. Templates are in `.github/ISSUE_TEMPLATE/`.
2. Branch off `dev` — **`dev` is the main development branch**, not `master`.
3. Follow the [commit message convention](#commit-messages).
4. Update `CHANGELOG.md` for user-visible changes.
5. Open the PR against `dev` and fill in `.github/pull_request_template.md`:
- Summary
- Related issue (**required for new logos**, otherwise the PR won't be accepted)
- Related issue (**required for new logos**; otherwise the PR is not accepted)
- Changes
- Screenshots (required for visual changes)
- Checklist: confirm you tested locally
@@ -649,13 +716,13 @@ cd build && ctest --output-on-failure # tests
---
## Gotchas
## Pitfalls
Things that are easy to get wrong when reading this codebase. Most of them follow from the "obsessively fast startup" goal.
The following points are easy to misinterpret when reading this codebase. Most of them follow from the startup-time objective described above.
### 1. Option structs must not exceed 256 bytes
`FF_OPTION_MAX_SIZE = 1 << 8`. Exceeding it is caught at compile time by `static_assert`. Don't try to raise the value it determines the stack cost of every module invocation.
`FF_OPTION_MAX_SIZE = 1 << 8`. Exceeding it is caught at compile time by `static_assert`. Do not attempt to increase the value: it determines the stack cost of every module invocation.
### 2. `FF_MODULE_DISABLE_*` controls registration, not compilation
@@ -668,9 +735,11 @@ Things that are easy to get wrong when reading this codebase. Most of them follo
**Disabling modules in a Debug build does not shrink the binary.** LTO is enabled whenever `CMAKE_BUILD_TYPE != Debug`, and the default `RelWithDebInfo` already satisfies that — so measure size with `RelWithDebInfo` or `Release`, never with `Debug`. (The "Release mode" wording in the comment above is imprecise.)
### 3. CMake globs don't trigger reconfiguration
### 3. Only two things are discovered by glob
Both the module directories and the logo files are discovered by glob. CMake will not notice new files on its own — you need to re-run `cmake -B build` (or delete `build/CMakeCache.txt` and start over). This is the classic CMake footgun.
The module sources (`CMakeLists.txt:134`) and the logo `.txt` files (`CMakeLists.txt:429`). Both use `CONFIGURE_DEPENDS`, so with the Makefile and Ninja generators the build re-checks the glob and re-runs CMake when the result changes; other generators (Visual Studio and Xcode in particular) do not track it as reliably. Re-run `cmake -B build` after adding a module source or a logo file rather than relying on that behavior.
Everything else — `src/detection/**` and `src/common/impl/**`, and any source outside the platform chain — must be listed in `CMakeLists.txt` by hand; see [step 5](#5-add-the-platform-sources-to-cmakeliststxt).
### 4. CLI module options are removed
@@ -686,9 +755,9 @@ Error: Unsupported module option: --cpu-temp
### 5. `defaultOrder = 0` hides a module from `--gen-config`
`collectModuleInfos` (`genconfig.c:186`) skips any module whose `defaultOrder` is `0`. C zero-initializes the field, so **omitting it is the same as setting it to 0**. Only `logo`, `command` and `custom` do this on purpose.
`collectModuleInfos` (`genconfig.c:186`) skips any module whose `defaultOrder` is `0`. C zero-initializes the field, so **omitting it is the same as setting it to 0**. Only `logo`, `command` and `custom` rely on this intentionally.
`defaultOrder` affects only the ordering in the interactive `--gen-config` picker; it has no effect on runtime output order.
`defaultOrder` affects only the ordering in the interactive `--gen-config` picker; it has no effect on the runtime output order.
### 6. Localization uses byte offsets, not enums
@@ -699,19 +768,23 @@ Error: Unsupported module option: --cpu-temp
(*(const char**) ((uint8_t*) &ff ## moduleName ## ModuleInfo.displayName + instance.config.display.keyLanguage))
```
Consequence: **the field order of `FFModuleDisplayName` must never change** — reordering silently scrambles every language.
Consequence: **the field order of `FFModuleDisplayName` must never change** — reordering the fields silently corrupts the output for every language.
### 7. The `multithreading` switch barely does anything
### 7. The `multithreading` option has limited effect
The global `multithreading` option currently takes effect in exactly one place: `common/impl/networking_linux.c:339`. Modules are still printed sequentially. Modules that need concurrency go through the `ffPrepare*` warm-up hooks instead, which start sampling or fire off requests before the print loop begins.
### 8. Don't print or read config inside `detection/`
This may change in the future. The main blocking issue is that there are dependencies between different modules.
### 8. Do not print or read configuration inside `detection/`
The `detection/` layer must stay pure: read system state, fill a struct, return an error string. Any `printf` or any read of `instance.config` there is a design error.
Use `FF_DEBUG` (`src/common/debug.h`) for logging.
### 9. `src/logo/builtin.c` and `3rdparty/` are formatting-exempt
The former is a large generated/hand-maintained data table, the latter is upstream code. Both are excluded via `.clang-format-ignore` — leave them alone.
The former is a large generated and hand-maintained data table; the latter is upstream code. Both are excluded via `.clang-format-ignore`; do not reformat either.
---