#include "fastfetch.h" #include "common/format.h" #include "common/parsing.h" #include "common/textModifier.h" #include "common/strutil.h" #include "common/library.h" #include void ffFormatAppendFormatArg(FFstrbuf* buffer, const FFformatarg* formatarg) { switch (formatarg->type) { case FF_ARG_TYPE_INT: ffStrbufAppendSInt(buffer, *(int32_t*) formatarg->value); break; case FF_ARG_TYPE_UINT: ffStrbufAppendUInt(buffer, *(uint32_t*) formatarg->value); break; case FF_ARG_TYPE_UINT64: ffStrbufAppendUInt(buffer, *(uint64_t*) formatarg->value); break; case FF_ARG_TYPE_UINT16: ffStrbufAppendUInt(buffer, *(uint16_t*) formatarg->value); break; case FF_ARG_TYPE_UINT8: ffStrbufAppendUInt(buffer, *(uint8_t*) formatarg->value); break; case FF_ARG_TYPE_STRING: ffStrbufAppendS(buffer, (const char*) formatarg->value); break; case FF_ARG_TYPE_STRBUF: ffStrbufAppend(buffer, (const FFstrbuf*) formatarg->value); break; case FF_ARG_TYPE_FLOAT: ffStrbufAppendDouble(buffer, *(float*) formatarg->value, instance.config.display.fractionNdigits, instance.config.display.fractionTrailingZeros != FF_FRACTION_TRAILING_ZEROS_TYPE_NEVER); break; case FF_ARG_TYPE_DOUBLE: ffStrbufAppendDouble(buffer, *(double*) formatarg->value, instance.config.display.fractionNdigits, instance.config.display.fractionTrailingZeros != FF_FRACTION_TRAILING_ZEROS_TYPE_NEVER); break; case FF_ARG_TYPE_BOOL: ffStrbufAppendS(buffer, *(bool*) formatarg->value ? "true" : "false"); break; case FF_ARG_TYPE_LIST: { const FFlist* list = (const FFlist*) formatarg->value; for (uint32_t i = 0; i < list->length; i++) { ffStrbufAppend(buffer, FF_LIST_GET(FFstrbuf, *list, i)); if (i < list->length - 1) { ffStrbufAppendS(buffer, ", "); } } break; } case FF_ARG_TYPE_BUFFER: { // Placeholder for binary data, just print the size for now const FFArgBuffer* argBuffer = (const FFArgBuffer*) formatarg->value; ffStrbufAppendF(buffer, "buffer(%u bytes)", argBuffer->length); break; } default: if (formatarg->type != FF_ARG_TYPE_NULL) { fprintf(stderr, "Error: format string \"%s\": argument is not implemented: %i\n", buffer->chars, formatarg->type); } break; } } // strtol() to an int32_t. Values that don't fit are clamped instead of wrapping around, // so that e.g. 4294967297 can't turn into 1. static int32_t strToInt32(const char* str, char** pEnd) { long long value = strtoll(str, pEnd, 10); return value > INT32_MAX ? INT32_MAX : value < INT32_MIN ? INT32_MIN : (int32_t) value; } /** * @brief parses a string to a uint32_t * * If the string can't be parsed, or is < 1, uint32_t max is returned. * * @param placeholderValue the string to parse * @return uint32_t the parsed value */ static uint32_t getArgumentIndex(const char* placeholderValue, uint32_t numArgs, const FFformatarg* arguments) { char firstChar = placeholderValue[0]; if (firstChar == '\0') { return 0; // use arg counter } if (firstChar >= '0' && firstChar <= '9') { char* pEnd = nullptr; unsigned long result = strtoul(placeholderValue, &pEnd, 10); if (result > numArgs) { return UINT32_MAX; } if (*pEnd != '\0') { return UINT32_MAX; } return (uint32_t) result; } else if (ffCharIsEnglishAlphabet(firstChar)) { for (uint32_t i = 0; i < numArgs; ++i) { const FFformatarg* arg = &arguments[i]; if (arg->name && ffStrEqualsIgnCase(placeholderValue, arg->name)) { return i + 1; } } } return UINT32_MAX; } static inline void appendInvalidPlaceholder(FFstrbuf* buffer, const char* start, const FFstrbuf* placeholderValue, uint32_t index, uint32_t formatStringLength) { ffStrbufAppendS(buffer, start); ffStrbufAppend(buffer, placeholderValue); if (index < formatStringLength) { ffStrbufAppendC(buffer, '}'); } } static inline bool formatArgSet(const FFformatarg* arg) { // clang-format off return arg->value != nullptr && ( (arg->type == FF_ARG_TYPE_DOUBLE && *(double*) arg->value > 0.0) || (arg->type == FF_ARG_TYPE_FLOAT && *(float*) arg->value > 0.0) || (arg->type == FF_ARG_TYPE_INT && *(int32_t*) arg->value > 0) || (arg->type == FF_ARG_TYPE_STRBUF && ((FFstrbuf*) arg->value)->length > 0) || (arg->type == FF_ARG_TYPE_STRING && ffStrSet((char*) arg->value)) || (arg->type == FF_ARG_TYPE_UINT8 && *(uint8_t*) arg->value > 0) || (arg->type == FF_ARG_TYPE_UINT16 && *(uint16_t*) arg->value > 0) || (arg->type == FF_ARG_TYPE_UINT && *(uint32_t*) arg->value > 0) || (arg->type == FF_ARG_TYPE_UINT64 && *(uint64_t*) arg->value > 0) || (arg->type == FF_ARG_TYPE_BOOL && *(bool*) arg->value) || (arg->type == FF_ARG_TYPE_LIST && ((FFlist*) arg->value)->length > 0) ); // clang-format on } [[maybe_unused]] static inline void normalizeArgName(FFstrbuf* dst, const char* src) { ffStrbufClear(dst); bool flag = false; for (const char* p = src; *p; ++p) { if (*p == '-') { flag = true; } else if (flag) { ffStrbufAppendC(dst, (char) toupper((unsigned char) *p)); flag = false; } else { ffStrbufAppendC(dst, *p); } } } #if FF_HAVE_LUA #include "common/lua.h" static void appendLuaError(FFstrbuf* buffer, const char* prefix, lua_State* L) { const char* err = lua_tolstring(L, -1, nullptr); if (err) { // Drop the position Lua puts in front of an error and keep the message only. Both places // that build one -- `luaG_addinfo` and `luaL_where` -- format it as `%s:%d: %s`, so a // position is a colon, a line number, a colon and a space: // general.preload.lua:1: expected near '=' -> expected near '=' // [string ""]:1: x -> x // Not every error has one, and cutting at the first colon mangled those. `luaL_loadfilex` // reports `cannot open : `, where that colon is the one after the path, so // the path -- the only part naming the problem -- was dropped and `No such file or // directory` was left on its own. A colon followed by a line number does not identify a // position either, because a path can contain one: `cannot open /tmp/x:12:y/nope.lua: ...` // has to survive whole. The trailing space is what separates the two, and a real position // always has it. for (const char* colon = strchr(err, ':'); colon; colon = strchr(colon + 1, ':')) { const char* line = colon + 1; while (*line >= '0' && *line <= '9') { ++line; } if (line > colon + 1 && line[0] == ':' && line[1] == ' ') { err = line + 2; break; } } } ffStrbufAppendF(buffer, "%s: %s", prefix, err ? err : "unknown"); } static bool parseLuaString(FFstrbuf* buffer, const char* script, uint32_t scriptLen, uint32_t numArgs, const FFformatarg* arguments) { const char* err = ffLuaLoadState(); if (err) { ffStrbufAppendF(buffer, "Lua init error: %s", err); return false; } FF_STRBUF_AUTO_DESTROY argNameBuf = ffStrbufCreate(); bool ret = false; lua_State* L = luaData.L; // Clear stack and load chunk lua_settop(L, 0); if (luaL_loadbuffer(L, script, scriptLen, "") != LUA_OK) { appendLuaError(buffer, "Lua load error", L); } else { // Build args table for name lookup only. lua_createtable(L, 0, 0); for (uint32_t i = 0; i < numArgs; ++i) { const FFformatarg* arg = &arguments[i]; switch (arg->type) { case FF_ARG_TYPE_INT: lua_pushinteger(L, (lua_Integer) * (int32_t*) arg->value); break; case FF_ARG_TYPE_UINT: lua_pushinteger(L, (lua_Integer) * (uint32_t*) arg->value); break; case FF_ARG_TYPE_UINT64: lua_pushinteger(L, (lua_Integer) * (uint64_t*) arg->value); break; case FF_ARG_TYPE_UINT16: lua_pushinteger(L, (lua_Integer) * (uint16_t*) arg->value); break; case FF_ARG_TYPE_UINT8: lua_pushinteger(L, (lua_Integer) * (uint8_t*) arg->value); break; case FF_ARG_TYPE_FLOAT: lua_pushnumber(L, (lua_Number) * (float*) arg->value); break; case FF_ARG_TYPE_DOUBLE: lua_pushnumber(L, (lua_Number) * (double*) arg->value); break; case FF_ARG_TYPE_BOOL: lua_pushboolean(L, *(bool*) arg->value); break; case FF_ARG_TYPE_STRING: if (!arg->value) { lua_pushlstring(L, "", 0); } else { lua_pushlstring(L, (const char*) arg->value, strlen((const char*) arg->value)); } break; case FF_ARG_TYPE_STRBUF: { const FFstrbuf* sb = (const FFstrbuf*) arg->value; lua_pushlstring(L, sb->chars, sb->length); break; } case FF_ARG_TYPE_LIST: { const FFlist* list = (const FFlist*) arg->value; lua_createtable(L, 0, 0); for (uint32_t li = 0; li < list->length; ++li) { const FFstrbuf* item = FF_LIST_GET(FFstrbuf, *list, li); lua_pushlstring(L, item->chars, item->length); lua_seti(L, -2, (lua_Integer) (li + 1)); } break; } default: lua_pushnil(L); break; } if (arg->name && arg->name[0]) { normalizeArgName(&argNameBuf, arg->name); } else { ffStrbufSetF(&argNameBuf, "arg%" PRIu32, i + 1); } lua_setfield(L, -2, argNameBuf.chars); } if (lua_pcall(L, 1, LUA_MULTRET, 0) != LUA_OK) { appendLuaError(buffer, "Lua runtime error", L); } else { int nresults = lua_gettop(L); if (nresults == 0) { ffStrbufAppendS(buffer, "Lua result error: no result"); } else { // Convert first result to string const char* res = lua_tolstring(L, 1, nullptr); if (res) { ffStrbufAppendS(buffer, res); } else { luaL_tolstring(L, 1, nullptr); const char* sval = lua_tolstring(L, -1, nullptr); if (sval) { ffStrbufAppendS(buffer, sval); } } ret = true; } } } lua_settop(L, 0); return ret; } #endif // Returns the length of the ANSI CSI sequence at `str`, or 0 if there is none. // A CSI sequence is `ESC [` + parameter bytes (0x30-0x3F) + intermediate bytes (0x20-0x2F) + final byte (0x40-0x7E) static uint32_t ansiEscapeLength(const char* str) { if (str[0] != '\e' || str[1] != '[') { return 0; } const char* p = str + 2; while (*p >= '0' && *p <= '?') { ++p; } while (*p >= ' ' && *p <= '/') { ++p; } if (*p < '@' || *p > '~') { return 0; // not a complete CSI sequence } return (uint32_t) (p - str) + 1; } static bool skipAnsiEscape(FFstrbuf* in, FFstrbuf* out, FFstrbuf* trailingEscape) { // Skip all ANSI escape codes at the start of the string, so that they don't count // towards the truncation / padding length. A value may carry more than one of them, // e.g. `\e[31m\e[1m`. uint32_t prefixLen = 0; while (in->length - prefixLen >= 2) { uint32_t len = ansiEscapeLength(in->chars + prefixLen); if (len == 0) { break; } prefixLen += len; } if (prefixLen == 0) { return false; } ffStrbufAppendNS(out, prefixLen, in->chars); ffStrbufSubstrAfter(in, prefixLen - 1); if (trailingEscape) { // likely have one or more `CSI m` resets at the end of the string uint32_t end = in->length; while (end >= 3) { uint32_t start = end - 1; while (start > 0 && in->chars[start] != '\e') { --start; } uint32_t len = ansiEscapeLength(in->chars + start); if (len == 0 || start + len != end) { break; } end = start; } if (end < in->length) { ffStrbufSetNS(trailingEscape, in->length - end, in->chars + end); ffStrbufSubstrBefore(in, end); } } return true; } // Returns the index of the `}` of the `{}` that closes the conditional we are in, or the // length of the format string if there is none. `i` is the index of the `}` that ends the opening // placeholder. The part to skip may hold conditionals of its own, which have to be passed over. // Like in `parseFormatString`, only a conditional with a valid argument opens a block. static uint32_t findConditionalEnd(const FFstrbuf* formatstr, uint32_t i, char marker, uint32_t numArgs, const FFformatarg* arguments) { FF_STRBUF_AUTO_DESTROY placeholderValue = ffStrbufCreate(); uint32_t depth = 0; for (++i; i < formatstr->length; ++i) { if (formatstr->chars[i] != '{') { continue; } ++i; if (i >= formatstr->length) { break; } // double {{ evaluates to a single { and doesn't start a placeholder if (formatstr->chars[i] == '{') { continue; } uint32_t iEnd = ffStrbufNextIndexC(formatstr, i, '}'); if (formatstr->chars[i] == marker) { if (iEnd - i == 1) { if (depth == 0) { return iEnd; } --depth; } else { ffStrbufClear(&placeholderValue); ffStrbufAppendNS(&placeholderValue, iEnd - i - 1, &formatstr->chars[i + 1]); uint32_t index = getArgumentIndex(placeholderValue.chars, numArgs, arguments); if (index >= 1 && index <= numArgs) { ++depth; } } } i = iEnd; } return formatstr->length; } static bool parseFormatString(FFstrbuf* buffer, const FFstrbuf* formatstr, uint32_t numArgs, const FFformatarg* arguments) { uint32_t argCounter = 0; uint32_t numOpenIfs = 0; uint32_t numOpenNotIfs = 0; FF_STRBUF_AUTO_DESTROY placeholderValue = ffStrbufCreate(); for (uint32_t i = 0; i < formatstr->length; ++i) { // if we don't have a placeholder start just copy the chars over to output buffer if (formatstr->chars[i] != '{') { ffStrbufAppendC(buffer, formatstr->chars[i]); continue; } // jump to next char, the start of the placeholder value ++i; // unmatched trailing '{' if (i >= formatstr->length) { ffStrbufAppendC(buffer, '{'); break; } // double {{ elvaluates to a single { and doesn't count as start if (formatstr->chars[i] == '{') { ffStrbufAppendC(buffer, '{'); continue; } ffStrbufClear(&placeholderValue); { uint32_t iEnd = ffStrbufNextIndexC(formatstr, i, '}'); ffStrbufAppendNS(&placeholderValue, iEnd - i, &formatstr->chars[i]); i = iEnd; } char firstChar = placeholderValue.chars[0]; if (placeholderValue.length == 1) { // test if for stop, if so break the loop if (firstChar == '-') { break; } // test for end of an if, if so do nothing if (firstChar == '?') { if (numOpenIfs == 0) { appendInvalidPlaceholder(buffer, "{", &placeholderValue, i, formatstr->length); } else { --numOpenIfs; } continue; } // test for end of a not if, if so do nothing if (firstChar == '/') { if (numOpenNotIfs == 0) { appendInvalidPlaceholder(buffer, "{", &placeholderValue, i, formatstr->length); } else { --numOpenNotIfs; } continue; } // test for end of a color, if so do nothing if (firstChar == '#') { if (!instance.config.display.pipe) { ffStrbufAppendS(buffer, FASTFETCH_TEXT_MODIFIER_RESET); } continue; } } // test for if, if so evaluate it if (firstChar == '?') { ffStrbufSubstrAfter(&placeholderValue, 0); uint32_t index = getArgumentIndex(placeholderValue.chars, numArgs, arguments); // testing for an invalid index if (index > numArgs || index < 1) { appendInvalidPlaceholder(buffer, "{?", &placeholderValue, i, formatstr->length); continue; } // continue normally if an format arg is set and the value is > 0 if (formatArgSet(&arguments[index - 1])) { ++numOpenIfs; continue; } // fastforward to the end of the if without printing the in between i = findConditionalEnd(formatstr, i, '?', numArgs, arguments); continue; } // test for not if, if so evaluate it if (firstChar == '/') { ffStrbufSubstrAfter(&placeholderValue, 0); uint32_t index = getArgumentIndex(placeholderValue.chars, numArgs, arguments); // testing for an invalid index if (index > numArgs || index < 1) { appendInvalidPlaceholder(buffer, "{/", &placeholderValue, i, formatstr->length); continue; } // continue normally if an format arg is not set or the value is 0 if (!formatArgSet(&arguments[index - 1])) { ++numOpenNotIfs; continue; } // fastforward to the end of the if without printing the in between i = findConditionalEnd(formatstr, i, '/', numArgs, arguments); continue; } // test for color, if so evaluate it if (firstChar == '#') { if (!instance.config.display.pipe) { ffStrbufAppendS(buffer, "\e["); ffOptionParseColorNoClear(placeholderValue.chars + 1, buffer); ffStrbufAppendC(buffer, 'm'); } continue; } // test for constant or env var, if so evaluate it if (firstChar == '$') { char* pend = nullptr; int32_t indexSigned = strToInt32(placeholderValue.chars + 1, &pend); if (pend == placeholderValue.chars + 1) { // treat placeholder as an environment variable char* envValue = getenv(placeholderValue.chars + 1); if (envValue) { ffStrbufAppendS(buffer, envValue); } else { appendInvalidPlaceholder(buffer, "{", &placeholderValue, i, formatstr->length); } } else { // treat placeholder as a constant uint32_t index = (uint32_t) (indexSigned < 0 ? (int32_t) instance.config.display.constants.length + indexSigned : indexSigned - 1); if (*pend != '\0' || instance.config.display.constants.length <= index) { appendInvalidPlaceholder(buffer, "{", &placeholderValue, i, formatstr->length); } else { FFstrbuf* item = FF_LIST_GET(FFstrbuf, instance.config.display.constants, index); ffStrbufAppend(buffer, item); } } continue; } char* pSep = placeholderValue.chars; char cSep = '\0'; while (*pSep && *pSep != ':' && *pSep != '<' && *pSep != '>' && *pSep != '|' && *pSep != '~') { ++pSep; } if (*pSep) { cSep = *pSep; *pSep = '\0'; } else { pSep = nullptr; } uint32_t index = getArgumentIndex(placeholderValue.chars, numArgs, arguments); // test for invalid index if (index == 0) { index = ++argCounter; } if (index > numArgs) { if (pSep) { *pSep = cSep; } appendInvalidPlaceholder(buffer, "{", &placeholderValue, i, formatstr->length); continue; } if (!cSep) { ffFormatAppendFormatArg(buffer, &arguments[index - 1]); } else if (cSep == '~') { FF_STRBUF_AUTO_DESTROY tempString = ffStrbufCreate(); ffFormatAppendFormatArg(&tempString, &arguments[index - 1]); FF_STRBUF_AUTO_DESTROY trailingEscape = ffStrbufCreate(); skipAnsiEscape(&tempString, buffer, &trailingEscape); char* pEnd = nullptr; int32_t start = strToInt32(pSep + 1, &pEnd); bool hasEnd = *pEnd == ','; int32_t end = 0; if (hasEnd) { end = strToInt32(pEnd + 1, &pEnd); } // Check the whole range before using it, so that a valid range that lies // outside of the string gives an empty result instead of an invalid placeholder if (!*pEnd) { if (start < 0) { start = (int32_t) tempString.length + start; } if (start >= 0 && (uint32_t) start < tempString.length) { if (!hasEnd) { ffStrbufAppendNS(buffer, tempString.length - (uint32_t) start, &tempString.chars[start]); } else { if (end < 0) { end = (int32_t) tempString.length + end; } if (end >= 0 && (uint32_t) end > tempString.length) { end = (int32_t) tempString.length; } if (end > start) { ffStrbufAppendNS(buffer, (uint32_t) (end - start), &tempString.chars[start]); } } } } if (trailingEscape.length > 0) { ffStrbufAppend(buffer, &trailingEscape); } if (*pEnd) { *pSep = cSep; appendInvalidPlaceholder(buffer, "{", &placeholderValue, i, formatstr->length); continue; } } else { char* pEnd = nullptr; int32_t truncLength = strToInt32(pSep + 1, &pEnd); if (*pEnd != '\0') { *pSep = cSep; appendInvalidPlaceholder(buffer, "{", &placeholderValue, i, formatstr->length); continue; } bool ellipsis = false; if (truncLength < 0) { ellipsis = true; truncLength = truncLength == INT32_MIN ? INT32_MAX : -truncLength; } FF_STRBUF_AUTO_DESTROY tempString = ffStrbufCreate(); ffFormatAppendFormatArg(&tempString, &arguments[index - 1]); FF_STRBUF_AUTO_DESTROY trailingEscape = ffStrbufCreate(); skipAnsiEscape(&tempString, buffer, &trailingEscape); if (tempString.length == (uint32_t) truncLength) { ffStrbufAppend(buffer, &tempString); } else if (tempString.length > (uint32_t) truncLength) { if (cSep == ':') { ffStrbufSubstrBefore(&tempString, (uint32_t) truncLength); ffStrbufTrimRightSpace(&tempString); } else { ffStrbufSubstrBefore(&tempString, (uint32_t) (!ellipsis ? truncLength : truncLength - 1)); } ffStrbufAppend(buffer, &tempString); if (ellipsis) { ffStrbufAppendS(buffer, "…"); } } else if (cSep == ':') { ffStrbufAppend(buffer, &tempString); } else { if (cSep == '<') { // left align ffStrbufAppend(buffer, &tempString); ffStrbufAppendNC(buffer, (uint32_t) truncLength - tempString.length, ' '); } else if (cSep == '>') { // right align ffStrbufAppendNC(buffer, (uint32_t) truncLength - tempString.length, ' '); ffStrbufAppend(buffer, &tempString); } else if (cSep == '|') { // center align uint32_t padding = ((uint32_t) truncLength - tempString.length) / 2; ffStrbufAppendNC(buffer, padding, ' '); ffStrbufAppend(buffer, &tempString); ffStrbufAppendNC(buffer, (uint32_t) truncLength - tempString.length - padding, ' '); } } if (trailingEscape.length > 0) { ffStrbufAppend(buffer, &trailingEscape); } } } if (!instance.config.display.pipe) { ffStrbufAppendS(buffer, FASTFETCH_TEXT_MODIFIER_RESET); } return true; } bool ffParseFormatString(FFstrbuf* buffer, const FFstrbuf* formatstr, uint32_t numArgs, const FFformatarg* arguments) { #if FF_HAVE_LUA if (ffStrbufStartsWithS(formatstr, "lua:")) { // If outputFormat starts with "lua:", treat the rest as a Lua script return parseLuaString(buffer, formatstr->chars + 4, formatstr->length - 4, numArgs, arguments); } #endif return parseFormatString(buffer, formatstr, numArgs, arguments); }