Global: fixes a lot bugs found by AI

This commit is contained in:
Carter Li
2026-09-22 14:49:43 +08:00
committed by 李通洲
parent fe49a9624e
commit c26bace3c2
91 changed files with 563 additions and 282 deletions
+61
View File
@@ -6,8 +6,10 @@ Changes:
* ImageMagick 6 support is deprecated. It is kept only for old Debian and Ubuntu releases that don't have ImageMagick 7 available, and is intended to be removed in a future release. Users are encouraged to upgrade to ImageMagick 7 when possible.
* The `--logo-recache` option has been replaced by `--logo-cache <bool|regen>`, and the `logo.recache` JSON property has been renamed to `logo.cache`. (Logo)
* `--logo-cache true` (the default) reuses a cached rendering when it is valid, and writes it back on a cache miss; `false` ignores the image logo cache completely, reading nothing from it and writing nothing to it; `regen` does what `--logo-recache true` used to do. `logo.cache` accepts a boolean, or the string `"regen"`.
* Load average detection on GNU/Hurd now uses `getloadavg()` instead of reading `/proc/loadavg`, sharing the Solaris implementation. (Loadavg, GNU/Hurd)
Features:
* Added Bluetooth Core 6.0, 6.1, 6.2 and 6.3 to the `BluetoothRadio` version table, so an adapter reporting LMP version 14, 15, 16 or 17 now prints its version instead of the vendor name alone. (BluetoothRadio)
* Added rum overlay package counting on RakuOS, exposed as `{rum}`. (Packages, Linux)
* Improved image logo support
* Backend rewritten
@@ -91,6 +93,65 @@ Bugfixes:
* Increased the terminal response wait time to 1s to improve compatibility with slower terminals. (General)
* Fixed the `PublicIp` and `Weather` modules exiting with "can only be used once due to internal limitations" on the round after a failed request when `--dynamic-interval` is used, instead of retrying. (PublicIp / Weather)
* Fixed the `Display`, `Monitor`, `WM`, `DE`, `Media`, `Player`, `Shell` and `Terminal` modules replaying the first round's result when `--dynamic-interval` is used. The detection results that are cached for the whole run are now dropped between rounds, so these modules detect again on every round. (Display / Monitor / WM / DE / Media / Player / Shell / Terminal)
* Fixed the `Command` module printing the internal marker `[BUG] command queue is empty` when it is selected with `--structure`; it now reports that module options are unsupported and exits with an error. (Command)
* Fixed `condition.succeeded` being evaluated differently in the prepare and the print pass, which could make a module print another module's output. (General)
* Fixed negative values for numeric options wrapping around to huge unsigned ones, which made `separator.times` write four billion characters and `waitTime` sleep for about 50 days. (Separator / CpuUsage / NetIO / DiskIO / LoadAvg)
* Fixed the module index wrapping around at 256 in the `command.splitLines` output. (Command)
* Fixed carriage returns not being stripped from the `command.splitLines` output on Windows. (Command, Windows)
* Fixed the `logo` module's JSON `type` never containing `"normal"`. (Logo)
* Fixed the JSON schema describing `custom.key` as the opposite of what the module does. (Custom)
* Fixed the JSON schema rejecting the `custom` and `logo` module types, the `GNU` and `Unknown` systems and the `sh` and `Unknown` architectures. (General)
* Fixed the JSON schema giving `command` a description different from the module registry. (Command)
* Added a JSON result to the `custom` module. (Custom)
* Fixed the `gpu` and `top` modules both declaring default order 36. (General)
* Removed the `kernel` format variable `{display-version}`, which was declared but never passed. (Kernel)
* Fixed `editor` detection treating a failure as a success and printing an empty or half-filled line without an error. (Editor)
* Fixed the `display` module's `order` option being ignored by `--format json`. (Display)
* Fixed compact mode printing `120Hz` where the normal mode prints `120 Hz`. (Display)
* Fixed the `display` module sorting the process-wide display server result in place, which reordered it for every other consumer. (Display)
* Removed 14 `*_nosupport.c` files that no platform block referenced. (General)
* Fixed the `media` module leaking a downloaded cover file on every run. (Media)
* Fixed the JSON schema for `packages.disabled` rejecting 8 names the parser accepts, and dropping the build-time default. (Packages)
* Fixed the JSON schema declaring `percent` for the `cpucache`, `cursor`, `datetime`, `editor` and `font` modules, which the parser rejects, and added the missing declaration for `wifi`. (General)
* Fixed the `custom` module's JSON `error` field being a dangling pointer when a script failed. (Custom)
* Fixed the `vulkan` module writing `deviceId` twice into every GPU object of its JSON result. (Vulkan)
* Fixed the `opencl` and `vulkan` modules writing `memory.*.used` from the total. (OpenCL / Vulkan)
* Fixed the `gpu` module's `hideType` filter being applied to the console output but not to `--format json`. (GPU)
* Fixed the `gpu` module's OpenGL fallback reporting all four memory fields as `0` instead of "unset", which made a format string print `0 B` and the percentage print `nan%`. (GPU)
* Fixed the `de` module not reporting a version for GNOME Classic. (DE, Linux)
* Fixed the `de` module's Trinity version detection never matching, as the display server reports `Trinity` or `TDE` rather than the lowercase `trinity` the dispatch compared against. (DE, Linux)
* Fixed the JSON schema pointing `keyColor` and `keyWidth` at non-existent `display.color.key` and `display.keyWidth` paths. (General)
* Fixed the JSON schema rejecting an empty module `key`, which the parser accepts and treats as "use the default name". (General)
* Fixed the JSON schema for `separator.times` having no upper bound, so values the runtime rejects passed validation. (Separator)
* Fixed the JSON schema declaring `showPeCoreCount` as defaulting to `false` when the runtime defaults it to `true`. (CPU)
* Fixed the JSON schema for `cpucache` not declaring the `compact` option, which made a valid configuration fail validation. (CPUCache)
* Fixed the JSON schema for `cpu` omitting the x86-only `{code-name}` and `{technology}` format variables. (CPU)
* Added the missing `{threads}` format variable to the `processes` module, which its default output already printed. (Processes)
* Added the missing `{kernel-release}` format variable to the `os` module. (OS)
* Fixed the `poweradapter` module documenting `{model}` while passing the value as `{model-name}`, so the documented variable was never substituted. (PowerAdapter)
* Fixed the JSON schema for `processes` omitting `{threads}`. (Processes)
* Fixed the `datetime` module's `{day-in-year}` variable wrapping around at day 256 and printing a wrong value for the rest of the year. (DateTime)
* Fixed the `datetime` module's `{hour-12}` variable being `0`-`11` while `{hour-12-pretty}` is `01`-`12`, so the two disagreed at midnight and noon. (DateTime)
* Fixed the `colors` module using out-of-range `paddingLeft` and `block.width` values instead of reporting them: `paddingLeft: -1` made the run write four billion spaces without ever finishing, and `block.width` was truncated to a byte (`256` printed nothing, `300` printed 44 cells). (Colors)
* Fixed the `vulkan` module's JSON result missing `instanceVersion`, which the detection layer already provides. (Vulkan)
* Fixed the `vulkan` module reporting every device that is not discrete as `Integrated`, so virtual and `OTHER` devices are no longer indistinguishable from a real integrated GPU. (Vulkan)
* Fixed the `camera` and `wifi` modules printing "Not support on this platform" instead of "Not supported on this platform" on platforms that have no implementation. (Camera / Wifi)
* Fixed the `colors` module reading a `block.range` element that is not an integer as `0`, so `[1.5, 3]` and `["x", 3]` printed the colours of `[0, 3]` without reporting anything. (Colors)
* Fixed the `bluetooth` module numbering a machine's only adapter (`Bluetooth 1`) instead of using the bare module name, which every other multi-entry module gets right. (Bluetooth)
* Fixed the `camera` module leaving `colorspace` uninitialized in its macOS backend, so a device reporting a colour space outside the known list produced a garbage string. (Camera, macOS)
* Added the missing `battery` field to the `gamepad` module's JSON result, which the detection layer already provides and the console output already prints. (Gamepad)
* Fixed the `gamepad` module leaking the name and the serial of every device when all of them are filtered out by `ignores`. (Gamepad)
* Fixed the `dns` module appending an uninitialized entry on Windows when an adapter reports a DNS server whose address family is neither IPv4 nor IPv6. (DNS, Windows)
* Added the missing `globalReservation` object (`used` / `total`) to the `btrfs` module's JSON result. The detection layer had been reading `allocation/global_rsv_size` and `allocation/global_rsv_reserved` into fields that neither the JSON nor a format variable exposed. (Btrfs)
* Fixed the `btrfs` module storing the free part of the global reservation as its used part. (Btrfs)
* Fixed the `keyboard` module leaking the name and the serial of every device that is filtered out by `ignores`. (Keyboard)
* Fixed the `keyboard` module reporting "No mouse support on this platform" instead of "No keyboard support on this platform" on platforms that have no implementation. (Keyboard)
* Fixed the `localip` module's `{is-default-route}` variable printing the internal bit field (`2`, `4`, `6`) instead of the families the interface carries the default route for (`ipv4`, `ipv6`, `ipv4,ipv6`). (LocalIp)
* Fixed the `sound` module always reporting itself as failed, which suppressed every following module gated on `condition.succeeded`. (Sound)
* Fixed the `top` module rejecting `sort: "threads"`, which the JSON schema documents and `--gen-config` emits. (Top)
* Fixed the `display.temp` validation errors naming the long-removed `display.temperature` key, which the config parser rejects. (General)
* Fixed a memory leak in the `wifi` module on Windows when the BSS list comes back empty. (Wifi, Windows)
* Fixed `--gen-config` hanging forever when stdout is redirected to `NUL` or MSYS2's `/dev/null`. On Windows `isatty()` reports a character device rather than a console, so a redirected run was mistaken for an interactive one. (General, Windows)
* Some internal cleanups and optimizations.
Logos:
+1 -1
View File
@@ -1343,7 +1343,7 @@ elseif(GNU)
src/detection/keyboard/keyboard_nosupport.c
src/detection/libc/libc_linux.c
src/detection/lm/lm_linux.c
src/detection/loadavg/loadavg_linux.c
src/detection/loadavg/loadavg_sunos.c
src/detection/locale/locale_linux.c
src/detection/localip/localip_linux.c
src/detection/gamepad/gamepad_nosupport.c
+38 -31
View File
@@ -19,15 +19,14 @@
},
"key": {
"description": "Module key\nUse a single space (` `) to hide the key",
"type": "string",
"minLength": 1
"type": "string"
},
"keyColor": {
"description": "Color of the module key. Overrides `display.color.key`",
"description": "Color of the module key. Overrides `display.color.keys`",
"$ref": "#/$defs/colors"
},
"keyWidth": {
"description": "Width of the module key. Overrides `display.keyWidth`",
"description": "Width of the module key. Overrides `display.key.width`",
"type": "integer",
"minimum": 1
},
@@ -131,7 +130,9 @@
"SunOS",
"OpenBSD",
"NetBSD",
"Haiku"
"Haiku",
"GNU",
"Unknown"
]
},
"architectures": {
@@ -150,7 +151,9 @@
"sparc",
"alpha",
"hppa",
"m68k"
"sh",
"m68k",
"Unknown"
]
},
"conditions": {
@@ -322,7 +325,7 @@
"type": "string"
},
"cpuFormat": {
"description": "Output format for the `CPU` module. See Wiki for formatting syntax\n 1. {name}: Name\n 2. {vendor}: Vendor\n 3. {cores-physical}: Physical core count\n 4. {cores-logical}: Logical core count\n 5. {cores-online}: Online core count\n 6. {freq-base}: Base frequency (formatted)\n 7. {freq-max}: Max frequency (formatted)\n 8. {temperature}: Temperature (formatted)\n 9. {core-types}: Logical core count grouped by frequency\n 10. {packages}: Processor package count\n 11. {march}: CPU microarchitecture\n 12. {numa-nodes}: NUMA node count",
"description": "Output format for the `CPU` module. See Wiki for formatting syntax\n 1. {name}: Name\n 2. {vendor}: Vendor\n 3. {cores-physical}: Physical core count\n 4. {cores-logical}: Logical core count\n 5. {cores-online}: Online core count\n 6. {freq-base}: Base frequency (formatted)\n 7. {freq-max}: Max frequency (formatted)\n 8. {temperature}: Temperature (formatted)\n 9. {core-types}: Logical core count grouped by frequency\n 10. {packages}: Processor package count\n 11. {march}: CPU microarchitecture\n 12. {numa-nodes}: NUMA node count\n 13. {code-name}: CPU code name (x86 only)\n 14. {technology}: CPU technology (x86 only)",
"type": "string"
},
"cpucacheFormat": {
@@ -390,7 +393,7 @@
"type": "string"
},
"kernelFormat": {
"description": "Output format for the `Kernel` module. See Wiki for formatting syntax\n 1. {sysname}: Sysname\n 2. {release}: Release\n 3. {version}: Version\n 4. {arch}: Architecture\n 5. {display-version}: Display version\n 6. {page-size}: Page size",
"description": "Output format for the `Kernel` module. See Wiki for formatting syntax\n 1. {sysname}: Sysname\n 2. {release}: Release\n 3. {version}: Version\n 4. {arch}: Architecture\n 5. {page-size}: Page size",
"type": "string"
},
"keyboardFormat": {
@@ -442,7 +445,7 @@
"type": "string"
},
"osFormat": {
"description": "Output format for the `OS` module. See Wiki for formatting syntax\n 1. {sysname}: Name of the kernel *\n 2. {name}: Name of the OS *\n 3. {pretty-name}: Pretty name of the OS, if available\n 4. {id}: ID of the OS\n 5. {id-like}: ID like of the OS\n 6. {variant}: Variant of the OS\n 7. {variant-id}: Variant ID of the OS\n 8. {version}: Version of the OS\n 9. {version-id}: Version ID of the OS\n 10. {codename}: Version codename of the OS\n 11. {build-id}: Build ID of the OS\n 12. {arch}: Architecture of the OS",
"description": "Output format for the `OS` module. See Wiki for formatting syntax\n 1. {sysname}: Name of the kernel *\n 2. {name}: Name of the OS *\n 3. {pretty-name}: Pretty name of the OS, if available\n 4. {id}: ID of the OS\n 5. {id-like}: ID like of the OS\n 6. {variant}: Variant of the OS\n 7. {variant-id}: Variant ID of the OS\n 8. {version}: Version of the OS\n 9. {version-id}: Version ID of the OS\n 10. {codename}: Version codename of the OS\n 11. {build-id}: Build ID of the OS\n 12. {arch}: Architecture of the OS\n 13. {kernel-release}: Release of the kernel",
"type": "string"
},
"packagesFormat": {
@@ -462,11 +465,11 @@
"type": "string"
},
"poweradapterFormat": {
"description": "Output format for the `PowerAdapter` module. See Wiki for formatting syntax\n 1. {watts}: Power adapter watts\n 2. {name}: Power adapter name\n 3. {manufacturer}: Power adapter manufacturer\n 4. {model}: Power adapter model\n 5. {description}: Power adapter description\n 6. {serial}: Power adapter serial number",
"description": "Output format for the `PowerAdapter` module. See Wiki for formatting syntax\n 1. {watts}: Power adapter watts\n 2. {name}: Power adapter name\n 3. {manufacturer}: Power adapter manufacturer\n 4. {model-name}: Power adapter model\n 5. {description}: Power adapter description\n 6. {serial}: Power adapter serial number",
"type": "string"
},
"processesFormat": {
"description": "Output format for the `Processes` module. See Wiki for formatting syntax\n 1. {result}: Process count",
"description": "Output format for the `Processes` module. See Wiki for formatting syntax\n 1. {result}: Process count\n 2. {threads}: Thread count",
"type": "string"
},
"publicipFormat": {
@@ -1576,6 +1579,7 @@
"command",
"colors",
"cursor",
"custom",
"datetime",
"display",
"disk",
@@ -1595,6 +1599,7 @@
"loadavg",
"locale",
"localip",
"logo",
"media",
"memory",
"monitor",
@@ -2100,7 +2105,7 @@
"showPeCoreCount": {
"description": "Whether to detect and display CPU frequencies for different core types (for example, P-cores and E-cores), if supported",
"type": "boolean",
"default": false
"default": true
},
"key": {
"$ref": "#/$defs/key"
@@ -2134,8 +2139,10 @@
"const": "cpucache",
"description": "Print CPU cache sizes"
},
"percent": {
"$ref": "#/$defs/percent"
"compact": {
"type": "boolean",
"description": "Print one line with a total per cache level instead of one line per level",
"default": false
},
"key": {
"$ref": "#/$defs/key"
@@ -2254,6 +2261,7 @@
"description": "Set the number of white spaces to print before the symbol",
"type": "integer",
"minimum": 0,
"maximum": 4294967295,
"default": 0
},
"block": {
@@ -2265,6 +2273,7 @@
"description": "Set the block width in spaces",
"type": "integer",
"minimum": 1,
"maximum": 9,
"default": 3
},
"range": {
@@ -2322,7 +2331,7 @@
"additionalProperties": false,
"properties": {
"type": {
"description": "Run a custom shell command",
"description": "Run custom shell scripts",
"const": "command"
},
"shell": {
@@ -2384,9 +2393,6 @@
"const": "cursor",
"description": "Print cursor style name"
},
"percent": {
"$ref": "#/$defs/percent"
},
"key": {
"$ref": "#/$defs/key"
},
@@ -2420,8 +2426,7 @@
"const": "custom"
},
"key": {
"description": "Leave empty to hide the key",
"type": "string"
"$ref": "#/$defs/key"
},
"keyColor": {
"$ref": "#/$defs/keyColor"
@@ -2453,9 +2458,6 @@
"const": "datetime",
"description": "Print the current date and time"
},
"percent": {
"$ref": "#/$defs/percent"
},
"key": {
"$ref": "#/$defs/key"
},
@@ -2832,9 +2834,6 @@
"const": "editor",
"description": "Print information about the default editor (`$VISUAL` or `$EDITOR`)"
},
"percent": {
"$ref": "#/$defs/percent"
},
"key": {
"$ref": "#/$defs/key"
},
@@ -2867,9 +2866,6 @@
"const": "font",
"description": "Print system font names"
},
"percent": {
"$ref": "#/$defs/percent"
},
"key": {
"$ref": "#/$defs/key"
},
@@ -3775,7 +3771,9 @@
"enum": [
"am",
"apk",
"appimage",
"brew",
"cards",
"choco",
"crux",
"dpkg",
@@ -3784,10 +3782,13 @@
"flatpak",
"guix",
"hpkg",
"installrelease",
"kiss",
"linglong",
"lpkg",
"lpkgbuild",
"macports",
"moss",
"mport",
"nix",
"opkg",
@@ -3796,11 +3797,14 @@
"paludis",
"pisi",
"pkg",
"pkgsrc",
"pkgtool",
"porg",
"rpm",
"rum",
"scoop",
"snap",
"soar",
"sorcery",
"winget",
"xbps"
@@ -3812,8 +3816,7 @@
"description": "Enable all package managers",
"type": "null"
}
],
"default": []
]
},
"combined": {
"description": "Whether to combine related package managers into single counts (e.g., nix-system + nix-user = nix)",
@@ -4101,6 +4104,7 @@
"description": "Number of times to repeat the separator string, or 0 to auto-detect",
"type": "integer",
"minimum": 0,
"maximum": 4294967295,
"default": 0
},
"condition": {
@@ -4805,6 +4809,9 @@
"const": "wifi",
"description": "Print connected Wi-Fi info (SSID, connection and security protocol)"
},
"percent": {
"$ref": "#/$defs/percent"
},
"key": {
"$ref": "#/$defs/key"
},
+19 -1
View File
@@ -85,11 +85,29 @@ void ffPrepareCommandOption(FFdata* data) {
#define FF_IF_MODULE_MATCH(moduleNameConstant) if (ffStrEqualsIgnCase(moduleType, moduleNameConstant) && !ffStrbufSeparatedContainIgnCaseS(&data->structureDisabled, moduleNameConstant, ':'))
switch (moduleType[0]) {
#if !FF_MODULE_DISABLE_CPUUSAGE
#if !FF_MODULE_DISABLE_CPUUSAGE || !FF_MODULE_DISABLE_COMMAND
case 'C':
case 'c':
#endif
#if !FF_MODULE_DISABLE_CPUUSAGE
FF_IF_MODULE_MATCH(ffCPUUsageModuleInfo.name)
ffPrepareCPUUsage(); // The rate is derived from the CPU's own counters; no options are involved
#endif
#if !FF_MODULE_DISABLE_COMMAND
FF_IF_MODULE_MATCH(ffCommandModuleInfo.name) {
// `command` runs its shell commands in parallel: `ffPrepareCommand` spawns them here,
// in the prepare pass, and `ffDetectCommand` collects them in the print pass. The
// command line itself can only be read from the JSON config, and this code path does
// not consult it, so nothing would ever be queued and the module would report an
// internal error marker. Refuse it up front instead of letting it get that far.
fputs("Error: module `command` is not supported with `--structure`\n", stderr);
fputs(" Its options can only be read from the JSON config, which `--structure` bypasses.\n", stderr);
fputs(" Add it to the config instead, e.g. `{ \"modules\": [ { \"type\": \"command\", \"text\": \"uname -r\" } ] }`\n", stderr);
exit(481);
}
#endif
#if !FF_MODULE_DISABLE_CPUUSAGE || !FF_MODULE_DISABLE_COMMAND
break;
#endif
+4
View File
@@ -329,6 +329,10 @@ FFNativeFD ffGetNullFD(void) {
return hNullFile;
}
bool ffIsTerminal(int fd) {
return isatty(fd) != 0;
}
bool ffRemoveFile(const char* fileName) {
return unlink(fileName) == 0;
}
+11
View File
@@ -500,6 +500,17 @@ FFNativeFD ffGetNullFD(void) {
return hNullFile;
}
bool ffIsTerminal(int fd) {
intptr_t handle = _get_osfhandle(fd);
if (handle == -1 || handle == -2) {
// -1: an invalid fd; -2: a valid fd with no file behind it (`_get_osfhandle`'s way of
// saying FF_INVALID_FD). Neither is a terminal.
return false;
}
DWORD mode;
return GetConsoleMode((HANDLE) handle, &mode) != 0;
}
bool ffRemoveFile(const char* fileName) {
wchar_t fileNameW[MAX_PATH];
ULONG len;
+26
View File
@@ -60,6 +60,22 @@ void ffJsonConfigGenerateModuleArgsConfig(yyjson_mut_doc* doc, yyjson_mut_val* m
}
}
bool ffJsonConfigParseUInt32(yyjson_val* val, uint32_t* result, uint32_t max) {
if (!yyjson_is_int(val)) {
return false;
}
// `yyjson_get_sint()` is exact for every value that fits into `int64_t`, and is negative for the
// `uint64_t` values above `INT64_MAX`, which no caller accepts anyway.
int64_t num = unsafe_yyjson_get_sint(val);
if (num < 0 || (uint64_t) num > max) {
return false;
}
*result = (uint32_t) num;
return true;
}
const char* ffJsonConfigParseEnum(yyjson_val* val, int* result, FFKeyValuePair pairs[]) {
if (yyjson_is_int(val)) {
int intVal = yyjson_get_int(val);
@@ -311,6 +327,16 @@ static const char* printJsonConfig(FFdata* data, bool prepare) {
if (!unsafe_yyjson_is_bool(previousSucceeded)) {
return "Property 'succeeded' in 'condition' must be a boolean";
}
if (prepare) {
// Whether this module is printed at all depends on the result of the previous
// one, which is only known in the print pass. Preparing it here would leave a
// result behind that no print pass ever consumes, and since the modules are
// paired by position (see `ffPrepareCommand` / `ffDetectCommand`), that
// would silently shift every later module onto the wrong result.
// Skip it, and let the module report the missing preparation if it turns out
// to be printed after all.
continue;
}
if (succeeded != unsafe_yyjson_get_bool(previousSucceeded)) {
continue;
}
+4 -4
View File
@@ -3,7 +3,7 @@
#include "common/textModifier.h"
#include "logo/logo.h"
void ffPrintLogoAndKey(const char* moduleName, uint8_t moduleIndex, const FFModuleArgs* moduleArgs, FFPrintType printType) {
void ffPrintLogoAndKey(const char* moduleName, uint32_t moduleIndex, const FFModuleArgs* moduleArgs, FFPrintType printType) {
ffLogoPrintLine();
// This is used by --set-keyless, in this case we want neither the module name nor the separator
@@ -40,7 +40,7 @@ void ffPrintLogoAndKey(const char* moduleName, uint8_t moduleIndex, const FFModu
fputs(moduleName, stdout);
if (moduleIndex > 0) {
printf(" %hhu", moduleIndex);
printf(" %u", moduleIndex);
}
} else {
FF_STRBUF_AUTO_DESTROY key = ffStrbufCreate();
@@ -82,7 +82,7 @@ void ffPrintLogoAndKey(const char* moduleName, uint8_t moduleIndex, const FFModu
}
}
bool ffPrintFormat(const char* moduleName, uint8_t moduleIndex, const FFModuleArgs* moduleArgs, FFPrintType printType, uint32_t numArgs, const FFformatarg* arguments) {
bool ffPrintFormat(const char* moduleName, uint32_t moduleIndex, const FFModuleArgs* moduleArgs, FFPrintType printType, uint32_t numArgs, const FFformatarg* arguments) {
FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
bool success;
if (__builtin_expect(moduleArgs != nullptr, 1)) {
@@ -102,7 +102,7 @@ bool ffPrintFormat(const char* moduleName, uint8_t moduleIndex, const FFModuleAr
return success;
}
void ffPrintError(const char* moduleName, uint8_t moduleIndex, const FFModuleArgs* moduleArgs, FFPrintType printType, const char* message, ...) {
void ffPrintError(const char* moduleName, uint32_t moduleIndex, const FFModuleArgs* moduleArgs, FFPrintType printType, const char* message, ...) {
if (!instance.config.display.showErrors) {
return;
}
+7
View File
@@ -286,6 +286,13 @@ static inline void wrapClosedir(HANDLE* pdir) {
}
FFNativeFD ffGetNullFD(void);
// Whether a descriptor is attached to a real terminal, i.e. whether a human is looking at it.
//
// Do not use `isatty()` for this on Windows. It is `_isatty()`, which answers "is this a character
// device" rather than "is this a console", and `NUL` -- as well as MSYS2's `/dev/null` -- is a
// character device. A redirected run therefore looks interactive, which is how
// `fastfetch --gen-config <path> > /dev/null` ends up on the interactive path and blocks forever.
bool ffIsTerminal(int fd);
// Returns whether the file was removed; callers that only want it gone discard that, so not `nodiscard`
[[gnu::nonnull(1)]] bool ffRemoveFile(const char* fileName);
// Modification time of a file, in milliseconds since the Unix epoch, or 0 if it can not be read.
+6
View File
@@ -8,6 +8,12 @@
// `val` may be null (yyjson predicates tolerate it); `result` and `pairs` may not.
// Returns an error string, null on success, so it is `nodiscard`.
[[gnu::nonnull(2, 3), nodiscard]] const char* ffJsonConfigParseEnum(yyjson_val* val, int* result, FFKeyValuePair pairs[]);
// Reads an integer in `[0, max]` into `result`. Returns false for anything else.
//
// `yyjson_get_uint()` accepts the signed number subtype as well and reinterprets its bits, so the
// bare `(uint32_t) yyjson_get_uint(val)` it replaces turned `-1` into `UINT32_MAX` — 4.3 billion
// characters for `separator.times`, a ~50 day sleep for `waitTime`.
[[gnu::nonnull(1, 2), nodiscard]] bool ffJsonConfigParseUInt32(yyjson_val* val, uint32_t* result, uint32_t max);
// The three helpers below are routinely called as statements, to add a key to a document, so their
// result is not `nodiscard`.
+3 -3
View File
@@ -14,14 +14,14 @@ typedef enum FFPrintType: uint8_t {
// Both `moduleName` and `moduleArgs` are optional and explicitly handled: a null `moduleName` is what
// `--set-keyless` relies on, and a null `moduleArgs` (used by ffPrintError callers) skips the custom
// key entirely. So neither takes `nonnull`.
void ffPrintLogoAndKey(const char* moduleName, uint8_t moduleIndex, const FFModuleArgs* moduleArgs, FFPrintType printType);
void ffPrintLogoAndKey(const char* moduleName, uint32_t moduleIndex, const FFModuleArgs* moduleArgs, FFPrintType printType);
// Same for `moduleArgs`; `numArgs` / `arguments` are only read when `moduleArgs` is non-null, so
// `arguments` is not `nonnull(6)` either. At least one caller discards the result, so not `nodiscard`.
bool ffPrintFormat(const char* moduleName, uint8_t moduleIndex, const FFModuleArgs* moduleArgs, FFPrintType printType, uint32_t numArgs, const FFformatarg* arguments);
bool ffPrintFormat(const char* moduleName, uint32_t moduleIndex, const FFModuleArgs* moduleArgs, FFPrintType printType, uint32_t numArgs, const FFformatarg* arguments);
#define FF_PRINT_FORMAT_CHECKED(moduleName, moduleIndex, moduleArgs, printType, arguments) \
ffPrintFormat((moduleName), (moduleIndex), (moduleArgs), (printType), (sizeof(arguments) / sizeof(*arguments)), (arguments));
// `moduleName` / `moduleArgs` are forwarded to ffPrintLogoAndKey, which accepts null for both;
// only `message` is dereferenced here (by `vprintf`).
[[gnu::format(printf, 5, 6), gnu::nonnull(5)]] void ffPrintError(const char* moduleName, uint8_t moduleIndex, const FFModuleArgs* moduleArgs, FFPrintType printType, const char* message, ...);
[[gnu::format(printf, 5, 6), gnu::nonnull(5)]] void ffPrintError(const char* moduleName, uint32_t moduleIndex, const FFModuleArgs* moduleArgs, FFPrintType printType, const char* message, ...);
[[gnu::nonnull(1)]] void ffPrintColor(const FFstrbuf* colorValue);
void ffPrintCharTimes(char c, uint32_t times);
+1 -1
View File
@@ -77,9 +77,9 @@ static const char* detectAllocation(FFBtrfsResult* item, int dfd, FFstrbuf* buff
}
if (ffReadFileBufferRelative(subfd, "global_rsv_reserved", buffer)) {
// global_rsv_reserved is already the used part of the reservation, global_rsv_size its total
item->globalReservationUsed = ffStrbufToUInt(buffer, 0);
}
item->globalReservationUsed = item->globalReservationTotal - item->globalReservationUsed;
#define FF_BTRFS_DETECT_PROFILE(_index, _type, _profile, _copies) \
else if (faccessat(subfd, _type "/" _profile "/", F_OK, 0) == 0) { \
+1
View File
@@ -37,6 +37,7 @@ const char* ffDetectCamera(FFlist* result)
ffStrbufInitS(&camera->name, device.localizedName.UTF8String);
ffStrbufInitS(&camera->vendor, device.manufacturer.UTF8String);
ffStrbufInitS(&camera->id, device.uniqueID.UTF8String);
ffStrbufInit(&camera->colorspace);
switch (device.activeColorSpace)
{
case AVCaptureColorSpace_sRGB: ffStrbufInitStatic(&camera->colorspace, "sRGB"); break;
+1 -1
View File
@@ -1,5 +1,5 @@
#include "camera.h"
const char* ffDetectCamera([[maybe_unused]] FFlist* result) {
return "Not support on this platform";
return "Not supported on this platform";
}
+7 -1
View File
@@ -42,7 +42,13 @@ const char* ffDetectCommand(FFCommandOptions* options, FFstrbuf* result) {
if (!options->parallel) {
bundle.error = spawnProcess(options, &bundle.handle);
} else if (!FF_LIST_SHIFT(commandQueue, &bundle)) {
return "[BUG] command queue is empty";
// The module was printed without having been prepared first, so there is no process to collect.
// That is the expected outcome for a module gated on `condition.succeeded`: whether it is
// printed at all is only known in the print pass, so the prepare pass skips it (see
// `printJsonConfig`). In every other case the queue and the printed modules must line up.
return "Module was not prepared, so it cannot run in parallel. Modules gated on "
"`condition.succeeded` are never prepared, which is the expected cause; "
"anything else is a bug in fastfetch, please report it";
}
if (bundle.error) {
-5
View File
@@ -1,5 +0,0 @@
#include "cpu.h"
const char* ffDetectCPUImpl([[maybe_unused]] const FFCPUOptions* options, [[maybe_unused]] FFCPUResult* cpu) {
return "Not supported on this platform";
}
@@ -1,6 +0,0 @@
#include "fastfetch.h"
#include "detection/cpuusage/cpuusage.h"
const char* ffGetCpuUsageInfo([[maybe_unused]] FFlist* cpuTimes) {
return "Not support on this platform";
}
+4 -1
View File
@@ -248,6 +248,9 @@ const char* ffDetectDEVersion(const FFstrbuf* deName, FFstrbuf* result, FFDEOpti
getKDE(result, options);
} else if (ffStrbufEqualS(deName, FF_DE_PRETTY_GNOME)) {
getGnome(result, options);
} else if (ffStrbufEqualS(deName, FF_DE_PRETTY_GNOME_CLASSIC)) {
// GNOME Classic is GNOME Shell in classic mode, so gnome-shell reports the same version
getGnome(result, options);
} else if (ffStrbufEqualS(deName, FF_DE_PRETTY_CINNAMON)) {
getCinnamon(result, options);
} else if (ffStrbufEqualS(deName, FF_DE_PRETTY_XFCE4)) {
@@ -260,7 +263,7 @@ const char* ffDetectDEVersion(const FFstrbuf* deName, FFstrbuf* result, FFDEOpti
getBudgie(result, options);
} else if (ffStrbufEqualS(deName, FF_DE_PRETTY_UNITY)) {
getUnity(result, options);
} else if (ffStrbufEqualS(deName, "trinity")) {
} else if (ffStrbufIgnCaseEqualS(deName, "trinity") || ffStrbufIgnCaseEqualS(deName, "tde")) {
getTrinity(result, options);
} else if (ffStrbufEqualS(deName, FF_DE_PRETTY_COSMIC)) {
getCosmic(result, options);
-5
View File
@@ -1,5 +0,0 @@
#include "disk.h"
const char* ffDetectDisksImpl([[maybe_unused]] FFDiskOptions* options, [[maybe_unused]] FFlist* disks) {
return "Not supported on this platform";
}
+5 -1
View File
@@ -46,13 +46,17 @@ const char* ffDetectDNS(FFDNSOptions* options, FFlist* results) {
}
for (IP_ADAPTER_DNS_SERVER_ADDRESS_XP* ifa = adapter->FirstDnsServerAddress; ifa; ifa = ifa->Next) {
FFstrbuf* item = FF_LIST_ADD(FFstrbuf, *results);
// The element must only be added once it can be filled: `FF_LIST_ADD` does not zero the
// new element, so an unrecognized address family used to leave an uninitialized FFstrbuf
// in the list, which the callers then read and free
if (ifa->Address.lpSockaddr->sa_family == AF_INET) {
SOCKADDR_IN* ipv4 = (SOCKADDR_IN*) ifa->Address.lpSockaddr;
FFstrbuf* item = FF_LIST_ADD(FFstrbuf, *results);
ffStrbufInitA(item, INET_ADDRSTRLEN);
item->length = (uint32_t) (RtlIpv4AddressToStringA(&ipv4->sin_addr, item->chars) - item->chars);
} else if (ifa->Address.lpSockaddr->sa_family == AF_INET6) {
SOCKADDR_IN6* ipv6 = (SOCKADDR_IN6*) ifa->Address.lpSockaddr;
FFstrbuf* item = FF_LIST_ADD(FFstrbuf, *results);
ffStrbufInitA(item, INET6_ADDRSTRLEN);
item->length = (uint32_t) (RtlIpv6AddressToStringA(&ipv6->sin6_addr, item->chars) - item->chars);
}
+36 -12
View File
@@ -32,17 +32,33 @@ static bool extractNanoVersionFromBinary(const char* str, [[maybe_unused]] uint3
return false;
}
// The message is printed to the user verbatim, so it has to name the variable that was wrong:
// `"Executable not found"` from `ffFindExecutableInPath()` doesn't say what was searched for.
static const char* notAnExecutable(bool isVisual) {
return isVisual ? "$VISUAL does not point to an executable" : "$EDITOR does not point to an executable";
}
const char* ffDetectEditor(FFEditorResult* result) {
bool isVisual = true;
ffStrbufSetS(&result->name, getenv("VISUAL"));
if (result->name.length) {
result->type = "Visual";
} else {
if (!result->name.length) {
isVisual = false;
ffStrbufSetS(&result->name, getenv("EDITOR"));
if (result->name.length) {
result->type = "Editor";
} else {
return "$VISUAL or $EDITOR not set";
}
}
if (!result->name.length) {
return "$VISUAL or $EDITOR not set";
}
result->type = isVisual ? "Visual" : "Editor";
// `$VISUAL`/`$EDITOR` hold a shell command line rather than a bare name, so they routinely
// carry arguments: `EDITOR="code -w"`, `EDITOR="vim -f"`. Only the first word names the
// program. The whole string used to be handed to `ffFindExecutableInPath()`, which could never
// match it, and the module then printed the unparsed value as if it had been resolved.
ffStrbufSubstrBefore(&result->name, (uint32_t) strcspn(result->name.chars, " \t\n\v\f\r"));
if (result->name.length == 0) {
return notAnExecutable(isVisual);
}
if (ffIsAbsolutePath(result->name.chars)) {
@@ -50,14 +66,19 @@ const char* ffDetectEditor(FFEditorResult* result) {
} else {
const char* error = ffFindExecutableInPath(result->name.chars, &result->path);
if (error) {
return nullptr;
// Reporting success here is what made `EDITOR="code -w"` print the raw value: the module
// reads `nullptr` as "detection succeeded" and then falls back to printing `name`.
return notAnExecutable(isVisual);
}
}
{
char buf[PATH_MAX + 1];
if (!realpath(result->path.chars, buf)) {
return nullptr;
// An absolute `$VISUAL`/`$EDITOR` is copied into `path` before it is verified, so a path
// that does not exist has to be reported here instead of being handed to the caller as
// if it had been checked.
return notAnExecutable(isVisual);
}
// WIN32: Should we handle scoop shim exe here?
@@ -77,11 +98,13 @@ const char* ffDetectEditor(FFEditorResult* result) {
#endif
);
if (index == result->path.length) {
return nullptr;
// `realpath()` always yields an absolute path, so neither of these two is reachable in
// practice. They used to `return nullptr`, which the module read as a success.
return "Failed to determine the executable name";
}
ffStrbufSetS(&result->exe, &result->path.chars[index + 1]);
if (!result->exe.length) {
return nullptr;
return "Failed to determine the executable name";
}
#ifdef _WIN32
@@ -92,6 +115,7 @@ const char* ffDetectEditor(FFEditorResult* result) {
}
if (!instance.config.general.detectVersion) {
// Not an error: the editor was found, version probing was just turned off.
return nullptr;
}
-7
View File
@@ -1,7 +0,0 @@
#include "fastfetch.h"
#include "font.h"
const char* ffDetectFontImpl([[maybe_unused]] FFFontResult* result) {
FF_UNUSED(result);
return "Not supported on this platform";
}
+1 -1
View File
@@ -124,7 +124,7 @@ const char* detectByOpenGL(FFlist* gpus) {
gpu->coreCount = FF_GPU_CORE_COUNT_UNSET;
gpu->frequency = FF_GPU_FREQUENCY_UNSET;
gpu->coreUsage = FF_GPU_CORE_USAGE_UNSET;
gpu->dedicated = gpu->shared = (FFGPUMemory) { 0, 0 };
gpu->dedicated.total = gpu->dedicated.used = gpu->shared.total = gpu->shared.used = FF_GPU_VMEM_SIZE_UNSET;
gpu->deviceId = 0;
gpu->pcieSpeed = FF_GPU_PCIE_SPEED_UNSET;
-6
View File
@@ -1,6 +0,0 @@
#include "gpu.h"
const char* ffDetectGPUImpl(const FFGPUOptions* options, FFlist* gpus) {
FF_UNUSED(options, gpus);
return "Not supported on this platform";
}
@@ -1,5 +0,0 @@
#include "initsystem.h"
const char* ffDetectInitSystem([[maybe_unused]] FFInitSystemResult* result) {
return "Not supported on this platform";
}
+1 -1
View File
@@ -1,5 +1,5 @@
#include "keyboard.h"
const char* ffDetectKeyboard([[maybe_unused]] FFlist* devices /* List of FFKeyboardDevice */) {
return "No mouse support on this platform";
return "No keyboard support on this platform";
}
+2 -3
View File
@@ -18,16 +18,15 @@ const char* ffDetectLoadavg(double result[3]) {
}
#endif
#ifndef __GNU__
// getloadavg requires higher ANDROID_API version
struct sysinfo si;
if (sysinfo(&si) < 0) {
return "sysinfo() failed";
return "both /proc/loadavg and sysinfo() failed";
}
for (int i = 0; i < 3; i++) {
result[i] = (double) si.loads[i] / (1 << SI_LOAD_SHIFT);
}
#endif
return nullptr;
}
+11 -4
View File
@@ -6,10 +6,11 @@ void ffDetectMediaImpl(FFMediaResult* media, bool saveCover);
static FFMediaResult result;
// The cover file of this run, if there is one. It is tracked here rather than in `result` because
// the cached result is dropped between `--dynamic-interval` rounds, while the file has to stay on
// disk for the whole run: the `kitty-direct` logo type lets the terminal open it by path, and the
// logo is replayed from the line cache on every round.
// The cover file of this run, if there is one. It is tracked here rather than in `result` because the
// cached result is dropped between `--dynamic-interval` rounds, and `result.cover` — the only other
// copy of the path — is destroyed with it. The file itself has to outlive that, since the
// `kitty-direct` logo type hands its path to the terminal, so it is kept for the whole run and
// removed at exit.
static FFstrbuf coverFile;
static bool coverFileRegistered = false;
@@ -71,6 +72,12 @@ const FFMediaResult* ffDetectMedia(bool saveCover) {
ffStrbufTrimRightSpace(&result.player);
if (saveCover && result.removeCoverAfterUse) {
// A new round can produce a new cover while the previous file is still on disk, and
// `coverFile` is about to forget its path. Remove it here, otherwise only the last one
// would ever be cleaned up by `atexit`.
if (coverFile.length > 0 && !ffStrbufEqual(&coverFile, &result.cover)) {
ffRemoveFile(coverFile.chars);
}
ffStrbufSet(&coverFile, &result.cover);
if (!coverFileRegistered) {
coverFileRegistered = true;
+27 -11
View File
@@ -111,7 +111,14 @@ static const char* getMediaByMediaRemote(FFMediaResult* result, bool saveCover)
}
#if !FF_MODULE_DISABLE_MEDIA
[[gnu::visibility("default"), gnu::used]] int ffPrintMediaByMediaRemote(int saveCover) {
// Called from the Perl helper process through `DynaLoader::dl_install_xsub`. That mechanism installs a
// plain C function as an XSUB, so Perl calls it with the `CV*` in the first parameter slot and no Perl
// argument ever reaches a parameter — a `bool saveCover` parameter read as a pointer and was always
// truthy. The flag has to travel in the environment instead; see `getMediaByAuthorizedProcess`.
[[gnu::visibility("default"), gnu::used]] int ffPrintMediaByMediaRemote(void) {
const char* saveCoverEnv = getenv("FF_MEDIA_REMOTE_SAVE_COVER");
bool saveCover = saveCoverEnv && saveCoverEnv[0] == '1';
FFMediaResult media = {
.status = ffStrbufCreate(),
.song = ffStrbufCreate(),
@@ -121,7 +128,7 @@ static const char* getMediaByMediaRemote(FFMediaResult* result, bool saveCover)
.player = ffStrbufCreate(),
.cover = ffStrbufCreate(),
};
if (getMediaByMediaRemote(&media, !!saveCover) != nullptr) {
if (getMediaByMediaRemote(&media, saveCover) != nullptr) {
return 1;
}
ffStrbufAppendC(&media.status, '\n');
@@ -156,13 +163,17 @@ static const char* getMediaByMediaRemote(FFMediaResult* result, bool saveCover)
static const char* getMediaByAuthorizedProcess(FFMediaResult* result, bool saveCover) {
// #1737
FF_STRBUF_AUTO_DESTROY script = ffStrbufCreateF("require DynaLoader;\
// `DynaLoader::dl_install_xsub` hands the C function the `CV*` as its first parameter, so the flag
// cannot be passed as a Perl argument — `ffPrintMediaByMediaRemote` reads it from the environment,
// which the child process inherits.
FF_STRBUF_AUTO_DESTROY script = ffStrbufCreateF("$ENV{'FF_MEDIA_REMOTE_SAVE_COVER'}='%c';\
require DynaLoader;\
my $so = DynaLoader::dl_load_file('%s', 0);\
my $sym = DynaLoader::dl_find_symbol($so, 'ffPrintMediaByMediaRemote');\
DynaLoader::dl_install_xsub('fn', $sym, __FILE__);\
exit(fn(%c))",
instance.state.platform.exePath.chars,
saveCover ? '1' : '0');
exit(fn())",
saveCover ? '1' : '0',
instance.state.platform.exePath.chars);
FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
const char* error = ffProcessAppendStdOut(
@@ -201,12 +212,17 @@ void ffDetectMediaImpl(FFMediaResult* media, bool saveCover) {
}
if (error) {
ffStrbufAppendS(&media->error, error);
} else if (media->player.length == 0 && media->playerId.length > 0) {
ffStrbufSet(&media->player, &media->playerId);
if (ffStrbufStartsWithIgnCaseS(&media->player, "com.")) {
ffStrbufSubstrAfter(&media->player, strlen("com.") - 1);
} else {
if (media->player.length == 0 && media->playerId.length > 0) {
ffStrbufSet(&media->player, &media->playerId);
if (ffStrbufStartsWithIgnCaseS(&media->player, "com.")) {
ffStrbufSubstrAfter(&media->player, strlen("com.") - 1);
}
ffStrbufReplaceAllC(&media->player, '.', ' ');
}
ffStrbufReplaceAllC(&media->player, '.', ' ');
// The cover is a temporary file that this process created, so it is ours to delete. This used
// to be flagged only in the branch above, which meant the file was left behind on every run
// whose player name was already known — that is, almost all of them.
if (media->cover.length > 0) {
media->removeCoverAfterUse = true;
}
-5
View File
@@ -1,5 +0,0 @@
#include "media.h"
void ffDetectMediaImpl(FFMediaResult* media) {
ffStrbufAppendS(&media->error, "Not supported on this platform");
}
-5
View File
@@ -1,5 +0,0 @@
#include "memory.h"
const char* ffDetectMemory([[maybe_unused]] FFMemoryResult* ram) {
return "Not supported on this platform";
}
@@ -1,4 +0,0 @@
#include "packages.h"
void ffDetectPackagesImpl([[maybe_unused]] FFPackagesResult* result, [[maybe_unused]] FFPackagesOptions* options) {
}
@@ -1,5 +0,0 @@
#include "processes.h"
const char* ffDetectProcesses([[maybe_unused]] const FFProcessesOptions* options, [[maybe_unused]] FFProcessesResult* result) {
return "Not supported on this platform";
}
-5
View File
@@ -1,5 +0,0 @@
#include "swap.h"
const char* ffDetectSwap(FFSwapResult* swap) {
return "Not supported on this platform";
}
-6
View File
@@ -1,6 +0,0 @@
#include "fastfetch.h"
#include "users.h"
const char* ffDetectUsers(FFUsersOptions* options, FFlist* users) {
return "Not supported on this platform";
}
+6 -1
View File
@@ -250,7 +250,12 @@ static const char* detectVulkan(FFVulkanResult* result) {
ffStrbufInitS(&gpu->name, physicalDeviceProperties.properties.deviceName);
gpu->type = physicalDeviceProperties.properties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU ? FF_GPU_TYPE_DISCRETE : FF_GPU_TYPE_INTEGRATED;
switch (physicalDeviceProperties.properties.deviceType) {
case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU: gpu->type = FF_GPU_TYPE_DISCRETE; break;
case VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU:
case VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU: gpu->type = FF_GPU_TYPE_INTEGRATED; break; // Virtual machine?
default: gpu->type = FF_GPU_TYPE_UNKNOWN; break;
}
ffStrbufInitS(&gpu->vendor, ffGPUGetVendorString(physicalDeviceProperties.properties.vendorID));
ffStrbufInitS(&gpu->driver, driverProperties.driverInfo);
ffStrbufInit(&gpu->memoryType);
@@ -1,5 +0,0 @@
#include "wallpaper.h"
const char* ffDetectWallpaper([[maybe_unused]] FFstrbuf* result) {
return "Not supported on this platform";
}
+1 -1
View File
@@ -1,5 +1,5 @@
#include "wifi.h"
const char* ffDetectWifi([[maybe_unused]] FFlist* result) {
return "Not support on this platform";
return "Not supported on this platform";
}
+4 -3
View File
@@ -252,9 +252,10 @@ const char* ffDetectWifi(FFlist* result) {
connInfo->wlanAssociationAttributes.dot11BssType,
connInfo->wlanSecurityAttributes.bSecurityEnabled,
nullptr,
&bssList) == ERROR_SUCCESS &&
bssList->dwNumberOfItems > 0) {
item->conn.frequency = (uint16_t) (bssList->wlanBssEntries[0].ulChCenterFrequency / 1000);
&bssList) == ERROR_SUCCESS) {
if (bssList->dwNumberOfItems > 0) {
item->conn.frequency = (uint16_t) (bssList->wlanBssEntries[0].ulChCenterFrequency / 1000);
}
ffWlanFreeMemory(bssList);
}
}
@@ -1,7 +0,0 @@
#include "fastfetch.h"
#include "wmtheme.h"
bool ffDetectWmTheme(FFstrbuf* themeOrError) {
ffStrbufAppendS(themeOrError, "Not supported on this platform");
return false;
}
+1 -1
View File
@@ -602,7 +602,7 @@ static void enableJsonOutput(FFdata* data) {
}
static void genConfigCommon(FFdata* data, const char* value) {
if (!getenv("NO_COLOR") && isatty(STDOUT_FILENO) && isatty(STDIN_FILENO)
if (!getenv("NO_COLOR") && ffIsTerminal(STDOUT_FILENO) && ffIsTerminal(STDIN_FILENO)
#ifdef _WIN32
&& ffIsWindows10OrGreater()
#endif
+1 -1
View File
@@ -345,5 +345,5 @@ FFModuleBaseInfo ffBatteryModuleInfo = {
{ "Battery time remaining seconds", "time-seconds" },
{ "Battery time remaining (formatted)", "time-formatted" },
})),
.defaultOrder = 44,
.defaultOrder = 45,
};
+2 -2
View File
@@ -71,7 +71,7 @@ bool ffPrintBluetooth(FFBluetoothOptions* options) {
uint8_t i = 1;
FF_LIST_FOR_EACH (FFBluetoothResult, device, devices) {
printDevice(options, device, devices.length == 0 ? 0 : i);
printDevice(options, device, devices.length == 1 ? 0 : i);
++i;
}
@@ -189,5 +189,5 @@ FFModuleBaseInfo ffBluetoothModuleInfo = {
{ "Is connected", "connected" },
{ "Battery percentage bar", "battery-percentage-bar" },
})),
.defaultOrder = 58,
.defaultOrder = 59,
};
+13 -1
View File
@@ -71,6 +71,18 @@ static void printDevice(FFBluetoothRadioOptions* options, const FFBluetoothRadio
case 13:
version = "5.4";
break;
case 14:
version = "6.0";
break;
case 15:
version = "6.1";
break;
case 16:
version = "6.2";
break;
case 17:
version = "6.3";
break;
}
if (options->moduleArgs.outputFormat.length == 0) {
@@ -229,5 +241,5 @@ FFModuleBaseInfo ffBluetoothRadioModuleInfo = {
{ "Discoverable", "discoverable" },
{ "Connectable / Pairable", "connectable" },
})),
.defaultOrder = 59,
.defaultOrder = 60,
};
+1 -1
View File
@@ -55,5 +55,5 @@ FFModuleBaseInfo ffBreakModuleInfo = {
.destroyOptions = (void*) ffDestroyBreakOptions,
.parseJsonObject = (void*) ffParseBreakJsonObject,
.printModule = (void*) ffPrintBreak,
.defaultOrder = 71,
.defaultOrder = 72,
};
+4 -1
View File
@@ -168,6 +168,9 @@ bool ffGenerateBtrfsJsonResult([[maybe_unused]] FFBtrfsOptions* options, yyjson_
yyjson_mut_obj_add_uint(doc, obj, "nodeSize", btrfs->nodeSize);
yyjson_mut_obj_add_uint(doc, obj, "sectorSize", btrfs->sectorSize);
yyjson_mut_obj_add_uint(doc, obj, "totalSize", btrfs->totalSize);
yyjson_mut_val* globalReservation = yyjson_mut_obj_add_obj(doc, obj, "globalReservation");
yyjson_mut_obj_add_uint(doc, globalReservation, "used", btrfs->globalReservationUsed);
yyjson_mut_obj_add_uint(doc, globalReservation, "total", btrfs->globalReservationTotal);
yyjson_mut_val* allocation = yyjson_mut_obj_add_arr(doc, obj, "allocation");
for (uint32_t i = 0; i < ARRAY_SIZE(btrfs->allocation); ++i) {
yyjson_mut_val* item = yyjson_mut_arr_add_obj(doc, allocation);
@@ -244,5 +247,5 @@ FFModuleBaseInfo ffBtrfsModuleInfo = {
{ "Node size", "node-size" },
{ "Sector size", "sector-size" },
})),
.defaultOrder = 42,
.defaultOrder = 43,
};
+1 -1
View File
@@ -153,5 +153,5 @@ FFModuleBaseInfo ffCameraModuleInfo = {
{ "Width (in px)", "width" },
{ "Height (in px)", "height" },
})),
.defaultOrder = 61,
.defaultOrder = 62,
};
+1 -1
View File
@@ -288,5 +288,5 @@ FFModuleBaseInfo ffCodecModuleInfo = {
{ "Compatibility alias of codec types", "types" },
{ "Platform API used for detection", "platform-api" },
})),
.defaultOrder = 37,
.defaultOrder = 38,
};
+20 -10
View File
@@ -172,7 +172,9 @@ void ffParseColorsJsonObject(FFColorsOptions* options, yyjson_val* module) {
}
if (unsafe_yyjson_equals_str(key, "paddingLeft")) {
options->paddingLeft = (uint32_t) yyjson_get_uint(val);
if (!ffJsonConfigParseUInt32(val, &options->paddingLeft, UINT32_MAX)) {
ffPrintError(FF_MODULE_GET_DISPLAY_NAME(Colors), 0, nullptr, FF_PRINT_TYPE_NO_CUSTOM_KEY, "Property 'paddingLeft' must be a non-negative integer no greater than 4294967295");
}
continue;
}
@@ -182,7 +184,13 @@ void ffParseColorsJsonObject(FFColorsOptions* options, yyjson_val* module) {
} else {
yyjson_val* width = yyjson_obj_get(val, "width");
if (width) {
options->block.width = (uint8_t) yyjson_get_uint(width);
uint32_t value;
// A width of 0 or more than 9 is not useful: it makes the block either invisible or unreadably long
if (!ffJsonConfigParseUInt32(width, &value, 9) || value == 0) {
ffPrintError(FF_MODULE_GET_DISPLAY_NAME(Colors), 0, nullptr, FF_PRINT_TYPE_NO_CUSTOM_KEY, "Property 'block.width' must be an integer between 1 and 9");
} else {
options->block.width = (uint8_t) value;
}
}
yyjson_val* range = yyjson_obj_get(val, "range");
@@ -190,15 +198,17 @@ void ffParseColorsJsonObject(FFColorsOptions* options, yyjson_val* module) {
if (!yyjson_is_arr(range) || yyjson_arr_size(range) != 2) {
ffPrintError(FF_MODULE_GET_DISPLAY_NAME(Colors), 0, nullptr, FF_PRINT_TYPE_NO_CUSTOM_KEY, "Invalid %s.range value: must be an array of 2 elements", unsafe_yyjson_get_str(key));
} else {
uint8_t start = (uint8_t) yyjson_get_uint(yyjson_arr_get(range, 0));
uint8_t end = (uint8_t) yyjson_get_uint(yyjson_arr_get(range, 1));
if (start > end) {
uint32_t start, end;
// Both elements must be integers in [0, 15]. `yyjson_get_uint()` used to read
// anything else as 0, so `[1.5, 3]` silently printed the same as `[0, 3]`
if (!ffJsonConfigParseUInt32(yyjson_arr_get(range, 0), &start, 15) ||
!ffJsonConfigParseUInt32(yyjson_arr_get(range, 1), &end, 15)) {
ffPrintError(FF_MODULE_GET_DISPLAY_NAME(Colors), 0, nullptr, FF_PRINT_TYPE_NO_CUSTOM_KEY, "Property '%s.range' must be an array of two integers between 0 and 15", unsafe_yyjson_get_str(key));
} else if (start > end) {
ffPrintError(FF_MODULE_GET_DISPLAY_NAME(Colors), 0, nullptr, FF_PRINT_TYPE_NO_CUSTOM_KEY, "Invalid %s.range value: range[0] > range[1]", unsafe_yyjson_get_str(key));
} else if (end > 15) {
ffPrintError(FF_MODULE_GET_DISPLAY_NAME(Colors), 0, nullptr, FF_PRINT_TYPE_NO_CUSTOM_KEY, "Invalid %s.range value: range[1] > 15", unsafe_yyjson_get_str(key));
} else {
options->block.range[0] = start;
options->block.range[1] = end;
options->block.range[0] = (uint8_t) start;
options->block.range[1] = (uint8_t) end;
}
}
}
@@ -324,5 +334,5 @@ FFModuleBaseInfo ffColorsModuleInfo = {
.parseJsonObject = (void*) ffParseColorsJsonObject,
.printModule = (void*) ffPrintColors,
.generateJsonConfig = (void*) ffGenerateColorsJsonConfig,
.defaultOrder = 72,
.defaultOrder = 73,
};
+29 -2
View File
@@ -3,6 +3,23 @@
#include "modules/command/command.h"
#include "detection/command/command.h"
#if defined(_WIN32)
// `ffStrbufGetline()` splits on `\n` only, so a CRLF line keeps its trailing `\r`. That is what every
// `cmd.exe` builtin and most native Windows tools produce, and the leftover carriage return makes the
// terminal jump back to the start of the line. The `\r` is overwritten with a NUL, so `line` stays a
// usable C string; the length without the `\r` is returned for callers that need to copy the line.
//
// `len` itself must stay the distance from the line start to the delimiter: `ffStrbufGetdelim()`
// advances with `*lineptr += *n`, so shortening it would make the next call start inside this line.
static size_t trimCarriageReturn(char* line, size_t len) {
if (len > 0 && line[len - 1] == '\r') {
line[len - 1] = '\0';
return len - 1;
}
return len;
}
#endif
bool ffPrintCommand(FFCommandOptions* options) {
FF_STRBUF_AUTO_DESTROY result = ffStrbufCreate();
const char* error = ffDetectCommand(options, &result);
@@ -18,10 +35,13 @@ bool ffPrintCommand(FFCommandOptions* options) {
}
if (options->splitLines) {
uint8_t index = 0;
uint32_t index = 0;
char* line = nullptr;
size_t len = 0;
while (ffStrbufGetline(&line, &len, &result)) {
#if defined(_WIN32)
trimCarriageReturn(line, len);
#endif
if (options->moduleArgs.outputFormat.length == 0) {
ffPrintLogoAndKey(FF_MODULE_GET_DISPLAY_NAME(Command), ++index, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
puts(line);
@@ -113,7 +133,14 @@ bool ffGenerateCommandJsonResult([[maybe_unused]] FFCommandOptions* options, yyj
char* line = nullptr;
size_t len = 0;
while (ffStrbufGetline(&line, &len, &result)) {
yyjson_mut_arr_add_strncpy(doc, jsonArray, line, len);
// `len` has to stay the distance to the delimiter (see `trimCarriageReturn`), so the
// possibly shorter length of the line itself is tracked separately.
#if defined(_WIN32)
size_t contentLen = trimCarriageReturn(line, len);
#else
size_t contentLen = len;
#endif
yyjson_mut_arr_add_strncpy(doc, jsonArray, line, contentLen);
}
} else {
yyjson_mut_obj_add_strbuf(doc, module, "result", &result);
+4 -1
View File
@@ -129,7 +129,10 @@ void ffParseCPUUsageJsonObject(FFCPUUsageOptions* options, yyjson_val* module) {
}
if (unsafe_yyjson_equals_str(key, "waitTime")) {
options->waitTime = (uint32_t) yyjson_get_uint(val);
if (!ffJsonConfigParseUInt32(val, &options->waitTime, UINT32_MAX)) {
ffPrintError(FF_MODULE_GET_DISPLAY_NAME(CPUUsage), 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Property 'waitTime' must be a non-negative integer no greater than 4294967295");
continue;
}
continue;
}
+20
View File
@@ -13,6 +13,25 @@ void ffGenerateCustomJsonConfig(FFCustomOptions* options, yyjson_mut_doc* doc, y
ffJsonConfigGenerateModuleArgsConfig(doc, module, &options->moduleArgs);
}
bool ffGenerateCustomJsonResult(FFCustomOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
FF_STRBUF_AUTO_DESTROY result = ffStrbufCreate();
// This module's output *is* its format string, so the JSON result is that string rendered —
// for a `qjs:`/`lua:` format that means the script's output, not the script. `ffPrintCustom()`
// gets the same value through `ffPrintFormat()`. Without this the module had no JSON result at
// all, so a script consuming `--format json` could not read a custom line's value.
if (!ffParseFormatString(&result, &options->moduleArgs.outputFormat, 0, (FFformatarg[]) {})) {
// `yyjson_mut_obj_add_str()` only copies strings that need escaping; for anything else it
// stores the pointer as-is. `result` is freed when this function returns, long before the
// document is serialised, so the error has to be copied into the document explicitly.
yyjson_mut_obj_add_strcpy(doc, module, "error", result.chars);
return false;
}
yyjson_mut_obj_add_strbuf(doc, module, "result", &result);
return true;
}
void ffParseCustomJsonObject(FFCustomOptions* options, yyjson_val* module) {
yyjson_val *key, *val;
size_t idx, max;
@@ -63,5 +82,6 @@ FFModuleBaseInfo ffCustomModuleInfo = {
.destroyOptions = (void*) ffDestroyCustomOptions,
.parseJsonObject = (void*) ffParseCustomJsonObject,
.printModule = (void*) ffPrintCustom,
.generateJsonResult = (void*) ffGenerateCustomJsonResult,
.generateJsonConfig = (void*) ffGenerateCustomJsonConfig,
};
+3 -3
View File
@@ -48,13 +48,13 @@ static void printDateTimeFormat(struct tm* tm, const FFModuleArgs* moduleArgs) {
result.week = (uint8_t) (tm->tm_yday / 7 + 1);
strftime(result.weekday, sizeof(result.weekday), "%A", tm);
strftime(result.weekdayShort, sizeof(result.weekdayShort), "%a", tm);
result.dayInYear = (uint8_t) (tm->tm_yday + 1);
result.dayInYear = (uint16_t) (tm->tm_yday + 1);
result.dayInMonth = (uint8_t) tm->tm_mday;
result.dayInWeek = tm->tm_wday == 0 ? 7 : (uint8_t) tm->tm_wday;
strftime(result.dayPretty, sizeof(result.dayPretty), "%d", tm);
result.hour = (uint8_t) tm->tm_hour;
strftime(result.hourPretty, sizeof(result.hourPretty), "%H", tm);
result.hour12 = (uint8_t) (result.hour % 12);
result.hour12 = (uint8_t) (result.hour % 12 ?: 12);
strftime(result.hour12Pretty, sizeof(result.hour12Pretty), "%I", tm);
result.minute = (uint8_t) tm->tm_min;
strftime(result.minutePretty, sizeof(result.minutePretty), "%M", tm);
@@ -206,5 +206,5 @@ FFModuleBaseInfo ffDateTimeModuleInfo = {
{ "Day in month with leading zero", "day-pretty" },
{ "AM or PM", "am-pm" },
})),
.defaultOrder = 52,
.defaultOrder = 53,
};
+1 -1
View File
@@ -510,5 +510,5 @@ FFModuleBaseInfo ffDiskModuleInfo = {
{ "Size free", "size-free" },
{ "Size available", "size-available" },
})),
.defaultOrder = 41,
.defaultOrder = 42,
};
+5 -2
View File
@@ -111,7 +111,10 @@ void ffParseDiskIOJsonObject(FFDiskIOOptions* options, yyjson_val* module) {
}
if (unsafe_yyjson_equals_str(key, "waitTime")) {
options->waitTime = (uint32_t) yyjson_get_uint(val);
if (!ffJsonConfigParseUInt32(val, &options->waitTime, UINT32_MAX)) {
ffPrintError(FF_MODULE_GET_DISPLAY_NAME(DiskIO), 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Property 'waitTime' must be a non-negative integer no greater than 4294967295");
continue;
}
if (options->waitTime == 0) {
options->waitTime = 1;
}
@@ -214,5 +217,5 @@ FFModuleBaseInfo ffDiskIOModuleInfo = {
{ "Number of reads", "read-count" },
{ "Number of writes", "write-count" },
})),
.defaultOrder = 67,
.defaultOrder = 68,
};
+39 -20
View File
@@ -7,31 +7,50 @@
#include <math.h>
static int sortByNameAsc(FFDisplayResult* a, FFDisplayResult* b) {
return ffStrbufComp(&a->name, &b->name);
static int sortByNameAsc(const void* a, const void* b) {
return ffStrbufComp(&((const FFDisplayResult*) a)->name, &((const FFDisplayResult*) b)->name);
}
static int sortByNameDesc(FFDisplayResult* a, FFDisplayResult* b) {
return -ffStrbufComp(&a->name, &b->name);
static int sortByNameDesc(const void* a, const void* b) {
return -ffStrbufComp(&((const FFDisplayResult*) a)->name, &((const FFDisplayResult*) b)->name);
}
bool ffPrintDisplay(FFDisplayOptions* options) {
const FFDisplayServerResult* dsResult = ffConnectDisplayServer();
if (dsResult->displays.length == 0) {
ffPrintError(FF_MODULE_GET_DISPLAY_NAME(Display), 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Couldn't detect display");
return false;
// `ffConnectDisplayServer()` hands out a process-wide result that other modules read too: `Monitor`
// prints the same list, and `Brightness` pairs its own readings with these entries by position. It
// must therefore not be reordered in place, which is what this module used to do through a cast
// that dropped `const`. Sort a copy instead, so both the text and the JSON path get the requested
// order without any other consumer seeing it.
//
// The copy is shallow: the entries keep owning their `FFstrbuf`s through the original result, and
// it is never destroyed element-wise, so only the array itself is freed.
static FFlist getOrderedDisplays(const FFDisplayOptions* options) {
const FFlist* displays = &ffConnectDisplayServer()->displays;
FFlist result = ffListCreateA(sizeof(FFDisplayResult), displays->length);
if (displays->length > 0) {
memcpy(result.data, displays->data, (size_t) displays->length * sizeof(FFDisplayResult));
result.length = displays->length;
}
if (options->order != FF_DISPLAY_ORDER_NONE) {
ffListSort((FFlist*) &dsResult->displays, sizeof(FFDisplayResult), (void*) (options->order == FF_DISPLAY_ORDER_ASC ? sortByNameAsc : sortByNameDesc));
ffListSort(&result, sizeof(FFDisplayResult), options->order == FF_DISPLAY_ORDER_ASC ? sortByNameAsc : sortByNameDesc);
}
return result;
}
bool ffPrintDisplay(FFDisplayOptions* options) {
FF_LIST_AUTO_DESTROY displays = getOrderedDisplays(options);
if (displays.length == 0) {
ffPrintError(FF_MODULE_GET_DISPLAY_NAME(Display), 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Couldn't detect display");
return false;
}
if (options->compactType != FF_DISPLAY_COMPACT_TYPE_NONE) {
ffPrintLogoAndKey(FF_MODULE_GET_DISPLAY_NAME(Display), 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
FF_LIST_FOR_EACH (FFDisplayResult, result, dsResult->displays) {
FF_LIST_FOR_EACH (FFDisplayResult, result, displays) {
if (options->compactType & FF_DISPLAY_COMPACT_TYPE_ORIGINAL_BIT) {
ffStrbufAppendF(&buffer, "%ix%i", result->width, result->height);
} else {
@@ -42,7 +61,7 @@ bool ffPrintDisplay(FFDisplayOptions* options) {
if (options->compactType & FF_DISPLAY_COMPACT_TYPE_REFRESH_RATE_BIT) {
if (result->refreshRate > 0) {
const char* space = instance.config.display.freqSpaceBeforeUnit == FF_SPACE_BEFORE_UNIT_ALWAYS ? " " : "";
const char* space = instance.config.display.freqSpaceBeforeUnit != FF_SPACE_BEFORE_UNIT_NEVER ? " " : "";
if (options->preciseRefreshRate) {
ffStrbufAppendF(&buffer, " @ %g%sHz", result->refreshRate, space);
} else {
@@ -62,9 +81,9 @@ bool ffPrintDisplay(FFDisplayOptions* options) {
FF_STRBUF_AUTO_DESTROY key = ffStrbufCreate();
for (uint32_t i = 0; i < dsResult->displays.length; i++) {
FFDisplayResult* result = FF_LIST_GET(FFDisplayResult, dsResult->displays, i);
uint32_t moduleIndex = dsResult->displays.length == 1 ? 0 : i + 1;
for (uint32_t i = 0; i < displays.length; i++) {
FFDisplayResult* result = FF_LIST_GET(FFDisplayResult, displays, i);
uint32_t moduleIndex = displays.length == 1 ? 0 : i + 1;
const char* displayType = result->type == FF_DISPLAY_TYPE_UNKNOWN ? nullptr : result->type == FF_DISPLAY_TYPE_BUILTIN ? "Built-in"
: "External";
@@ -295,16 +314,16 @@ void ffGenerateDisplayJsonConfig(FFDisplayOptions* options, yyjson_mut_doc* doc,
}
}
bool ffGenerateDisplayJsonResult([[maybe_unused]] FFDisplayOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
const FFDisplayServerResult* dsResult = ffConnectDisplayServer();
bool ffGenerateDisplayJsonResult(FFDisplayOptions* options, yyjson_mut_doc* doc, yyjson_mut_val* module) {
FF_LIST_AUTO_DESTROY displays = getOrderedDisplays(options);
if (dsResult->displays.length == 0) {
if (displays.length == 0) {
yyjson_mut_obj_add_str(doc, module, "error", "Couldn't detect display");
return false;
}
yyjson_mut_val* arr = yyjson_mut_obj_add_arr(doc, module, "result");
FF_LIST_FOR_EACH (FFDisplayResult, item, dsResult->displays) {
FF_LIST_FOR_EACH (FFDisplayResult, item, displays) {
yyjson_mut_val* obj = yyjson_mut_arr_add_obj(doc, arr);
yyjson_mut_obj_add_uint(doc, obj, "id", item->id);
yyjson_mut_obj_add_strbuf(doc, obj, "name", &item->name);
+1 -1
View File
@@ -167,5 +167,5 @@ FFModuleBaseInfo ffDNSModuleInfo = {
.formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
{ "DNS result", "result" },
})),
.defaultOrder = 50,
.defaultOrder = 51,
};
+15 -8
View File
@@ -5,6 +5,17 @@
#include "detection/editor/editor.h"
#include "modules/editor/editor.h"
// `FFEditorResult` owns four buffers and has no destructor of its own. Both entry points below
// have to free them on the error path too — which used to be skipped, and which became the common
// path once `ffDetectEditor()` started reporting an `$EDITOR` that does not name an executable
// instead of quietly succeeding.
static void destroyEditorResult(FFEditorResult* result) {
ffStrbufDestroy(&result->name);
ffStrbufDestroy(&result->path);
ffStrbufDestroy(&result->exe);
ffStrbufDestroy(&result->version);
}
bool ffPrintEditor(FFEditorOptions* options) {
FFEditorResult result = {
.type = "Unknown",
@@ -17,6 +28,7 @@ bool ffPrintEditor(FFEditorOptions* options) {
if (error) {
ffPrintError(FF_MODULE_GET_DISPLAY_NAME(Editor), 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "%s", error);
destroyEditorResult(&result);
return false;
}
@@ -41,10 +53,7 @@ bool ffPrintEditor(FFEditorOptions* options) {
}));
}
ffStrbufDestroy(&result.name);
ffStrbufDestroy(&result.path);
ffStrbufDestroy(&result.exe);
ffStrbufDestroy(&result.version);
destroyEditorResult(&result);
return true;
}
@@ -76,6 +85,7 @@ bool ffGenerateEditorJsonResult([[maybe_unused]] FFEditorOptions* options, yyjso
if (error) {
yyjson_mut_obj_add_str(doc, module, "error", error);
destroyEditorResult(&result);
return false;
}
@@ -86,10 +96,7 @@ bool ffGenerateEditorJsonResult([[maybe_unused]] FFEditorOptions* options, yyjso
yyjson_mut_obj_add_strbuf(doc, obj, "exe", &result.exe);
yyjson_mut_obj_add_strbuf(doc, obj, "version", &result.version);
ffStrbufDestroy(&result.name);
ffStrbufDestroy(&result.path);
ffStrbufDestroy(&result.exe);
ffStrbufDestroy(&result.version);
destroyEditorResult(&result);
return true;
}
+9 -5
View File
@@ -88,11 +88,13 @@ bool ffPrintGamepad(FFGamepadOptions* options) {
FFGamepadDevice* device = *pdevice;
printDevice(options, device, filtered.length > 1 ? ++index : 0);
}
}
FF_LIST_FOR_EACH (FFGamepadDevice, device, result) {
ffStrbufDestroy(&device->serial);
ffStrbufDestroy(&device->name);
}
// `filtered` only holds borrowed pointers, so the strings of all devices — including the ignored
// ones — have to be released here, not in the printing branch
FF_LIST_FOR_EACH (FFGamepadDevice, device, result) {
ffStrbufDestroy(&device->serial);
ffStrbufDestroy(&device->name);
}
return ret;
@@ -153,6 +155,8 @@ bool ffGenerateGamepadJsonResult([[maybe_unused]] FFGamepadOptions* options, yyj
yyjson_mut_val* obj = yyjson_mut_arr_add_obj(doc, arr);
yyjson_mut_obj_add_strbuf(doc, obj, "serial", &device->serial);
yyjson_mut_obj_add_strbuf(doc, obj, "name", &device->name);
// 0 means "the device does not report a battery level", not "empty battery"
yyjson_mut_obj_add_uint(doc, obj, "battery", device->battery);
bool ignored = false;
FF_LIST_FOR_EACH (FFstrbuf, ignore, options->ignores) {
@@ -225,5 +229,5 @@ FFModuleBaseInfo ffGamepadModuleInfo = {
{ "Battery percentage num", "battery-percentage" },
{ "Battery percentage bar", "battery-percentage-bar" },
})),
.defaultOrder = 62,
.defaultOrder = 63,
};
+22 -16
View File
@@ -11,6 +11,21 @@
#include <stdlib.h>
// `hideType` is a filter, and FF_GPU_TYPE_NONE means "hide nothing" -- no detected GPU ever has
// that type. Both the text and the JSON path must apply it, or the two disagree about which GPUs
// exist (see bug.md B29).
static bool gpuIsHidden(const FFGPUOptions* options, const FFGPUResult* gpu) {
return options->hideType != FF_GPU_TYPE_NONE && gpu->type == options->hideType;
}
static void selectGPUs(const FFGPUOptions* options, const FFlist* gpus, FFlist* selectedGPUs) {
FF_LIST_FOR_EACH (FFGPUResult, gpu, *gpus) {
if (!gpuIsHidden(options, gpu)) {
*FF_LIST_ADD(const FFGPUResult*, *selectedGPUs) = gpu;
}
}
}
static void printGPUResult(FFGPUOptions* options, uint8_t index, const FFGPUResult* gpu) {
const char* type;
switch (gpu->type) {
@@ -182,21 +197,7 @@ bool ffPrintGPU(FFGPUOptions* options) {
FF_LIST_AUTO_DESTROY selectedGPUs = ffListCreateA(sizeof(const FFGPUResult*), gpus.length);
FF_LIST_FOR_EACH (FFGPUResult, gpu, gpus) {
if (gpu->type == FF_GPU_TYPE_UNKNOWN && options->hideType == FF_GPU_TYPE_UNKNOWN) {
continue;
}
if (gpu->type == FF_GPU_TYPE_INTEGRATED && options->hideType == FF_GPU_TYPE_INTEGRATED) {
continue;
}
if (gpu->type == FF_GPU_TYPE_DISCRETE && options->hideType == FF_GPU_TYPE_DISCRETE) {
continue;
}
*FF_LIST_ADD(const FFGPUResult*, selectedGPUs) = gpu;
}
selectGPUs(options, &gpus, &selectedGPUs);
uint32_t i = 0;
FF_LIST_FOR_EACH (const FFGPUResult*, pgpu, selectedGPUs) {
@@ -339,8 +340,13 @@ bool ffGenerateGPUJsonResult(FFGPUOptions* options, yyjson_mut_doc* doc, yyjson_
return false;
}
FF_LIST_AUTO_DESTROY selectedGPUs = ffListCreateA(sizeof(const FFGPUResult*), gpus.length);
selectGPUs(options, &gpus, &selectedGPUs);
yyjson_mut_val* arr = yyjson_mut_obj_add_arr(doc, module, "result");
FF_LIST_FOR_EACH (FFGPUResult, gpu, gpus) {
FF_LIST_FOR_EACH (const FFGPUResult*, pgpu, selectedGPUs) {
const FFGPUResult* gpu = *pgpu;
yyjson_mut_val* obj = yyjson_mut_arr_add_obj(doc, arr);
if (gpu->index != FF_GPU_INDEX_UNSET) {
-1
View File
@@ -97,7 +97,6 @@ FFModuleBaseInfo ffKernelModuleInfo = {
{ "Release", "release" },
{ "Version", "version" },
{ "Architecture", "arch" },
{ "Display version", "display-version" },
{ "Page size", "page-size" },
})),
.defaultOrder = 9,
+9 -5
View File
@@ -54,13 +54,17 @@ bool ffPrintKeyboard(FFKeyboardOptions* options) {
} else {
uint8_t index = 0;
FF_LIST_FOR_EACH (FFKeyboardDevice*, pdevice, filtered) {
FFKeyboardDevice* device = *pdevice;
printDevice(options, device, filtered.length > 1 ? ++index : 0);
ffStrbufDestroy(&device->serial);
ffStrbufDestroy(&device->name);
printDevice(options, *pdevice, filtered.length > 1 ? ++index : 0);
}
}
// `filtered` only holds borrowed pointers, so the strings of every device — including the
// ignored ones and the ones skipped when every device is ignored — have to be released here
FF_LIST_FOR_EACH (FFKeyboardDevice, device, result) {
ffStrbufDestroy(&device->serial);
ffStrbufDestroy(&device->name);
}
return ret;
}
@@ -183,5 +187,5 @@ FFModuleBaseInfo ffKeyboardModuleInfo = {
{ "Name", "name" },
{ "Serial number", "serial" },
})),
.defaultOrder = 64,
.defaultOrder = 65,
};
+8 -1
View File
@@ -92,7 +92,14 @@ void ffParseLoadavgJsonObject(FFLoadavgOptions* options, yyjson_val* module) {
}
if (unsafe_yyjson_equals_str(key, "ndigits")) {
options->ndigits = (uint8_t) yyjson_get_uint(val);
// The schema documents 0..9 and the value ends up as the precision of `printf("%.*f")`,
// so a wider value is not truncated to fit `uint8_t` silently any more.
uint32_t ndigits = 0;
if (!ffJsonConfigParseUInt32(val, &ndigits, 9)) {
ffPrintError(FF_MODULE_GET_DISPLAY_NAME(Loadavg), 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Property 'ndigits' must be an integer between 0 and 9");
continue;
}
options->ndigits = (uint8_t) ndigits;
continue;
}
+1 -1
View File
@@ -100,5 +100,5 @@ FFModuleBaseInfo ffLocaleModuleInfo = {
.formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
{ "Locale code", "result" },
})),
.defaultOrder = 53,
.defaultOrder = 54,
};
+16 -2
View File
@@ -112,6 +112,9 @@ bool ffPrintLocalIp(FFLocalIpOptions* options) {
ffListSort(&results, sizeof(FFLocalIpResult), (const void*) sortIps);
FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
// `{is-default-route}` reports the address families the interface carries the default route for
// ("ipv4", "ipv6" or "ipv4,ipv6") instead of exposing the internal bit field as a number.
FF_STRBUF_AUTO_DESTROY defaultRoute = ffStrbufCreate();
if (options->showType & FF_LOCALIP_TYPE_COMPACT_BIT) {
ffPrintLogoAndKey(FF_MODULE_GET_DISPLAY_NAME(LocalIP), 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT);
@@ -138,12 +141,22 @@ bool ffPrintLocalIp(FFLocalIpOptions* options) {
if (ip->speed > 0) {
appendSpeed(ip, &buffer);
}
ffStrbufClear(&defaultRoute);
if (ip->defaultRoute & FF_LOCALIP_TYPE_IPV4_BIT) {
ffStrbufAppendS(&defaultRoute, "ipv4");
}
if (ip->defaultRoute & FF_LOCALIP_TYPE_IPV6_BIT) {
if (defaultRoute.length > 0) {
ffStrbufAppendC(&defaultRoute, ',');
}
ffStrbufAppendS(&defaultRoute, "ipv6");
}
FF_PRINT_FORMAT_CHECKED(key.chars, 0, &options->moduleArgs, FF_PRINT_TYPE_NO_CUSTOM_KEY, ((FFformatarg[]) {
FF_ARG(ip->ipv4, "ipv4"),
FF_ARG(ip->ipv6, "ipv6"),
FF_ARG(ip->mac, "mac"),
FF_ARG(ip->name, "ifname"),
FF_ARG(ip->defaultRoute, "is-default-route"),
FF_ARG(defaultRoute, "is-default-route"),
FF_ARG(ip->mtu, "mtu"),
FF_ARG(buffer, "speed"),
FF_ARG(ip->flags, "flags"),
@@ -151,6 +164,7 @@ bool ffPrintLocalIp(FFLocalIpOptions* options) {
}
++index;
ffStrbufClear(&buffer);
ffStrbufClear(&defaultRoute);
}
}
@@ -462,5 +476,5 @@ FFModuleBaseInfo ffLocalIPModuleInfo = {
{ "Link speed (formatted)", "speed" },
{ "Interface flags", "flags" },
})),
.defaultOrder = 49,
.defaultOrder = 50,
};
+3 -1
View File
@@ -57,7 +57,9 @@ bool ffGenerateLogoJsonResult([[maybe_unused]] FFLogoOptions* options, yyjson_mu
yyjson_mut_obj_add_str(doc, obj, "colorTitle", logo->colorTitle);
yyjson_mut_val* typeArr = yyjson_mut_obj_add_arr(doc, obj, "type");
if (logo->type & FF_LOGO_LINE_TYPE_NORMAL) {
// `FF_LOGO_LINE_TYPE_NORMAL` is 0, so a bit test here is always false and "normal" would be dead
// code. `src/logo/logo.c` tests the same flag with `!=`, which is the intended semantics.
if (logo->type == FF_LOGO_LINE_TYPE_NORMAL) {
yyjson_mut_arr_add_str(doc, typeArr, "normal");
}
if (logo->type & FF_LOGO_LINE_TYPE_SMALL_BIT) {
+1 -1
View File
@@ -296,5 +296,5 @@ FFModuleBaseInfo ffMediaModuleInfo = {
{ "Player ID", "player-id" },
{ "URL", "url" },
})),
.defaultOrder = 47,
.defaultOrder = 48,
};
+1 -1
View File
@@ -154,5 +154,5 @@ FFModuleBaseInfo ffMemoryModuleInfo = {
{ "Percentage used (num)", "percentage" },
{ "Percentage used (bar)", "percentage-bar" },
})),
.defaultOrder = 38,
.defaultOrder = 39,
};
+1 -1
View File
@@ -186,5 +186,5 @@ FFModuleBaseInfo ffMouseModuleInfo = {
{ "Mouse name", "name" },
{ "Mouse serial number", "serial" },
})),
.defaultOrder = 63,
.defaultOrder = 64,
};
+5 -2
View File
@@ -126,7 +126,10 @@ void ffParseNetIOJsonObject(FFNetIOOptions* options, yyjson_val* module) {
}
if (unsafe_yyjson_equals_str(key, "waitTime")) {
options->waitTime = (uint32_t) yyjson_get_uint(val);
if (!ffJsonConfigParseUInt32(val, &options->waitTime, UINT32_MAX)) {
ffPrintError(FF_MODULE_GET_DISPLAY_NAME(NetIO), 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Property 'waitTime' must be a non-negative integer no greater than 4294967295");
continue;
}
if (options->waitTime == 0) {
options->waitTime = 1;
}
@@ -245,5 +248,5 @@ FFModuleBaseInfo ffNetIOModuleInfo = {
{ "Number of packets dropped when receiving [per second]", "rx-drops" },
{ "Number of packets dropped when sending [per second]", "tx-drops" },
})),
.defaultOrder = 66,
.defaultOrder = 67,
};
+2 -2
View File
@@ -84,7 +84,7 @@ bool ffGenerateOpenCLJsonResult([[maybe_unused]] FFOpenCLOptions* options, yyjso
}
if (gpu->dedicated.used != FF_GPU_VMEM_SIZE_UNSET) {
yyjson_mut_obj_add_uint(doc, dedicatedMemory, "used", gpu->dedicated.total);
yyjson_mut_obj_add_uint(doc, dedicatedMemory, "used", gpu->dedicated.used);
} else {
yyjson_mut_obj_add_null(doc, dedicatedMemory, "used");
}
@@ -155,5 +155,5 @@ FFModuleBaseInfo ffOpenCLModuleInfo = {
{ "Platform name", "name" },
{ "Platform vendor", "vendor" },
})),
.defaultOrder = 56,
.defaultOrder = 57,
};
+1 -1
View File
@@ -164,5 +164,5 @@ FFModuleBaseInfo ffOpenGLModuleInfo = {
{ "OpenGL shading language version", "slv" },
{ "OpenGL library used", "library" },
})),
.defaultOrder = 55,
.defaultOrder = 56,
};
+1
View File
@@ -216,6 +216,7 @@ FFModuleBaseInfo ffOSModuleInfo = {
{ "Version codename of the OS", "codename" },
{ "Build ID of the OS", "build-id" },
{ "Architecture of the OS", "arch" },
{ "Release of the kernel", "kernel-release" },
})),
.defaultOrder = 3,
};
+1 -1
View File
@@ -306,5 +306,5 @@ FFModuleBaseInfo ffPhysicalDiskModuleInfo = {
{ "Product revision", "revision" },
{ "Device temperature (formatted)", "temperature" },
})),
.defaultOrder = 68,
.defaultOrder = 69,
};
+1 -1
View File
@@ -201,5 +201,5 @@ FFModuleBaseInfo ffPhysicalMemoryModuleInfo = {
{ "True if ECC enabled", "is-ecc-enabled" },
{ "True if a memory module is installed in the slot", "is-installed" },
})),
.defaultOrder = 39,
.defaultOrder = 40,
};
+1 -1
View File
@@ -143,5 +143,5 @@ FFModuleBaseInfo ffPlayerModuleInfo = {
{ "Player Identifier", "id" },
{ "URL name", "url" },
})),
.defaultOrder = 46,
.defaultOrder = 47,
};
+2 -2
View File
@@ -142,9 +142,9 @@ FFModuleBaseInfo ffPowerAdapterModuleInfo = {
{ "Power adapter watts", "watts" },
{ "Power adapter name", "name" },
{ "Power adapter manufacturer", "manufacturer" },
{ "Power adapter model", "model" },
{ "Power adapter model", "model-name" },
{ "Power adapter description", "description" },
{ "Power adapter serial number", "serial" },
})),
.defaultOrder = 45,
.defaultOrder = 46,
};
+5 -1
View File
@@ -18,7 +18,10 @@ bool ffPrintProcesses(FFProcessesOptions* options) {
printf("%u (%u threads)\n", result.processes, result.threads);
} else {
FF_PRINT_FORMAT_CHECKED(FF_MODULE_GET_DISPLAY_NAME(Processes), 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) { FF_ARG(result.processes, "result") }));
FF_PRINT_FORMAT_CHECKED(FF_MODULE_GET_DISPLAY_NAME(Processes), 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, ((FFformatarg[]) {
FF_ARG(result.processes, "result"),
FF_ARG(result.threads, "threads")
}));
}
return true;
@@ -105,6 +108,7 @@ FFModuleBaseInfo ffProcessesModuleInfo = {
.generateJsonConfig = (void*) ffGenerateProcessesJsonConfig,
.formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
{ "Process count", "result" },
{ "Thread count", "threads" },
})),
.defaultOrder = 13,
};
+1 -1
View File
@@ -142,5 +142,5 @@ FFModuleBaseInfo ffPublicIPModuleInfo = {
{ "Public IP address", "ip" },
{ "Location", "location" },
})),
.defaultOrder = 48,
.defaultOrder = 49,
};
+4 -1
View File
@@ -90,7 +90,10 @@ void ffParseSeparatorJsonObject(FFSeparatorOptions* options, yyjson_val* module)
}
if (unsafe_yyjson_equals_str(key, "times")) {
options->times = (uint32_t) yyjson_get_uint(val);
if (!ffJsonConfigParseUInt32(val, &options->times, UINT32_MAX)) {
ffPrintError(FF_MODULE_GET_DISPLAY_NAME(Separator), 0, nullptr, FF_PRINT_TYPE_NO_CUSTOM_KEY, "Property 'times' must be a non-negative integer no greater than 4294967295");
continue;
}
continue;
}
+2 -3
View File
@@ -67,7 +67,6 @@ static void printDevice(FFSoundOptions* options, const FFSoundDevice* device, ui
}
bool ffPrintSound(FFSoundOptions* options) {
bool success = false;
FF_LIST_AUTO_DESTROY result = ffListCreate();
const char* error = ffDetectSound(options, &result);
@@ -93,7 +92,7 @@ bool ffPrintSound(FFSoundOptions* options) {
ffStrbufDestroy(&device->platformApi);
}
return success;
return true;
}
void ffParseSoundJsonObject(FFSoundOptions* options, yyjson_val* module) {
@@ -238,5 +237,5 @@ FFModuleBaseInfo ffSoundModuleInfo = {
{ "Volume (in percentage bar)", "volume-percentage-bar" },
{ "Platform API used", "platform-api" },
})),
.defaultOrder = 60,
.defaultOrder = 61,
};
+1 -1
View File
@@ -220,5 +220,5 @@ FFModuleBaseInfo ffSwapModuleInfo = {
{ "Percentage used (bar)", "percentage-bar" },
{ "Name *", "name" },
})),
.defaultOrder = 40,
.defaultOrder = 41,
};
+2 -1
View File
@@ -111,6 +111,7 @@ void ffParseTopJsonObject(FFTopOptions* options, yyjson_val* module) {
{ "disk-read", FF_TOP_TYPE_DISK_READ },
{ "disk-write", FF_TOP_TYPE_DISK_WRITE },
{ "start-time", FF_TOP_TYPE_START_TIME },
{ "threads", FF_TOP_TYPE_THREADS },
{},
});
if (error) {
@@ -305,5 +306,5 @@ FFModuleBaseInfo ffTopModuleInfo = {
{ "Disk read formatted", "disk-read-formatted" },
{ "Disk write formatted", "disk-write-formatted" },
})),
.defaultOrder = 36,
.defaultOrder = 37,
};
+1 -1
View File
@@ -117,5 +117,5 @@ FFModuleBaseInfo ffTPMModuleInfo = {
{ "TPM device version", "version" },
{ "TPM general description", "description" },
})),
.defaultOrder = 69,
.defaultOrder = 70,
};
+1 -1
View File
@@ -225,5 +225,5 @@ FFModuleBaseInfo ffUsersModuleInfo = {
{ "Days of year after login", "days-of-year" },
{ "Years fraction after login", "years-fraction" },
})),
.defaultOrder = 57,
.defaultOrder = 58,
};
+1 -1
View File
@@ -136,5 +136,5 @@ FFModuleBaseInfo ffVersionModuleInfo = {
{ "Compiler used when compiling", "compiler" },
{ "Libc used when compiling", "libc" },
})),
.defaultOrder = 70,
.defaultOrder = 71,
};
+3 -4
View File
@@ -73,6 +73,7 @@ bool ffGenerateVulkanJsonResult([[maybe_unused]] FFVulkanOptions* options, yyjso
yyjson_mut_val* obj = yyjson_mut_obj_add_obj(doc, module, "result");
yyjson_mut_obj_add_strbuf(doc, obj, "apiVersion", &result->apiVersion);
yyjson_mut_obj_add_strbuf(doc, obj, "conformanceVersion", &result->conformanceVersion);
yyjson_mut_obj_add_strbuf(doc, obj, "instanceVersion", &result->instanceVersion);
yyjson_mut_obj_add_strbuf(doc, obj, "driver", &result->driver);
yyjson_mut_val* gpus = yyjson_mut_obj_add_arr(doc, obj, "gpus");
FF_LIST_FOR_EACH (FFGPUResult, vulkanGpu, result->gpus) {
@@ -96,7 +97,7 @@ bool ffGenerateVulkanJsonResult([[maybe_unused]] FFVulkanOptions* options, yyjso
}
if (vulkanGpu->dedicated.used != FF_GPU_VMEM_SIZE_UNSET) {
yyjson_mut_obj_add_uint(doc, dedicatedMemory, "used", vulkanGpu->dedicated.total);
yyjson_mut_obj_add_uint(doc, dedicatedMemory, "used", vulkanGpu->dedicated.used);
} else {
yyjson_mut_obj_add_null(doc, dedicatedMemory, "used");
}
@@ -116,8 +117,6 @@ bool ffGenerateVulkanJsonResult([[maybe_unused]] FFVulkanOptions* options, yyjso
yyjson_mut_obj_add_null(doc, sharedMemory, "used");
}
}
yyjson_mut_obj_add_uint(doc, gpuObj, "deviceId", vulkanGpu->deviceId);
}
return true;
@@ -168,5 +167,5 @@ FFModuleBaseInfo ffVulkanModuleInfo = {
{ "Conformance version", "conformance-version" },
{ "Instance version", "instance-version" },
})),
.defaultOrder = 54,
.defaultOrder = 55,
};
+1 -1
View File
@@ -125,5 +125,5 @@ FFModuleBaseInfo ffWeatherModuleInfo = {
.formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) {
{ "Weather result", "result" },
})),
.defaultOrder = 65,
.defaultOrder = 66,
};
+1 -1
View File
@@ -261,5 +261,5 @@ FFModuleBaseInfo ffWifiModuleInfo = {
{ "Connection channel width in MHz", "channel-width" },
{ "Connection channel band in GHz", "band" },
})),
.defaultOrder = 51,
.defaultOrder = 52,
};
+1 -1
View File
@@ -238,5 +238,5 @@ FFModuleBaseInfo ffZpoolModuleInfo = {
{ "Fragmentation percentage bar", "frag-percentage-bar" },
{ "Is read-only", "is-readonly" },
})),
.defaultOrder = 43,
.defaultOrder = 44,
};
+5 -4
View File
@@ -1,5 +1,6 @@
#include "fastfetch.h"
#include "common/color.h"
#include "common/io.h"
#include "common/jsonconfig.h"
#include "common/percent.h"
#include "common/strutil.h"
@@ -286,11 +287,11 @@ const char* ffOptionsParseDisplayJsonConfig(FFOptionsDisplay* options, yyjson_va
yyjson_val* ndigits = yyjson_obj_get(val, "ndigits");
if (ndigits) {
if (!yyjson_is_uint(ndigits)) {
return "display.temperature.ndigits must be an unsigned integer";
return "display.temp.ndigits must be an unsigned integer";
}
uint64_t val = yyjson_get_uint(ndigits);
if (val > 9) {
return "display.temperature.ndigits must be between 0 and 9";
return "display.temp.ndigits must be between 0 and 9";
}
options->tempNdigits = (uint8_t) val;
}
@@ -298,7 +299,7 @@ const char* ffOptionsParseDisplayJsonConfig(FFOptionsDisplay* options, yyjson_va
yyjson_val* color = yyjson_obj_get(val, "color");
if (color) {
if (!yyjson_is_obj(color)) {
return "display.temperature.color must be an object";
return "display.temp.color must be an object";
}
yyjson_val* green = yyjson_obj_get(color, "green");
@@ -952,7 +953,7 @@ void ffOptionsInitDisplay(FFOptionsDisplay* options) {
ffStrbufInitStatic(&options->keyValueSeparator, ": ");
options->showErrors = false;
options->pipe = !isatty(STDOUT_FILENO) || !!getenv("NO_COLOR");
options->pipe = !ffIsTerminal(STDOUT_FILENO) || !!getenv("NO_COLOR");
options->disableLinewrap = false;
#ifndef NDEBUG