diff --git a/.gitignore b/.gitignore index 1394fe931..9fe9c7bfb 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ fastfetch.kdev4 *.user.* *.swp *.log +/*.zh.md +/.*-ai/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..0da08c892 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,724 @@ +# Contributing to fastfetch + +
+ + Note + + + 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. +
+ +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. + +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. + +--- + +## Table of contents + +- [Contributor quick reference](#contributor-quick-reference) +- [Getting started](#getting-started) +- [Code map](#code-map) +- [Core architecture: modules vs. detection](#core-architecture-modules-vs-detection) +- [Module registration](#module-registration) +- [Walkthrough: adding a module](#walkthrough-adding-a-module) +- [Walkthrough: adding a logo](#walkthrough-adding-a-logo) +- [Platform conventions](#platform-conventions) +- [Code style](#code-style) +- [Commit messages](#commit-messages) +- [Changelog](#changelog) +- [Tests](#tests) +- [Pull requests](#pull-requests) +- [Gotchas](#gotchas) + +--- + +## Contributor quick reference + +| Task | Start here | Verify with | +|---|---|---| +| Build and run fastfetch | `run.sh`, `CMakeLists.txt` | `./run.sh` | +| Add a module | `src/modules//`, `src/detection//`, `src/modules/modules.c` | `cmake -B build && cmake --build build -j` | +| Add a platform implementation | `src/detection//_.c` | Build on the target platform or CI | +| Add an ASCII logo | `src/logo/ascii//.txt`, matching `.inc` | `./build/fastfetch --logo ` | +| Change formatting or JSON output | `src/modules//.c` | `./build/fastfetch -s --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. + +--- + +## Getting started + +### Building + +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 +./run.sh --format json # arguments are forwarded to fastfetch +``` + +Manual build: + +```sh +cmake -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo +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). + +### Common build options + +| Option | Default | Notes | +|---|---|---| +| `CMAKE_BUILD_TYPE` | `RelWithDebInfo` | LTO is on for anything but `Debug` | +| `BUILD_TESTS` | `OFF` | Builds the unit tests in `tests/` | +| `BUILD_FLASHFETCH` | `ON` | Also builds the stripped-down `flashfetch` | +| `BINARY_LINK_TYPE` | `dlopen` | `dlopen` / `dynamic` / `static` | +| `ENABLE_ASAN` | `OFF` | Address Sanitizer | +| `ENABLE_LTO` | `ON` | Link-time optimization | +| `MODULE_DISABLE_` | `OFF` | Disable a module, e.g. `MODULE_DISABLE_GPU=ON` | +| `SET_TWEAK` | `ON` | Appends a tweak to the dev version; turned off for releases | + +`BINARY_LINK_TYPE=dlopen` (the default) is a deliberate design choice: optional dependencies (Vulkan, Wayland, DBus, ImageMagick, chafa, …) are loaded at runtime, so a missing library never prevents startup. When adding a third-party dependency, use the dlopen helpers in `common/library.h` — do not link it directly. + +For a minimal-dependency build, see `.github/workflows/build-no-features-test.yml`: + +```sh +cmake -DBUILD_TESTS=On -DENABLE_VULKAN=OFF -DENABLE_WAYLAND=OFF -DENABLE_X11=OFF \ + -DENABLE_DBUS=OFF -DENABLE_ZLIB=OFF ... . +``` + +### Dependencies + +Required: CMake ≥ 3.21 and a C23 compiler (GCC, Clang or MSVC). Everything else is optional. + +Optional dependencies are auto-detected: libpci, libdrm, vulkan, wayland, xcb, xrandr, dbus, sqlite3, rpm, imagemagick{6,7}, chafa, zlib, egl, glx, opencl, freetype, pulse, ddcutil, elf, libzfs, and more. + +--- + +## Code map + +``` +src/ +├── fastfetch.c entry point: CLI parsing, module dispatch, main loop (36 KB) +├── flashfetch.c entry point for the stripped-down build +├── options/ global (non-module) config: general / display / logo +├── modules/ module layer — module formatting and output +├── detection/ detection layer — platform-specific data retrieval +├── common/ infrastructure: FFstrbuf, FFlist, formatting, printing, networking +├── logo/ 530 ASCII logos plus the image logo backends +└── 3rdparty/ yyjson, widecharwidth, display-library +``` + +Flow of control: + +``` +fastfetch.c → options/ (global configuration) + → modules/ (76 modules, formatting and output) + → detection/ (one platform implementation each, raw data) + → 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: + +- **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`). +- **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//` always has a matching `detection//`. + +### Layer boundaries + +| Layer | Owns | Must not | +|---|---|---| +| `options/` | **Global** config (colors, logo, threading, timeouts) | hold per-module options | +| `modules/` | formatting, printing, JSON serialization | read `/proc`, call Win32 APIs, parse sysfs | +| `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__`. + +--- + +## 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: + +``` +modules/cpu/option.h module option struct +modules/cpu/cpu.c formatting and output +detection/cpu/cpu.h platform-independent interface +detection/cpu/cpu_linux.c ┐ +detection/cpu/cpu_apple.c │ +detection/cpu/cpu_windows.c ├─ platform implementations, one linked per build +detection/cpu/cpu_bsd.c │ +detection/cpu/cpu_nosupport.c ┘ +``` + +The interface is deliberately minimal — usually just two things: + +```c +typedef struct FFCPUResult { ... } FFCPUResult; // result struct +const char* ffDetectCPU(const FFCPUOptions* options, FFCPUResult* cpu); +``` + +The `const char*` return value is an **error string**; `nullptr` means success. This pattern is used throughout `detection/` — please keep it consistent. + +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/`. + +--- + +## Module registration + +### The descriptor + +Every module exports one `FFModuleBaseInfo` (defined in `common/option.h:46`) — essentially a hand-written vtable: + +```c +typedef struct FFModuleBaseInfo { + const char* name; // lookup key, e.g. "cpu" + const char* description; // help text + FFModuleDisplayName displayName; // module name in 20 languages + + void (*initOptions)(void* options); + void (*destroyOptions)(void* options); + void (*parseJsonObject)(void* options, struct yyjson_val* module); + bool (*printModule)(void* options); + bool (*generateJsonResult)(void* options, yyjson_mut_doc*, yyjson_mut_val*); + void (*generateJsonConfig)(void* options, yyjson_mut_doc*, yyjson_mut_val*); + + FFModuleFormatArgList formatArgs; // format placeholder table + const uint8_t defaultOrder; // sort weight +} 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 registry: a first-letter hash bucket + +`src/modules/modules.c` defines 26 `static FFModuleBaseInfo*` arrays (`A[]` through `Z[]`), each terminated by `nullptr`, collected into `ffModuleInfos[26]`: + +```c +FFModuleBaseInfo** modules = ffModuleInfos[toupper(name[0]) - 'A']; // O(1) bucket lookup +for (; *modules; ++modules) { // linear scan in the bucket + if (ffStrEqualsIgnCase(name, (*modules)->name)) { ... } +} +``` + +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. + +### The calling convention: zero heap allocation + +Every dispatch site (`jsonconfig.c:98`, `commandoption.c:189`) follows the same pattern: + +```c +alignas(uint64_t) uint8_t optionBuf[FF_OPTION_MAX_SIZE]; // 256 bytes, on the stack +baseInfo->initOptions(optionBuf); +baseInfo->parseJsonObject(optionBuf, jsonVal); // JSONC path only +baseInfo->printModule(optionBuf); // or generateJsonResult +baseInfo->destroyOptions(optionBuf); +``` + +`FF_OPTION_MAX_SIZE = 1 << 8` (256 bytes). Every module's `option.h` must end with: + +```c +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. + +The three dispatch entry points: + +| Entry point | Location | Used for | +|---|---|---| +| `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_` option per directory: + +```cmake +file(GLOB FF_MODULE_DIRS RELATIVE "..." ".../src/modules/*/") +foreach(FF_MODULE_DIR ${FF_MODULE_DIRS}) + 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`). + +--- + +## Walkthrough: adding a module + +Adding a `Foo` module, step by step. + +### 1. Define the option struct + +`src/modules/foo/option.h`: + +```c +#pragma once + +#include "common/option.h" + +typedef struct FFFooOptions { + FFModuleArgs moduleArgs; // must be the first field + bool showBar; +} 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)). + +### 2. Implement detection + +`src/detection/foo/foo.h`: + +```c +#pragma once +#include "fastfetch.h" + +typedef struct FFFooResult { + FFstrbuf name; + uint32_t count; +} FFFooResult; + +const char* ffDetectFoo(const FFFooOptions* options, FFFooResult* result); +``` + +`src/detection/foo/foo_linux.c`: + +```c +#include "foo.h" + +const char* ffDetectFoo(const FFFooOptions* options, FFFooResult* result) { + // read /sys, /proc, sysfs, dbus, ... + return nullptr; // success; return an error string on failure +} +``` + +**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. + +### 3. Implement the module + +`src/modules/foo/foo.c` — implement the six functions, then define the descriptor: + +```c +FFModuleBaseInfo ffFooModuleInfo = { + .name = "Foo", + .description = "Print foo information", + .displayName = { + .en = "Foo", .ar = "Foo", .cs = "Foo", .de = "Foo", .es = "Foo", + .fr = "Foo", .gl = "Foo", .he = "Foo", .id = "Foo", .it = "Foo", + .ja = "Foo", .ko = "Foo", .pl = "Foo", .pt = "Foo", .ru = "Foo", + .tr = "Foo", .uk = "Foo", .vi = "Foo", .zh_CN = "Foo", .zh_TW = "Foo", + }, + .initOptions = (void*) ffInitFooOptions, + .destroyOptions = (void*) ffDestroyFooOptions, + .parseJsonObject = (void*) ffParseFooJsonObject, + .printModule = (void*) ffPrintFoo, + .generateJsonResult = (void*) ffGenerateFooJsonResult, + .generateJsonConfig = (void*) ffGenerateFooJsonConfig, + .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) { + { "Name", "name" }, + { "Count", "count" }, + })), + .defaultOrder = 73, +}; +``` + +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.** +- 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. + +### 4. Register it + +- Add `#include "modules/foo/foo.h"` to `src/modules/modules.h` +- Insert into the `F[]` array in `src/modules/modules.c`: + +```c +#if !FF_MODULE_DISABLE_FOO + &ffFooModuleInfo, +#endif +``` + +`FF_MODULE_DISABLE_FOO` is generated by CMake — you do not define it yourself. + +### 5. 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 right first-letter `case`. Six modules do this today: CPUUsage, DiskIO, NetIO, PublicIP, Top and Weather. + +### 6. Verify + +```sh +cmake -B build && cmake --build build -j +./build/fastfetch -s foo --format json # JSON output +./build/fastfetch -h foo-format # list formatArgs placeholders +./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`. + +--- + +## Walkthrough: adding a logo + +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 + +``` +src/logo/ascii//.txt +``` + +For example `src/logo/ascii/o/omarchy.txt`. Directories are already split by first letter (`a/` … `z/`, plus `_/`). + +The file is plain ASCII art with `$1` … `$9` as color placeholders: + +``` + $3 ,-^-___ +$3 /\\\/// +$2refined.$1 /\\\\// +``` + +- `$1` … `$9` map to palette slots 1–9; at most 9 (`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 +- 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_` macro (`CMakeLists.txt:418`), but **the registry itself is maintained by hand**. Edit `src/logo/ascii/.inc`: + +```c +#ifdef FASTFETCH_DATATEXT_LOGO_OMARCHY +// Omarchy +{ + .names = { "omarchy", "Omarchy" }, + .lines = FASTFETCH_DATATEXT_LOGO_OMARCHY, + .colors = { + FF_COLOR_FG_BLUE, + FF_COLOR_FG_WHITE, + FF_COLOR_FG_CYAN, + }, +}, +#endif +``` + +- `names` is matched case-insensitively. Do not add names that differ only by case, because they can never be distinguished. Every name is also shown by `--list-logos`, so use user-friendly spelling such as an initial capital where appropriate. +- For a new logo, use exactly one name unless there is a specific compatibility reason to add more. That name should first be the `ID` from `/etc/os-release`. If the ID cannot distinguish this logo from another logo for the same OS, use the `NAME` value instead. `logoGetBuiltinDetected` in `src/logo/logo.c` documents the detection order: `ID`, then `NAME`, then tokens from `ID_LIKE`, then the platform name fallback. Manual `logo-source` selection uses the name directly. +- `colors` is positional — entry 0 is `$1`, entry 1 is `$2`, and so on +- `colors[0]` also becomes the title color and `colors[1]` the key color, unless the user overrides them + +If the same OS has multiple logo variants, mark the variant explicitly with `.type`: + +```c +.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 `. + +### 3. Verify + +```sh +./build/fastfetch --logo omarchy +./build/fastfetch --list-logos | grep -i omarchy +``` + +--- + +## Platform conventions + +The `detection/` layer picks its implementation by **filename suffix**; the choice is made at build time from `CMAKE_SYSTEM_NAME`: + +| Suffix | Platform | +|---|---| +| `_linux.c` | Linux | +| `_apple.c` / `_apple.m` | macOS / iOS (`.m` for Objective-C) | +| `_windows.c` / `_windows.cpp` | Windows (`.cpp` for WinRT / WMI) | +| `_bsd.c` | FreeBSD | +| `_nbsd.c` | NetBSD | +| `_obsd.c` | OpenBSD | +| `_sunos.c` | Solaris / illumos | +| `_haiku.c` / `.cpp` | Haiku | +| `_android.c` | Android (Termux) | +| `_gnu.c` | GNU/Hurd | +| `_nosupport.c` | Empty fallback | + +Current distribution of platform files in `detection/`: + +| Suffix | Files | Suffix | Files | +|---|---|---|---| +| `_linux` | 58 | `_obsd` | 16 | +| `_windows` | 47 | `_haiku` | 13 | +| `_apple` | 46 | `_android` | 12 | +| `_nosupport` | 45 | `_gnu` | 3 | +| `_bsd` | 29 | | | +| `_sunos` | 19 | | | +| `_nbsd` | 17 | | | + +(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. + +`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. + +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. + +--- + +## Code style + +### Formatting + +The project uses clang-format with `BasedOnStyle: LLVM` plus these key overrides: + +```yaml +IndentWidth: 4 +UseTab: Never +ColumnLimit: 0 # never wrap +InsertBraces: true # braces even on single-statement ifs +PointerAlignment: Left # char* p, not char *p +SortIncludes: Never # include order is managed by hand +AlignAfterOpenBracket: DontAlign +BinPackParameters: false # all params on one line, or one per line +``` + +Before committing: + +```sh +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). + +### Naming + +| Kind | Convention | Example | +|---|---|---| +| Functions | `ff` + PascalCase | `ffDetectCPU`, `ffPrintCPU` | +| Types | `FF` + PascalCase | `FFCPUResult`, `FFModuleBaseInfo` | +| Variables / fields | camelCase | `coresPhysical`, `keyWidth` | +| Macros | SCREAMING_SNAKE_CASE | `FF_OPTION_MAX_SIZE` | +| Module options | `FFOptions` | `FFCPUOptions` | +| Detection results | `FFResult` | `FFCPUResult` | + +### 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. + +### Compiler warnings + +The build enables `-Wall -Wextra -Wconversion` plus several `-Werror`s: + +``` +-Werror=uninitialized -Werror=return-type -Werror=vla +-Werror=incompatible-pointer-types -Werror=implicit-function-declaration -Werror=int-conversion +``` + +**`-Wconversion` is strict**: every implicit narrowing conversion needs an explicit cast. + +--- + +## Commit messages + +Format: + +``` +[ (Platform)]: +``` + +Scope is the module or subsystem name; Platform is optional. Use the third-person singular present tense, capitalize the first letter, no trailing period. + +Real examples from recent history: + +``` +Processes (Haiku): honors `options->countKprocs` +WM (macOS): improves reliability of WM plugin detection +Memory (Windows): prefers `NQSI` +Top (Linux): improves performance of `stat` parsing +Logo (Builtin): adds omarchy +Global: introduces global macro `FF_PATH_PKG_BASE` to replace `_PATH_LOCALBASE` +LM (OpenBSD): adds support +CI: disables fail on alert +Doc: updates README +Presets: moves `top` to the bottom of running modules [ci skip] +``` + +Common verbs: `adds`, `removes`, `fixes`, `improves`, `updates`, `corrects`, `prefers`, `honors`, `disables`, `enables`, `introduces`, `detects`, `reports`, `skips`, `uses`. + +Scopes in use: `Top`, `Processes`, `Memory`, `Logo (Builtin)`, `CI`, `Doc`, `Presets`, `Global`, plus individual module names. + +Documentation-only changes get a `[ci skip]` suffix. + +--- + +## Changelog + +User-visible changes go into `CHANGELOG.md`, under the topmost version heading: + +```markdown +# Unreleased + +Changes: +* The DE / WM / LM modules now reports the full name ... + +Features: +* Added Top module to print processes with the highest CPU, memory or disk I/O usage. (Top) +* Improved Wi-Fi module + * Added Wifi channel width detection, exposed via `{channel-width}` in custom format. + * Improved Wifi channel frequency accuracy on Windows, macOS. +* Added Battery detection support on SunOS. (Battery, SunOS) + +Bugfixes: +* Fixed I/O rate calculation precision in DiskIO and NetIO. (DiskIO / NetIO) +``` + +Rules: + +- Three sections: `Changes:` (behavior changes), `Features:`, `Bugfixes:` +- Past tense: `Added` / `Fixed` / `Improved` +- End each entry with its scope: `(Module, Platform)` or `(ModuleA / ModuleB)` +- Sub-bullets are indented 4 spaces + +--- + +## Tests + +Tests are standalone executables in `tests/` using a `VERIFY` macro that exits non-zero on failure: + +```c +#define VERIFY(expression) \ + if (!(expression)) testFailed(&strbuf, #expression, __LINE__) +``` + +Current tests: `strbuf.c`, `list.c`, `format.c`, `color.c`, `duration.c`, `strutil.c`. + +```sh +cmake -B build -DBUILD_TESTS=On +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. + +**If you touch `common/FFstrbuf.h`, `common/format.h` or `common/color.h`, please extend the matching 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/`. +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) + - Changes + - Screenshots (required for visual changes) + - Checklist: confirm you tested locally + +### Pre-submit checklist + +```sh +clang-format -i # format +codespell # spelling +cmake -B build -DBUILD_TESTS=On && cmake --build build -j +cd build && ctest --output-on-failure # tests +./build/fastfetch --format json # verify JSON output is well-formed +./build/fastfetch -c presets/all.jsonc --stat false # smoke-test every module +``` + +--- + +## Gotchas + +Things that are easy to get wrong when reading this codebase. Most of them follow from the "obsessively fast startup" goal. + +### 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. + +### 2. `FF_MODULE_DISABLE_*` controls registration, not compilation + +```c +// verbatim from the top of modules/modules.c: +// FF_MODULE_DISABLE_ only controls if the module is registered, +// the module code itself is still compiled. +// We rely `LTO` to remove the unused code (only enabled in Release mode) +``` + +**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 + +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. + +### 4. CLI module options are removed + +Per-module command-line flags such as `--cpu-temp` are **no longer supported**. The only job of `ffParseModuleOptions` now is to translate the flag into a JSON key, then `exit(477)` with a pointer to the config file: + +``` +Error: Unsupported module option: --cpu-temp + Support of module options has been removed. Please add the flag to the JSON config instead. + Example (demonstration only): `{ "modules": [ { "type": "cpu", "temp": true } ] }` +``` + +**JSONC is the first-class configuration interface; the command line is not.** When adding module options, implement `parseJsonObject` only — do not add a CLI branch. + +### 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. + +`defaultOrder` affects only the ordering in the interactive `--gen-config` picker; it has no effect on runtime output order. + +### 6. Localization uses byte offsets, not enums + +`instance.config.display.keyLanguage` does not hold a language enum — it holds a **byte offset** such as `offsetof(FFModuleDisplayName, zh_CN)`. The macro `FF_MODULE_GET_DISPLAY_NAME` (`common/option.h:123`) does plain pointer arithmetic with it: + +```c +#define FF_MODULE_GET_DISPLAY_NAME(moduleName) \ + (*(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. + +### 7. The `multithreading` switch barely does anything + +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/` + +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. + +### 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. + +--- + +## Reference + +- Configuration guide: +- JSON schema: `doc/json_schema.json` +- Example presets: `presets/` (`all.jsonc`, `neofetch.jsonc`, `ci.jsonc`, …) +- Man page source: `doc/fastfetch.1.in` +- Code generation scripts: `scripts/` (`gen-pciids.py`, `gen-amdgpuids.py`, `gen-man.py`)