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}