From 76424950e373d3b04ac3dd13019151bfba3e8423 Mon Sep 17 00:00:00 2001 From: sumuel Date: Mon, 17 Aug 2026 20:44:55 +0000 Subject: Add the files --- src/fastfetch.c | 864 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 864 insertions(+) create mode 100644 src/fastfetch.c (limited to 'src/fastfetch.c') 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 +#include +#include + +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\": \"{}\" }\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\e[m\n\n", stdout); + } else { + fputs("Usage: fastfetch \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 `\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")) { // -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 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 \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); +} -- cgit v1.2.3