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}