master ohmu/fastfetch-openbsd / src / common / impl / jsonconfig.c
ohmu  ·  2026-10-15
  1#include "fastfetch.h"
  2#include "common/color.h"
  3#include "common/debug.h"
  4#include "common/jsonconfig.h"
  5#include "common/printing.h"
  6#include "common/io.h"
  7#include "common/time.h"
  8#include "common/strutil.h"
  9#include "detection/version/version.h"
 10#include "modules/modules.h"
 11
 12#include <assert.h>
 13#include <ctype.h>
 14#include <inttypes.h>
 15
 16bool ffJsonConfigParseModuleArgs(yyjson_val* key, yyjson_val* val, FFModuleArgs* moduleArgs) {
 17    if (unsafe_yyjson_equals_str(key, "type") || unsafe_yyjson_equals_str(key, "condition")) {
 18        return true;
 19    }
 20
 21    if (unsafe_yyjson_equals_str(key, "key")) {
 22        ffStrbufSetJsonVal(&moduleArgs->key, val);
 23        return true;
 24    } else if (unsafe_yyjson_equals_str(key, "format")) {
 25        ffStrbufSetJsonVal(&moduleArgs->outputFormat, val);
 26        return true;
 27    } else if (unsafe_yyjson_equals_str(key, "outputColor")) {
 28        ffOptionParseColor(yyjson_get_str(val), &moduleArgs->outputColor);
 29        return true;
 30    } else if (unsafe_yyjson_equals_str(key, "keyColor")) {
 31        ffOptionParseColor(yyjson_get_str(val), &moduleArgs->keyColor);
 32        return true;
 33    } else if (unsafe_yyjson_equals_str(key, "keyWidth")) {
 34        moduleArgs->keyWidth = (uint32_t) yyjson_get_uint(val);
 35        return true;
 36    } else if (unsafe_yyjson_equals_str(key, "keyIcon")) {
 37        ffStrbufSetJsonVal(&moduleArgs->keyIcon, val);
 38        return true;
 39    }
 40    return false;
 41}
 42
 43void ffJsonConfigGenerateModuleArgsConfig(yyjson_mut_doc* doc, yyjson_mut_val* module, FFModuleArgs* moduleArgs) {
 44    if (moduleArgs->key.length > 0) {
 45        yyjson_mut_obj_add_strbuf(doc, module, "key", &moduleArgs->key);
 46    }
 47    if (moduleArgs->outputFormat.length > 0) {
 48        yyjson_mut_obj_add_strbuf(doc, module, "format", &moduleArgs->outputFormat);
 49    }
 50    if (moduleArgs->outputColor.length > 0) {
 51        yyjson_mut_obj_add_strbuf(doc, module, "outputColor", &moduleArgs->outputColor);
 52    }
 53    if (moduleArgs->keyColor.length > 0) {
 54        yyjson_mut_obj_add_strbuf(doc, module, "keyColor", &moduleArgs->keyColor);
 55    }
 56    if (moduleArgs->keyWidth > 0) {
 57        yyjson_mut_obj_add_uint(doc, module, "keyWidth", moduleArgs->keyWidth);
 58    }
 59    if (moduleArgs->keyIcon.length > 0) {
 60        yyjson_mut_obj_add_strbuf(doc, module, "keyIcon", &moduleArgs->keyIcon);
 61    }
 62}
 63
 64bool ffJsonConfigParseUInt32(yyjson_val* val, uint32_t* result, uint32_t max) {
 65    if (!yyjson_is_int(val)) {
 66        return false;
 67    }
 68
 69    // `yyjson_get_sint()` is exact for every value that fits into `int64_t`, and is negative for the
 70    // `uint64_t` values above `INT64_MAX`, which no caller accepts anyway.
 71    int64_t num = unsafe_yyjson_get_sint(val);
 72    if (num < 0 || (uint64_t) num > max) {
 73        return false;
 74    }
 75
 76    *result = (uint32_t) num;
 77    return true;
 78}
 79
 80const char* ffJsonConfigParseEnum(yyjson_val* val, int* result, FFKeyValuePair pairs[]) {
 81    if (yyjson_is_int(val)) {
 82        int intVal = yyjson_get_int(val);
 83
 84        for (const FFKeyValuePair* pPair = pairs; pPair->key; ++pPair) {
 85            if (intVal == pPair->value) {
 86                *result = pPair->value;
 87                return nullptr;
 88            }
 89        }
 90
 91        FF_DEBUG("The enum value %d is not one of the accepted integers", intVal);
 92        return "Invalid enum integer";
 93    } else if (yyjson_is_str(val)) {
 94        const char* strVal = yyjson_get_str(val);
 95        for (const FFKeyValuePair* pPair = pairs; pPair->key; ++pPair) {
 96            if (ffStrEqualsIgnCase(strVal, pPair->key)) {
 97                *result = pPair->value;
 98                return nullptr;
 99            }
100        }
101
102        FF_DEBUG("The enum string \"%s\" is not one of the accepted names", strVal);
103        return "Invalid enum string";
104    } else {
105        FF_DEBUG("The enum value is of JSON type %s, not a string or an integer",
106            yyjson_get_type_desc(val));
107        return "Invalid enum value type; must be a string or integer";
108    }
109}
110
111static bool parseModuleJsonObject(const char* type, yyjson_val* jsonVal, yyjson_mut_doc* jsonDoc) {
112    if (!ffCharIsEnglishAlphabet(type[0])) {
113        return false;
114    }
115
116    for (FFModuleBaseInfo** modules = ffModuleInfos[toupper(type[0]) - 'A']; *modules; ++modules) {
117        FFModuleBaseInfo* baseInfo = *modules;
118        if (ffStrEqualsIgnCase(type, baseInfo->name)) {
119            alignas(uint64_t) uint8_t optionBuf[FF_OPTION_MAX_SIZE];
120            baseInfo->initOptions(optionBuf);
121            if (jsonVal) {
122                baseInfo->parseJsonObject(optionBuf, jsonVal);
123            }
124            bool succeeded;
125            if (jsonDoc) {
126                yyjson_mut_val* module = yyjson_mut_arr_add_obj(jsonDoc, jsonDoc->root);
127                yyjson_mut_obj_add_str(jsonDoc, module, "type", baseInfo->name);
128                if (baseInfo->generateJsonResult) {
129                    succeeded = baseInfo->generateJsonResult(optionBuf, jsonDoc, module);
130                } else {
131                    yyjson_mut_obj_add_str(jsonDoc, module, "error", "Unsupported for JSON format");
132                    succeeded = false;
133                }
134            } else {
135                succeeded = baseInfo->printModule(optionBuf);
136            }
137            baseInfo->destroyOptions(optionBuf);
138            return succeeded;
139        }
140    }
141
142    if (jsonDoc) {
143        yyjson_mut_val* module = yyjson_mut_arr_add_obj(jsonDoc, jsonDoc->root);
144        yyjson_mut_obj_add_strcpy(jsonDoc, module, "type", type);
145        yyjson_mut_obj_add_str(jsonDoc, module, "error", "Unknown module type");
146    } else {
147        FFModuleArgs moduleArgs;
148        ffOptionInitModuleArg(&moduleArgs, "");
149        ffPrintError(type, 0, &moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown module type");
150        ffOptionDestroyModuleArg(&moduleArgs);
151    }
152    return false;
153}
154
155static void prepareModuleJsonObject(const char* type, yyjson_val* module) {
156    switch (type[0]) {
157        // Both `cpuusage` and `command` start with a c, and they are disabled by separate options,
158        // so the block has to survive either one being off -- otherwise a build with only the other
159        // one enabled would never prepare this module (see the same guard in `commandoption.c`).
160        #if !FF_MODULE_DISABLE_CPUUSAGE || !FF_MODULE_DISABLE_COMMAND
161        case 'c':
162        case 'C': {
163            if (ffStrEqualsIgnCase(type, ffCPUUsageModuleInfo.name)) {
164                ffPrepareCPUUsage();
165            } else if (ffStrEqualsIgnCase(type, ffCommandModuleInfo.name)) {
166                [[gnu::cleanup(ffDestroyCommandOptions)]] FFCommandOptions options;
167                ffInitCommandOptions(&options);
168                if (module) {
169                    ffCommandModuleInfo.parseJsonObject(&options, module);
170                }
171                ffPrepareCommand(&options);
172            }
173            break;
174        }
175        #endif
176
177        #if !FF_MODULE_DISABLE_DISKIO
178        case 'd':
179        case 'D': {
180            if (ffStrEqualsIgnCase(type, ffDiskIOModuleInfo.name)) {
181                [[gnu::cleanup(ffDestroyDiskIOOptions)]] FFDiskIOOptions options;
182                ffInitDiskIOOptions(&options);
183                if (module) {
184                    ffDiskIOModuleInfo.parseJsonObject(&options, module);
185                }
186                ffPrepareDiskIO(&options);
187            }
188            break;
189        }
190        #endif
191
192        #if !FF_MODULE_DISABLE_NETIO
193        case 'n':
194        case 'N': {
195            if (ffStrEqualsIgnCase(type, ffNetIOModuleInfo.name)) {
196                [[gnu::cleanup(ffDestroyNetIOOptions)]] FFNetIOOptions options;
197                ffInitNetIOOptions(&options);
198                if (module) {
199                    ffNetIOModuleInfo.parseJsonObject(&options, module);
200                }
201                ffPrepareNetIO(&options);
202            }
203            break;
204        }
205        #endif
206
207        #if !FF_MODULE_DISABLE_PUBLICIP
208        case 'p':
209        case 'P': {
210            if (ffStrEqualsIgnCase(type, ffPublicIPModuleInfo.name)) {
211                [[gnu::cleanup(ffDestroyPublicIpOptions)]] FFPublicIPOptions options;
212                ffInitPublicIpOptions(&options);
213                if (module) {
214                    ffPublicIPModuleInfo.parseJsonObject(&options, module);
215                }
216                ffPreparePublicIp(&options);
217            }
218            break;
219        }
220        #endif
221
222        #if !FF_MODULE_DISABLE_TOP
223        case 't':
224        case 'T': {
225            if (ffStrEqualsIgnCase(type, ffTopModuleInfo.name)) {
226                [[gnu::cleanup(ffDestroyTopOptions)]] FFTopOptions options;
227                ffInitTopOptions(&options);
228                if (module) {
229                    ffTopModuleInfo.parseJsonObject(&options, module);
230                }
231                ffPrepareTopProcesses(options.showTypes);
232            }
233            break;
234        }
235        #endif
236
237        #if !FF_MODULE_DISABLE_WEATHER
238        case 'w':
239        case 'W': {
240            if (ffStrEqualsIgnCase(type, ffWeatherModuleInfo.name)) {
241                [[gnu::cleanup(ffDestroyWeatherOptions)]] FFWeatherOptions options;
242                ffInitWeatherOptions(&options);
243                if (module) {
244                    ffWeatherModuleInfo.parseJsonObject(&options, module);
245                }
246                ffPrepareWeather(&options);
247            }
248            break;
249        }
250        #endif
251    }
252}
253
254static bool matchesJsonArray(const char* str, yyjson_val* val) {
255    assert(val);
256
257    if (unsafe_yyjson_is_str(val)) {
258        return ffStrEqualsIgnCase(str, unsafe_yyjson_get_str(val));
259    }
260
261    if (!unsafe_yyjson_is_arr(val)) {
262        return false;
263    }
264
265    size_t idx, max;
266    yyjson_val* item;
267    yyjson_arr_foreach (val, idx, max, item) {
268        if (yyjson_is_str(item) && ffStrEqualsIgnCase(str, unsafe_yyjson_get_str(item))) {
269            return true;
270        }
271    }
272    return false;
273}
274
275static const char* printJsonConfig(FFdata* data, bool prepare) {
276    yyjson_mut_doc* jsonDoc = data->resultDoc;
277    yyjson_val* const root = yyjson_doc_get_root(data->configDoc);
278    assert(root);
279
280    if (!yyjson_is_obj(root)) {
281        FF_DEBUG("The config root is of JSON type %s", yyjson_get_type_desc(root));
282        return "Invalid JSON config format. Root value must be an object";
283    }
284
285    yyjson_val* modules = yyjson_obj_get(root, "modules");
286    if (!modules) {
287        return nullptr;
288    }
289    if (!yyjson_is_arr(modules)) {
290        FF_DEBUG("'modules' is of JSON type %s", yyjson_get_type_desc(modules));
291        return "Property 'modules' must be an array of strings or objects";
292    }
293
294    bool succeeded = true;
295    int32_t thres = instance.config.display.stat;
296    yyjson_val* item;
297    size_t idx, max;
298    yyjson_arr_foreach (modules, idx, max, item) {
299        double ms = 0;
300        if (!prepare && thres >= 0) {
301            ms = ffTimeGetTick();
302        }
303
304        yyjson_val* module = item;
305        bool gatedOnSucceeded = false;
306        const char* type = yyjson_get_str(module);
307        if (type) {
308            module = nullptr;
309        } else if (yyjson_is_obj(module)) {
310            yyjson_val* conditions = yyjson_obj_get(module, "condition");
311            if (conditions) {
312                if (!yyjson_is_obj(conditions)) {
313                    FF_DEBUG("'condition' is of JSON type %s", yyjson_get_type_desc(conditions));
314                    return "Property 'condition' must be an object";
315                }
316
317                yyjson_val* system = yyjson_obj_get(conditions, "system");
318                if (system && !matchesJsonArray(ffVersionResult.sysName, system)) {
319                    continue;
320                }
321
322                system = yyjson_obj_get(conditions, "!system");
323                if (system && matchesJsonArray(ffVersionResult.sysName, system)) {
324                    continue;
325                }
326
327                yyjson_val* arch = yyjson_obj_get(conditions, "arch");
328                if (arch && !matchesJsonArray(ffVersionResult.architecture, arch)) {
329                    continue;
330                }
331
332                arch = yyjson_obj_get(conditions, "!arch");
333                if (arch && matchesJsonArray(ffVersionResult.architecture, arch)) {
334                    continue;
335                }
336
337                yyjson_val* previousSucceeded = yyjson_obj_get(conditions, "succeeded");
338                if (previousSucceeded && !unsafe_yyjson_is_null(previousSucceeded)) {
339                    if (!unsafe_yyjson_is_bool(previousSucceeded)) {
340                        FF_DEBUG("'condition.succeeded' is of JSON type %s",
341                            yyjson_get_type_desc(previousSucceeded));
342                        return "Property 'succeeded' in 'condition' must be a boolean";
343                    }
344                    gatedOnSucceeded = true;
345
346                    if (prepare) {
347                        // Whether this module is printed at all depends on the result of the previous
348                        // one, which is only known in the print pass. Preparing it here would leave a
349                        // result behind that no print pass ever consumes, and since the modules are
350                        // paired by position (see `ffPrepareCommand` / `ffDetectCommand`), that
351                        // would silently shift every later module onto the wrong result.
352                        // Skip it, and let the module report the missing preparation if it turns out
353                        // to be printed after all.
354                        continue;
355                    }
356
357                    #if !FF_MODULE_DISABLE_COMMAND
358                    // A `command` module that runs in parallel is exactly the pairing above, so this
359                    // combination cannot be left to the print pass: the module would be handed the
360                    // result prepared for the next `command` module and print that instead of its
361                    // own output. Refused here rather than in the prepare pass, because returning
362                    // from there would leave every later module unprepared -- which the print pass
363                    // would then report as a failure of theirs.
364                    // `parallel` is on by default, so leaving the key out is the parallel case too.
365                    if (gatedOnSucceeded && ffStrEqualsIgnCase(yyjson_get_str(yyjson_obj_get(module, "type")), ffCommandModuleInfo.name)) {
366                        yyjson_val* parallel = yyjson_obj_get(module, "parallel");
367                        if (parallel == nullptr || yyjson_get_bool(parallel)) {
368                            FF_DEBUG("The command module at index %zu is gated on 'condition.succeeded' and runs in parallel", idx);
369                            return "Module \"command\" cannot be combined with \"condition.succeeded\" while it runs in "
370                                   "parallel: whether it is printed is only known in the print pass, so it cannot be "
371                                   "prepared, and the prepared results are matched to modules by position. Set "
372                                   "\"parallel\": false on it, or drop the condition.";
373                        }
374                    }
375                    #endif
376
377                    if (succeeded != unsafe_yyjson_get_bool(previousSucceeded)) {
378                        continue;
379                    }
380                }
381            }
382
383            type = yyjson_get_str(yyjson_obj_get(module, "type"));
384            if (!type) {
385                FF_DEBUG("The module object at index %zu has no \"type\" key, or it is not a string", idx);
386                return "module object must contain a \"type\" key ( case sensitive )";
387            }
388            if (yyjson_obj_size(module) == 1) { // contains only Property type
389                module = nullptr;
390            }
391        } else {
392            FF_DEBUG("The modules entry at index %zu is of JSON type %s", idx, yyjson_get_type_desc(module));
393            return "modules must be an array of strings or objects";
394        }
395
396        if (ffStrbufSeparatedContainIgnCaseS(&data->structureDisabled, type, ':')) {
397            continue;
398        }
399
400        if (prepare) {
401            prepareModuleJsonObject(type, module);
402        } else {
403            succeeded = parseModuleJsonObject(type, module, jsonDoc);
404        }
405
406        if (!prepare && thres >= 0) {
407            ms = ffTimeGetTick() - ms;
408            if (jsonDoc) {
409                yyjson_mut_val* moduleJson = yyjson_mut_arr_get_last(jsonDoc->root);
410                yyjson_mut_obj_add_real(jsonDoc, moduleJson, "stat", ms);
411            } else {
412                char str[64];
413                int len = snprintf(str, sizeof str, "%.3fms", ms);
414                if (thres > 0) {
415                    snprintf(str, sizeof str, "\e[%sm%.3fms\e[m", (ms <= thres ? FF_COLOR_FG_GREEN : ms <= 2 * thres ? FF_COLOR_FG_YELLOW
416                                                                                                                     : FF_COLOR_FG_RED),
417                        ms);
418                }
419                printf("\e7\e[1A\e[9999999C\e[%dD%s\e8", len - 1, str); // Save; Up 1; Right 9999999; Left <len - 1>; Print <str>; Load
420            }
421        }
422
423#if defined(_WIN32)
424        if (!instance.config.display.noBuffer && !jsonDoc) {
425            fflush(stdout);
426        }
427#endif
428    }
429
430    return nullptr;
431}
432
433[[gnu::cold]]
434void ffPrintJsonConfig(FFdata* data, bool prepare) {
435    yyjson_mut_doc* jsonDoc = data->resultDoc;
436    const char* error = printJsonConfig(data, prepare);
437    if (error) {
438        if (jsonDoc) {
439            yyjson_mut_val* obj = yyjson_mut_obj(jsonDoc);
440            yyjson_mut_obj_add_str(jsonDoc, obj, "error", error);
441            yyjson_mut_doc_set_root(jsonDoc, obj);
442        } else {
443            ffPrintError("JsonConfig", 0, nullptr, FF_PRINT_TYPE_NO_CUSTOM_KEY, "%s", error);
444        }
445    }
446}