mirror of
https://github.com/fastfetch-cli/fastfetch.git
synced 2026-10-28 07:44:06 +01:00
447 lines
17 KiB
C
447 lines
17 KiB
C
#include "fastfetch.h"
|
|
#include "common/color.h"
|
|
#include "common/debug.h"
|
|
#include "common/jsonconfig.h"
|
|
#include "common/printing.h"
|
|
#include "common/io.h"
|
|
#include "common/time.h"
|
|
#include "common/strutil.h"
|
|
#include "detection/version/version.h"
|
|
#include "modules/modules.h"
|
|
|
|
#include <assert.h>
|
|
#include <ctype.h>
|
|
#include <inttypes.h>
|
|
|
|
bool ffJsonConfigParseModuleArgs(yyjson_val* key, yyjson_val* val, FFModuleArgs* moduleArgs) {
|
|
if (unsafe_yyjson_equals_str(key, "type") || unsafe_yyjson_equals_str(key, "condition")) {
|
|
return true;
|
|
}
|
|
|
|
if (unsafe_yyjson_equals_str(key, "key")) {
|
|
ffStrbufSetJsonVal(&moduleArgs->key, val);
|
|
return true;
|
|
} else if (unsafe_yyjson_equals_str(key, "format")) {
|
|
ffStrbufSetJsonVal(&moduleArgs->outputFormat, val);
|
|
return true;
|
|
} else if (unsafe_yyjson_equals_str(key, "outputColor")) {
|
|
ffOptionParseColor(yyjson_get_str(val), &moduleArgs->outputColor);
|
|
return true;
|
|
} else if (unsafe_yyjson_equals_str(key, "keyColor")) {
|
|
ffOptionParseColor(yyjson_get_str(val), &moduleArgs->keyColor);
|
|
return true;
|
|
} else if (unsafe_yyjson_equals_str(key, "keyWidth")) {
|
|
moduleArgs->keyWidth = (uint32_t) yyjson_get_uint(val);
|
|
return true;
|
|
} else if (unsafe_yyjson_equals_str(key, "keyIcon")) {
|
|
ffStrbufSetJsonVal(&moduleArgs->keyIcon, val);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void ffJsonConfigGenerateModuleArgsConfig(yyjson_mut_doc* doc, yyjson_mut_val* module, FFModuleArgs* moduleArgs) {
|
|
if (moduleArgs->key.length > 0) {
|
|
yyjson_mut_obj_add_strbuf(doc, module, "key", &moduleArgs->key);
|
|
}
|
|
if (moduleArgs->outputFormat.length > 0) {
|
|
yyjson_mut_obj_add_strbuf(doc, module, "format", &moduleArgs->outputFormat);
|
|
}
|
|
if (moduleArgs->outputColor.length > 0) {
|
|
yyjson_mut_obj_add_strbuf(doc, module, "outputColor", &moduleArgs->outputColor);
|
|
}
|
|
if (moduleArgs->keyColor.length > 0) {
|
|
yyjson_mut_obj_add_strbuf(doc, module, "keyColor", &moduleArgs->keyColor);
|
|
}
|
|
if (moduleArgs->keyWidth > 0) {
|
|
yyjson_mut_obj_add_uint(doc, module, "keyWidth", moduleArgs->keyWidth);
|
|
}
|
|
if (moduleArgs->keyIcon.length > 0) {
|
|
yyjson_mut_obj_add_strbuf(doc, module, "keyIcon", &moduleArgs->keyIcon);
|
|
}
|
|
}
|
|
|
|
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);
|
|
|
|
for (const FFKeyValuePair* pPair = pairs; pPair->key; ++pPair) {
|
|
if (intVal == pPair->value) {
|
|
*result = pPair->value;
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
FF_DEBUG("The enum value %d is not one of the accepted integers", intVal);
|
|
return "Invalid enum integer";
|
|
} else if (yyjson_is_str(val)) {
|
|
const char* strVal = yyjson_get_str(val);
|
|
for (const FFKeyValuePair* pPair = pairs; pPair->key; ++pPair) {
|
|
if (ffStrEqualsIgnCase(strVal, pPair->key)) {
|
|
*result = pPair->value;
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
FF_DEBUG("The enum string \"%s\" is not one of the accepted names", strVal);
|
|
return "Invalid enum string";
|
|
} else {
|
|
FF_DEBUG("The enum value is of JSON type %s, not a string or an integer",
|
|
yyjson_get_type_desc(val));
|
|
return "Invalid enum value type; must be a string or integer";
|
|
}
|
|
}
|
|
|
|
static bool parseModuleJsonObject(const char* type, yyjson_val* jsonVal, yyjson_mut_doc* jsonDoc) {
|
|
if (!ffCharIsEnglishAlphabet(type[0])) {
|
|
return false;
|
|
}
|
|
|
|
for (FFModuleBaseInfo** modules = ffModuleInfos[toupper(type[0]) - 'A']; *modules; ++modules) {
|
|
FFModuleBaseInfo* baseInfo = *modules;
|
|
if (ffStrEqualsIgnCase(type, baseInfo->name)) {
|
|
alignas(uint64_t) uint8_t optionBuf[FF_OPTION_MAX_SIZE];
|
|
baseInfo->initOptions(optionBuf);
|
|
if (jsonVal) {
|
|
baseInfo->parseJsonObject(optionBuf, jsonVal);
|
|
}
|
|
bool succeeded;
|
|
if (jsonDoc) {
|
|
yyjson_mut_val* module = yyjson_mut_arr_add_obj(jsonDoc, jsonDoc->root);
|
|
yyjson_mut_obj_add_str(jsonDoc, module, "type", baseInfo->name);
|
|
if (baseInfo->generateJsonResult) {
|
|
succeeded = baseInfo->generateJsonResult(optionBuf, jsonDoc, module);
|
|
} else {
|
|
yyjson_mut_obj_add_str(jsonDoc, module, "error", "Unsupported for JSON format");
|
|
succeeded = false;
|
|
}
|
|
} else {
|
|
succeeded = baseInfo->printModule(optionBuf);
|
|
}
|
|
baseInfo->destroyOptions(optionBuf);
|
|
return succeeded;
|
|
}
|
|
}
|
|
|
|
if (jsonDoc) {
|
|
yyjson_mut_val* module = yyjson_mut_arr_add_obj(jsonDoc, jsonDoc->root);
|
|
yyjson_mut_obj_add_strcpy(jsonDoc, module, "type", type);
|
|
yyjson_mut_obj_add_str(jsonDoc, module, "error", "Unknown module type");
|
|
} else {
|
|
FFModuleArgs moduleArgs;
|
|
ffOptionInitModuleArg(&moduleArgs, "");
|
|
ffPrintError(type, 0, &moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown module type");
|
|
ffOptionDestroyModuleArg(&moduleArgs);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static void prepareModuleJsonObject(const char* type, yyjson_val* module) {
|
|
switch (type[0]) {
|
|
// Both `cpuusage` and `command` start with a c, and they are disabled by separate options,
|
|
// so the block has to survive either one being off -- otherwise a build with only the other
|
|
// one enabled would never prepare this module (see the same guard in `commandoption.c`).
|
|
#if !FF_MODULE_DISABLE_CPUUSAGE || !FF_MODULE_DISABLE_COMMAND
|
|
case 'c':
|
|
case 'C': {
|
|
if (ffStrEqualsIgnCase(type, ffCPUUsageModuleInfo.name)) {
|
|
ffPrepareCPUUsage();
|
|
} else if (ffStrEqualsIgnCase(type, ffCommandModuleInfo.name)) {
|
|
[[gnu::cleanup(ffDestroyCommandOptions)]] FFCommandOptions options;
|
|
ffInitCommandOptions(&options);
|
|
if (module) {
|
|
ffCommandModuleInfo.parseJsonObject(&options, module);
|
|
}
|
|
ffPrepareCommand(&options);
|
|
}
|
|
break;
|
|
}
|
|
#endif
|
|
|
|
#if !FF_MODULE_DISABLE_DISKIO
|
|
case 'd':
|
|
case 'D': {
|
|
if (ffStrEqualsIgnCase(type, ffDiskIOModuleInfo.name)) {
|
|
[[gnu::cleanup(ffDestroyDiskIOOptions)]] FFDiskIOOptions options;
|
|
ffInitDiskIOOptions(&options);
|
|
if (module) {
|
|
ffDiskIOModuleInfo.parseJsonObject(&options, module);
|
|
}
|
|
ffPrepareDiskIO(&options);
|
|
}
|
|
break;
|
|
}
|
|
#endif
|
|
|
|
#if !FF_MODULE_DISABLE_NETIO
|
|
case 'n':
|
|
case 'N': {
|
|
if (ffStrEqualsIgnCase(type, ffNetIOModuleInfo.name)) {
|
|
[[gnu::cleanup(ffDestroyNetIOOptions)]] FFNetIOOptions options;
|
|
ffInitNetIOOptions(&options);
|
|
if (module) {
|
|
ffNetIOModuleInfo.parseJsonObject(&options, module);
|
|
}
|
|
ffPrepareNetIO(&options);
|
|
}
|
|
break;
|
|
}
|
|
#endif
|
|
|
|
#if !FF_MODULE_DISABLE_PUBLICIP
|
|
case 'p':
|
|
case 'P': {
|
|
if (ffStrEqualsIgnCase(type, ffPublicIPModuleInfo.name)) {
|
|
[[gnu::cleanup(ffDestroyPublicIpOptions)]] FFPublicIPOptions options;
|
|
ffInitPublicIpOptions(&options);
|
|
if (module) {
|
|
ffPublicIPModuleInfo.parseJsonObject(&options, module);
|
|
}
|
|
ffPreparePublicIp(&options);
|
|
}
|
|
break;
|
|
}
|
|
#endif
|
|
|
|
#if !FF_MODULE_DISABLE_TOP
|
|
case 't':
|
|
case 'T': {
|
|
if (ffStrEqualsIgnCase(type, ffTopModuleInfo.name)) {
|
|
[[gnu::cleanup(ffDestroyTopOptions)]] FFTopOptions options;
|
|
ffInitTopOptions(&options);
|
|
if (module) {
|
|
ffTopModuleInfo.parseJsonObject(&options, module);
|
|
}
|
|
ffPrepareTopProcesses(options.showTypes);
|
|
}
|
|
break;
|
|
}
|
|
#endif
|
|
|
|
#if !FF_MODULE_DISABLE_WEATHER
|
|
case 'w':
|
|
case 'W': {
|
|
if (ffStrEqualsIgnCase(type, ffWeatherModuleInfo.name)) {
|
|
[[gnu::cleanup(ffDestroyWeatherOptions)]] FFWeatherOptions options;
|
|
ffInitWeatherOptions(&options);
|
|
if (module) {
|
|
ffWeatherModuleInfo.parseJsonObject(&options, module);
|
|
}
|
|
ffPrepareWeather(&options);
|
|
}
|
|
break;
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
static bool matchesJsonArray(const char* str, yyjson_val* val) {
|
|
assert(val);
|
|
|
|
if (unsafe_yyjson_is_str(val)) {
|
|
return ffStrEqualsIgnCase(str, unsafe_yyjson_get_str(val));
|
|
}
|
|
|
|
if (!unsafe_yyjson_is_arr(val)) {
|
|
return false;
|
|
}
|
|
|
|
size_t idx, max;
|
|
yyjson_val* item;
|
|
yyjson_arr_foreach (val, idx, max, item) {
|
|
if (yyjson_is_str(item) && ffStrEqualsIgnCase(str, unsafe_yyjson_get_str(item))) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static const char* printJsonConfig(FFdata* data, bool prepare) {
|
|
yyjson_mut_doc* jsonDoc = data->resultDoc;
|
|
yyjson_val* const root = yyjson_doc_get_root(data->configDoc);
|
|
assert(root);
|
|
|
|
if (!yyjson_is_obj(root)) {
|
|
FF_DEBUG("The config root is of JSON type %s", yyjson_get_type_desc(root));
|
|
return "Invalid JSON config format. Root value must be an object";
|
|
}
|
|
|
|
yyjson_val* modules = yyjson_obj_get(root, "modules");
|
|
if (!modules) {
|
|
return nullptr;
|
|
}
|
|
if (!yyjson_is_arr(modules)) {
|
|
FF_DEBUG("'modules' is of JSON type %s", yyjson_get_type_desc(modules));
|
|
return "Property 'modules' must be an array of strings or objects";
|
|
}
|
|
|
|
bool succeeded = true;
|
|
int32_t thres = instance.config.display.stat;
|
|
yyjson_val* item;
|
|
size_t idx, max;
|
|
yyjson_arr_foreach (modules, idx, max, item) {
|
|
double ms = 0;
|
|
if (!prepare && thres >= 0) {
|
|
ms = ffTimeGetTick();
|
|
}
|
|
|
|
yyjson_val* module = item;
|
|
bool gatedOnSucceeded = false;
|
|
const char* type = yyjson_get_str(module);
|
|
if (type) {
|
|
module = nullptr;
|
|
} else if (yyjson_is_obj(module)) {
|
|
yyjson_val* conditions = yyjson_obj_get(module, "condition");
|
|
if (conditions) {
|
|
if (!yyjson_is_obj(conditions)) {
|
|
FF_DEBUG("'condition' is of JSON type %s", yyjson_get_type_desc(conditions));
|
|
return "Property 'condition' must be an object";
|
|
}
|
|
|
|
yyjson_val* system = yyjson_obj_get(conditions, "system");
|
|
if (system && !matchesJsonArray(ffVersionResult.sysName, system)) {
|
|
continue;
|
|
}
|
|
|
|
system = yyjson_obj_get(conditions, "!system");
|
|
if (system && matchesJsonArray(ffVersionResult.sysName, system)) {
|
|
continue;
|
|
}
|
|
|
|
yyjson_val* arch = yyjson_obj_get(conditions, "arch");
|
|
if (arch && !matchesJsonArray(ffVersionResult.architecture, arch)) {
|
|
continue;
|
|
}
|
|
|
|
arch = yyjson_obj_get(conditions, "!arch");
|
|
if (arch && matchesJsonArray(ffVersionResult.architecture, arch)) {
|
|
continue;
|
|
}
|
|
|
|
yyjson_val* previousSucceeded = yyjson_obj_get(conditions, "succeeded");
|
|
if (previousSucceeded && !unsafe_yyjson_is_null(previousSucceeded)) {
|
|
if (!unsafe_yyjson_is_bool(previousSucceeded)) {
|
|
FF_DEBUG("'condition.succeeded' is of JSON type %s",
|
|
yyjson_get_type_desc(previousSucceeded));
|
|
return "Property 'succeeded' in 'condition' must be a boolean";
|
|
}
|
|
gatedOnSucceeded = true;
|
|
|
|
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 !FF_MODULE_DISABLE_COMMAND
|
|
// A `command` module that runs in parallel is exactly the pairing above, so this
|
|
// combination cannot be left to the print pass: the module would be handed the
|
|
// result prepared for the next `command` module and print that instead of its
|
|
// own output. Refused here rather than in the prepare pass, because returning
|
|
// from there would leave every later module unprepared -- which the print pass
|
|
// would then report as a failure of theirs.
|
|
// `parallel` is on by default, so leaving the key out is the parallel case too.
|
|
if (gatedOnSucceeded && ffStrEqualsIgnCase(yyjson_get_str(yyjson_obj_get(module, "type")), ffCommandModuleInfo.name)) {
|
|
yyjson_val* parallel = yyjson_obj_get(module, "parallel");
|
|
if (parallel == nullptr || yyjson_get_bool(parallel)) {
|
|
FF_DEBUG("The command module at index %zu is gated on 'condition.succeeded' and runs in parallel", idx);
|
|
return "Module \"command\" cannot be combined with \"condition.succeeded\" while it runs in "
|
|
"parallel: whether it is printed is only known in the print pass, so it cannot be "
|
|
"prepared, and the prepared results are matched to modules by position. Set "
|
|
"\"parallel\": false on it, or drop the condition.";
|
|
}
|
|
}
|
|
#endif
|
|
|
|
if (succeeded != unsafe_yyjson_get_bool(previousSucceeded)) {
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
type = yyjson_get_str(yyjson_obj_get(module, "type"));
|
|
if (!type) {
|
|
FF_DEBUG("The module object at index %zu has no \"type\" key, or it is not a string", idx);
|
|
return "module object must contain a \"type\" key ( case sensitive )";
|
|
}
|
|
if (yyjson_obj_size(module) == 1) { // contains only Property type
|
|
module = nullptr;
|
|
}
|
|
} else {
|
|
FF_DEBUG("The modules entry at index %zu is of JSON type %s", idx, yyjson_get_type_desc(module));
|
|
return "modules must be an array of strings or objects";
|
|
}
|
|
|
|
if (ffStrbufSeparatedContainIgnCaseS(&data->structureDisabled, type, ':')) {
|
|
continue;
|
|
}
|
|
|
|
if (prepare) {
|
|
prepareModuleJsonObject(type, module);
|
|
} else {
|
|
succeeded = parseModuleJsonObject(type, module, jsonDoc);
|
|
}
|
|
|
|
if (!prepare && thres >= 0) {
|
|
ms = ffTimeGetTick() - ms;
|
|
if (jsonDoc) {
|
|
yyjson_mut_val* moduleJson = yyjson_mut_arr_get_last(jsonDoc->root);
|
|
yyjson_mut_obj_add_real(jsonDoc, moduleJson, "stat", ms);
|
|
} else {
|
|
char str[64];
|
|
int len = snprintf(str, sizeof str, "%.3fms", ms);
|
|
if (thres > 0) {
|
|
snprintf(str, sizeof str, "\e[%sm%.3fms\e[m", (ms <= thres ? FF_COLOR_FG_GREEN : ms <= 2 * thres ? FF_COLOR_FG_YELLOW
|
|
: FF_COLOR_FG_RED),
|
|
ms);
|
|
}
|
|
printf("\e7\e[1A\e[9999999C\e[%dD%s\e8", len - 1, str); // Save; Up 1; Right 9999999; Left <len - 1>; Print <str>; Load
|
|
}
|
|
}
|
|
|
|
#if defined(_WIN32)
|
|
if (!instance.config.display.noBuffer && !jsonDoc) {
|
|
fflush(stdout);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
[[gnu::cold]]
|
|
void ffPrintJsonConfig(FFdata* data, bool prepare) {
|
|
yyjson_mut_doc* jsonDoc = data->resultDoc;
|
|
const char* error = printJsonConfig(data, prepare);
|
|
if (error) {
|
|
if (jsonDoc) {
|
|
yyjson_mut_val* obj = yyjson_mut_obj(jsonDoc);
|
|
yyjson_mut_obj_add_str(jsonDoc, obj, "error", error);
|
|
yyjson_mut_doc_set_root(jsonDoc, obj);
|
|
} else {
|
|
ffPrintError("JsonConfig", 0, nullptr, FF_PRINT_TYPE_NO_CUSTOM_KEY, "%s", error);
|
|
}
|
|
}
|
|
}
|