master ohmu/fastfetch-openbsd / src / common / impl / font.c
ohmu  ·  2026-10-15
  1#include "fastfetch.h"
  2#include "common/FFlist.h"
  3#include "common/FFstrbuf.h"
  4#include "common/strutil.h"
  5#include "common/font.h"
  6
  7#include <string.h>
  8#include <ctype.h>
  9
 10void ffFontInit(FFfont* font) {
 11    // Ensure no memory allocates
 12    ffStrbufInit(&font->pretty);
 13    ffStrbufInit(&font->name);
 14    ffStrbufInit(&font->size);
 15    ffListInit(&font->styles);
 16}
 17
 18static void strbufAppendNSExcludingC(FFstrbuf* strbuf, uint32_t length, const char* value, char exclude) {
 19    if (value == nullptr || length == 0) {
 20        return;
 21    }
 22
 23    ffStrbufEnsureFree(strbuf, length);
 24
 25    for (uint32_t i = 0; i < length; i++) {
 26        if (value[i] != exclude) {
 27            strbuf->chars[strbuf->length++] = value[i];
 28        }
 29    }
 30
 31    strbuf->chars[strbuf->length] = '\0';
 32}
 33
 34static void fontInitPretty(FFfont* font) {
 35    ffStrbufAppend(&font->pretty, &font->name);
 36
 37    if (font->size.length == 0 && font->styles.length == 0) {
 38        return;
 39    } else if (font->pretty.length == 0) {
 40        ffStrbufAppendS(&font->pretty, "default");
 41    }
 42
 43    ffStrbufAppendS(&font->pretty, " (");
 44
 45    if (font->size.length > 0) {
 46        ffStrbufAppend(&font->pretty, &font->size);
 47        if (!ffStrbufEndsWithS(&font->size, "pt") && !ffStrbufEndsWithS(&font->size, "px")) {
 48            ffStrbufAppendS(&font->pretty, "pt");
 49        }
 50
 51        if (font->styles.length > 0) {
 52            ffStrbufAppendS(&font->pretty, ", ");
 53        }
 54    }
 55
 56    for (uint32_t i = 0; i < font->styles.length; i++) {
 57        ffStrbufAppend(&font->pretty, FF_LIST_GET(FFstrbuf, font->styles, i));
 58
 59        if (i < font->styles.length - 1) {
 60            ffStrbufAppendS(&font->pretty, ", ");
 61        }
 62    }
 63
 64    ffStrbufAppendC(&font->pretty, ')');
 65}
 66
 67void ffFontInitQt(FFfont* font, const char* data) {
 68    ffFontInit(font);
 69
 70    // See https://doc.qt.io/qt-5/qfont.html#toString
 71
 72    // Family
 73    data = ffStrbufAppendSUntilC(&font->name, data, ',');
 74    ffStrbufTrim(&font->name, ' ');
 75    if (!data) {
 76        goto exit;
 77    }
 78    data++;
 79
 80    // Size
 81    data = ffStrbufAppendSUntilC(&font->size, data, ',');
 82    ffStrbufTrim(&font->size, ' ');
 83    if (!data) {
 84        goto exit;
 85    }
 86    data++;
 87
 88    // Style
 89    data = strrchr(data, ',');
 90    if (!data) {
 91        goto exit;
 92    }
 93    data++;
 94    if (isalpha(*data)) {
 95        do {
 96            FFstrbuf* style = FF_LIST_ADD(FFstrbuf, font->styles);
 97            ffStrbufInit(style);
 98            data = ffStrbufAppendSUntilC(style, data, ' ');
 99            if (data) {
100                data++;
101            }
102        } while (data);
103    }
104
105exit:
106    fontInitPretty(font);
107}
108
109static void fontPangoParseWord(const char** data, FFfont* font, FFstrbuf* alternativeBuffer) {
110    while (**data == ' ' || **data == '\t' || **data == ',') {
111        ++(*data);
112    }
113
114    const char* wordStart = *data;
115
116    while (**data != ' ' && **data != '\t' && **data != ',' && **data != '\0' && **data != '`' && **data != '\\') {
117        ++(*data);
118    }
119
120    uint32_t wordLength = (uint32_t) (*data - wordStart);
121    if (wordLength == 0) {
122        return;
123    }
124
125    if (**data == '\0' || **data == '`' || **data == '\\') {
126        ffStrbufAppendNS(&font->size, wordLength, wordStart);
127        if (ffStrbufEndsWithS(&font->size, "px")) {
128            ffStrbufSubstrBefore(&font->size, font->size.length - 2);
129        }
130
131        double dummy;
132        if (sscanf(font->size.chars, "%lf", &dummy) == 1) {
133            return;
134        }
135
136        ffStrbufClear(&font->size);
137    }
138
139    if (
140        ffStrStartsWithIgnCase(wordStart, "Ultra") ||
141        ffStrStartsWithIgnCase(wordStart, "Extra") ||
142        ffStrStartsWithIgnCase(wordStart, "Semi") ||
143        ffStrStartsWithIgnCase(wordStart, "Demi") ||
144        ffStrStartsWithIgnCase(wordStart, "Normal") ||
145        ffStrStartsWithIgnCase(wordStart, "Roman") ||
146        ffStrStartsWithIgnCase(wordStart, "Oblique") ||
147        ffStrStartsWithIgnCase(wordStart, "Italic") ||
148        ffStrStartsWithIgnCase(wordStart, "Thin") ||
149        ffStrStartsWithIgnCase(wordStart, "Light") ||
150        ffStrStartsWithIgnCase(wordStart, "Bold") ||
151        ffStrStartsWithIgnCase(wordStart, "Black") ||
152        ffStrStartsWithIgnCase(wordStart, "Condensed") ||
153        ffStrStartsWithIgnCase(wordStart, "Expanded")) {
154        if (alternativeBuffer == nullptr) {
155            alternativeBuffer = FF_LIST_ADD(FFstrbuf, font->styles);
156            ffStrbufInit(alternativeBuffer);
157        }
158
159        strbufAppendNSExcludingC(alternativeBuffer, wordLength, wordStart, '-');
160
161        if (
162            ffStrStartsWithIgnCase(wordStart, "Ultra ") ||
163            ffStrStartsWithIgnCase(wordStart, "Extra ") ||
164            ffStrStartsWithIgnCase(wordStart, "Semi ") ||
165            ffStrStartsWithIgnCase(wordStart, "Demi ")) {
166            fontPangoParseWord(data, font, alternativeBuffer);
167        }
168
169        return;
170    }
171
172    if (alternativeBuffer != nullptr) {
173        strbufAppendNSExcludingC(alternativeBuffer, wordLength, wordStart, '-');
174        return;
175    }
176
177    if (font->name.length > 0) {
178        ffStrbufAppendC(&font->name, ' ');
179    }
180    ffStrbufAppendNS(&font->name, wordLength, wordStart);
181}
182
183void ffFontInitPango(FFfont* font, const char* data) {
184    ffFontInit(font);
185
186    while (*data != '\0' && *data != '`' && *data != '\\') {
187        fontPangoParseWord(&data, font, nullptr);
188    }
189
190    fontInitPretty(font);
191}
192
193void ffFontInitValues(FFfont* font, const char* name, const char* size) {
194    ffFontInit(font);
195
196    ffStrbufAppendS(&font->name, name);
197    ffStrbufTrim(&font->name, '"');
198    ffStrbufAppendS(&font->size, size);
199
200    fontInitPretty(font);
201}
202
203void ffFontInitXlfd(FFfont* font, const char* xlfd) {
204    assert(xlfd && *xlfd);
205
206    // https://en.wikipedia.org/wiki/X_logical_font_description
207    ffFontInit(font);
208
209    // XLFD: -foundry-family-weight-slant-setwidth-addstyle-pixelsize-pointsize-xres-yres-spacing-averagewidth-charsetregistry-charsetencoding
210    // It often starts with '-', which would create an empty first field. Skip it to align indexes.
211    if (*xlfd == '-') {
212        xlfd++;
213    }
214
215    const char* pstart = xlfd;
216
217    for (int field = 0; field < 14; field++) {
218        const char* pend = strchr(pstart, '-');
219        uint32_t length = pend ? (uint32_t) (pend - pstart) : (uint32_t) strlen(pstart);
220
221        if (length > 0) {
222            if (field == 1) // family
223            {
224                ffStrbufAppendNS(&font->name, length, pstart);
225            } else if (field == 7) // pointsize (decipoints, preferred)
226            {
227                // parse positive integer from substring
228                long deciPt = 0;
229                bool ok = true;
230                for (uint32_t i = 0; i < length; i++) {
231                    char c = pstart[i];
232                    if (c < '0' || c > '9') {
233                        ok = false;
234                        break;
235                    }
236                    deciPt = deciPt * 10 + (c - '0');
237                }
238
239                if (ok && deciPt > 0) {
240                    ffStrbufClear(&font->size);
241
242                    char tmp[32];
243                    if (deciPt % 10 == 0) {
244                        snprintf(tmp, sizeof(tmp), "%ldpt", deciPt / 10);
245                    } else {
246                        snprintf(tmp, sizeof(tmp), "%ld.%ldpt", deciPt / 10, deciPt % 10);
247                    }
248
249                    ffStrbufAppendS(&font->size, tmp);
250                }
251            } else if (field == 6) // pixelsize (fallback if pointsize missing/invalid)
252            {
253                if (font->size.length == 0) {
254                    long px = 0;
255                    bool ok = true;
256                    for (uint32_t i = 0; i < length; i++) {
257                        char c = pstart[i];
258                        if (c < '0' || c > '9') {
259                            ok = false;
260                            break;
261                        }
262                        px = px * 10 + (c - '0');
263                    }
264
265                    if (ok && px > 0) {
266                        ffStrbufAppendNS(&font->size, length, pstart);
267                        ffStrbufAppendS(&font->size, "px");
268                    }
269                }
270            } else if (field >= 2 && field <= 5) // weight/slant/setwidth/addstyle
271            {
272                // ignore "normal" (case-insensitive)
273                if (!(length == 6 && ffStrStartsWithIgnCase(pstart, "normal"))) {
274                    FFstrbuf* style = FF_LIST_ADD(FFstrbuf, font->styles);
275                    ffStrbufInitNS(style, length, pstart);
276                }
277            }
278        }
279
280        if (!pend) {
281            break;
282        }
283
284        pstart = pend + 1;
285    }
286
287    fontInitPretty(font);
288}
289
290void ffFontInitXft(FFfont* font, const char* xft) {
291    assert(xft);
292
293    // https://en.wikipedia.org/wiki/Xft
294    // Xft/Fontconfig pattern examples:
295    //   "DejaVu Sans Mono-10"
296    //   "monospace:size=10:weight=bold:slant=italic"
297    //   "Fira Code-12:style=Regular"
298    // Goal: extract family(name), size, and some common styles.
299
300    ffFontInit(font);
301
302    // 1) Parse "head" part before first ':' => usually "family[-size]" (may include commas)
303    const char* p = xft;
304
305    while (*p == ' ' || *p == '\t') {
306        ++p;
307    }
308
309    const char* headStart = p;
310    while (*p != '\0' && *p != ':') {
311        ++p;
312    }
313    const char* headEnd = p;
314
315    // trim tail spaces
316    while (headEnd > headStart && (headEnd[-1] == ' ' || headEnd[-1] == '\t')) {
317        --headEnd;
318    }
319
320    // If multiple families are listed, take the first one (up to comma)
321    for (const char* q = headStart; q < headEnd; ++q) {
322        if (*q == ',') {
323            headEnd = q;
324            while (headEnd > headStart && (headEnd[-1] == ' ' || headEnd[-1] == '\t')) {
325                --headEnd;
326            }
327            break;
328        }
329    }
330
331    // Try parse trailing "-<number>" as size, otherwise entire head is name
332    const char* dashPos = nullptr;
333    const char* sizeStart = nullptr;
334
335    for (const char* q = headEnd; q > headStart;) {
336        --q;
337        if (*q == '-' && (q + 1) < headEnd && ffCharIsDigit(q[1])) {
338            dashPos = q;
339            sizeStart = q + 1;
340            break;
341        }
342    }
343
344    if (dashPos) {
345        bool ok = true;
346        bool seenDigit = false;
347        for (const char* q = sizeStart; q < headEnd; ++q) {
348            if (ffCharIsDigit(*q)) {
349                seenDigit = true;
350            } else if (*q == '.') {
351                continue;
352            } else {
353                ok = false;
354                break;
355            }
356        }
357
358        if (ok && seenDigit) {
359            const char* nameEnd = dashPos;
360            while (nameEnd > headStart && (nameEnd[-1] == ' ' || nameEnd[-1] == '\t')) {
361                --nameEnd;
362            }
363
364            if (nameEnd > headStart) {
365                ffStrbufAppendNS(&font->name, (uint32_t) (nameEnd - headStart), headStart);
366            }
367
368            if (headEnd > sizeStart) {
369                ffStrbufAppendNS(&font->size, (uint32_t) (headEnd - sizeStart), sizeStart);
370            }
371        } else {
372            if (headEnd > headStart) {
373                ffStrbufAppendNS(&font->name, (uint32_t) (headEnd - headStart), headStart);
374            }
375        }
376    } else {
377        if (headEnd > headStart) {
378            ffStrbufAppendNS(&font->name, (uint32_t) (headEnd - headStart), headStart);
379        }
380    }
381
382    ffStrbufTrim(&font->name, ' ');
383    ffStrbufTrim(&font->name, '"');
384
385    // 2) Parse key=value fields after ':' (Fontconfig-like). Fields separated by ':'.
386    // Common keys: size, pixelsize, pointsize, style, weight, slant, width
387    while (*p == ':') {
388        ++p;
389
390        // key
391        const char* keyStart = p;
392        while (*p != '\0' && *p != '=' && *p != ':') {
393            ++p;
394        }
395        const char* keyEnd = p;
396
397        if (*p != '=') {
398            continue; // skip tokens without '='
399        }
400
401        ++p; // skip '='
402
403        // value (until next ':', allow backslash-escaping)
404        FF_STRBUF_AUTO_DESTROY value = ffStrbufCreate();
405
406        while (*p != '\0' && *p != ':') {
407            if (*p == '\\' && p[1] != '\0') {
408                ++p;
409                ffStrbufAppendC(&value, *p);
410                ++p;
411                continue;
412            }
413
414            ffStrbufAppendC(&value, *p);
415            ++p;
416        }
417
418        ffStrbufTrim(&value, ' ');
419        ffStrbufTrim(&value, '"');
420
421        uint32_t keyLen = (uint32_t) (keyEnd - keyStart);
422
423        // helper: set numeric size if not set yet
424        const bool sizeEmpty = (font->size.length == 0);
425        if (value.length > 0) {
426            if (
427                (keyLen == 4 && ffStrStartsWithIgnCase(keyStart, "size")) ||
428                (keyLen == 9 && (ffStrStartsWithIgnCase(keyStart, "pixelsize") || ffStrStartsWithIgnCase(keyStart, "pointsize")))) {
429                if (sizeEmpty && ffCharIsDigit(value.chars[0])) {
430                    ffStrbufAppend(&font->size, &value);
431                    ffStrbufAppendS(&font->size,
432                        (keyLen == 9 && ffStrStartsWithIgnCase(keyStart, "pixelsize")) ? "px" : "pt");
433                }
434            } else if (keyLen == 5 && ffStrStartsWithIgnCase(keyStart, "style")) {
435                // style may contain multiple words: "Bold Italic"
436                const char* s = value.chars;
437                while (*s != '\0') {
438                    while (*s == ' ' || *s == '\t' || *s == ',') {
439                        ++s;
440                    }
441
442                    const char* w = s;
443                    while (*s != '\0' && *s != ' ' && *s != '\t' && *s != ',') {
444                        ++s;
445                    }
446
447                    if (s > w) {
448                        FFstrbuf* style = FF_LIST_ADD(FFstrbuf, font->styles);
449                        ffStrbufInitNS(style, (uint32_t) (s - w), w);
450                    }
451                }
452            } else if (
453                (keyLen == 6 && ffStrStartsWithIgnCase(keyStart, "weight")) ||
454                (keyLen == 5 && ffStrStartsWithIgnCase(keyStart, "slant")) ||
455                (keyLen == 5 && ffStrStartsWithIgnCase(keyStart, "width"))) {
456                // normalize: remove '-' to align with other parsers ("Semi-Bold" -> "SemiBold")
457                FFstrbuf* style = FF_LIST_ADD(FFstrbuf, font->styles);
458                ffStrbufInit(style);
459                strbufAppendNSExcludingC(style, value.length, value.chars, '-');
460                ffStrbufTrim(style, ' ');
461                if (style->length == 0) {
462                    ffStrbufDestroy(style);
463                    --font->styles.length;
464                }
465            }
466        }
467    }
468
469    fontInitPretty(font);
470}
471
472void ffFontInitMoveValues(FFfont* font, FFstrbuf* name, FFstrbuf* size, FFstrbuf* style) {
473    ffFontInit(font);
474
475    if (name) {
476        ffStrbufInitMove(&font->name, name);
477    }
478    if (size) {
479        ffStrbufInitMove(&font->size, size);
480    }
481    if (style) {
482        FFstrbuf* styleBuf = FF_LIST_ADD(FFstrbuf, font->styles);
483        ffStrbufInitMove(styleBuf, style);
484    }
485
486    fontInitPretty(font);
487}
488
489void ffFontInitWithSpace(FFfont* font, const char* rawName) {
490    const char* pspace = strrchr(rawName, ' ');
491    if (pspace == nullptr) {
492        ffFontInitCopy(font, rawName);
493        return;
494    }
495
496    ffFontInit(font);
497
498    ffStrbufAppendNS(&font->name, (uint32_t) (pspace - rawName), rawName);
499    ffStrbufAppendS(&font->size, pspace + 1);
500
501    fontInitPretty(font);
502}
503
504void ffFontDestroy(FFfont* font) {
505    ffStrbufDestroy(&font->pretty);
506    ffStrbufDestroy(&font->name);
507    ffStrbufDestroy(&font->size);
508
509    FF_LIST_FOR_EACH (FFstrbuf, str, font->styles) {
510        ffStrbufDestroy(str);
511    }
512    ffListDestroy(&font->styles);
513}