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}