summaryrefslogtreecommitdiffstats
path: root/src/fastfetch.c
diff options
context:
space:
mode:
authorsumuel <samuel@yakubos.org>2026-08-17 20:44:55 +0000
committersumuel <samuel@yakubos.org>2026-08-17 20:44:55 +0000
commit76424950e373d3b04ac3dd13019151bfba3e8423 (patch)
tree4c3cfdbda039e592b9186be3e28f8d7cfd439e8a /src/fastfetch.c
Add the files
Diffstat (limited to 'src/fastfetch.c')
-rw-r--r--src/fastfetch.c864
1 files changed, 864 insertions, 0 deletions
diff --git a/src/fastfetch.c b/src/fastfetch.c
new file mode 100644
index 0000000..dc5c179
--- /dev/null
+++ b/src/fastfetch.c
@@ -0,0 +1,864 @@
+#include "fastfetch.h"
+#include "common/ffdata.h"
+#include "detection/version/version.h"
+#include "logo/logo.h"
+#include "common/commandoption.h"
+#include "common/init.h"
+#include "common/io.h"
+#include "common/jsonconfig.h"
+#include "common/time.h"
+#include "common/strutil.h"
+#include "common/mallocHelper.h"
+#include "fastfetch_datatext.h"
+
+#include <stdlib.h>
+#include <ctype.h>
+#include <string.h>
+
+static void printCommandFormatHelpJson(void) {
+ yyjson_mut_doc* doc = yyjson_mut_doc_new(NULL);
+ yyjson_mut_val* root = yyjson_mut_obj(doc);
+ yyjson_mut_doc_set_root(doc, root);
+
+ for (uint32_t i = 0; i <= 'Z' - 'A'; ++i) {
+ for (FFModuleBaseInfo** modules = ffModuleInfos[i]; *modules; ++modules) {
+ FFModuleBaseInfo* baseInfo = *modules;
+ if (!baseInfo->formatArgs.count) {
+ continue;
+ }
+
+ FF_STRBUF_AUTO_DESTROY type = ffStrbufCreateS(baseInfo->name);
+ ffStrbufLowerCase(&type);
+ ffStrbufAppendS(&type, "Format");
+
+ yyjson_mut_val* obj = yyjson_mut_obj(doc);
+ if (yyjson_mut_obj_add(root, yyjson_mut_strbuf(doc, &type), obj)) {
+ FF_STRBUF_AUTO_DESTROY content = ffStrbufCreateF("Output format for the `%s` module. See Wiki for formatting syntax\n", baseInfo->name);
+ for (unsigned i = 0; i < baseInfo->formatArgs.count; i++) {
+ const FFModuleFormatArg* arg = &baseInfo->formatArgs.args[i];
+ ffStrbufAppendF(&content, " %u. {%s}: %s\n", i + 1, arg->name, arg->desc);
+ }
+ ffStrbufTrimRight(&content, '\n');
+ yyjson_mut_obj_add_strbuf(doc, obj, "description", &content);
+ yyjson_mut_obj_add_str(doc, obj, "type", "string");
+ }
+ }
+ }
+ yyjson_mut_write_fp(stdout, doc, YYJSON_WRITE_PRETTY, NULL, NULL);
+ putchar('\n');
+ yyjson_mut_doc_free(doc);
+}
+
+static void printCommandFormatHelp(const char* command) {
+ FF_STRBUF_AUTO_DESTROY type = ffStrbufCreateNS((uint32_t) (strlen(command) - strlen("-format")), command);
+ ffStrbufLowerCase(&type);
+ for (FFModuleBaseInfo** modules = ffModuleInfos[toupper(command[0]) - 'A']; *modules; ++modules) {
+ FFModuleBaseInfo* baseInfo = *modules;
+ if (ffStrbufIgnCaseEqualS(&type, baseInfo->name)) {
+ if (baseInfo->formatArgs.count > 0) {
+ FF_STRBUF_AUTO_DESTROY variable = ffStrbufCreate();
+ printf("-- In config file: { \"type\": \"%s\", \"format\": \"{<format-variable>}\" }\n", type.chars);
+ printf("Sets the format string for %s output.\n", baseInfo->name);
+ puts("To see how a format string is constructed, take a look at https://github.com/fastfetch-cli/fastfetch/wiki/Format-String-Guide.");
+ puts("The following variables are passed:");
+
+ uint32_t maxWidth = 20;
+ for (unsigned i = 0; i < baseInfo->formatArgs.count; i++) {
+ uint32_t len = (uint32_t) strlen(baseInfo->formatArgs.args[i].name) + 2;
+ if (len > maxWidth) {
+ maxWidth = len;
+ }
+ }
+
+ for (unsigned i = 0; i < baseInfo->formatArgs.count; i++) {
+ const FFModuleFormatArg* arg = &baseInfo->formatArgs.args[i];
+ ffStrbufSetF(&variable, "{%s}", arg->name);
+ printf("%*s: %s\n", (int) maxWidth, variable.chars, arg->desc);
+ }
+ } else {
+ fprintf(stderr, "Error: Module '%s' doesn't support output formatting\n", baseInfo->name);
+ }
+ return;
+ }
+ }
+
+ fprintf(stderr, "Error: Module '%s' is not supported\n", type.chars);
+}
+
+static void printFullHelp() {
+ fputs("Fastfetch is a neofetch-like tool for fetching system information and displaying them in a pretty way\n\n", stdout);
+ if (!instance.config.display.pipe) {
+ fputs("\e[1;4mUsage:\e[m \e[1mfastfetch\e[m \e[3m<?options>\e[m\n\n", stdout);
+ } else {
+ fputs("Usage: fastfetch <?options>\n\n", stdout);
+ }
+
+ yyjson_doc* doc = yyjson_read(FASTFETCH_DATATEXT_JSON_HELP, strlen(FASTFETCH_DATATEXT_JSON_HELP), YYJSON_READ_NOFLAG);
+ assert(doc);
+ yyjson_val *groupKey, *flagArr;
+ size_t groupIdx, groupMax;
+ yyjson_obj_foreach (yyjson_doc_get_root(doc), groupIdx, groupMax, groupKey, flagArr) {
+ if (!instance.config.display.pipe) {
+ fputs("\e[1;4m", stdout);
+ }
+ printf("%s options:", yyjson_get_str(groupKey));
+ if (!instance.config.display.pipe) {
+ fputs("\e[m", stdout);
+ }
+ putchar('\n');
+
+ yyjson_val* flagObj;
+ size_t flagIdx, flagMax;
+ yyjson_arr_foreach (flagArr, flagIdx, flagMax, flagObj) {
+ yyjson_val* shortKey = yyjson_obj_get(flagObj, "short");
+ if (shortKey) {
+ fputs(" ", stdout);
+ if (!instance.config.display.pipe) {
+ fputs("\e[1m", stdout);
+ }
+ printf("-%s", yyjson_get_str(shortKey));
+ if (!instance.config.display.pipe) {
+ fputs("\e[m", stdout);
+ }
+ fputs(", ", stdout);
+ } else {
+ fputs(" ", stdout);
+ }
+ yyjson_val* longKey = yyjson_obj_get(flagObj, "long");
+ assert(longKey);
+ if (!instance.config.display.pipe) {
+ fputs("\e[1m", stdout);
+ }
+ printf("--%s", yyjson_get_str(longKey));
+ if (!instance.config.display.pipe) {
+ fputs("\e[m", stdout);
+ }
+
+ yyjson_val* argObj = yyjson_obj_get(flagObj, "arg");
+ if (argObj) {
+ yyjson_val* typeKey = yyjson_obj_get(argObj, "type");
+ assert(typeKey);
+ yyjson_val* optionalKey = yyjson_obj_get(argObj, "optional");
+ bool optional = optionalKey && yyjson_get_bool(optionalKey);
+ putchar(' ');
+ if (!instance.config.display.pipe) {
+ fputs("\e[3m", stdout);
+ }
+ printf("<%s%s>", optional ? "?" : "", yyjson_get_str(typeKey));
+ if (!instance.config.display.pipe) {
+ fputs("\e[m", stdout);
+ }
+ }
+
+ yyjson_val* descKey = yyjson_obj_get(flagObj, "desc");
+ assert(descKey);
+ if (yyjson_is_arr(descKey)) {
+ if (instance.config.display.pipe) {
+ putchar(':');
+ }
+
+ yyjson_val* descStr;
+ size_t descIdx, descMax;
+ yyjson_arr_foreach (descKey, descIdx, descMax, descStr) {
+ if (!instance.config.display.pipe) {
+ printf("\e[46G%s\n", yyjson_get_str(descStr));
+ } else {
+ printf(" %s", yyjson_get_str(descStr));
+ }
+ }
+ if (instance.config.display.pipe) {
+ putchar('\n');
+ }
+ } else {
+ if (!instance.config.display.pipe) {
+ fputs("\e[46G", stdout);
+ } else {
+ fputs(": ", stdout);
+ }
+ puts(yyjson_get_str(descKey));
+ }
+ }
+
+ putchar('\n');
+ }
+ yyjson_doc_free(doc);
+
+ puts("\n\
+Command flags are not case sensitive. E.g. `--print-logos` is equal to `--Print-Logos`\n\
+If a value starts with a ?, it is optional. An optional boolean value defaults to true if not specified.\n\
+More detailed help messages for each options can be printed with `-h <option_without_dash_prefix>`\n\
+For detailed information on logo options, module configuration, and formatting, visit:\n\
+ https://github.com/fastfetch-cli/fastfetch/wiki/Configuration");
+}
+
+static bool printSpecificCommandHelp(const char* command) {
+ yyjson_doc* doc = yyjson_read(FASTFETCH_DATATEXT_JSON_HELP, strlen(FASTFETCH_DATATEXT_JSON_HELP), YYJSON_READ_NOFLAG);
+ assert(doc);
+ yyjson_val *groupKey, *flagArr;
+ size_t groupIdx, groupMax;
+ yyjson_obj_foreach (yyjson_doc_get_root(doc), groupIdx, groupMax, groupKey, flagArr) {
+ yyjson_val* flagObj;
+ size_t flagIdx, flagMax;
+ yyjson_arr_foreach (flagArr, flagIdx, flagMax, flagObj) {
+ yyjson_val* pseudo = yyjson_obj_get(flagObj, "pseudo");
+ if (pseudo && yyjson_get_bool(pseudo)) {
+ continue;
+ }
+
+ yyjson_val* longKey = yyjson_obj_get(flagObj, "long");
+ assert(longKey);
+ if (ffStrEqualsIgnCase(command, yyjson_get_str(longKey))) {
+ puts(yyjson_get_str(yyjson_obj_get(flagObj, "desc")));
+
+ printf("%10s: ", "Usage");
+ yyjson_val* shortKey = yyjson_obj_get(flagObj, "short");
+ if (shortKey) {
+ if (!instance.config.display.pipe) {
+ fputs("\e[1m", stdout);
+ }
+ printf("-%s", yyjson_get_str(shortKey));
+ if (!instance.config.display.pipe) {
+ fputs("\e[m", stdout);
+ }
+ fputs(", ", stdout);
+ }
+ if (!instance.config.display.pipe) {
+ fputs("\e[1m", stdout);
+ }
+ printf("--%s", yyjson_get_str(longKey));
+ if (!instance.config.display.pipe) {
+ fputs("\e[m", stdout);
+ }
+
+ yyjson_val* argObj = yyjson_obj_get(flagObj, "arg");
+ if (argObj) {
+ yyjson_val* typeKey = yyjson_obj_get(argObj, "type");
+ assert(typeKey);
+ yyjson_val* optionalKey = yyjson_obj_get(argObj, "optional");
+ bool optional = optionalKey && yyjson_get_bool(optionalKey);
+ putchar(' ');
+ if (!instance.config.display.pipe) {
+ fputs("\e[3m", stdout);
+ }
+ printf("<%s%s>", optional ? "?" : "", yyjson_get_str(typeKey));
+ if (!instance.config.display.pipe) {
+ fputs("\e[m", stdout);
+ }
+ putchar('\n');
+
+ yyjson_val* defaultKey = yyjson_obj_get(argObj, "default");
+ if (defaultKey) {
+ if (ffStrEqualsIgnCase(yyjson_get_str(typeKey), "structure")) {
+ printf("%10s: %s\n", "Default", FASTFETCH_DATATEXT_STRUCTURE);
+ } else if (yyjson_is_bool(defaultKey)) {
+ printf("%10s: %s\n", "Default", yyjson_get_bool(defaultKey) ? "true" : "false");
+ } else if (yyjson_is_num(defaultKey)) {
+ printf("%10s: %d\n", "Default", yyjson_get_int(defaultKey));
+ } else if (yyjson_is_str(defaultKey)) {
+ printf("%10s: %s\n", "Default", yyjson_get_str(defaultKey));
+ } else {
+ printf("%10s: Unknown\n", "Default");
+ }
+ }
+
+ yyjson_val* enumKey = yyjson_obj_get(argObj, "enum");
+ if (enumKey) {
+ printf("%10s:\n", "Options");
+ yyjson_val *optKey, *optVal;
+ size_t optIdx, optMax;
+ yyjson_obj_foreach (enumKey, optIdx, optMax, optKey, optVal) {
+ printf("%12s: %s\n", yyjson_get_str(optKey), yyjson_get_str(optVal));
+ }
+ }
+ } else {
+ putchar('\n');
+ }
+
+ yyjson_val* remarkKey = yyjson_obj_get(flagObj, "remark");
+ if (remarkKey) {
+ if (yyjson_is_str(remarkKey)) {
+ printf("%10s: %s\n", "Remark", yyjson_get_str(remarkKey));
+ } else if (yyjson_is_arr(remarkKey) && yyjson_arr_size(remarkKey) > 0) {
+ yyjson_val* remarkStr;
+ size_t remarkIdx, remarkMax;
+ yyjson_arr_foreach (remarkKey, remarkIdx, remarkMax, remarkStr) {
+ if (remarkIdx == 0) {
+ printf("%10s: %s\n", "Remark", yyjson_get_str(remarkStr));
+ } else {
+ printf(" %s\n", yyjson_get_str(remarkStr));
+ }
+ }
+ }
+ }
+
+ yyjson_doc_free(doc);
+ return true;
+ }
+ }
+ }
+
+ yyjson_doc_free(doc);
+ return false;
+}
+
+static void printCommandHelp(const char* command) {
+ if (command == NULL) {
+ printFullHelp();
+ } else if (ffStrEqualsIgnCase(command, "format-json")) {
+ printCommandFormatHelpJson();
+ } else if (ffCharIsEnglishAlphabet(command[0]) && ffStrEndsWithIgnCase(command, "-format")) { // <module>-format
+ printCommandFormatHelp(command);
+ } else if (!printSpecificCommandHelp(command)) {
+ fprintf(stderr, "Error: No specific help for command '%s' provided\n", command);
+ }
+}
+
+static void listAvailablePresets(bool pretty) {
+ FF_LIST_FOR_EACH (FFstrbuf, path, instance.state.platform.dataDirs) {
+ ffStrbufAppendS(path, "fastfetch/presets/");
+ ffListFilesRecursively(path->chars, pretty);
+ }
+
+ if (instance.state.platform.exePath.length) {
+ FF_STRBUF_AUTO_DESTROY absolutePath = ffStrbufCreateCopy(&instance.state.platform.exePath);
+ ffStrbufSubstrBeforeLastC(&absolutePath, '/');
+ ffStrbufAppendS(&absolutePath, "/presets/");
+ ffListFilesRecursively(absolutePath.chars, pretty);
+ }
+}
+
+static void listAvailableLogos(void) {
+ FF_LIST_FOR_EACH (FFstrbuf, path, instance.state.platform.dataDirs) {
+ ffStrbufAppendS(path, "fastfetch/logos/");
+ ffListFilesRecursively(path->chars, true);
+ }
+}
+
+static void listConfigPaths(void) {
+ FF_LIST_FOR_EACH (FFstrbuf, folder, instance.state.platform.configDirs) {
+ bool exists = false;
+ uint32_t length = folder->length + (uint32_t) strlen("fastfetch") + 1 /* trailing slash */;
+ ffStrbufAppendS(folder, "fastfetch/config.jsonc");
+ exists = ffPathExists(folder->chars, FF_PATHTYPE_FILE);
+ ffStrbufSubstrBefore(folder, length);
+ printf("%s%s\n", folder->chars, exists ? " (*)" : "");
+ }
+}
+
+static void listDataPaths(void) {
+ FF_LIST_FOR_EACH (FFstrbuf, folder, instance.state.platform.dataDirs) {
+ ffStrbufAppendS(folder, "fastfetch/");
+ puts(folder->chars);
+ }
+}
+
+static void listModules(bool pretty) {
+ unsigned count = 0;
+ for (int i = 0; i <= 'Z' - 'A'; ++i) {
+ for (FFModuleBaseInfo** modules = ffModuleInfos[i]; *modules; ++modules) {
+ ++count;
+ if (pretty) {
+ printf("%d)%s%-14s: %s\n", count, count > 9 ? " " : " ", (*modules)->name, (*modules)->description);
+ } else {
+ printf("%s:%s\n", (*modules)->name, (*modules)->description);
+ }
+ }
+ }
+}
+
+static bool parseJsoncFile(FFdata* data, const char* path, yyjson_read_flag flg) {
+ assert(!data->configDoc);
+
+ {
+ yyjson_read_err error;
+ data->configDoc = path
+ ? yyjson_read_file(path, flg, NULL, &error)
+ : yyjson_read_fp(stdin, flg, NULL, &error);
+ if (!data->configDoc) {
+ if (error.code != YYJSON_READ_ERROR_FILE_OPEN) {
+ if (path) {
+ size_t row = 0, col = error.pos;
+ FF_STRBUF_AUTO_DESTROY content = ffStrbufCreate();
+ if (ffAppendFileBuffer(path, &content)) {
+ yyjson_locate_pos(content.chars, content.length, error.pos, &row, &col, NULL);
+ }
+ fprintf(stderr, "Error: failed to parse JSON config file `%s` at (%zu, %zu): %s\n", path, row, col, error.msg);
+ } else {
+ fprintf(stderr, "Error: failed to parse JSON from stdin at %zu: %s\n", error.pos, error.msg);
+ }
+
+ exit(477);
+ }
+ return false;
+ }
+ }
+
+ {
+ const char* error = NULL;
+
+ yyjson_val* const root = yyjson_doc_get_root(data->configDoc);
+ if (!yyjson_is_obj(root)) {
+ error = "Invalid JSON config format. Root value must be an object";
+ }
+
+ if (
+ error ||
+ (error = ffOptionsParseLogoJsonConfig(&instance.config.logo, root)) ||
+ (error = ffOptionsParseGeneralJsonConfig(&instance.config.general, root)) ||
+ (error = ffOptionsParseDisplayJsonConfig(&instance.config.display, root)) ||
+ false) {
+ fprintf(stderr, "JsonConfig Error: %s\n", error);
+ exit(477);
+ }
+ }
+
+ return true;
+}
+
+static void generateConfigFile(FFdata* data, bool force, const char* filePath, bool fullConfig) {
+ if (data->resultDoc) {
+ fprintf(stderr, "Error: duplicated `--gen-config` or `--format json` flags found\n");
+ exit(477);
+ }
+
+ if (!filePath) {
+ if (instance.state.platform.configDirs.length == 0) {
+ fprintf(stderr, "Error: No config directory found to generate config file in. Use --gen-config <path> to specify a path\n");
+ exit(477);
+ }
+
+ FFstrbuf* configDir = FF_LIST_FIRST(FFstrbuf, instance.state.platform.configDirs);
+ ffStrbufEnsureFixedLengthFree(&data->genConfigPath, configDir->length + strlen("fastfetch/config.jsonc"));
+ ffStrbufSet(&data->genConfigPath, configDir);
+ ffStrbufAppendS(&data->genConfigPath, "fastfetch/config.jsonc");
+ } else {
+ ffStrbufSetS(&data->genConfigPath, filePath);
+ }
+
+ if (!force && ffPathExists(data->genConfigPath.chars, FF_PATHTYPE_ANY)) {
+ fprintf(stderr, "Error: file `%s` exists. Use `--gen-config%s-force` to overwrite\n", data->genConfigPath.chars, fullConfig ? "-full" : "");
+ exit(477);
+ }
+
+ data->docType = fullConfig ? FF_RESULT_DOC_TYPE_CONFIG_FULL : FF_RESULT_DOC_TYPE_CONFIG;
+ data->resultDoc = yyjson_mut_doc_new(NULL);
+}
+
+static void optionParseConfigFile(FFdata* data, const char* key, const char* value) {
+ if (data->configLoaded) {
+ fprintf(stderr, "Error: only one config file can be loaded\n");
+ exit(413);
+ }
+
+ data->configLoaded = true;
+
+ if (value == NULL) {
+ fprintf(stderr, "Error: usage: %s <config>\n", key);
+ exit(413);
+ }
+
+ if (value[0] == '\0' || ffStrEqualsIgnCase(value, "none")) {
+ return;
+ }
+
+ if (value[0] == '-' && value[1] == '\0') {
+ parseJsoncFile(data, NULL, false);
+ return;
+ }
+
+ // Try to load as an absolute path
+
+ FF_STRBUF_AUTO_DESTROY absolutePath = ffStrbufCreateA(128);
+ ffStrbufSetS(&absolutePath, value);
+ bool strictJson = ffStrbufEndsWithIgnCaseS(&absolutePath, ".json");
+ bool jsonc = !strictJson && ffStrbufEndsWithIgnCaseS(&absolutePath, ".jsonc");
+ bool json5 = !strictJson && !jsonc && ffStrbufEndsWithIgnCaseS(&absolutePath, ".json5");
+ bool needExtension = !strictJson && !jsonc && !json5;
+ if (needExtension) {
+ ffStrbufAppendS(&absolutePath, ".jsonc");
+ }
+
+ yyjson_read_flag flag = strictJson
+ ? 0
+ : jsonc
+ ? YYJSON_READ_ALLOW_COMMENTS | YYJSON_READ_ALLOW_TRAILING_COMMAS
+ : YYJSON_READ_JSON5;
+
+ if (parseJsoncFile(data, absolutePath.chars, flag)) {
+ return;
+ }
+
+ // Try to load as a relative path with the config directory
+
+ FF_LIST_FOR_EACH (FFstrbuf, path, instance.state.platform.configDirs) {
+ ffStrbufSet(&absolutePath, path);
+ ffStrbufAppendS(&absolutePath, "fastfetch/");
+ ffStrbufAppendS(&absolutePath, value);
+ if (needExtension) {
+ ffStrbufAppendS(&absolutePath, ".jsonc");
+ }
+
+ if (parseJsoncFile(data, absolutePath.chars, flag)) {
+ return;
+ }
+ }
+
+ // Try to load as a preset
+
+ FF_LIST_FOR_EACH (FFstrbuf, path, instance.state.platform.dataDirs) {
+ ffStrbufSet(&absolutePath, path);
+ ffStrbufAppendS(&absolutePath, "fastfetch/presets/");
+ ffStrbufAppendS(&absolutePath, value);
+ if (needExtension) {
+ ffStrbufAppendS(&absolutePath, ".jsonc");
+ }
+
+ if (parseJsoncFile(data, absolutePath.chars, flag)) {
+ return;
+ }
+ }
+
+ // Try to load as a relative path with the directory of fastfetch binary, for Windows support
+
+ if (instance.state.platform.exePath.length) {
+ uint32_t lastSlash = ffStrbufLastIndexC(&instance.state.platform.exePath, '/') + 1;
+ assert(lastSlash < instance.state.platform.exePath.length);
+
+ // Try {exePath}/
+ ffStrbufSetNS(&absolutePath, lastSlash, instance.state.platform.exePath.chars);
+ ffStrbufAppendS(&absolutePath, value);
+ if (needExtension) {
+ ffStrbufAppendS(&absolutePath, ".jsonc");
+ }
+ if (parseJsoncFile(data, absolutePath.chars, flag)) {
+ return;
+ }
+
+ // Try {exePath}/presets/
+ ffStrbufSubstrBefore(&absolutePath, lastSlash);
+ ffStrbufAppendS(&absolutePath, "presets/");
+ ffStrbufAppendS(&absolutePath, value);
+ if (needExtension) {
+ ffStrbufAppendS(&absolutePath, ".jsonc");
+ }
+ if (parseJsoncFile(data, absolutePath.chars, flag)) {
+ return;
+ }
+ }
+
+ // File not found
+
+ fprintf(stderr, "Error: couldn't find config: %s\n", value);
+ exit(414);
+}
+
+static void printVersion() {
+ FFVersionResult* result = &ffVersionResult;
+ printf("%s %s%s%s (%s)\n", result->projectName, result->version, result->versionTweak, result->debugMode ? "-debug" : "", result->architecture);
+}
+
+static void enableJsonOutput(FFdata* data) {
+ if (data->resultDoc) {
+ fprintf(stderr, "Error: duplicated `--gen-config` or `--format json` flags found\n");
+ exit(477);
+ }
+
+ data->resultDoc = yyjson_mut_doc_new(NULL);
+ data->docType = FF_RESULT_DOC_TYPE_JSON;
+ yyjson_mut_doc_set_root(data->resultDoc, yyjson_mut_arr(data->resultDoc));
+}
+
+static void parseCommand(FFdata* data, char* key, char* value) {
+ if (ffStrEqualsIgnCase(key, "-h") || ffStrEqualsIgnCase(key, "--help")) {
+ printCommandHelp(value);
+ exit(0);
+ }
+ if (ffStrEqualsIgnCase(key, "--help-raw")) {
+ puts(FASTFETCH_DATATEXT_JSON_HELP);
+ exit(0);
+ } else if (ffStrEqualsIgnCase(key, "-v") || ffStrEqualsIgnCase(key, "--version")) {
+ printVersion();
+ exit(0);
+ } else if (ffStrEqualsIgnCase(key, "--version-raw")) {
+ puts(FASTFETCH_PROJECT_VERSION);
+ exit(0);
+ } else if (ffStrStartsWithIgnCase(key, "--print-")) {
+ const char* subkey = key + strlen("--print-");
+ if (ffStrEndsWithIgnCase(subkey, "structure")) {
+ puts(FASTFETCH_DATATEXT_STRUCTURE);
+ } else if (ffStrEqualsIgnCase(subkey, "logos")) {
+ ffLogoBuiltinPrint();
+ } else {
+ fprintf(stderr, "Error: unsupported print option: %s\n", key);
+ exit(415);
+ }
+ exit(0);
+ } else if (ffStrStartsWithIgnCase(key, "--list-")) {
+ const char* subkey = key + strlen("--list-");
+ if (ffStrEqualsIgnCase(subkey, "modules")) {
+ listModules(!value || !ffStrEqualsIgnCase(value, "autocompletion"));
+ } else if (ffStrEqualsIgnCase(subkey, "presets")) {
+ listAvailablePresets(!value || !ffStrEqualsIgnCase(value, "autocompletion"));
+ } else if (ffStrEqualsIgnCase(subkey, "config-paths")) {
+ listConfigPaths();
+ } else if (ffStrEqualsIgnCase(subkey, "data-paths")) {
+ listDataPaths();
+ } else if (ffStrEqualsIgnCase(subkey, "features")) {
+ ffListFeatures();
+ } else if (ffStrEqualsIgnCase(subkey, "logos")) {
+ if (value) {
+ if (ffStrEqualsIgnCase(value, "autocompletion")) {
+ ffLogoBuiltinListAutocompletion();
+ } else if (ffStrEqualsIgnCase(value, "builtin")) {
+ ffLogoBuiltinList();
+ } else if (ffStrEqualsIgnCase(value, "custom")) {
+ listAvailableLogos();
+ } else {
+ fprintf(stderr, "Error: unsupported logo type: %s\n", value);
+ exit(415);
+ }
+ } else {
+ puts("Builtin logos:");
+ ffLogoBuiltinList();
+ puts("\nCustom logos:");
+ listAvailableLogos();
+ }
+ } else {
+ fprintf(stderr, "Error: unsupported list option: %s\n", key);
+ exit(415);
+ }
+
+ exit(0);
+ } else if (ffStrEqualsIgnCase(key, "--gen-config")) {
+ generateConfigFile(data, false, value, false);
+ } else if (ffStrEqualsIgnCase(key, "--gen-config-force")) {
+ generateConfigFile(data, true, value, false);
+ } else if (ffStrEqualsIgnCase(key, "--gen-config-full")) {
+ generateConfigFile(data, false, value, true);
+ } else if (ffStrEqualsIgnCase(key, "--gen-config-full-force")) {
+ generateConfigFile(data, true, value, true);
+ } else if (ffStrEqualsIgnCase(key, "-c") || ffStrEqualsIgnCase(key, "--config")) {
+ optionParseConfigFile(data, key, value);
+ } else if (ffStrEqualsIgnCase(key, "-j") || ffStrEqualsIgnCase(key, "--json")) {
+ if (ffOptionParseBoolean(value)) {
+ enableJsonOutput(data);
+ }
+ } else if (ffStrEqualsIgnCase(key, "--format")) {
+ if (!!ffOptionParseEnum(key, value, (FFKeyValuePair[]) {
+ { "default", false },
+ { "json", true },
+ {},
+ })) {
+ enableJsonOutput(data);
+ }
+ } else if (ffStrEqualsIgnCase(key, "--dynamic-interval")) {
+ instance.state.dynamicInterval = ffOptionParseUInt32(key, value); // seconds to milliseconds
+ } else {
+ return;
+ }
+
+ // Don't parse it again in parseOption.
+ // This is necessary because parseOption doesn't understand this option and will result in an unknown option error.
+ key[0] = '\0';
+ if (value) {
+ value[0] = '\0';
+ }
+}
+
+static void parseOption(FFdata* data, const char* key, const char* value) {
+ if (ffStrEqualsIgnCase(key, "-s") || ffStrEqualsIgnCase(key, "--structure")) {
+ ffOptionParseString(key, value, &data->structure);
+ }
+
+ else if (
+ ffOptionsParseGeneralCommandLine(&instance.config.general, key, value) ||
+ ffOptionsParseLogoCommandLine(&instance.config.logo, key, value) ||
+ ffOptionsParseDisplayCommandLine(&instance.config.display, key, value) ||
+ ffParseModuleOptions(key, value)) {
+ }
+
+ else if (ffStrEqualsIgnCase(key, "--structure-disabled")) {
+ ffOptionParseString(key, value, &data->structureDisabled);
+ }
+
+ else {
+ fprintf(stderr, "Error: unknown option: %s\n", key);
+ exit(400);
+ }
+}
+
+static void parseConfigFiles(FFdata* data) {
+ if (__builtin_expect(data->genConfigPath.length == 0, true)) {
+ FF_LIST_FOR_EACH (FFstrbuf, dir, instance.state.platform.configDirs) {
+ uint32_t dirLength = dir->length;
+
+ ffStrbufAppendS(dir, "fastfetch/config.jsonc");
+ bool success = parseJsoncFile(data, dir->chars, YYJSON_READ_ALLOW_COMMENTS | YYJSON_READ_ALLOW_TRAILING_COMMAS);
+ ffStrbufSubstrBefore(dir, dirLength);
+ if (success) {
+ return;
+ }
+
+ ffStrbufAppendS(dir, "fastfetch/config.json5");
+ success = parseJsoncFile(data, dir->chars, YYJSON_READ_JSON5);
+ ffStrbufSubstrBefore(dir, dirLength);
+ if (success) {
+ return;
+ }
+ }
+ }
+}
+
+static void parseArguments(FFdata* data, int argc, char** argv, void (*parser)(FFdata* data, char* key, char* value)) {
+ for (int i = 1; i < argc; i++) {
+ const char* key = argv[i];
+ if (*key == '\0') {
+ continue; // has been handled by parseCommand
+ }
+
+ if (*key != '-') {
+ fprintf(stderr, "Error: invalid option: %s. An option must start with `-`\n", key);
+ exit(400);
+ }
+
+ if (i == argc - 1 || (argv[i + 1][0] == '-' && argv[i + 1][1] != '\0' && // `-` is used as an alias for `/dev/stdin`
+ !ffStrEqualsIgnCase(argv[i], "--separator-string") // Separator string can start with a -
+ )) {
+ parser(data, argv[i], NULL);
+ } else {
+ parser(data, argv[i], argv[i + 1]);
+ ++i;
+ }
+ }
+}
+
+static void run(FFdata* data) {
+ const bool useJsonConfig = data->structure.length == 0 && data->configDoc;
+
+ if (useJsonConfig) {
+ ffPrintJsonConfig(data, true /* prepare */);
+ } else {
+ // If we don't have a custom structure, use the default one
+ if (data->structure.length == 0) {
+ ffStrbufAppendS(&data->structure, FASTFETCH_DATATEXT_STRUCTURE); // Cannot use `ffStrbufSetStatic` here because we will modify the string
+ }
+ ffPrepareCommandOption(data);
+ }
+
+ ffStart();
+
+ if (!data->resultDoc) {
+ ffLogoPrint();
+ }
+
+#if defined(_WIN32)
+ if (!instance.config.display.noBuffer) {
+ fflush(stdout);
+ }
+#endif
+
+ while (true) {
+ if (useJsonConfig) {
+ ffPrintJsonConfig(data, false);
+ } else {
+ ffPrintCommandOption(data);
+ }
+
+ if (instance.state.dynamicInterval > 0) {
+ ffLogoPrintRemaining(); // `logoLineCacheClear` inside so that ffLogoPrintLine will use `\e[nC` to move the cursor to the right position instead of reprinting the logo
+ 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
+ fflush(stdout);
+ ffTimeSleep(instance.state.dynamicInterval);
+ fputs("\e[H", stdout); // Move cursor to the top left corner to overwrite the previous output
+ instance.state.keysHeight = 0; // Reset keysHeight so `ffLogoPrintRemaining` will recalculate it
+ } else {
+ break;
+ }
+
+ if (useJsonConfig) {
+ ffPrintJsonConfig(data, true /* prepare */);
+ } else {
+ ffPrepareCommandOption(data);
+ }
+ }
+
+ if (data->resultDoc) {
+ yyjson_mut_write_fp(stdout, data->resultDoc, YYJSON_WRITE_INF_AND_NAN_AS_NULL | YYJSON_WRITE_PRETTY_TWO_SPACES | YYJSON_WRITE_NEWLINE_AT_END, NULL, NULL);
+ } else {
+ if (instance.config.logo.printRemaining) {
+ ffLogoPrintRemaining();
+ }
+ ffFinish();
+ }
+}
+
+static void writeConfigFile(FFdata* data) {
+ const FFstrbuf* filename = &data->genConfigPath;
+
+ yyjson_mut_doc* doc = data->resultDoc;
+ yyjson_mut_val* root = yyjson_mut_obj(doc);
+ yyjson_mut_doc_set_root(doc, root);
+ yyjson_mut_obj_add_str(doc, root, "$schema", "https://github.com/fastfetch-cli/fastfetch/raw/master/doc/json_schema.json");
+
+ if (data->docType == FF_RESULT_DOC_TYPE_CONFIG_FULL) {
+ ffOptionsGenerateLogoJsonConfig(data, &instance.config.logo);
+ ffOptionsGenerateDisplayJsonConfig(data, &instance.config.display);
+ ffOptionsGenerateGeneralJsonConfig(data, &instance.config.general);
+ }
+ ffMigrateCommandOptionToJsonc(data);
+
+ if (ffStrbufEqualS(filename, "-")) {
+ yyjson_mut_write_fp(stdout, doc, YYJSON_WRITE_INF_AND_NAN_AS_NULL | YYJSON_WRITE_PRETTY_TWO_SPACES | YYJSON_WRITE_NEWLINE_AT_END, NULL, NULL);
+ } else {
+ size_t len;
+ 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);
+ if (!str) {
+ printf("Error: failed to generate config file\n");
+ exit(1);
+ }
+ if (ffWriteFileData(filename->chars, len, str)) {
+ printf("✓ Configuration file generated: `%s`\n"
+ "* Tip: Use a JSON schema-aware editor for better editing experience\n"
+ "* Documentation: https://github.com/fastfetch-cli/fastfetch/wiki/Configuration\n",
+ filename->chars);
+ } else {
+ printf("Error: failed to write file in `%s`\n", filename->chars);
+ exit(1);
+ }
+ }
+}
+
+int main(int argc, char** argv) {
+ ffInitInstance();
+ atexit(ffDestroyInstance);
+
+ // Data stores things only needed for the configuration of fastfetch
+ FFdata data = {
+ .structure = ffStrbufCreate(),
+ .structureDisabled = ffStrbufCreate(),
+ .genConfigPath = ffStrbufCreate(),
+ };
+
+ parseArguments(&data, argc, argv, parseCommand);
+ if (instance.state.dynamicInterval && data.resultDoc) {
+ fprintf(stderr, "Error: --dynamic-interval cannot be used with --json\n");
+ exit(400);
+ }
+
+ if (!data.configLoaded && !getenv("NO_CONFIG")) {
+ parseConfigFiles(&data);
+ }
+ parseArguments(&data, argc, argv, (void*) parseOption);
+
+ if (__builtin_expect(data.genConfigPath.length == 0, true)) {
+ run(&data);
+ } else {
+ writeConfigFile(&data);
+ }
+
+ ffStrbufDestroy(&data.structure);
+ ffStrbufDestroy(&data.structureDisabled);
+ yyjson_doc_free(data.configDoc);
+ yyjson_mut_doc_free(data.resultDoc);
+ ffStrbufDestroy(&data.genConfigPath);
+}