master ohmu/fastfetch-openbsd / src / common / impl / commandoption.c
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}