#include "fastfetch.h" #include "common/FFlist.h" #include "common/FFstrbuf.h" #include "common/strutil.h" #include "common/font.h" #include #include void ffFontInit(FFfont* font) { // Ensure no memory allocates ffStrbufInit(&font->pretty); ffStrbufInit(&font->name); ffStrbufInit(&font->size); ffListInit(&font->styles); } static void strbufAppendNSExcludingC(FFstrbuf* strbuf, uint32_t length, const char* value, char exclude) { if (value == NULL || length == 0) { return; } ffStrbufEnsureFree(strbuf, length); for (uint32_t i = 0; i < length; i++) { if (value[i] != exclude) { strbuf->chars[strbuf->length++] = value[i]; } } strbuf->chars[strbuf->length] = '\0'; } static void fontInitPretty(FFfont* font) { ffStrbufAppend(&font->pretty, &font->name); if (font->size.length == 0 && font->styles.length == 0) { return; } else if (font->pretty.length == 0) { ffStrbufAppendS(&font->pretty, "default"); } ffStrbufAppendS(&font->pretty, " ("); if (font->size.length > 0) { ffStrbufAppend(&font->pretty, &font->size); if (!ffStrbufEndsWithS(&font->size, "pt") && !ffStrbufEndsWithS(&font->size, "px")) { ffStrbufAppendS(&font->pretty, "pt"); } if (font->styles.length > 0) { ffStrbufAppendS(&font->pretty, ", "); } } for (uint32_t i = 0; i < font->styles.length; i++) { ffStrbufAppend(&font->pretty, FF_LIST_GET(FFstrbuf, font->styles, i)); if (i < font->styles.length - 1) { ffStrbufAppendS(&font->pretty, ", "); } } ffStrbufAppendC(&font->pretty, ')'); } void ffFontInitQt(FFfont* font, const char* data) { ffFontInit(font); // See https://doc.qt.io/qt-5/qfont.html#toString // Family data = ffStrbufAppendSUntilC(&font->name, data, ','); ffStrbufTrim(&font->name, ' '); if (!data) { goto exit; } data++; // Size data = ffStrbufAppendSUntilC(&font->size, data, ','); ffStrbufTrim(&font->size, ' '); if (!data) { goto exit; } data++; // Style data = strrchr(data, ','); if (!data) { goto exit; } data++; if (isalpha(*data)) { do { FFstrbuf* style = FF_LIST_ADD(FFstrbuf, font->styles); ffStrbufInit(style); data = ffStrbufAppendSUntilC(style, data, ' '); if (data) { data++; } } while (data); } exit: fontInitPretty(font); } static void fontPangoParseWord(const char** data, FFfont* font, FFstrbuf* alternativeBuffer) { while (**data == ' ' || **data == '\t' || **data == ',') { ++(*data); } const char* wordStart = *data; while (**data != ' ' && **data != '\t' && **data != ',' && **data != '\0' && **data != '`' && **data != '\\') { ++(*data); } uint32_t wordLength = (uint32_t) (*data - wordStart); if (wordLength == 0) { return; } if (**data == '\0' || **data == '`' || **data == '\\') { ffStrbufAppendNS(&font->size, wordLength, wordStart); if (ffStrbufEndsWithS(&font->size, "px")) { ffStrbufSubstrBefore(&font->size, font->size.length - 2); } double dummy; if (sscanf(font->size.chars, "%lf", &dummy) == 1) { return; } ffStrbufClear(&font->size); } if ( ffStrStartsWithIgnCase(wordStart, "Ultra") || ffStrStartsWithIgnCase(wordStart, "Extra") || ffStrStartsWithIgnCase(wordStart, "Semi") || ffStrStartsWithIgnCase(wordStart, "Demi") || ffStrStartsWithIgnCase(wordStart, "Normal") || ffStrStartsWithIgnCase(wordStart, "Roman") || ffStrStartsWithIgnCase(wordStart, "Oblique") || ffStrStartsWithIgnCase(wordStart, "Italic") || ffStrStartsWithIgnCase(wordStart, "Thin") || ffStrStartsWithIgnCase(wordStart, "Light") || ffStrStartsWithIgnCase(wordStart, "Bold") || ffStrStartsWithIgnCase(wordStart, "Black") || ffStrStartsWithIgnCase(wordStart, "Condensed") || ffStrStartsWithIgnCase(wordStart, "Expanded")) { if (alternativeBuffer == NULL) { alternativeBuffer = FF_LIST_ADD(FFstrbuf, font->styles); ffStrbufInit(alternativeBuffer); } strbufAppendNSExcludingC(alternativeBuffer, wordLength, wordStart, '-'); if ( ffStrStartsWithIgnCase(wordStart, "Ultra ") || ffStrStartsWithIgnCase(wordStart, "Extra ") || ffStrStartsWithIgnCase(wordStart, "Semi ") || ffStrStartsWithIgnCase(wordStart, "Demi ")) { fontPangoParseWord(data, font, alternativeBuffer); } return; } if (alternativeBuffer != NULL) { strbufAppendNSExcludingC(alternativeBuffer, wordLength, wordStart, '-'); return; } if (font->name.length > 0) { ffStrbufAppendC(&font->name, ' '); } ffStrbufAppendNS(&font->name, wordLength, wordStart); } void ffFontInitPango(FFfont* font, const char* data) { ffFontInit(font); while (*data != '\0' && *data != '`' && *data != '\\') { fontPangoParseWord(&data, font, NULL); } fontInitPretty(font); } void ffFontInitValues(FFfont* font, const char* name, const char* size) { ffFontInit(font); ffStrbufAppendS(&font->name, name); ffStrbufTrim(&font->name, '"'); ffStrbufAppendS(&font->size, size); fontInitPretty(font); } void ffFontInitXlfd(FFfont* font, const char* xlfd) { assert(xlfd && *xlfd); // https://en.wikipedia.org/wiki/X_logical_font_description ffFontInit(font); // XLFD: -foundry-family-weight-slant-setwidth-addstyle-pixelsize-pointsize-xres-yres-spacing-averagewidth-charsetregistry-charsetencoding // It often starts with '-', which would create an empty first field. Skip it to align indexes. if (*xlfd == '-') { xlfd++; } const char* pstart = xlfd; for (int field = 0; field < 14; field++) { const char* pend = strchr(pstart, '-'); uint32_t length = pend ? (uint32_t) (pend - pstart) : (uint32_t) strlen(pstart); if (length > 0) { if (field == 1) // family { ffStrbufAppendNS(&font->name, length, pstart); } else if (field == 7) // pointsize (decipoints, preferred) { // parse positive integer from substring long deciPt = 0; bool ok = true; for (uint32_t i = 0; i < length; i++) { char c = pstart[i]; if (c < '0' || c > '9') { ok = false; break; } deciPt = deciPt * 10 + (c - '0'); } if (ok && deciPt > 0) { ffStrbufClear(&font->size); char tmp[32]; if (deciPt % 10 == 0) { snprintf(tmp, sizeof(tmp), "%ldpt", deciPt / 10); } else { snprintf(tmp, sizeof(tmp), "%ld.%ldpt", deciPt / 10, deciPt % 10); } ffStrbufAppendS(&font->size, tmp); } } else if (field == 6) // pixelsize (fallback if pointsize missing/invalid) { if (font->size.length == 0) { long px = 0; bool ok = true; for (uint32_t i = 0; i < length; i++) { char c = pstart[i]; if (c < '0' || c > '9') { ok = false; break; } px = px * 10 + (c - '0'); } if (ok && px > 0) { ffStrbufAppendNS(&font->size, length, pstart); ffStrbufAppendS(&font->size, "px"); } } } else if (field >= 2 && field <= 5) // weight/slant/setwidth/addstyle { // ignore "normal" (case-insensitive) if (!(length == 6 && ffStrStartsWithIgnCase(pstart, "normal"))) { FFstrbuf* style = FF_LIST_ADD(FFstrbuf, font->styles); ffStrbufInitNS(style, length, pstart); } } } if (!pend) { break; } pstart = pend + 1; } fontInitPretty(font); } void ffFontInitXft(FFfont* font, const char* xft) { assert(xft); // https://en.wikipedia.org/wiki/Xft // Xft/Fontconfig pattern examples: // "DejaVu Sans Mono-10" // "monospace:size=10:weight=bold:slant=italic" // "Fira Code-12:style=Regular" // Goal: extract family(name), size, and some common styles. ffFontInit(font); // 1) Parse "head" part before first ':' => usually "family[-size]" (may include commas) const char* p = xft; while (*p == ' ' || *p == '\t') { ++p; } const char* headStart = p; while (*p != '\0' && *p != ':') { ++p; } const char* headEnd = p; // trim tail spaces while (headEnd > headStart && (headEnd[-1] == ' ' || headEnd[-1] == '\t')) { --headEnd; } // If multiple families are listed, take the first one (up to comma) for (const char* q = headStart; q < headEnd; ++q) { if (*q == ',') { headEnd = q; while (headEnd > headStart && (headEnd[-1] == ' ' || headEnd[-1] == '\t')) { --headEnd; } break; } } // Try parse trailing "-" as size, otherwise entire head is name const char* dashPos = NULL; const char* sizeStart = NULL; for (const char* q = headEnd; q > headStart;) { --q; if (*q == '-' && (q + 1) < headEnd && ffCharIsDigit(q[1])) { dashPos = q; sizeStart = q + 1; break; } } if (dashPos) { bool ok = true; bool seenDigit = false; for (const char* q = sizeStart; q < headEnd; ++q) { if (ffCharIsDigit(*q)) { seenDigit = true; } else if (*q == '.') { continue; } else { ok = false; break; } } if (ok && seenDigit) { const char* nameEnd = dashPos; while (nameEnd > headStart && (nameEnd[-1] == ' ' || nameEnd[-1] == '\t')) { --nameEnd; } if (nameEnd > headStart) { ffStrbufAppendNS(&font->name, (uint32_t) (nameEnd - headStart), headStart); } if (headEnd > sizeStart) { ffStrbufAppendNS(&font->size, (uint32_t) (headEnd - sizeStart), sizeStart); } } else { if (headEnd > headStart) { ffStrbufAppendNS(&font->name, (uint32_t) (headEnd - headStart), headStart); } } } else { if (headEnd > headStart) { ffStrbufAppendNS(&font->name, (uint32_t) (headEnd - headStart), headStart); } } ffStrbufTrim(&font->name, ' '); ffStrbufTrim(&font->name, '"'); // 2) Parse key=value fields after ':' (Fontconfig-like). Fields separated by ':'. // Common keys: size, pixelsize, pointsize, style, weight, slant, width while (*p == ':') { ++p; // key const char* keyStart = p; while (*p != '\0' && *p != '=' && *p != ':') { ++p; } const char* keyEnd = p; if (*p != '=') { continue; // skip tokens without '=' } ++p; // skip '=' // value (until next ':', allow backslash-escaping) FF_STRBUF_AUTO_DESTROY value = ffStrbufCreate(); while (*p != '\0' && *p != ':') { if (*p == '\\' && p[1] != '\0') { ++p; ffStrbufAppendC(&value, *p); ++p; continue; } ffStrbufAppendC(&value, *p); ++p; } ffStrbufTrim(&value, ' '); ffStrbufTrim(&value, '"'); uint32_t keyLen = (uint32_t) (keyEnd - keyStart); // helper: set numeric size if not set yet const bool sizeEmpty = (font->size.length == 0); if (value.length > 0) { if ( (keyLen == 4 && ffStrStartsWithIgnCase(keyStart, "size")) || (keyLen == 9 && (ffStrStartsWithIgnCase(keyStart, "pixelsize") || ffStrStartsWithIgnCase(keyStart, "pointsize")))) { if (sizeEmpty && ffCharIsDigit(value.chars[0])) { ffStrbufAppend(&font->size, &value); ffStrbufAppendS(&font->size, (keyLen == 9 && ffStrStartsWithIgnCase(keyStart, "pixelsize")) ? "px" : "pt"); } } else if (keyLen == 5 && ffStrStartsWithIgnCase(keyStart, "style")) { // style may contain multiple words: "Bold Italic" const char* s = value.chars; while (*s != '\0') { while (*s == ' ' || *s == '\t' || *s == ',') { ++s; } const char* w = s; while (*s != '\0' && *s != ' ' && *s != '\t' && *s != ',') { ++s; } if (s > w) { FFstrbuf* style = FF_LIST_ADD(FFstrbuf, font->styles); ffStrbufInitNS(style, (uint32_t) (s - w), w); } } } else if ( (keyLen == 6 && ffStrStartsWithIgnCase(keyStart, "weight")) || (keyLen == 5 && ffStrStartsWithIgnCase(keyStart, "slant")) || (keyLen == 5 && ffStrStartsWithIgnCase(keyStart, "width"))) { // normalize: remove '-' to align with other parsers ("Semi-Bold" -> "SemiBold") FFstrbuf* style = FF_LIST_ADD(FFstrbuf, font->styles); ffStrbufInit(style); strbufAppendNSExcludingC(style, value.length, value.chars, '-'); ffStrbufTrim(style, ' '); if (style->length == 0) { ffStrbufDestroy(style); --font->styles.length; } } } } fontInitPretty(font); } void ffFontInitMoveValues(FFfont* font, FFstrbuf* name, FFstrbuf* size, FFstrbuf* style) { ffFontInit(font); if (name) { ffStrbufInitMove(&font->name, name); } if (size) { ffStrbufInitMove(&font->size, size); } if (style) { FFstrbuf* styleBuf = FF_LIST_ADD(FFstrbuf, font->styles); ffStrbufInitMove(styleBuf, style); } fontInitPretty(font); } void ffFontInitWithSpace(FFfont* font, const char* rawName) { const char* pspace = strrchr(rawName, ' '); if (pspace == NULL) { ffFontInitCopy(font, rawName); return; } ffFontInit(font); ffStrbufAppendNS(&font->name, (uint32_t) (pspace - rawName), rawName); ffStrbufAppendS(&font->size, pspace + 1); fontInitPretty(font); } void ffFontDestroy(FFfont* font) { ffStrbufDestroy(&font->pretty); ffStrbufDestroy(&font->name); ffStrbufDestroy(&font->size); FF_LIST_FOR_EACH (FFstrbuf, str, font->styles) { ffStrbufDestroy(str); } ffListDestroy(&font->styles); }