Files

798 lines
36 KiB
Markdown
Raw Permalink Normal View History

2026-08-31 15:24:48 +08:00
# Contributing to fastfetch
<details>
<summary>
2026-09-02 10:44:36 +08:00
Disclaimer
2026-08-31 15:24:48 +08:00
</summary>
2026-09-02 10:44:36 +08:00
This document is generated by AI and reviewed by humans. It is primarily intended for AI-assisted development, while remaining useful to human developers.
2026-08-31 15:24:48 +08:00
</details>
2026-09-02 10:44:36 +08:00
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.
2026-08-31 15:24:48 +08:00
2026-09-02 10:44:36 +08:00
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.
2026-08-31 15:24:48 +08:00
---
## 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)
2026-09-02 10:44:36 +08:00
- [Pitfalls](#pitfalls)
2026-08-31 15:24:48 +08:00
---
## Contributor quick reference
| Task | Start here | Verify with |
|---|---|---|
| Build and run fastfetch | `run.sh`, `CMakeLists.txt` | `./run.sh` |
2026-09-02 10:44:36 +08:00
| 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 |
2026-08-31 15:24:48 +08:00
| 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` |
2026-09-02 10:44:36 +08:00
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.
2026-08-31 15:24:48 +08:00
---
## Getting started
### Building
2026-09-02 10:44:36 +08:00
The quickest way is `run.sh` in the repository root: it creates `build/`, configures, compiles and runs the binary.
2026-08-31 15:24:48 +08:00
```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
```
2026-09-02 10:44:36 +08:00
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).
2026-08-31 15:24:48 +08:00
### 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_<NAME>` | `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)
```
2026-09-02 10:44:36 +08:00
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:
2026-08-31 15:24:48 +08:00
2026-09-02 10:44:36 +08:00
- **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`).
2026-08-31 15:24:48 +08:00
- **4 detection directories serve modules with different names** — `displayserver` (→ `display`, `monitor`), `gtk_qt` (→ `theme`, `icons`, `font`, `cursor`), `terminalshell` (→ `terminal`, `shell`) and `libc`.
2026-09-02 10:44:36 +08:00
Therefore, do not assume that `modules/<x>/` always has a matching `detection/<x>/`.
2026-08-31 15:24:48 +08:00
### 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 |
2026-09-02 10:44:36 +08:00
A simple check: **no file under `detection/` should contain `printf` or read `instance.config`.** Symmetrically, no file under `modules/` should contain `#ifdef __linux__`.
2026-08-31 15:24:48 +08:00
---
## Core architecture: modules vs. detection
2026-09-02 10:44:36 +08:00
This is the most important convention in the codebase. Every feature consists of a fixed set of files; using CPU as the example:
2026-08-31 15:24:48 +08:00
```
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 ┘
```
2026-09-02 10:44:36 +08:00
The interface is deliberately minimal, typically consisting of only two declarations:
2026-08-31 15:24:48 +08:00
```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`).
2026-09-02 10:44:36 +08:00
**The benefit of this separation:** adding a platform never touches `modules/`; fixing output formatting never touches `detection/`.
2026-08-31 15:24:48 +08:00
---
## 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;
```
2026-09-02 10:44:36 +08:00
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.
2026-08-31 15:24:48 +08:00
### 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)) { ... }
}
```
2026-09-02 10:44:36 +08:00
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.
2026-08-31 15:24:48 +08:00
### 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");
```
2026-09-02 10:44:36 +08:00
**In other words, no module option struct may exceed 256 bytes.** This is a hard constraint, enforced at compile time.
2026-08-31 15:24:48 +08:00
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 |
### Build-time module discovery
2026-09-02 10:44:36 +08:00
`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:
2026-08-31 15:24:48 +08:00
```cmake
2026-09-02 10:44:36 +08:00
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()
2026-08-31 15:24:48 +08:00
foreach(FF_MODULE_DIR ${FF_MODULE_DIRS})
2026-09-02 10:44:36 +08:00
string(TOUPPER "${FF_MODULE_DIR}" FF_MODULE_UPPER)
2026-08-31 15:24:48 +08:00
option(MODULE_DISABLE_${FF_MODULE_UPPER} "Disable module ${FF_MODULE_DIR}" OFF)
endforeach()
```
2026-09-02 10:44:36 +08:00
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).
2026-08-31 15:24:48 +08:00
---
## 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");
```
2026-09-02 10:44:36 +08:00
`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**.
2026-08-31 15:24:48 +08:00
### 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
}
```
2026-09-02 10:44:36 +08:00
**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.
2026-08-31 15:24:48 +08:00
### 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:
2026-09-02 10:44:36 +08:00
- **`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.**
2026-08-31 15:24:48 +08:00
- 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.
2026-09-02 10:44:36 +08:00
- **`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.
2026-08-31 15:24:48 +08:00
### 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.
2026-09-02 10:44:36 +08:00
### 5. Add the platform sources to `CMakeLists.txt`
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:
2026-08-31 15:24:48 +08:00
2026-09-02 10:44:36 +08:00
| 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.
2026-08-31 15:24:48 +08:00
2026-09-02 10:44:36 +08:00
### 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
2026-08-31 15:24:48 +08:00
```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
```
2026-09-02 10:44:36 +08:00
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.
2026-08-31 15:24:48 +08:00
---
## 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.
2026-09-02 10:44:36 +08:00
### 1. Add the ASCII file
2026-08-31 15:24:48 +08:00
```
src/logo/ascii/<first-letter>/<distro>.txt
```
2026-09-02 10:44:36 +08:00
For example `src/logo/ascii/d/distro.txt`. Directories are already split by first letter (`a/` … `z/`, plus `_/`).
2026-08-31 15:24:48 +08:00
The file is plain ASCII art with `$1` … `$9` as color placeholders:
```
$3 ,-^-___
$3 /\\\///
$2refined.$1 /\\\\//
```
2026-09-02 10:44:36 +08:00
- `$1` … `$9` map to palette slots 19; at most 9 are supported (`FASTFETCH_LOGO_MAX_COLORS = 9`)
2026-08-31 15:24:48 +08:00
- `$$` is a literal `$`
2026-09-02 10:44:36 +08:00
- 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
2026-08-31 15:24:48 +08:00
- Tabs are expanded to 4 spaces
- Colors can be overridden with `--logo-color-1` … `--logo-color-9`
### 2. Register it in the `.inc`
2026-09-02 10:44:36 +08:00
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`:
2026-08-31 15:24:48 +08:00
```c
2026-09-02 10:44:36 +08:00
// src/logo/ascii/d.inc
#ifdef FASTFETCH_DATATEXT_LOGO_DISTRO
// Distro
2026-08-31 15:24:48 +08:00
{
2026-09-02 10:44:36 +08:00
.names = { "Distro" }, // ID (preferred) or NAME from /etc/os-release, do NOT add both
.lines = FASTFETCH_DATATEXT_LOGO_DISTRO,
2026-08-31 15:24:48 +08:00
.colors = {
2026-09-02 10:44:36 +08:00
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
2026-08-31 15:24:48 +08:00
},
},
#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,
```
2026-09-02 10:44:36 +08:00
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>`.
2026-08-31 15:24:48 +08:00
### 3. Verify
```sh
2026-09-02 10:44:36 +08:00
./build/fastfetch -l distro
./build/fastfetch --list-logos | grep -i distro
2026-08-31 15:24:48 +08:00
```
---
## 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.)
2026-09-02 10:44:36 +08:00
**Do not accumulate `#ifdef __linux__` blocks in a single file.** When adding platform support, copy the closest existing implementation and change the suffix.
2026-08-31 15:24:48 +08:00
2026-09-02 10:44:36 +08:00
`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.
2026-08-31 15:24:48 +08:00
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.**
2026-09-02 10:44:36 +08:00
`.editorconfig` specifies LF line endings, a 4-space indent, a final newline, and trailing whitespace trimmed (except in Markdown).
2026-08-31 15:24:48 +08:00
### 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 | `FF<Name>Options` | `FFCPUOptions` |
| Detection results | `FF<Name>Result` | `FFCPUResult` |
### Spelling
2026-09-02 10:44:36 +08:00
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.
2026-08-31 15:24:48 +08:00
### 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:
```
<Scope>[ (Platform)]: <third-person singular verb> <object>
```
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.
2026-09-02 10:44:36 +08:00
Documentation-only changes use a `[ci skip]` suffix.
2026-08-31 15:24:48 +08:00
---
## 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
```
2026-09-02 10:44:36 +08:00
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.
2026-08-31 15:24:48 +08:00
2026-09-02 10:44:36 +08:00
**If you modify `common/FFstrbuf.h`, `common/format.h` or `common/color.h`, extend the corresponding test.**
2026-08-31 15:24:48 +08:00
---
## Pull requests
2026-09-02 10:44:36 +08:00
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/`.
2026-08-31 15:24:48 +08:00
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
2026-09-02 10:44:36 +08:00
- Related issue (**required for new logos**; otherwise the PR is not accepted)
2026-08-31 15:24:48 +08:00
- Changes
- Screenshots (required for visual changes)
- Checklist: confirm you tested locally
### Pre-submit checklist
```sh
clang-format -i <changed files> # 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
```
---
2026-09-02 10:44:36 +08:00
## Pitfalls
2026-08-31 15:24:48 +08:00
2026-09-02 10:44:36 +08:00
The following points are easy to misinterpret when reading this codebase. Most of them follow from the startup-time objective described above.
2026-08-31 15:24:48 +08:00
### 1. Option structs must not exceed 256 bytes
2026-09-02 10:44:36 +08:00
`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.
2026-08-31 15:24:48 +08:00
### 2. `FF_MODULE_DISABLE_*` controls registration, not compilation
```c
// verbatim from the top of modules/modules.c:
// FF_MODULE_DISABLE_<module> 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.)
2026-09-02 10:44:36 +08:00
### 3. Only two things are discovered by glob
2026-08-31 15:24:48 +08:00
2026-09-02 10:44:36 +08:00
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).
2026-08-31 15:24:48 +08:00
### 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`
2026-09-02 10:44:36 +08:00
`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.
2026-08-31 15:24:48 +08:00
2026-09-02 10:44:36 +08:00
`defaultOrder` affects only the ordering in the interactive `--gen-config` picker; it has no effect on the runtime output order.
2026-08-31 15:24:48 +08:00
### 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))
```
2026-09-02 10:44:36 +08:00
Consequence: **the field order of `FFModuleDisplayName` must never change** — reordering the fields silently corrupts the output for every language.
2026-08-31 15:24:48 +08:00
2026-09-02 10:44:36 +08:00
### 7. The `multithreading` option has limited effect
2026-08-31 15:24:48 +08:00
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.
2026-09-02 10:44:36 +08:00
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/`
2026-08-31 15:24:48 +08:00
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.
2026-09-02 10:44:36 +08:00
Use `FF_DEBUG` (`src/common/debug.h`) for logging.
2026-08-31 15:24:48 +08:00
### 9. `src/logo/builtin.c` and `3rdparty/` are formatting-exempt
2026-09-02 10:44:36 +08:00
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.
2026-08-31 15:24:48 +08:00
---
## Reference
- Configuration guide: <https://github.com/fastfetch-cli/fastfetch/wiki/Configuration>
- 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`)