master ohmu/fastfetch-openbsd / src / fastfetch.c
ohmu  ·  2026-10-15
  1#include "fastfetch.h"
  2#include "common/ffdata.h"
  3#include "detection/version/version.h"
  4#include "logo/logo.h"
  5#include "common/FFcache.h"
  6#include "common/commandoption.h"
  7#include "common/genconfig.h"
  8#include "common/init.h"
  9#include "common/io.h"
 10#include "common/jsonconfig.h"
 11#include "common/time.h"
 12#include "common/strutil.h"
 13#include "common/mallocHelper.h"
 14#include "fastfetch_datatext.h"
 15
 16#include <stdlib.h>
 17#include <ctype.h>
 18#include <string.h>
 19
 20#ifndef _WIN32
 21    #include <unistd.h>
 22#else
 23    #include <windows.h>
 24    #include "common/windows/nt.h"
 25#endif
 26
 27[[gnu::cold]]
 28static void printCommandFormatHelpJson(void) {
 29    yyjson_mut_doc* doc = yyjson_mut_doc_new(nullptr);
 30    yyjson_mut_val* root = yyjson_mut_obj(doc);
 31    yyjson_mut_doc_set_root(doc, root);
 32
 33    for (uint32_t i = 0; i <= 'Z' - 'A'; ++i) {
 34        for (FFModuleBaseInfo** modules = ffModuleInfos[i]; *modules; ++modules) {
 35            FFModuleBaseInfo* baseInfo = *modules;
 36            if (!baseInfo->formatArgs.count) {
 37                continue;
 38            }
 39
 40            FF_STRBUF_AUTO_DESTROY type = ffStrbufCreateS(baseInfo->name);
 41            ffStrbufLowerCase(&type);
 42            ffStrbufAppendS(&type, "Format");
 43
 44            yyjson_mut_val* obj = yyjson_mut_obj(doc);
 45            if (yyjson_mut_obj_add(root, yyjson_mut_strbuf(doc, &type), obj)) {
 46                FF_STRBUF_AUTO_DESTROY content = ffStrbufCreateF("Output format for the `%s` module. See Wiki for formatting syntax\n", baseInfo->name);
 47                for (unsigned i = 0; i < baseInfo->formatArgs.count; i++) {
 48                    const FFModuleFormatArg* arg = &baseInfo->formatArgs.args[i];
 49                    ffStrbufAppendF(&content, "    %u. {%s}: %s\n", i + 1, arg->name, arg->desc);
 50                }
 51                ffStrbufTrimRight(&content, '\n');
 52                yyjson_mut_obj_add_strbuf(doc, obj, "description", &content);
 53                yyjson_mut_obj_add_str(doc, obj, "type", "string");
 54            }
 55        }
 56    }
 57    yyjson_mut_write_fp(stdout, doc, YYJSON_WRITE_PRETTY, nullptr, nullptr);
 58    putchar('\n');
 59    yyjson_mut_doc_free(doc);
 60}
 61
 62[[gnu::cold]]
 63static void printCommandFormatHelp(const char* command) {
 64    FF_STRBUF_AUTO_DESTROY type = ffStrbufCreateNS((uint32_t) (strlen(command) - strlen("-format")), command);
 65    ffStrbufLowerCase(&type);
 66    for (FFModuleBaseInfo** modules = ffModuleInfos[toupper(command[0]) - 'A']; *modules; ++modules) {
 67        FFModuleBaseInfo* baseInfo = *modules;
 68        if (ffStrbufIgnCaseEqualS(&type, baseInfo->name)) {
 69            if (baseInfo->formatArgs.count > 0) {
 70                FF_STRBUF_AUTO_DESTROY variable = ffStrbufCreate();
 71                printf("-- In config file: { \"type\": \"%s\", \"format\": \"{<format-variable>}\" }\n", type.chars);
 72                printf("Sets the format string for %s output.\n", baseInfo->name);
 73                puts("To see how a format string is constructed, take a look at https://github.com/fastfetch-cli/fastfetch/wiki/Format-String-Guide.");
 74                puts("Descriptions which end with a '*' are available in key format too.");
 75                puts("The following variables are passed:");
 76
 77                uint32_t maxWidth = 20;
 78                for (unsigned i = 0; i < baseInfo->formatArgs.count; i++) {
 79                    uint32_t len = (uint32_t) strlen(baseInfo->formatArgs.args[i].name) + 2;
 80                    if (len > maxWidth) {
 81                        maxWidth = len;
 82                    }
 83                }
 84
 85                for (unsigned i = 0; i < baseInfo->formatArgs.count; i++) {
 86                    const FFModuleFormatArg* arg = &baseInfo->formatArgs.args[i];
 87                    ffStrbufSetF(&variable, "{%s}", arg->name);
 88                    printf("%*s: %s\n", (int) maxWidth, variable.chars, arg->desc);
 89                }
 90            } else {
 91                fprintf(stderr, "Error: Module '%s' doesn't support output formatting\n", baseInfo->name);
 92            }
 93            return;
 94        }
 95    }
 96
 97    fprintf(stderr, "Error: Module '%s' is not supported\n", type.chars);
 98}
 99
