Skip to content

Latest commit

 

History

History
797 lines (567 loc) · 36 KB

File metadata and controls

797 lines (567 loc) · 36 KB

Contributing to fastfetch

Disclaimer
This document is generated by AI and reviewed by humans. It is primarily intended for AI-assisted development, while remaining useful to human developers.

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 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.


Table of contents


Contributor quick reference

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, 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 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.


Getting started

Building

The quickest way is run.sh in the repository root: it creates build/, configures, compiles and runs the binary.

./run.sh                 # build and run
./run.sh --format json   # arguments are forwarded to fastfetch

Manual build:

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 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.

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:

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 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 another module's detection result (display, monitor, kernel, shell, terminal, player, custom, datetime).
  • 4 detection directories serve modules with different namesdisplayserver (→ display, monitor), gtk_qt (→ theme, icons, font, cursor), terminalshell (→ terminal, shell) and libc.

Therefore, do not assume that modules/<x>/ always has a matching detection/<x>/.

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

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 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
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, typically consisting of only two declarations:

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 benefit of this separation: 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:

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 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

src/modules/modules.c defines 26 static FFModuleBaseInfo* arrays (A[] through Z[]), each terminated by nullptr, collected into ffModuleInfos[26]:

FFModuleBaseInfo** modules = ffModuleInfos[toupper(name[0]) - 'A'];  // O(1) bucket lookup
for (; *modules; ++modules) {                                        // linear scan in the bucket
    if (ffStrEqualsIgnCase(name, (*modules)->name)) { ... }
}

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

Every dispatch site (jsonconfig.c:98, commandoption.c:189) follows the same pattern:

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:

static_assert(sizeof(FFCPUOptions) <= FF_OPTION_MAX_SIZE, "FFCPUOptions size exceeds maximum allowed size");

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:

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

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:

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()

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):

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.


Walkthrough: adding a module

Adding a Foo module, step by step.

1. Define the option struct

src/modules/foo/option.h:

#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() handles these generically, so no parsing code is required.

2. Implement detection

src/detection/foo/foo.h:

#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:

#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, 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

src/modules/foo/foo.c — implement the six functions, then define the descriptor:

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 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) 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, 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

  • Add #include "modules/foo/foo.h" to src/modules/modules.h
  • Insert into the F[] array in src/modules/modules.c:
#if !FF_MODULE_DISABLE_FOO
    &ffFooModuleInfo,
#endif

FF_MODULE_DISABLE_FOO is generated by CMake — you do not define it yourself.

5. Add the platform sources to CMakeLists.txt

The module layer is discovered by glob (see Build-time module discovery), but src/detection/** and src/common/impl/** are not. CMakeLists.txt lists them explicitly, inside one mutually exclusive chain of platform blocks:

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:

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

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 is not supported; only fastfetch -h foo-format works.

If you added detection sources, re-check step 5 before pushing: a platform block you missed compiles fine locally and fails only when that platform is built.


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. Add the ASCII file

src/logo/ascii/<first-letter>/<distro>.txt

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:

                 $3 ,-^-___
$3                /\\\///
$2refined.$1       /\\\\//
  • $1$9 map to palette slots 1–9; 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, 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 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:

// src/logo/ascii/d.inc

#ifdef FASTFETCH_DATATEXT_LOGO_DISTRO
// Distro
{
    .names = { "Distro" }, // ID (preferred) or NAME from /etc/os-release, do NOT add both
    .lines = FASTFETCH_DATATEXT_LOGO_DISTRO,
    .colors = {
        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
  • 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:

.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 -l <source>.

3. Verify

./build/fastfetch -l distro
./build/fastfetch --list-logos | grep -i distro

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.)

Do not accumulate #ifdef __linux__ blocks in a single file. When adding platform support, copy the closest existing implementation and change the suffix.

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.


Code style

Formatting

The project uses clang-format with BasedOnStyle: LLVM plus these key overrides:

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:

clang-format -i src/modules/foo/*.c src/modules/foo/*.h

src/3rdparty/**, build/** and src/logo/builtin.c are listed in .clang-format-ignoredo not reformat them.

.editorconfig specifies LF line endings, a 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 FF<Name>Options FFCPUOptions
Detection results FF<Name>Result FFCPUResult

Spelling

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

The build enables -Wall -Wextra -Wconversion plus several -Werrors:

-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.

Documentation-only changes use a [ci skip] suffix.


Changelog

User-visible changes go into CHANGELOG.md, under the topmost version heading:

# 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:

#define VERIFY(expression) \
    if (!(expression)) testFailed(&strbuf, #expression, __LINE__)

Current tests: strbuf.c, list.c, format.c, color.c, duration.c, strutil.c.

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, 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 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) to confirm that the change is wanted before investing effort. Templates are in .github/ISSUE_TEMPLATE/.
  2. Branch off devdev is the main development branch, not master.
  3. Follow the commit message convention.
  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 is not accepted)
    • Changes
    • Screenshots (required for visual changes)
    • Checklist: confirm you tested locally

Pre-submit checklist

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

Pitfalls

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. Do not attempt to increase the value: it determines the stack cost of every module invocation.

2. FF_MODULE_DISABLE_* controls registration, not compilation

// 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.)

3. Only two things are discovered by glob

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.

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 rely on this intentionally.

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

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:

#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 the fields silently corrupts the output for every language.

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.

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 and hand-maintained data table; the latter is upstream code. Both are excluded via .clang-format-ignore; do not reformat either.


Reference