diff options
| author | sumuel <samuel@yakubos.org> | 2026-08-17 20:44:55 +0000 |
|---|---|---|
| committer | sumuel <samuel@yakubos.org> | 2026-08-17 20:44:55 +0000 |
| commit | 76424950e373d3b04ac3dd13019151bfba3e8423 (patch) | |
| tree | 4c3cfdbda039e592b9186be3e28f8d7cfd439e8a /src/common/impl/font.c | |
Add the files
Diffstat (limited to 'src/common/impl/font.c')
| -rw-r--r-- | src/common/impl/font.c | 513 |
1 files changed, 513 insertions, 0 deletions
diff --git a/src/common/impl/font.c b/src/common/impl/font.c new file mode 100644 index 0000000..93feff1 --- /dev/null +++ b/src/common/impl/font.c @@ -0,0 +1,513 @@ +#include "fastfetch.h" +#include "common/FFlist.h" +#include "common/FFstrbuf.h" +#include "common/strutil.h" +#include "common/font.h" + +#include <string.h> +#include <ctype.h> + +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 "-<number>" 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); +} |