diff --git a/CHANGELOG.md b/CHANGELOG.md index 282914047..67bd87f4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 `, 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: diff --git a/CMakeLists.txt b/CMakeLists.txt index 81ef1afd9..420354b3c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/doc/json_schema.json b/doc/json_schema.json index a807a823e..2ed64ec2a 100644 --- a/doc/json_schema.json +++ b/doc/json_schema.json @@ -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" }, diff --git a/src/common/impl/commandoption.c b/src/common/impl/commandoption.c index 2b11078b3..bc5ee9e2e 100644 --- a/src/common/impl/commandoption.c +++ b/src/common/impl/commandoption.c @@ -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 diff --git a/src/common/impl/io_unix.c b/src/common/impl/io_unix.c index 83ef3e8d0..56f9922ef 100644 --- a/src/common/impl/io_unix.c +++ b/src/common/impl/io_unix.c @@ -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; } diff --git a/src/common/impl/io_windows.c b/src/common/impl/io_windows.c index 48380c2ff..e052b9f56 100644 --- a/src/common/impl/io_windows.c +++ b/src/common/impl/io_windows.c @@ -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; diff --git a/src/common/impl/jsonconfig.c b/src/common/impl/jsonconfig.c index a7597ca40..9b9316150 100644 --- a/src/common/impl/jsonconfig.c +++ b/src/common/impl/jsonconfig.c @@ -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; } diff --git a/src/common/impl/printing.c b/src/common/impl/printing.c index 832c6301c..298054428 100644 --- a/src/common/impl/printing.c +++ b/src/common/impl/printing.c @@ -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; } diff --git a/src/common/io.h b/src/common/io.h index 95f3ccc41..6faaf4d66 100644 --- a/src/common/io.h +++ b/src/common/io.h @@ -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 > /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. diff --git a/src/common/jsonconfig.h b/src/common/jsonconfig.h index 9790739db..ba6400b8c 100644 --- a/src/common/jsonconfig.h +++ b/src/common/jsonconfig.h @@ -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`. diff --git a/src/common/printing.h b/src/common/printing.h index fd1f0a44d..c8d13284c 100644 --- a/src/common/printing.h +++ b/src/common/printing.h @@ -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); diff --git a/src/detection/btrfs/btrfs_linux.c b/src/detection/btrfs/btrfs_linux.c index 120b3f6c2..077b1d5dc 100644 --- a/src/detection/btrfs/btrfs_linux.c +++ b/src/detection/btrfs/btrfs_linux.c @@ -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) { \ diff --git a/src/detection/camera/camera_apple.m b/src/detection/camera/camera_apple.m index ba1509e0d..547a16060 100644 --- a/src/detection/camera/camera_apple.m +++ b/src/detection/camera/camera_apple.m @@ -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; diff --git a/src/detection/camera/camera_nosupport.c b/src/detection/camera/camera_nosupport.c index ca8e64a16..91e9bd8bd 100644 --- a/src/detection/camera/camera_nosupport.c +++ b/src/detection/camera/camera_nosupport.c @@ -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"; } diff --git a/src/detection/command/command.c b/src/detection/command/command.c index ef0095a57..18ea9c915 100644 --- a/src/detection/command/command.c +++ b/src/detection/command/command.c @@ -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) { diff --git a/src/detection/cpu/cpu_nosupport.c b/src/detection/cpu/cpu_nosupport.c deleted file mode 100644 index 3b014a4cb..000000000 --- a/src/detection/cpu/cpu_nosupport.c +++ /dev/null @@ -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"; -} diff --git a/src/detection/cpuusage/cpuusage_nosupport.c b/src/detection/cpuusage/cpuusage_nosupport.c deleted file mode 100644 index 4b0ba7aa8..000000000 --- a/src/detection/cpuusage/cpuusage_nosupport.c +++ /dev/null @@ -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"; -} diff --git a/src/detection/de/de_linux.c b/src/detection/de/de_linux.c index c6cebbae5..e93b604d9 100644 --- a/src/detection/de/de_linux.c +++ b/src/detection/de/de_linux.c @@ -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); diff --git a/src/detection/disk/disk_nosupport.c b/src/detection/disk/disk_nosupport.c deleted file mode 100644 index 0623336dc..000000000 --- a/src/detection/disk/disk_nosupport.c +++ /dev/null @@ -1,5 +0,0 @@ -#include "disk.h" - -const char* ffDetectDisksImpl([[maybe_unused]] FFDiskOptions* options, [[maybe_unused]] FFlist* disks) { - return "Not supported on this platform"; -} diff --git a/src/detection/dns/dns_windows.c b/src/detection/dns/dns_windows.c index 4f63d91f9..edab21e86 100644 --- a/src/detection/dns/dns_windows.c +++ b/src/detection/dns/dns_windows.c @@ -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); } diff --git a/src/detection/editor/editor.c b/src/detection/editor/editor.c index a4176636a..a6423645e 100644 --- a/src/detection/editor/editor.c +++ b/src/detection/editor/editor.c @@ -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; } diff --git a/src/detection/font/font_nosupport.c b/src/detection/font/font_nosupport.c deleted file mode 100644 index 8dc7b1c77..000000000 --- a/src/detection/font/font_nosupport.c +++ /dev/null @@ -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"; -} diff --git a/src/detection/gpu/gpu.c b/src/detection/gpu/gpu.c index 6fb0fe7f7..70313683c 100644 --- a/src/detection/gpu/gpu.c +++ b/src/detection/gpu/gpu.c @@ -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; diff --git a/src/detection/gpu/gpu_nosupport.c b/src/detection/gpu/gpu_nosupport.c deleted file mode 100644 index 95fb2f350..000000000 --- a/src/detection/gpu/gpu_nosupport.c +++ /dev/null @@ -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"; -} diff --git a/src/detection/initsystem/initsystem_nosupport.c b/src/detection/initsystem/initsystem_nosupport.c deleted file mode 100644 index b3a5036f4..000000000 --- a/src/detection/initsystem/initsystem_nosupport.c +++ /dev/null @@ -1,5 +0,0 @@ -#include "initsystem.h" - -const char* ffDetectInitSystem([[maybe_unused]] FFInitSystemResult* result) { - return "Not supported on this platform"; -} diff --git a/src/detection/keyboard/keyboard_nosupport.c b/src/detection/keyboard/keyboard_nosupport.c index a80dfd0ae..51d51d749 100644 --- a/src/detection/keyboard/keyboard_nosupport.c +++ b/src/detection/keyboard/keyboard_nosupport.c @@ -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"; } diff --git a/src/detection/loadavg/loadavg_linux.c b/src/detection/loadavg/loadavg_linux.c index ab4c7373f..fb27551b2 100644 --- a/src/detection/loadavg/loadavg_linux.c +++ b/src/detection/loadavg/loadavg_linux.c @@ -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; } diff --git a/src/detection/media/media.c b/src/detection/media/media.c index 93ce674d2..0c6e70ea1 100644 --- a/src/detection/media/media.c +++ b/src/detection/media/media.c @@ -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; diff --git a/src/detection/media/media_apple.m b/src/detection/media/media_apple.m index a9f2ada5e..32f2c6166 100644 --- a/src/detection/media/media_apple.m +++ b/src/detection/media/media_apple.m @@ -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; } diff --git a/src/detection/media/media_nosupport.c b/src/detection/media/media_nosupport.c deleted file mode 100644 index e7d1c0f5b..000000000 --- a/src/detection/media/media_nosupport.c +++ /dev/null @@ -1,5 +0,0 @@ -#include "media.h" - -void ffDetectMediaImpl(FFMediaResult* media) { - ffStrbufAppendS(&media->error, "Not supported on this platform"); -} diff --git a/src/detection/memory/memory_nosupport.c b/src/detection/memory/memory_nosupport.c deleted file mode 100644 index 6c11799a3..000000000 --- a/src/detection/memory/memory_nosupport.c +++ /dev/null @@ -1,5 +0,0 @@ -#include "memory.h" - -const char* ffDetectMemory([[maybe_unused]] FFMemoryResult* ram) { - return "Not supported on this platform"; -} diff --git a/src/detection/packages/packages_nosupport.c b/src/detection/packages/packages_nosupport.c deleted file mode 100644 index facc9f545..000000000 --- a/src/detection/packages/packages_nosupport.c +++ /dev/null @@ -1,4 +0,0 @@ -#include "packages.h" - -void ffDetectPackagesImpl([[maybe_unused]] FFPackagesResult* result, [[maybe_unused]] FFPackagesOptions* options) { -} diff --git a/src/detection/processes/processes_nosupport.c b/src/detection/processes/processes_nosupport.c deleted file mode 100644 index 04a5eafd8..000000000 --- a/src/detection/processes/processes_nosupport.c +++ /dev/null @@ -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"; -} diff --git a/src/detection/swap/swap_nosupport.c b/src/detection/swap/swap_nosupport.c deleted file mode 100644 index ebc29b485..000000000 --- a/src/detection/swap/swap_nosupport.c +++ /dev/null @@ -1,5 +0,0 @@ -#include "swap.h" - -const char* ffDetectSwap(FFSwapResult* swap) { - return "Not supported on this platform"; -} diff --git a/src/detection/users/users_nosupport.c b/src/detection/users/users_nosupport.c deleted file mode 100644 index 0a7e30a4c..000000000 --- a/src/detection/users/users_nosupport.c +++ /dev/null @@ -1,6 +0,0 @@ -#include "fastfetch.h" -#include "users.h" - -const char* ffDetectUsers(FFUsersOptions* options, FFlist* users) { - return "Not supported on this platform"; -} diff --git a/src/detection/vulkan/vulkan.c b/src/detection/vulkan/vulkan.c index 635df6ff6..9d2194831 100644 --- a/src/detection/vulkan/vulkan.c +++ b/src/detection/vulkan/vulkan.c @@ -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); diff --git a/src/detection/wallpaper/wallpaper_nosupport.c b/src/detection/wallpaper/wallpaper_nosupport.c deleted file mode 100644 index dd9783ea8..000000000 --- a/src/detection/wallpaper/wallpaper_nosupport.c +++ /dev/null @@ -1,5 +0,0 @@ -#include "wallpaper.h" - -const char* ffDetectWallpaper([[maybe_unused]] FFstrbuf* result) { - return "Not supported on this platform"; -} diff --git a/src/detection/wifi/wifi_nosupport.c b/src/detection/wifi/wifi_nosupport.c index e49d6607f..26008b0ca 100644 --- a/src/detection/wifi/wifi_nosupport.c +++ b/src/detection/wifi/wifi_nosupport.c @@ -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"; } diff --git a/src/detection/wifi/wifi_windows.c b/src/detection/wifi/wifi_windows.c index 838295484..76fe29942 100644 --- a/src/detection/wifi/wifi_windows.c +++ b/src/detection/wifi/wifi_windows.c @@ -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); } } diff --git a/src/detection/wmtheme/wmtheme_nosupport.c b/src/detection/wmtheme/wmtheme_nosupport.c deleted file mode 100644 index 8cba51acf..000000000 --- a/src/detection/wmtheme/wmtheme_nosupport.c +++ /dev/null @@ -1,7 +0,0 @@ -#include "fastfetch.h" -#include "wmtheme.h" - -bool ffDetectWmTheme(FFstrbuf* themeOrError) { - ffStrbufAppendS(themeOrError, "Not supported on this platform"); - return false; -} diff --git a/src/fastfetch.c b/src/fastfetch.c index 715991977..e431969af 100644 --- a/src/fastfetch.c +++ b/src/fastfetch.c @@ -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 diff --git a/src/modules/battery/battery.c b/src/modules/battery/battery.c index b89af77c4..bd98043d4 100644 --- a/src/modules/battery/battery.c +++ b/src/modules/battery/battery.c @@ -345,5 +345,5 @@ FFModuleBaseInfo ffBatteryModuleInfo = { { "Battery time remaining seconds", "time-seconds" }, { "Battery time remaining (formatted)", "time-formatted" }, })), - .defaultOrder = 44, + .defaultOrder = 45, }; diff --git a/src/modules/bluetooth/bluetooth.c b/src/modules/bluetooth/bluetooth.c index 9f6986392..8651dbb59 100644 --- a/src/modules/bluetooth/bluetooth.c +++ b/src/modules/bluetooth/bluetooth.c @@ -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, }; diff --git a/src/modules/bluetoothradio/bluetoothradio.c b/src/modules/bluetoothradio/bluetoothradio.c index dcff4a6f5..1ea2d8699 100644 --- a/src/modules/bluetoothradio/bluetoothradio.c +++ b/src/modules/bluetoothradio/bluetoothradio.c @@ -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, }; diff --git a/src/modules/break/break.c b/src/modules/break/break.c index 5a6f35673..11872495a 100644 --- a/src/modules/break/break.c +++ b/src/modules/break/break.c @@ -55,5 +55,5 @@ FFModuleBaseInfo ffBreakModuleInfo = { .destroyOptions = (void*) ffDestroyBreakOptions, .parseJsonObject = (void*) ffParseBreakJsonObject, .printModule = (void*) ffPrintBreak, - .defaultOrder = 71, + .defaultOrder = 72, }; diff --git a/src/modules/btrfs/btrfs.c b/src/modules/btrfs/btrfs.c index 05ad2eb81..3cbacd9fd 100644 --- a/src/modules/btrfs/btrfs.c +++ b/src/modules/btrfs/btrfs.c @@ -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, }; diff --git a/src/modules/camera/camera.c b/src/modules/camera/camera.c index 95101c23d..3263e0f18 100644 --- a/src/modules/camera/camera.c +++ b/src/modules/camera/camera.c @@ -153,5 +153,5 @@ FFModuleBaseInfo ffCameraModuleInfo = { { "Width (in px)", "width" }, { "Height (in px)", "height" }, })), - .defaultOrder = 61, + .defaultOrder = 62, }; diff --git a/src/modules/codec/codec.c b/src/modules/codec/codec.c index b432cf471..1419d78c3 100644 --- a/src/modules/codec/codec.c +++ b/src/modules/codec/codec.c @@ -288,5 +288,5 @@ FFModuleBaseInfo ffCodecModuleInfo = { { "Compatibility alias of codec types", "types" }, { "Platform API used for detection", "platform-api" }, })), - .defaultOrder = 37, + .defaultOrder = 38, }; diff --git a/src/modules/colors/colors.c b/src/modules/colors/colors.c index b9fa813e7..b7e74c261 100644 --- a/src/modules/colors/colors.c +++ b/src/modules/colors/colors.c @@ -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, }; diff --git a/src/modules/command/command.c b/src/modules/command/command.c index 21ac4378c..817bcf0d1 100644 --- a/src/modules/command/command.c +++ b/src/modules/command/command.c @@ -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); diff --git a/src/modules/cpuusage/cpuusage.c b/src/modules/cpuusage/cpuusage.c index 96c45c28d..a5a7f2dab 100644 --- a/src/modules/cpuusage/cpuusage.c +++ b/src/modules/cpuusage/cpuusage.c @@ -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; } diff --git a/src/modules/custom/custom.c b/src/modules/custom/custom.c index 13a1fed0e..2c11d87c6 100644 --- a/src/modules/custom/custom.c +++ b/src/modules/custom/custom.c @@ -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, }; diff --git a/src/modules/datetime/datetime.c b/src/modules/datetime/datetime.c index 1a7b12f25..b3c505a16 100644 --- a/src/modules/datetime/datetime.c +++ b/src/modules/datetime/datetime.c @@ -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, }; diff --git a/src/modules/disk/disk.c b/src/modules/disk/disk.c index b73bcab2b..c9c42e270 100644 --- a/src/modules/disk/disk.c +++ b/src/modules/disk/disk.c @@ -510,5 +510,5 @@ FFModuleBaseInfo ffDiskModuleInfo = { { "Size free", "size-free" }, { "Size available", "size-available" }, })), - .defaultOrder = 41, + .defaultOrder = 42, }; diff --git a/src/modules/diskio/diskio.c b/src/modules/diskio/diskio.c index 202772606..f87aee72b 100644 --- a/src/modules/diskio/diskio.c +++ b/src/modules/diskio/diskio.c @@ -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, }; diff --git a/src/modules/display/display.c b/src/modules/display/display.c index b2ec90459..b6d8a13f2 100644 --- a/src/modules/display/display.c +++ b/src/modules/display/display.c @@ -7,31 +7,50 @@ #include -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); diff --git a/src/modules/dns/dns.c b/src/modules/dns/dns.c index 517deb4ad..3c76a0b7f 100644 --- a/src/modules/dns/dns.c +++ b/src/modules/dns/dns.c @@ -167,5 +167,5 @@ FFModuleBaseInfo ffDNSModuleInfo = { .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) { { "DNS result", "result" }, })), - .defaultOrder = 50, + .defaultOrder = 51, }; diff --git a/src/modules/editor/editor.c b/src/modules/editor/editor.c index b55a24c69..7a0f4240e 100644 --- a/src/modules/editor/editor.c +++ b/src/modules/editor/editor.c @@ -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; } diff --git a/src/modules/gamepad/gamepad.c b/src/modules/gamepad/gamepad.c index 470abd8e3..81dad9e49 100644 --- a/src/modules/gamepad/gamepad.c +++ b/src/modules/gamepad/gamepad.c @@ -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, }; diff --git a/src/modules/gpu/gpu.c b/src/modules/gpu/gpu.c index 159cbaf6b..f7efcecb8 100644 --- a/src/modules/gpu/gpu.c +++ b/src/modules/gpu/gpu.c @@ -11,6 +11,21 @@ #include +// `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) { diff --git a/src/modules/kernel/kernel.c b/src/modules/kernel/kernel.c index 616e04f13..322b2b41d 100644 --- a/src/modules/kernel/kernel.c +++ b/src/modules/kernel/kernel.c @@ -97,7 +97,6 @@ FFModuleBaseInfo ffKernelModuleInfo = { { "Release", "release" }, { "Version", "version" }, { "Architecture", "arch" }, - { "Display version", "display-version" }, { "Page size", "page-size" }, })), .defaultOrder = 9, diff --git a/src/modules/keyboard/keyboard.c b/src/modules/keyboard/keyboard.c index b1ff75871..d7aae3ae5 100644 --- a/src/modules/keyboard/keyboard.c +++ b/src/modules/keyboard/keyboard.c @@ -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, }; diff --git a/src/modules/loadavg/loadavg.c b/src/modules/loadavg/loadavg.c index f737f84dd..8d19eabef 100644 --- a/src/modules/loadavg/loadavg.c +++ b/src/modules/loadavg/loadavg.c @@ -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; } diff --git a/src/modules/locale/locale.c b/src/modules/locale/locale.c index 4c91a04f5..97d45a8d2 100644 --- a/src/modules/locale/locale.c +++ b/src/modules/locale/locale.c @@ -100,5 +100,5 @@ FFModuleBaseInfo ffLocaleModuleInfo = { .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) { { "Locale code", "result" }, })), - .defaultOrder = 53, + .defaultOrder = 54, }; diff --git a/src/modules/localip/localip.c b/src/modules/localip/localip.c index 6ad26bd54..49103bd0b 100644 --- a/src/modules/localip/localip.c +++ b/src/modules/localip/localip.c @@ -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, }; diff --git a/src/modules/logo/logo.c b/src/modules/logo/logo.c index dfa92ce06..4a30e1def 100644 --- a/src/modules/logo/logo.c +++ b/src/modules/logo/logo.c @@ -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) { diff --git a/src/modules/media/media.c b/src/modules/media/media.c index 6090e6eca..8205ad340 100644 --- a/src/modules/media/media.c +++ b/src/modules/media/media.c @@ -296,5 +296,5 @@ FFModuleBaseInfo ffMediaModuleInfo = { { "Player ID", "player-id" }, { "URL", "url" }, })), - .defaultOrder = 47, + .defaultOrder = 48, }; diff --git a/src/modules/memory/memory.c b/src/modules/memory/memory.c index 6e4092d22..9b4ac2394 100644 --- a/src/modules/memory/memory.c +++ b/src/modules/memory/memory.c @@ -154,5 +154,5 @@ FFModuleBaseInfo ffMemoryModuleInfo = { { "Percentage used (num)", "percentage" }, { "Percentage used (bar)", "percentage-bar" }, })), - .defaultOrder = 38, + .defaultOrder = 39, }; diff --git a/src/modules/mouse/mouse.c b/src/modules/mouse/mouse.c index 293f31931..6cd28e487 100644 --- a/src/modules/mouse/mouse.c +++ b/src/modules/mouse/mouse.c @@ -186,5 +186,5 @@ FFModuleBaseInfo ffMouseModuleInfo = { { "Mouse name", "name" }, { "Mouse serial number", "serial" }, })), - .defaultOrder = 63, + .defaultOrder = 64, }; diff --git a/src/modules/netio/netio.c b/src/modules/netio/netio.c index 55218ce4d..70d74f64a 100644 --- a/src/modules/netio/netio.c +++ b/src/modules/netio/netio.c @@ -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, }; diff --git a/src/modules/opencl/opencl.c b/src/modules/opencl/opencl.c index c483c430d..b6c45430b 100644 --- a/src/modules/opencl/opencl.c +++ b/src/modules/opencl/opencl.c @@ -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, }; diff --git a/src/modules/opengl/opengl.c b/src/modules/opengl/opengl.c index 0ef5472c8..79bfc58f5 100644 --- a/src/modules/opengl/opengl.c +++ b/src/modules/opengl/opengl.c @@ -164,5 +164,5 @@ FFModuleBaseInfo ffOpenGLModuleInfo = { { "OpenGL shading language version", "slv" }, { "OpenGL library used", "library" }, })), - .defaultOrder = 55, + .defaultOrder = 56, }; diff --git a/src/modules/os/os.c b/src/modules/os/os.c index ad7a2bdcb..085a2058e 100644 --- a/src/modules/os/os.c +++ b/src/modules/os/os.c @@ -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, }; diff --git a/src/modules/physicaldisk/physicaldisk.c b/src/modules/physicaldisk/physicaldisk.c index 3275bb468..9da0d8214 100644 --- a/src/modules/physicaldisk/physicaldisk.c +++ b/src/modules/physicaldisk/physicaldisk.c @@ -306,5 +306,5 @@ FFModuleBaseInfo ffPhysicalDiskModuleInfo = { { "Product revision", "revision" }, { "Device temperature (formatted)", "temperature" }, })), - .defaultOrder = 68, + .defaultOrder = 69, }; diff --git a/src/modules/physicalmemory/physicalmemory.c b/src/modules/physicalmemory/physicalmemory.c index a374422e6..29f281fde 100644 --- a/src/modules/physicalmemory/physicalmemory.c +++ b/src/modules/physicalmemory/physicalmemory.c @@ -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, }; diff --git a/src/modules/player/player.c b/src/modules/player/player.c index 1474494c2..f50aca999 100644 --- a/src/modules/player/player.c +++ b/src/modules/player/player.c @@ -143,5 +143,5 @@ FFModuleBaseInfo ffPlayerModuleInfo = { { "Player Identifier", "id" }, { "URL name", "url" }, })), - .defaultOrder = 46, + .defaultOrder = 47, }; diff --git a/src/modules/poweradapter/poweradapter.c b/src/modules/poweradapter/poweradapter.c index 2174a1b26..bdd79e455 100644 --- a/src/modules/poweradapter/poweradapter.c +++ b/src/modules/poweradapter/poweradapter.c @@ -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, }; diff --git a/src/modules/processes/processes.c b/src/modules/processes/processes.c index d6220aa03..d7bafbff0 100644 --- a/src/modules/processes/processes.c +++ b/src/modules/processes/processes.c @@ -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, }; diff --git a/src/modules/publicip/publicip.c b/src/modules/publicip/publicip.c index e93f34e09..6e4f30429 100644 --- a/src/modules/publicip/publicip.c +++ b/src/modules/publicip/publicip.c @@ -142,5 +142,5 @@ FFModuleBaseInfo ffPublicIPModuleInfo = { { "Public IP address", "ip" }, { "Location", "location" }, })), - .defaultOrder = 48, + .defaultOrder = 49, }; diff --git a/src/modules/separator/separator.c b/src/modules/separator/separator.c index 60ab8e1ce..0ef3e41ee 100644 --- a/src/modules/separator/separator.c +++ b/src/modules/separator/separator.c @@ -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; } diff --git a/src/modules/sound/sound.c b/src/modules/sound/sound.c index dc506efb1..4f51fa368 100644 --- a/src/modules/sound/sound.c +++ b/src/modules/sound/sound.c @@ -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, }; diff --git a/src/modules/swap/swap.c b/src/modules/swap/swap.c index f5441b4c8..803db7069 100644 --- a/src/modules/swap/swap.c +++ b/src/modules/swap/swap.c @@ -220,5 +220,5 @@ FFModuleBaseInfo ffSwapModuleInfo = { { "Percentage used (bar)", "percentage-bar" }, { "Name *", "name" }, })), - .defaultOrder = 40, + .defaultOrder = 41, }; diff --git a/src/modules/top/top.c b/src/modules/top/top.c index 807533f35..02d811617 100644 --- a/src/modules/top/top.c +++ b/src/modules/top/top.c @@ -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, }; diff --git a/src/modules/tpm/tpm.c b/src/modules/tpm/tpm.c index 8902efe70..726d60ae6 100644 --- a/src/modules/tpm/tpm.c +++ b/src/modules/tpm/tpm.c @@ -117,5 +117,5 @@ FFModuleBaseInfo ffTPMModuleInfo = { { "TPM device version", "version" }, { "TPM general description", "description" }, })), - .defaultOrder = 69, + .defaultOrder = 70, }; diff --git a/src/modules/users/users.c b/src/modules/users/users.c index 6f24527ed..0b84901b8 100644 --- a/src/modules/users/users.c +++ b/src/modules/users/users.c @@ -225,5 +225,5 @@ FFModuleBaseInfo ffUsersModuleInfo = { { "Days of year after login", "days-of-year" }, { "Years fraction after login", "years-fraction" }, })), - .defaultOrder = 57, + .defaultOrder = 58, }; diff --git a/src/modules/version/version.c b/src/modules/version/version.c index 13e6f52b0..c744343fb 100644 --- a/src/modules/version/version.c +++ b/src/modules/version/version.c @@ -136,5 +136,5 @@ FFModuleBaseInfo ffVersionModuleInfo = { { "Compiler used when compiling", "compiler" }, { "Libc used when compiling", "libc" }, })), - .defaultOrder = 70, + .defaultOrder = 71, }; diff --git a/src/modules/vulkan/vulkan.c b/src/modules/vulkan/vulkan.c index 938c67e23..c8586b6f5 100644 --- a/src/modules/vulkan/vulkan.c +++ b/src/modules/vulkan/vulkan.c @@ -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, }; diff --git a/src/modules/weather/weather.c b/src/modules/weather/weather.c index 17753251c..ac9dc229c 100644 --- a/src/modules/weather/weather.c +++ b/src/modules/weather/weather.c @@ -125,5 +125,5 @@ FFModuleBaseInfo ffWeatherModuleInfo = { .formatArgs = FF_FORMAT_ARG_LIST(((FFModuleFormatArg[]) { { "Weather result", "result" }, })), - .defaultOrder = 65, + .defaultOrder = 66, }; diff --git a/src/modules/wifi/wifi.c b/src/modules/wifi/wifi.c index c48e46f51..ee723c497 100644 --- a/src/modules/wifi/wifi.c +++ b/src/modules/wifi/wifi.c @@ -261,5 +261,5 @@ FFModuleBaseInfo ffWifiModuleInfo = { { "Connection channel width in MHz", "channel-width" }, { "Connection channel band in GHz", "band" }, })), - .defaultOrder = 51, + .defaultOrder = 52, }; diff --git a/src/modules/zpool/zpool.c b/src/modules/zpool/zpool.c index 440e58d86..a1dd31032 100644 --- a/src/modules/zpool/zpool.c +++ b/src/modules/zpool/zpool.c @@ -238,5 +238,5 @@ FFModuleBaseInfo ffZpoolModuleInfo = { { "Fragmentation percentage bar", "frag-percentage-bar" }, { "Is read-only", "is-readonly" }, })), - .defaultOrder = 43, + .defaultOrder = 44, }; diff --git a/src/options/display.c b/src/options/display.c index 3dbfce1ec..f764e2d7a 100644 --- a/src/options/display.c +++ b/src/options/display.c @@ -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