Files
fastfetch/src/common/impl/font.c
T
2026-07-10 18:10:09 +08:00

514 lines
15 KiB
C

#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 == nullptr || 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 == nullptr) {
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 != nullptr) {
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, nullptr);
}
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 = nullptr;
const char* sizeStart = nullptr;
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 == nullptr) {
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);
}