From 302f0fc85f80d10959fe5aad97345a753cdd1d8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=80=9A=E6=B4=B2?= Date: Tue, 23 Dec 2025 11:05:50 +0800 Subject: [PATCH] TerminalFont (Linux): supports urxvt Fixes #2105 --- src/common/font.c | 328 +++++++++++++++++- src/common/font.h | 2 + .../terminalfont/terminalfont_linux.c | 38 ++ 3 files changed, 350 insertions(+), 18 deletions(-) diff --git a/src/common/font.c b/src/common/font.c index 308e094c4..d01629f9e 100644 --- a/src/common/font.c +++ b/src/common/font.c @@ -1,6 +1,7 @@ #include "fastfetch.h" #include "util/FFlist.h" #include "util/FFstrbuf.h" +#include "util/stringUtils.h" #include "common/font.h" #include @@ -127,20 +128,20 @@ static void fontPangoParseWord(const char** data, FFfont* font, FFstrbuf* altern } if( - strncasecmp(wordStart, "Ultra", 5) == 0 || - strncasecmp(wordStart, "Extra", 5) == 0 || - strncasecmp(wordStart, "Semi", 4) == 0 || - strncasecmp(wordStart, "Demi", 4) == 0 || - strncasecmp(wordStart, "Normal", wordLength) == 0 || - strncasecmp(wordStart, "Roman", wordLength) == 0 || - strncasecmp(wordStart, "Oblique", wordLength) == 0 || - strncasecmp(wordStart, "Italic", wordLength) == 0 || - strncasecmp(wordStart, "Thin", wordLength) == 0 || - strncasecmp(wordStart, "Light", wordLength) == 0 || - strncasecmp(wordStart, "Bold", wordLength) == 0 || - strncasecmp(wordStart, "Black", wordLength) == 0 || - strncasecmp(wordStart, "Condensed", wordLength) == 0 || - strncasecmp(wordStart, "Expanded", wordLength) == 0 + 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) { @@ -151,10 +152,10 @@ static void fontPangoParseWord(const char** data, FFfont* font, FFstrbuf* altern strbufAppendNSExcludingC(alternativeBuffer, wordLength, wordStart, '-'); if( - strncasecmp(wordStart, "Ultra ", 6) == 0 || - strncasecmp(wordStart, "Extra ", 6) == 0 || - strncasecmp(wordStart, "Semi ", 5) == 0 || - strncasecmp(wordStart, "Demi ", 5) == 0 + ffStrStartsWithIgnCase(wordStart, "Ultra ") || + ffStrStartsWithIgnCase(wordStart, "Extra ") || + ffStrStartsWithIgnCase(wordStart, "Semi ") || + ffStrStartsWithIgnCase(wordStart, "Demi ") ) { fontPangoParseWord(data, font, alternativeBuffer); } @@ -194,6 +195,297 @@ void ffFontInitValues(FFfont* font, const char* name, const char* 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), "%ld", deciPt / 10); + else + snprintf(tmp, sizeof(tmp), "%ld.%ld", 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); + } + } + else if(field >= 2 && field <= 5) // weight/slant/setwidth/addstyle + { + // ignore "normal" (case-insensitive) + if(!(length == 6 && ffStrStartsWithIgnCase(pstart, "normal"))) + { + FFstrbuf* style = (FFstrbuf*) ffListAdd(&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")) + { + if(sizeEmpty) + { + if(ffStrbufEndsWithS(&value, "px") || ffStrbufEndsWithS(&value, "pt")) + ffStrbufSubstrBefore(&value, value.length - 2); + + if(ffCharIsDigit(value.chars[0])) + ffStrbufAppend(&font->size, &value); + } + } + else if( + (keyLen == 9 && ffStrStartsWithIgnCase(keyStart, "pixelsize")) || + (keyLen == 9 && ffStrStartsWithIgnCase(keyStart, "pointsize")) || + (keyLen == 8 && ffStrStartsWithIgnCase(keyStart, "fontsize")) + ) { + if(sizeEmpty) + { + if(ffStrbufEndsWithS(&value, "px") || ffStrbufEndsWithS(&value, "pt")) + ffStrbufSubstrBefore(&value, value.length - 2); + + if(ffCharIsDigit(value.chars[0])) + ffStrbufAppend(&font->size, &value); + } + } + 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, ' '); + } + } + } + + fontInitPretty(font); +} + void ffFontInitMoveValues(FFfont* font, FFstrbuf* name, FFstrbuf* size, FFstrbuf* style) { ffFontInit(font); diff --git a/src/common/font.h b/src/common/font.h index 2b94f463a..ac6932c8b 100644 --- a/src/common/font.h +++ b/src/common/font.h @@ -15,6 +15,8 @@ void ffFontInit(FFfont* font); void ffFontInitQt(FFfont* font, const char* data); void ffFontInitPango(FFfont* font, const char* data); void ffFontInitValues(FFfont* font, const char* name, const char* size); +void ffFontInitXlfd(FFfont* font, const char* xlfd); +void ffFontInitXft(FFfont* font, const char* xft); void ffFontInitMoveValues(FFfont* font, FFstrbuf* name, FFstrbuf* size, FFstrbuf* style); void ffFontInitWithSpace(FFfont* font, const char* rawName); void ffFontDestroy(FFfont* font); diff --git a/src/detection/terminalfont/terminalfont_linux.c b/src/detection/terminalfont/terminalfont_linux.c index 8e1151a49..058b64c51 100644 --- a/src/detection/terminalfont/terminalfont_linux.c +++ b/src/detection/terminalfont/terminalfont_linux.c @@ -1,3 +1,4 @@ +#include "common/font.h" #include "terminalfont.h" #include "common/settings.h" #include "common/properties.h" @@ -418,6 +419,41 @@ static void detectWestonTerminal(FFTerminalFontResult* terminalFont) ffFontInitValues(&terminalFont->font, font.chars, size.chars); } +static void detectUrxvt(FFTerminalFontResult* terminalFont) +{ + FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); + + if (!(ffParsePropFileHomeValues(".Xresources", 1, (FFpropquery[]) { + {"URxvt.font:", &buffer}, + }) || ffParsePropFileHomeValues(".Xdefaults", 1, (FFpropquery[]) { + {"URxvt.font:", &buffer}, + }))) + { + ffStrbufAppendS(&terminalFont->error, "Could not find URxvt.font in .Xresources or .Xdefaults"); + return; + } + + uint32_t index = 0; + + char* line = NULL; + size_t len = 0; + while (ffStrbufGetdelim(&line, &len, ',', &buffer)) + { + FFfont* font = index == 0 ? &terminalFont->font : &terminalFont->fallback; + if (line[0] == '-') + ffFontInitXlfd(font, line); + else if (ffStrStartsWith(line, "xft:")) + ffFontInitXft(font, line + 4); + else + { + ffStrbufAppendF(&terminalFont->error, "Unknown URxvt font format: %s", line); + continue; + } + index++; + if (index > 1) break; + } +} + #ifdef __HAIKU__ static void detectHaikuTerminal(FFTerminalFontResult* terminalFont) { @@ -483,6 +519,8 @@ ffDetectTerminalFontPlatform #endif else if(ffStrbufStartsWithIgnCaseS(&terminal->processName, "termite")) detectFromConfigFile("termite/config", "font =", terminalFont); + else if(ffStrbufStartsWithIgnCaseS(&terminal->processName, "urxvt")) + detectUrxvt(terminalFont); else return false; return true;