ohmu
·
2026-10-15
1#include "common/commandoption.h"
2#include "common/color.h"
3#include "common/printing.h"
4#include "common/time.h"
5#include "common/jsonconfig.h"
6#include "common/strutil.h"
7#include "fastfetch_datatext.h"
8#include "modules/modules.h"
9
10#include <ctype.h>
11#include <inttypes.h>
12
13bool ffParseModuleOptions(const char* key, const char* value) {
14 if (!ffStrStartsWith(key, "--") || !ffCharIsEnglishAlphabet(key[2])) {
15 return false;
16 }
17 if (value && !*value) {
18 value = nullptr;
19 }
20 for (FFModuleBaseInfo** modules = ffModuleInfos[toupper(key[2]) - 'A']; *modules; ++modules) {
21 FFModuleBaseInfo* baseInfo = *modules;
22 const char* subKey = ffOptionTestPrefix(key, baseInfo->name);
23 if (subKey != nullptr) {
24 if (subKey[0] == '\0' || subKey[0] == '-') // Key is exactly the module name or has a leading '-'
25 {
26 fprintf(stderr, "Error: unknown module key %s\n", key);
27 exit(477);
28 }
29
30 FF_STRBUF_AUTO_DESTROY moduleName = ffStrbufCreateS(baseInfo->name);
31 ffStrbufLowerCase(&moduleName);
32
33 FF_STRBUF_AUTO_DESTROY jsonKey = ffStrbufCreate();
34 bool flag = false;
35 for (const char* p = subKey; *p; ++p) {
36 if (*p == '-') {
37 if (flag) {
38 fprintf(stderr, "Error: invalid double `-` in module key %s\n", key);
39 exit(477);
40 }
41 flag = true;
42 } else {
43 if (!isalpha((unsigned char) *p) && !isdigit((unsigned char) *p)) {
44 fprintf(stderr, "Error: invalid character `%c` in module key %s\n", *p, key);
45 exit(477);
46 }
47
48 if (flag) {
49 flag = false;
50 ffStrbufAppendC(&jsonKey, (char) toupper((unsigned char) *p));
51 } else {
52 ffStrbufAppendC(&jsonKey, *p);
53 }
54 }
55 }
56 fprintf(stderr, "Error: Unsupported module option: %s\n", key);
57 fputs(" Support of module options has been removed. Please add the flag to the JSON config instead.\n", stderr);
58 fprintf(stderr, " Example (demonstration only): `{ \"modules\": [ { \"type\": \"%s\", \"%s\": %s%s%s } ] }`\n", moduleName.chars, jsonKey.chars, value ? "\"" : "", value ?: "true", value ? "\"" : "");
59 fputs(" See <https://github.com/fastfetch-cli/fastfetch/wiki/Configuration> for more information.\n", stderr);
60 exit(477);
61 }
62 }
63 return false;
64}
65
66static yyjson_val* findStructureModuleConfig(FFdata* data, const char* moduleType);
67
68// The module options live in the JSON config only. They must be merged here as well as in
69// `parseStructureCommand`, because `ffPrepareXxx` takes the first snapshot while `ffDetectXxx`
70// takes the second one. If the two disagree, they list different entries and the deltas are
71// meaningless. Keeping both call sites on this single helper is what makes them agree.
72static void initStructureModuleOptions(FFdata* data, FFModuleBaseInfo* baseInfo, void* options) {
73 baseInfo->initOptions(options);
74
75 yyjson_val* configModule = findStructureModuleConfig(data, baseInfo->name);
76 if (configModule != nullptr) {
77 baseInfo->parseJsonObject(options, configModule);
78 }
79}
80
81void ffPrepareCommandOption(FFdata* data) {
82 char* moduleType = nullptr;
83 size_t moduleLen = 0;
84 while (ffStrbufGetdelim(&moduleType, &moduleLen, ':', &data->structure)) {
85#define FF_IF_MODULE_MATCH(moduleNameConstant) if (ffStrEqualsIgnCase(moduleType, moduleNameConstant) && !ffStrbufSeparatedContainIgnCaseS(&data->structureDisabled, moduleNameConstant, ':'))
86
87 switch (moduleType[0]) {
88#if !FF_MODULE_DISABLE_CPUUSAGE || !FF_MODULE_DISABLE_COMMAND
89 case 'C':
90 case 'c':
91#endif
92#if !FF_MODULE_DISABLE_CPUUSAGE
93 FF_IF_MODULE_MATCH(ffCPUUsageModuleInfo.name)
94 ffPrepareCPUUsage(); // The rate is derived from the CPU's own counters; no options are involved
95#endif
96
97#if !FF_MODULE_DISABLE_COMMAND
98 FF_IF_MODULE_MATCH(ffCommandModuleInfo.name) {
99 // `command` runs its shell commands in parallel: `ffPrepareCommand` spawns them here,
100 // in the prepare pass, and `ffDetectCommand` collects them in the print pass. The
101 // command line itself can only be read from the JSON config, and this code path does
102 // not consult it, so nothing would ever be queued and the module would report an
103 // internal error marker. Refuse it up front instead of letting it get that far.
104 fputs("Error: module `command` is not supported with `--structure`\n", stderr);
105 fputs(" Its options can only be read from the JSON config, which `--structure` bypasses.\n", stderr);
106 fputs(" Add it to the config instead, e.g. `{ \"modules\": [ { \"type\": \"command\", \"text\": \"uname -r\" } ] }`\n", stderr);
107 exit(481);
108 }
109#endif
110#if !FF_MODULE_DISABLE_CPUUSAGE || !FF_MODULE_DISABLE_COMMAND
111 break;
112#endif
113
114#if !FF_MODULE_DISABLE_DISKIO
115 case 'D':
116 case 'd':
117 FF_IF_MODULE_MATCH(ffDiskIOModuleInfo.name) {
118 [[gnu::cleanup(ffDestroyDiskIOOptions)]] FFDiskIOOptions options;
119 initStructureModuleOptions(data, &ffDiskIOModuleInfo, &options);
120 ffPrepareDiskIO(&options);
121 }
122 break;
123#endif
124
125#if !FF_MODULE_DISABLE_NETIO
126 case 'N':
127 case 'n':
128 FF_IF_MODULE_MATCH(ffNetIOModuleInfo.name) {
129 [[gnu::cleanup(ffDestroyNetIOOptions)]] FFNetIOOptions options;
130 initStructureModuleOptions(data, &ffNetIOModuleInfo, &options);
131 ffPrepareNetIO(&options);
132 }
133 break;
134#endif
135
136#if !FF_MODULE_DISABLE_PUBLICIP
137 case 'P':
138 case 'p':
139 FF_IF_MODULE_MATCH(ffPublicIPModuleInfo.name) {
140 [[gnu::cleanup(ffDestroyPublicIpOptions)]] FFPublicIPOptions options;
141 initStructureModuleOptions(data, &ffPublicIPModuleInfo, &options);
142 ffPreparePublicIp(&options);
143 }
144 break;
145#endif
146
147#if !FF_MODULE_DISABLE_TOP
148 case 'T':
149 case 't':
150 FF_IF_MODULE_MATCH(ffTopModuleInfo.name) {
151 [[gnu::cleanup(ffDestroyTopOptions)]] FFTopOptions options;
152 initStructureModuleOptions(data, &ffTopModuleInfo, &options);
153 ffPrepareTopProcesses(options.showTypes);
154 }
155 break;
156#endif
157
158#if !FF_MODULE_DISABLE_WEATHER
159 case 'W':
160 case 'w':
161 FF_IF_MODULE_MATCH(ffWeatherModuleInfo.name) {
162 [[gnu::cleanup(ffDestroyWeatherOptions)]] FFWeatherOptions options;
163 initStructureModuleOptions(data, &ffWeatherModuleInfo, &options);
164 ffPrepareWeather(&options);
165 }
166 break;
167#endif
168 }
169
170#undef FF_IF_MODULE_MATCH
171 }
172}
173
174static void genJsonConfig(FFdata* data, FFModuleBaseInfo* baseInfo, void* options) {
175 yyjson_mut_doc* doc = data->resultDoc;
176
177 yyjson_mut_val* modules = yyjson_mut_obj_get(doc->root, "modules");
178 if (!modules) {
179 modules = yyjson_mut_obj_add_arr(doc, doc->root, "modules");
180 }
181
182 FF_STRBUF_AUTO_DESTROY type = ffStrbufCreateS(baseInfo->name);
183 ffStrbufLowerCase(&type);
184
185 if (data->docType == FF_RESULT_DOC_TYPE_CONFIG_FULL) {
186 yyjson_mut_val* module = yyjson_mut_obj(doc);
187 yyjson_mut_obj_add_strbuf(doc, module, "type", &type);
188
189 if (baseInfo->generateJsonConfig) {
190 baseInfo->generateJsonConfig(options, doc, module);
191 }
192
193 if (yyjson_mut_obj_size(module) > 1) {
194 yyjson_mut_arr_add_val(modules, module);
195 } else {
196 yyjson_mut_arr_add_strbuf(doc, modules, &type);
197 }
198 } else {
199 yyjson_mut_arr_add_strbuf(doc, modules, &type);
200 }
201}
202
203static void genJsonResult(FFdata* data, FFModuleBaseInfo* baseInfo, void* options) {
204 yyjson_mut_doc* doc = data->resultDoc;
205 yyjson_mut_val* module = yyjson_mut_arr_add_obj(doc, doc->root);
206 yyjson_mut_obj_add_str(doc, module, "type", baseInfo->name);
207 if (baseInfo->generateJsonResult) {
208 baseInfo->generateJsonResult(options, doc, module);
209 } else {
210 yyjson_mut_obj_add_str(doc, module, "error", "Unsupported for JSON format");
211 }
212}
213
214static yyjson_val* findStructureModuleConfig(FFdata* data, const char* moduleType) {
215 if (data->configDoc == nullptr) {
216 return nullptr;
217 }
218
219 yyjson_val* root = yyjson_doc_get_root(data->configDoc);
220 if (root == nullptr) {
221 return nullptr;
222 }
223
224 yyjson_val* modules = yyjson_obj_get(root, "modules");
225 if (!yyjson_is_arr(modules)) {
226 return nullptr;
227 }
228
229 yyjson_val* item;
230 size_t idx, max;
231 yyjson_arr_foreach (modules, idx, max, item) {
232 if (yyjson_is_obj(item)) {
233 const char* type = yyjson_get_str(yyjson_obj_get(item, "type"));
234 if (type != nullptr && ffStrEqualsIgnCase(type, moduleType)) {
235 return item;
236 }
237 }
238 }
239 return nullptr;
240}
241
242static bool parseStructureCommand(
243 FFdata* data,
244 const char* line,
245 void (*fn)(FFdata*, FFModuleBaseInfo* baseInfo, void* options)) {
246 if (ffCharIsEnglishAlphabet(line[0])) {
247 for (FFModuleBaseInfo** modules = ffModuleInfos[toupper(line[0]) - 'A']; *modules; ++modules) {
248 FFModuleBaseInfo* baseInfo = *modules;
249 if (ffStrEqualsIgnCase(line, baseInfo->name)) {
250 alignas(uint64_t) uint8_t optionBuf[FF_OPTION_MAX_SIZE];
251 initStructureModuleOptions(data, baseInfo, optionBuf);
252 if (data->resultDoc != nullptr) {
253 fn(data, baseInfo, optionBuf);
254 } else {
255 baseInfo->printModule(optionBuf);
256 }
257 baseInfo->destroyOptions(optionBuf);
258 return true;
259 }
260 }
261 }
262
263 if (data->resultDoc) {
264 yyjson_mut_doc* doc = data->resultDoc;
265 yyjson_mut_val* module = yyjson_mut_arr_add_obj(doc, doc->root);
266 yyjson_mut_obj_add_str(doc, module, "type", line);
267 yyjson_mut_obj_add_str(doc, module, "error", "Unknown module type");
268 } else {
269 ffPrintError(line, 0, nullptr, FF_PRINT_TYPE_NO_CUSTOM_KEY, "<no implementation provided>");
270 }
271 return false;
272}
273
274void ffPrintCommandOption(FFdata* data) {
275 // Parse the structure and call the modules
276 int32_t thres = instance.config.display.stat;
277
278 char* moduleType = nullptr;
279 size_t moduleLen = 0;
280 while (ffStrbufGetdelim(&moduleType, &moduleLen, ':', &data->structure)) {
281 if (ffStrbufSeparatedContainIgnCaseS(&data->structureDisabled, moduleType, ':')) {
282 continue;
283 }
284
285 double ms = 0;
286 if (thres >= 0) {
287 ms = ffTimeGetTick();
288 }
289
290 parseStructureCommand(data, moduleType, genJsonResult);
291
292 if (thres >= 0) {
293 ms = ffTimeGetTick() - ms;
294
295 if (data->resultDoc) {
296 yyjson_mut_val* moduleJson = yyjson_mut_arr_get_last(data->resultDoc->root);
297 yyjson_mut_obj_add_real(data->resultDoc, moduleJson, "stat", ms);
298 } else {
299 char str[64];
300 int len = snprintf(str, sizeof str, "%.3fms", ms);
301 if (thres > 0) {
302 snprintf(str, sizeof str, "\e[%sm%.3fms\e[m", (ms <= thres ? FF_COLOR_FG_GREEN : ms <= 2 * thres ? FF_COLOR_FG_YELLOW
303 : FF_COLOR_FG_RED),
304 ms);
305 }
306 printf("\e7\e[1A\e[9999999C\e[%dD%s\e8", len - 1, str); // Save; Up 1; Right 9999999; Left <len - 1>; Print <str>; Load
307 }
308 }
309
310#if defined(_WIN32)
311 if (!data->resultDoc && !instance.config.display.noBuffer) {
312 fflush(stdout);
313 }
314#endif
315 }
316}
317
318void ffMigrateCommandOptionToJsonc(FFdata* data) {
319 char* moduleType = nullptr;
320 size_t moduleLen = 0;
321 while (ffStrbufGetdelim(&moduleType, &moduleLen, ':', &data->structure)) {
322 if (ffStrbufSeparatedContainIgnCaseS(&data->structureDisabled, moduleType, ':')) {
323 continue;
324 }
325
326 parseStructureCommand(data, moduleType, genJsonConfig);
327 }
328}