100[[gnu::cold]]
101static void printFullHelp() {
102    fputs("Fastfetch is a neofetch-like tool for fetching system information and displaying them in a pretty way\n\n", stdout);
103    if (!instance.config.display.pipe) {
104        fputs("\e[1;4mUsage:\e[m \e[1mfastfetch\e[m \e[3m<?options>\e[m\n\n", stdout);
105    } else {
106        fputs("Usage: fastfetch <?options>\n\n", stdout);
107    }
108
109    yyjson_doc* doc = yyjson_read(FASTFETCH_DATATEXT_JSON_HELP, strlen(FASTFETCH_DATATEXT_JSON_HELP), YYJSON_READ_NOFLAG);
110    assert(doc);
111    yyjson_val *groupKey, *flagArr;
112    size_t groupIdx, groupMax;
113    yyjson_obj_foreach (yyjson_doc_get_root(doc), groupIdx, groupMax, groupKey, flagArr) {
114        if (!instance.config.display.pipe) {
115            fputs("\e[1;4m", stdout);
116        }
117        printf("%s options:", yyjson_get_str(groupKey));
118        if (!instance.config.display.pipe) {
119            fputs("\e[m", stdout);
120        }
121        putchar('\n');
122
123        yyjson_val* flagObj;
124        size_t flagIdx, flagMax;
125        yyjson_arr_foreach (flagArr, flagIdx, flagMax, flagObj) {
126            yyjson_val* shortKey = yyjson_obj_get(flagObj, "short");
127            if (shortKey) {
128                fputs("  ", stdout);
129                if (!instance.config.display.pipe) {
130                    fputs("\e[1m", stdout);
131                }
132                printf("-%s", yyjson_get_str(shortKey));
133                if (!instance.config.display.pipe) {
134                    fputs("\e[m", stdout);
135                }
136                fputs(", ", stdout);
137            } else {
138                fputs("      ", stdout);
139            }
140            yyjson_val* longKey = yyjson_obj_get(flagObj, "long");
141            assert(longKey);
142            if (!instance.config.display.pipe) {
143                fputs("\e[1m", stdout);
144            }
145            printf("--%s", yyjson_get_str(longKey));
146            if (!instance.config.display.pipe) {
147                fputs("\e[m", stdout);
148            }
149
150            yyjson_val* argObj = yyjson_obj_get(flagObj, "arg");
151            if (argObj) {
152                yyjson_val* typeKey = yyjson_obj_get(argObj, "type");
153                assert(typeKey);
154                yyjson_val* optionalKey = yyjson_obj_get(argObj, "optional");
155                bool optional = optionalKey && yyjson_get_bool(optionalKey);
156                putchar(' ');
157                if (!instance.config.display.pipe) {
158                    fputs("\e[3m", stdout);
159                }
160                printf("<%s%s>", optional ? "?" : "", yyjson_get_str(typeKey));
161                if (!instance.config.display.pipe) {
162                    fputs("\e[m", stdout);
163                }
164            }
165
166            yyjson_val* descKey = yyjson_obj_get(flagObj, "desc");
167            assert(descKey);
168            if (yyjson_is_arr(descKey)) {
169                if (instance.config.display.pipe) {
170                    putchar(':');
171                }
172
173                yyjson_val* descStr;
174                size_t descIdx, descMax;
175                yyjson_arr_foreach (descKey, descIdx, descMax, descStr) {
176                    if (!instance.config.display.pipe) {
177                        printf("\e[46G%s\n", yyjson_get_str(descStr));
178                    } else {
179                        printf(" %s", yyjson_get_str(descStr));
180                    }
181                }
182                if (instance.config.display.pipe) {
183                    putchar('\n');
184                }
185            } else {
186                if (!instance.config.display.pipe) {
187                    fputs("\e[46G", stdout);
188                } else {
189                    fputs(": ", stdout);
190                }
191                puts(yyjson_get_str(descKey));
192            }
193        }
194
195        putchar('\n');
196    }
197    yyjson_doc_free(doc);
198
199    puts("\n\
200Command flags are not case sensitive. E.g. `--print-logos` is equal to `--Print-Logos`\n\
201If a value starts with a ?, it is optional. An optional boolean value defaults to true if not specified.\n\
202More detailed help messages for each options can be printed with `-h <option_without_dash_prefix>`\n\
203For detailed information on logo options, module configuration, and formatting, visit:\n\
204      https://github.com/fastfetch-cli/fastfetch/wiki/Configuration");
205}
206
207[[gnu::cold]]
208static bool printSpecificCommandHelp(const char* command) {
209    yyjson_doc* doc = yyjson_read(FASTFETCH_DATATEXT_JSON_HELP, strlen(FASTFETCH_DATATEXT_JSON_HELP), YYJSON_READ_NOFLAG);
210    assert(doc);
211    yyjson_val *groupKey, *flagArr;
212    size_t groupIdx, groupMax;
213    yyjson_obj_foreach (yyjson_doc_get_root(doc), groupIdx, groupMax, groupKey, flagArr) {
214        yyjson_val* flagObj;
215        size_t flagIdx, flagMax;
216        yyjson_arr_foreach (flagArr, flagIdx, flagMax, flagObj) {
217            yyjson_val* pseudo = yyjson_obj_get(flagObj, "pseudo");
218            if (pseudo && yyjson_get_bool(pseudo)) {
219                continue;
220            }
221
222            yyjson_val* longKey = yyjson_obj_get(flagObj, "long");
223            assert(longKey);
224            if (ffStrEqualsIgnCase(command, yyjson_get_str(longKey))) {
225                puts(yyjson_get_str(yyjson_obj_get(flagObj, "desc")));
226
227                printf("%10s: ", "Usage");
228                yyjson_val* shortKey = yyjson_obj_get(flagObj, "short");
229                if (shortKey) {
230                    if (!instance.config.display.pipe) {
231                        fputs("\e[1m", stdout);
232                    }
233                    printf("-%s", yyjson_get_str(shortKey));
234                    if (!instance.config.display.pipe) {
235                        fputs("\e[m", stdout);
236                    }
237                    fputs(", ", stdout);
238                }
239                if (!instance.config.display.pipe) {
240                    fputs("\e[1m", stdout);
241                }
242                printf("--%s", yyjson_get_str(longKey));
243                if (!instance.config.display.pipe) {
244                    fputs("\e[m", stdout);
245                }
246
247                yyjson_val* argObj = yyjson_obj_get(flagObj, "arg");
248                if (argObj) {
249                    yyjson_val* typeKey = yyjson_obj_get(argObj, "type");
250                    assert(typeKey);
251                    yyjson_val* optionalKey = yyjson_obj_get(argObj, "optional");
252                    bool optional = optionalKey && yyjson_get_bool(optionalKey);
253                    putchar(' ');
254                    if (!instance.config.display.pipe) {
255                        fputs("\e[3m", stdout);
256                    }
257                    printf("<%s%s>", optional ? "?" : "", yyjson_get_str(typeKey));
258                    if (!instance.config.display.pipe) {
259                        fputs("\e[m", stdout);
260                    }
261                    putchar('\n');
262
263                    yyjson_val* defaultKey = yyjson_obj_get(argObj, "default");
264                    if (defaultKey) {
265                        if (ffStrEqualsIgnCase(yyjson_get_str(typeKey), "structure")) {
266                            printf("%10s: %s\n", "Default", FASTFETCH_DATATEXT_STRUCTURE);
267                        } else if (yyjson_is_bool(defaultKey)) {
268                            printf("%10s: %s\n", "Default", yyjson_get_bool(defaultKey) ? "true" : "false");
269                        } else if (yyjson_is_num(defaultKey)) {
270                            printf("%10s: %d\n", "Default", yyjson_get_int(defaultKey));
271                        } else if (yyjson_is_str(defaultKey)) {
272                            printf("%10s: %s\n", "Default", yyjson_get_str(defaultKey));
273                        } else {
274                            printf("%10s: Unknown\n", "Default");
275                        }
276                    }
277
278                    yyjson_val* enumKey = yyjson_obj_get(argObj, "enum");
279                    if (enumKey) {
280                        printf("%10s:\n", "Options");
281                        yyjson_val *optKey, *optVal;
282                        size_t optIdx, optMax;
283                        yyjson_obj_foreach (enumKey, optIdx, optMax, optKey, optVal) {
284                            printf("%12s: %s\n", yyjson_get_str(optKey), yyjson_get_str(optVal));
285                        }
286                    }
287                } else {
288                    putchar('\n');
289                }
290
291                yyjson_val* remarkKey = yyjson_obj_get(flagObj, "remark");
292                if (remarkKey) {
293                    if (yyjson_is_str(remarkKey)) {
294                        printf("%10s: %s\n", "Remark", yyjson_get_str(remarkKey));
295                    } else if (yyjson_is_arr(remarkKey) && yyjson_arr_size(remarkKey) > 0) {
296                        yyjson_val* remarkStr;
297                        size_t remarkIdx, remarkMax;
298                        yyjson_arr_foreach (remarkKey, remarkIdx, remarkMax, remarkStr) {
299                            if (remarkIdx == 0) {
300                                printf("%10s: %s\n", "Remark", yyjson_get_str(remarkStr));
301                            } else {
302                                printf("            %s\n", yyjson_get_str(remarkStr));
303                            }
304                        }
305                    }
306                }
307
308                yyjson_doc_free(doc);
309                return true;
310            }
311        }
312    }
313
314    yyjson_doc_free(doc);
315    return false;
316}
317
318[[gnu::cold]]
319static void printCommandHelp(const char* command) {
320    if (command == nullptr) {
321        printFullHelp();
322    } else if (ffStrEqualsIgnCase(command, "format-json")) {
323        printCommandFormatHelpJson();
324    } else if (ffCharIsEnglishAlphabet(command[0]) && ffStrEndsWithIgnCase(command, "-format")) { // <module>-format
325        printCommandFormatHelp(command);
326    } else if (!printSpecificCommandHelp(command)) {
327        fprintf(stderr, "Error: No specific help for command '%s' provided\n", command);
328    }
329}
330
331[[gnu::cold]]
332static void listAvailablePresets(bool pretty) {
333    FF_LIST_FOR_EACH (FFstrbuf, path, instance.state.platform.dataDirs) {
334        ffStrbufAppendS(path, "fastfetch/presets/");
335        ffListFilesRecursively(path->chars, pretty);
336    }
337
338    if (instance.state.platform.exePath.length) {
339        FF_STRBUF_AUTO_DESTROY absolutePath = ffStrbufCreateCopy(&instance.state.platform.exePath);
340        ffStrbufSubstrBeforeLastC(&absolutePath, '/');
341        ffStrbufAppendS(&absolutePath, "/presets/");
342        ffListFilesRecursively(absolutePath.chars, pretty);
343    }
344}
345
346[[gnu::cold]]
347static void listAvailableLogos(void) {
348    FF_LIST_FOR_EACH (FFstrbuf, path, instance.state.platform.dataDirs) {
349        ffStrbufAppendS(path, "fastfetch/logos/");
350        ffListFilesRecursively(path->chars, true);
351    }
352}
353
354[[gnu::cold]]
355static void listConfigPaths(void) {
356    FF_LIST_FOR_EACH (FFstrbuf, folder, instance.state.platform.configDirs) {
357        bool exists = false;
358        uint32_t length = folder->length + (uint32_t) strlen("fastfetch") + 1 /* trailing slash */;
359        ffStrbufAppendS(folder, "fastfetch/config.jsonc");
360        exists = ffPathExists(folder->chars, FF_PATHTYPE_FILE);
361        ffStrbufSubstrBefore(folder, length);
362        printf("%s%s\n", folder->chars, exists ? " (*)" : "");
363    }
364}
365
366[[gnu::cold]]
367static void listDataPaths(void) {
368    FF_LIST_FOR_EACH (FFstrbuf, folder, instance.state.platform.dataDirs) {
369        ffStrbufAppendS(folder, "fastfetch/");
370        puts(folder->chars);
371    }
372}
373
374[[gnu::cold]]
375static void listModules(bool pretty) {
376    unsigned count = 0;
377    for (int i = 0; i <= 'Z' - 'A'; ++i) {
378        for (FFModuleBaseInfo** modules = ffModuleInfos[i]; *modules; ++modules) {
379            ++count;
380            if (pretty) {
381                printf("%d)%s%-14s: %s\n", count, count > 9 ? " " : "  ", (*modules)->name, (*modules)->description);
382            } else {
383                printf("%s:%s\n", (*modules)->name, (*modules)->description);
384            }
385        }
386    }
387}
388
389static bool parseJsoncFile(FFdata* data, const char* path, yyjson_read_flag flg) {
390    assert(!data->configDoc);
391
392    {
393        yyjson_read_err error;
394        data->configDoc = path
395            ? yyjson_read_file(path, flg, nullptr, &error)
396            : yyjson_read_fp(stdin, flg, nullptr, &error);
397        if (!data->configDoc) {
398            if (error.code != YYJSON_READ_ERROR_FILE_OPEN) {
399                if (path) {
400                    size_t row = 0, col = error.pos;
401                    FF_STRBUF_AUTO_DESTROY content = ffStrbufCreate();
402                    if (ffAppendFileBuffer(path, &content)) {
403                        yyjson_locate_pos(content.chars, content.length, error.pos, &row, &col, nullptr);
404                    }
405                    fprintf(stderr, "Error: failed to parse JSON config file `%s` at (%zu, %zu): %s\n", path, row, col, error.msg);
406                } else {
407                    fprintf(stderr, "Error: failed to parse JSON from stdin at %zu: %s\n", error.pos, error.msg);
408                }
409
410                exit(477);
411            }
412            return false;
413        }
414    }
415
416    {
417        yyjson_val* const root = yyjson_doc_get_root(data->configDoc);
418        if (!yyjson_is_obj(root)) {
419            fputs("JsonConfig Error: Invalid JSON config format. Root value must be an object", stderr);
420            exit(477);
421        }
422
423        yyjson_val* problematicKey = nullptr;
424        const char* error = nullptr;
425        const char* problematicModule = nullptr;
426        if (
427            error ||
428            ((error = ffOptionsParseLogoJsonConfig(&instance.config.logo, root, &problematicKey)) && (problematicModule = "logo")) ||
429            ((error = ffOptionsParseGeneralJsonConfig(&instance.config.general, root, &problematicKey)) && (problematicModule = "general")) ||
430            ((error = ffOptionsParseDisplayJsonConfig(&instance.config.display, root, &problematicKey)) && (problematicModule = "display")) ||
431            false) {
432            if (problematicKey) {
433                fprintf(stderr, "JsonConfig Error (%s.%s): %s\n", problematicModule, unsafe_yyjson_get_str(problematicKey), error);
434            } else {
435                fprintf(stderr, "JsonConfig Error (%s): %s\n", problematicModule, error);
436            }
437            exit(477);
438        }
439    }
440
441    return true;
442}
443
444[[gnu::cold]]
445static void setupGenConfigPath(FFdata* data, const char* filePath) {
446    if (!filePath) {
447        if (instance.state.platform.configDirs.length == 0) {
448            fprintf(stderr, "Error: No config directory found to generate config file in. Use --gen-config <path> to specify a path\n");
449            exit(477);
450        }
451
452        FFstrbuf* configDir = FF_LIST_FIRST(FFstrbuf, instance.state.platform.configDirs);
453        ffStrbufEnsureFixedLengthFree(&data->genConfigPath, configDir->length + strlen("fastfetch/config.jsonc"));
454        ffStrbufSet(&data->genConfigPath, configDir);
455        ffStrbufAppendS(&data->genConfigPath, "fastfetch/config.jsonc");
456    } else {
457        ffStrbufSetS(&data->genConfigPath, filePath);
458    }
459}
460
461[[gnu::cold]]
462static void generateConfigFile(FFdata* data, const char* filePath) {
463    if (data->resultDoc) {
464        fprintf(stderr, "Error: duplicated `--gen-config` or `--format json` flags found\n");
465        exit(477);
466    }
467
468    setupGenConfigPath(data, filePath);
469
470    if (ffPathExists(data->genConfigPath.chars, FF_PATHTYPE_ANY)) {
471        fprintf(stderr, "Error: file `%s` exists. Please remove it before generating a new one\n", data->genConfigPath.chars);
472        exit(477);
473    }
474
475    data->docType = FF_RESULT_DOC_TYPE_CONFIG;
476    data->resultDoc = yyjson_mut_doc_new(nullptr);
477}
478
479static void optionParseConfigFile(FFdata* data, const char* key, const char* value) {
480    if (data->configLoaded) {
481        fprintf(stderr, "Error: only one config file can be loaded\n");
482        exit(413);
483    }
484
485    data->configLoaded = true;
486
487    if (value == nullptr) {
488        fprintf(stderr, "Error: usage: %s <config>\n", key);
489        exit(413);
490    }
491
492    if (value[0] == '\0' || ffStrEqualsIgnCase(value, "none")) {
493        return;
494    }
495
496    if (value[0] == '-' && value[1] == '\0') {
497        parseJsoncFile(data, nullptr, false);
498        return;
499    }
500
501    // Try to load as an absolute path
502
503    FF_STRBUF_AUTO_DESTROY absolutePath = ffStrbufCreateA(128);
504    ffStrbufSetS(&absolutePath, value);
505    bool strictJson = ffStrbufEndsWithIgnCaseS(&absolutePath, ".json");
506    bool jsonc = !strictJson && ffStrbufEndsWithIgnCaseS(&absolutePath, ".jsonc");
507    bool json5 = !strictJson && !jsonc && ffStrbufEndsWithIgnCaseS(&absolutePath, ".json5");
508    bool needExtension = !strictJson && !jsonc && !json5;
509    if (needExtension) {
510        ffStrbufAppendS(&absolutePath, ".jsonc");
511    }
512
513    yyjson_read_flag flag = strictJson
514        ? 0
515        : jsonc
516        ? YYJSON_READ_ALLOW_COMMENTS | YYJSON_READ_ALLOW_TRAILING_COMMAS
517        : YYJSON_READ_JSON5;
518
519    if (parseJsoncFile(data, absolutePath.chars, flag)) {
520        return;
521    }
522
523    // Try to load as a relative path with the config directory
524
525    FF_LIST_FOR_EACH (FFstrbuf, path, instance.state.platform.configDirs) {
526        ffStrbufSet(&absolutePath, path);
527        ffStrbufAppendS(&absolutePath, "fastfetch/");
528        ffStrbufAppendS(&absolutePath, value);
529        if (needExtension) {
530            ffStrbufAppendS(&absolutePath, ".jsonc");
531        }
532
533        if (parseJsoncFile(data, absolutePath.chars, flag)) {
534            return;
535        }
536    }
537
538    // Try to load as a preset
539
540    FF_LIST_FOR_EACH (FFstrbuf, path, instance.state.platform.dataDirs) {
541        ffStrbufSet(&absolutePath, path);
542        ffStrbufAppendS(&absolutePath, "fastfetch/presets/");
543        ffStrbufAppendS(&absolutePath, value);
544        if (needExtension) {
545            ffStrbufAppendS(&absolutePath, ".jsonc");
546        }
547
548        if (parseJsoncFile(data, absolutePath.chars, flag)) {
549            return;
550        }
551    }
552
553    // Try to load as a relative path with the directory of fastfetch binary, for Windows support
554
555    if (instance.state.platform.exePath.length) {
556        uint32_t lastSlash = ffStrbufLastIndexC(&instance.state.platform.exePath, '/') + 1;
557        assert(lastSlash < instance.state.platform.exePath.length);
558
559        // Try {exePath}/
560        ffStrbufSetNS(&absolutePath, lastSlash, instance.state.platform.exePath.chars);
561        ffStrbufAppendS(&absolutePath, value);
562        if (needExtension) {
563            ffStrbufAppendS(&absolutePath, ".jsonc");
564        }
565        if (parseJsoncFile(data, absolutePath.chars, flag)) {
566            return;
567        }
568
569        // Try {exePath}/presets/
570        ffStrbufSubstrBefore(&absolutePath, lastSlash);
571        ffStrbufAppendS(&absolutePath, "presets/");
572        ffStrbufAppendS(&absolutePath, value);
573        if (needExtension) {
574            ffStrbufAppendS(&absolutePath, ".jsonc");
575        }
576        if (parseJsoncFile(data, absolutePath.chars, flag)) {
577            return;
578        }
579    }
580
581    // File not found
582
583    fprintf(stderr, "Error: couldn't find config: %s\n", value);
584    exit(414);
585}
586
587[[gnu::cold]]
588static void printVersion() {
589    FFVersionResult* result = &ffVersionResult;
590    printf("%s %s%s%s (%s)\n", result->projectName, result->version, result->versionTweak, result->debugMode ? "-debug" : "", result->architecture);
591}
592
593static void enableJsonOutput(FFdata* data) {
594    if (data->resultDoc) {
595        fprintf(stderr, "Error: duplicated `--gen-config` or `--format json` flags found\n");
596        exit(477);
597    }
598
599    data->resultDoc = yyjson_mut_doc_new(nullptr);
600    data->docType = FF_RESULT_DOC_TYPE_JSON;
601    yyjson_mut_doc_set_root(data->resultDoc, yyjson_mut_arr(data->resultDoc));
602}
603
604static void genConfigCommon(FFdata* data, const char* value) {
605    if (!getenv("NO_COLOR") && ffIsTerminal(STDOUT_FILENO) && ffIsTerminal(STDIN_FILENO)
606        #ifdef _WIN32
607            && ffIsWindows10OrGreater()
608        #endif
609    ) {
610        data->genConfigInteractive = true;
611        setupGenConfigPath(data, value);
612    } else {
613        generateConfigFile(data, value);
614    }
615}
616
617static void parseCommand(FFdata* data, char* key, char* value) {
618    if (ffStrEqualsIgnCase(key, "-h") || ffStrEqualsIgnCase(key, "--help")) {
619        printCommandHelp(value);
620        exit(0);
621    } else if (ffStrEqualsIgnCase(key, "--help-raw")) {
622        puts(FASTFETCH_DATATEXT_JSON_HELP);
623        exit(0);
624    } else if (ffStrEqualsIgnCase(key, "-v") || ffStrEqualsIgnCase(key, "--version")) {
625        printVersion();
626        exit(0);
627    } else if (ffStrEqualsIgnCase(key, "--version-raw")) {
628        puts(FASTFETCH_PROJECT_VERSION);
629        exit(0);
630    } else if (ffStrStartsWithIgnCase(key, "--print-")) {
631        const char* subkey = key + strlen("--print-");
632        if (ffStrEndsWithIgnCase(subkey, "structure")) {
633            puts(FASTFETCH_DATATEXT_STRUCTURE);
634        } else if (ffStrEqualsIgnCase(subkey, "logos")) {
635            ffLogoBuiltinPrint();
636        } else {
637            fprintf(stderr, "Error: unsupported print option: %s\n", key);
638            exit(415);
639        }
640        exit(0);
641    } else if (ffStrStartsWithIgnCase(key, "--list-")) {
642        const char* subkey = key + strlen("--list-");
643        if (ffStrEqualsIgnCase(subkey, "modules")) {
644            listModules(!value || !ffStrEqualsIgnCase(value, "autocompletion"));
645        } else if (ffStrEqualsIgnCase(subkey, "presets")) {
646            listAvailablePresets(!value || !ffStrEqualsIgnCase(value, "autocompletion"));
647        } else if (ffStrEqualsIgnCase(subkey, "config-paths")) {
648            listConfigPaths();
649        } else if (ffStrEqualsIgnCase(subkey, "data-paths")) {
650            listDataPaths();
651        } else if (ffStrEqualsIgnCase(subkey, "features")) {
652            ffListFeatures();
653        } else if (ffStrEqualsIgnCase(subkey, "logos")) {
654            if (value) {
655                if (ffStrEqualsIgnCase(value, "autocompletion")) {
656                    ffLogoBuiltinListAutocompletion();
657                } else if (ffStrEqualsIgnCase(value, "builtin")) {
658                    ffLogoBuiltinList();
659                } else if (ffStrEqualsIgnCase(value, "custom")) {
660                    listAvailableLogos();
661                } else {
662                    fprintf(stderr, "Error: unsupported logo type: %s\n", value);
663                    exit(415);
664                }
665            } else {
666                puts("Builtin logos:");
667                ffLogoBuiltinList();
668                puts("\nCustom logos:");
669                listAvailableLogos();
670            }
671        } else {
672            fprintf(stderr, "Error: unsupported list option: %s\n", key);
673            exit(415);
674        }
675
676        exit(0);
677    } else if (ffStrEqualsIgnCase(key, "--gen-config")) {
678        genConfigCommon(data, value);
679    } else if (ffStrEqualsIgnCase(key, "-c") || ffStrEqualsIgnCase(key, "--config")) {
680        optionParseConfigFile(data, key, value);
681    } else if (ffStrEqualsIgnCase(key, "-j") || ffStrEqualsIgnCase(key, "--json")) {
682        if (ffOptionParseBoolean(value)) {
683            enableJsonOutput(data);
684        }
685    } else if (ffStrEqualsIgnCase(key, "--format")) {
686        if (!!ffOptionParseEnum(key, value, (FFKeyValuePair[]) {
687                                                { "default", false },
688                                                { "json", true },
689                                                {},
690                                            })) {
691            enableJsonOutput(data);
692        }
693    } else if (ffStrEqualsIgnCase(key, "--dynamic-interval")) {
694        instance.state.dynamicInterval = ffOptionParseUInt32(key, value); // ms
695    } else if (ffStrEqualsIgnCase(key, "-w") || ffStrEqualsIgnCase(key, "--watch")) {
696        if (value == nullptr) {
697            instance.state.dynamicInterval = 1000; // default to 1 second if no value is provided
698        } else {
699            instance.state.dynamicInterval = ffOptionParseUInt32(key, value) * 1000; // seconds to milliseconds
700        }
701    } else {
702        return;
703    }
704
705    // Don't parse it again in parseOption.
706    // This is necessary because parseOption doesn't understand this option and will result in an unknown option error.
707    key[0] = '\0';
708    if (value) {
709        value[0] = '\0';
710    }
711}
712
713static void parseOption(FFdata* data, const char* key, const char* value) {
714    if (ffStrEqualsIgnCase(key, "-s") || ffStrEqualsIgnCase(key, "--structure")) {
715        ffOptionParseString(key, value, &data->structure);
716    }
717
718    else if (
719        ffOptionsParseGeneralCommandLine(&instance.config.general, key, value) ||
720        ffOptionsParseLogoCommandLine(&instance.config.logo, key, value) ||
721        ffOptionsParseDisplayCommandLine(&instance.config.display, key, value) ||
722        ffParseModuleOptions(key, value)) {
723    }
724
725    else if (ffStrEqualsIgnCase(key, "--structure-disabled")) {
726        ffOptionParseString(key, value, &data->structureDisabled);
727    }
728
729    else {
730        fprintf(stderr, "Error: unknown option: %s\n", key);
731        exit(400);
732    }
733}
734
735static void parseConfigFiles(FFdata* data) {
736    if (__builtin_expect(data->genConfigPath.length == 0, true)) {
737        FF_LIST_FOR_EACH (FFstrbuf, dir, instance.state.platform.configDirs) {
738            uint32_t dirLength = dir->length;
739
740            ffStrbufAppendS(dir, "fastfetch/config.jsonc");
741            bool success = parseJsoncFile(data, dir->chars, YYJSON_READ_ALLOW_COMMENTS | YYJSON_READ_ALLOW_TRAILING_COMMAS);
742            ffStrbufSubstrBefore(dir, dirLength);
743            if (success) {
744                return;
745            }
746
747            ffStrbufAppendS(dir, "fastfetch/config.json5");
748            success = parseJsoncFile(data, dir->chars, YYJSON_READ_JSON5);
749            ffStrbufSubstrBefore(dir, dirLength);
750            if (success) {
751                return;
752            }
753        }
754    }
755}
756
757static void parseArguments(FFdata* data, int argc, char** argv, void (*parser)(FFdata* data, char* key, char* value)) {
758    for (int i = 1; i < argc; i++) {
759        const char* key = argv[i];
760        if (*key == '\0') {
761            continue; // has been handled by parseCommand
762        }
763
764        if (*key != '-') {
765            fprintf(stderr, "Error: invalid option: %s. An option must start with `-`\n", key);
766            exit(400);
767        }
768
769        if (i == argc - 1 || (argv[i + 1][0] == '-' && argv[i + 1][1] != '\0' &&    // `-` is used as an alias for `/dev/stdin`
770                                 !ffStrEqualsIgnCase(argv[i], "--separator-string") // Separator string can start with a -
771                                 )) {
772            parser(data, argv[i], nullptr);
773        } else {
774            parser(data, argv[i], argv[i + 1]);
775            ++i;
776        }
777    }
778}
779
780static void run(FFdata* data) {
781    const bool useJsonConfig = data->structure.length == 0 && data->configDoc;
782
783    if (useJsonConfig) {
784        ffPrintJsonConfig(data, true /* prepare */);
785    } else {
786        // If we don't have a custom structure, use the default one
787        if (data->structure.length == 0) {
788            ffStrbufAppendS(&data->structure, FASTFETCH_DATATEXT_STRUCTURE); // Cannot use `ffStrbufSetStatic` here because we will modify the string
789        }
790        ffPrepareCommandOption(data);
791    }
792
793    ffStart();
794
795    if (!data->resultDoc) {
796        ffLogoPrint();
797    }
798
799#if defined(_WIN32)
800    if (!instance.config.display.noBuffer) {
801        fflush(stdout);
802    }
803#endif
804
805    while (true) {
806        if (useJsonConfig) {
807            ffPrintJsonConfig(data, false);
808        } else {
809            ffPrintCommandOption(data);
810        }
811
812        if (instance.state.dynamicInterval > 0) {
813            ffLogoPrintRemaining(); // `logoLineCacheClear` inside so that ffLogoPrintLine will use `\e[nC` to move the cursor to the right position instead of reprinting the logo
814            fputs("\e[J", stdout); // Clear from cursor to the end of the screen to prevent artifacts when the new output is shorter than the previous one
815            fflush(stdout);
816            ffTimeSleep(instance.state.dynamicInterval);
817            fputs("\e[H", stdout); // Move cursor to the top left corner to overwrite the previous output
818            instance.state.keysHeight = 0; // Reset keysHeight so `ffLogoPrintRemaining` will recalculate it
819            ffCacheInvalidateAll(); // Drop the detection results cached for the previous round, so that this round re-detects them
820        } else {
821            break;
822        }
823
824        if (useJsonConfig) {
825            ffPrintJsonConfig(data, true /* prepare */);
826        } else {
827            ffPrepareCommandOption(data);
828        }
829    }
830
831    if (data->resultDoc) {
832        yyjson_mut_write_fp(stdout, data->resultDoc, YYJSON_WRITE_INF_AND_NAN_AS_NULL | YYJSON_WRITE_PRETTY_TWO_SPACES | YYJSON_WRITE_NEWLINE_AT_END, nullptr, nullptr);
833    } else {
834        if (instance.config.logo.printRemaining) {
835            ffLogoPrintRemaining();
836        }
837        ffFinish();
838    }
839}
840
841[[gnu::cold]]
842static void writeConfigFile(FFdata* data) {
843    const FFstrbuf* filename = &data->genConfigPath;
844
845    yyjson_mut_doc* doc = data->resultDoc;
846    yyjson_mut_val* root = yyjson_mut_obj(doc);
847    yyjson_mut_doc_set_root(doc, root);
848    yyjson_mut_obj_add_str(doc, root, "$schema", "https://github.com/fastfetch-cli/fastfetch/raw/master/doc/json_schema.json");
849
850    if (data->docType == FF_RESULT_DOC_TYPE_CONFIG_FULL) {
851        ffOptionsGenerateLogoJsonConfig(data, &instance.config.logo);
852        ffOptionsGenerateDisplayJsonConfig(data, &instance.config.display);
853        ffOptionsGenerateGeneralJsonConfig(data, &instance.config.general);
854    } else if (data->genConfigInteractive) {
855        if (instance.config.logo.type == FF_LOGO_TYPE_NONE) {
856            yyjson_mut_obj_add_null(doc, root, "logo");
857        } else if (instance.config.logo.type == FF_LOGO_TYPE_SMALL && instance.config.logo.position == FF_LOGO_POSITION_LEFT) {
858            yyjson_mut_obj_add_str(doc, root, "logo", "small");
859        } else if (instance.config.logo.position != FF_LOGO_POSITION_LEFT || instance.config.logo.type == FF_LOGO_TYPE_SMALL) {
860            yyjson_mut_val* logo = yyjson_mut_obj(doc);
861            if (instance.config.logo.type == FF_LOGO_TYPE_SMALL) {
862                yyjson_mut_obj_add_str(doc, logo, "type", "small");
863            }
864            if (instance.config.logo.position != FF_LOGO_POSITION_LEFT) {
865                yyjson_mut_obj_add_str(doc, logo, "position", ffLogoPositionToString(instance.config.logo.position));
866            }
867            yyjson_mut_obj_add_val(doc, root, "logo", logo);
868        }
869    }
870    ffMigrateCommandOptionToJsonc(data);
871
872    if (ffStrbufEqualS(filename, "-")) {
873        yyjson_mut_write_fp(stdout, doc, YYJSON_WRITE_INF_AND_NAN_AS_NULL | YYJSON_WRITE_PRETTY_TWO_SPACES | YYJSON_WRITE_NEWLINE_AT_END, nullptr, nullptr);
874    } else {
875        size_t len;
876        FF_AUTO_FREE const char* str = yyjson_mut_write(doc, YYJSON_WRITE_INF_AND_NAN_AS_NULL | YYJSON_WRITE_PRETTY_TWO_SPACES | YYJSON_WRITE_NEWLINE_AT_END, &len);
877        if (!str) {
878            printf("Error: failed to generate config file\n");
879            exit(1);
880        }
881        if (ffWriteFileData(filename->chars, len, str)) {
882            printf("✓ Configuration file generated: `%s`\n"
883                   "* Tip: Use a JSON schema-aware editor for better editing experience\n"
884                   "* Documentation: https://github.com/fastfetch-cli/fastfetch/wiki/Configuration\n",
885                filename->chars);
886        } else {
887            printf("Error: failed to write file in `%s`\n", filename->chars);
888            exit(1);
889        }
890    }
891}
892
893int main(int argc, char** argv) {
894    ffInitInstance();
895    atexit(ffDestroyInstance);
896
897    // Data stores things only needed for the configuration of fastfetch
898    FFdata data = {
899        .structure = ffStrbufCreate(),
900        .structureDisabled = ffStrbufCreate(),
901        .genConfigPath = ffStrbufCreate(),
902    };
903
904    parseArguments(&data, argc, argv, parseCommand);
905    if (instance.state.dynamicInterval && data.resultDoc) {
906        fprintf(stderr, "Error: --dynamic-interval cannot be used with --json\n");
907        exit(400);
908    }
909
910    if (!data.configLoaded && !getenv("NO_CONFIG")) {
911        parseConfigFiles(&data);
912    }
913    parseArguments(&data, argc, argv, (void*) parseOption);
914
915    if (__builtin_expect(data.genConfigPath.length == 0, true)) {
916        run(&data);
917    } else {
918        // If we don't have a custom structure, use the default one
919        if (data.structure.length == 0) {
920            ffStrbufSetS(&data.structure, FASTFETCH_DATATEXT_STRUCTURE); // Cannot use `ffStrbufSetStatic` here because we will modify the string
921        }
922        if (data.genConfigInteractive && !ffGenConfigInteractive(&data)) {
923            // User canceled the interactive config generation
924            ffStrbufDestroy(&data.structure);
925            ffStrbufDestroy(&data.structureDisabled);
926            yyjson_doc_free(data.configDoc);
927            ffStrbufDestroy(&data.genConfigPath);
928            return 0;
929        }
930        writeConfigFile(&data);
931    }
932
933    ffStrbufDestroy(&data.structure);
934    ffStrbufDestroy(&data.structureDisabled);
935    yyjson_doc_free(data.configDoc);
936    yyjson_mut_doc_free(data.resultDoc);
937    ffStrbufDestroy(&data.genConfigPath);
938}