I've recently upgraded my system and noticed that many Unicode characters are no longer displayed in st, although other applications were unaffected. After some investigation, I found that it's a font fallback failure: the font returned by FcFontSetMatch lacked the character requested via the charset. Apparently, this is possible if the pattern is nonsensical, in my case, the pattern was something like this (simplified):
JetBrainsMonoNerdFontMono:charset=2807:lang=und-zsye,en U2807 is a Braille character, for which st needed a fallback. The lang=und-zsye is for emojis. This combination confused fontconfig and it returned 'Noto Color Emoji', which doesn't contain Braille characters (although I still find it strange that fontconfig doesn't prioritize the charset). I propose two possible solutions for this: 1) Fix the pattern. "lang=und-zsye" was added to the pattern by a call to FcConfigSubstitute. On my system there are some fontconfig rules that eventually add emoji fonts to every pattern. Although these rules may be buggy, other applications were not affected, indicating that st does something differently. I think the difference is that it calls FcConfigSubstitute twice: the first time when it searches for the main font and the second time when it searches for the fallback font. It's the second call that makes the pattern nonsensical. My guess is that calling it twice is not correct. This patch removes the second call and fixes the issue: diff --git a/x.c b/x.c index bd23686..639b06b 100644 --- a/x.c +++ b/x.c @@ -1330,8 +1330,6 @@ xmakeglyphfontspecs(XftGlyphFontSpec *specs, const Glyph *glyphs, int len, int x fccharset); FcPatternAddBool(fcpattern, FC_SCALABLE, 1); - FcConfigSubstitute(0, fcpattern, - FcMatchPattern); FcDefaultSubstitute(fcpattern); fontpattern = FcFontSetMatch(0, fcsets, 1, 2) Explicitly verify whether the fallback font contains the character (I think, xterm does something like this). Please find the patch in the attachment. Please consider applying one or both of these patches to the mainline (or addressing the issue in some other way). Note that I have no prior experience with fontconfig, so I don't really know what I'm doing, and the patches may require some modifications.
From 1420a2218d5f65c4bf76a0390be58fe9ec89a6aa Mon Sep 17 00:00:00 2001 From: Sergei Grechanik <sergei.grecha...@gmail.com> Date: Mon, 10 Jun 2024 20:16:58 -0700 Subject: [PATCH] Check if the fallback font actually contains the char For some reason FcFontSetMatch may return a font that doesn't contain the character specified via the charset. This commit works around this by calling FcFontSort instead and then trying the fonts one by one until we find a font that actually contains the character. --- x.c | 41 ++++++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/x.c b/x.c index bd23686..84cc0ae 100644 --- a/x.c +++ b/x.c @@ -128,7 +128,6 @@ typedef struct { short lbearing; short rbearing; XftFont *match; - FcFontSet *set; FcPattern *pattern; } Font; @@ -966,7 +965,6 @@ xloadfont(Font *f, FcPattern *pattern) (const FcChar8 *) ascii_printable, strlen(ascii_printable), &extents); - f->set = NULL; f->pattern = configured; f->ascent = f->match->ascent; @@ -1055,8 +1053,6 @@ xunloadfont(Font *f) { XftFontClose(xw.dpy, f->match); FcPatternDestroy(f->pattern); - if (f->set) - FcFontSetDestroy(f->set); } void @@ -1251,9 +1247,9 @@ xmakeglyphfontspecs(XftGlyphFontSpec *specs, const Glyph *glyphs, int len, int x FT_UInt glyphidx; FcResult fcres; FcPattern *fcpattern, *fontpattern; - FcFontSet *fcsets[] = { NULL }; FcCharSet *fccharset; - int i, f, numspecs = 0; + FcFontSet *candidatefonts; + int i, j, f, numspecs = 0; for (i = 0, xp = winx, yp = winy + font->ascent; i < len; ++i) { /* Fetch rune and mode for current glyph. */ @@ -1310,11 +1306,6 @@ xmakeglyphfontspecs(XftGlyphFontSpec *specs, const Glyph *glyphs, int len, int x /* Nothing was found. Use fontconfig to find matching font. */ if (f >= frclen) { - if (!font->set) - font->set = FcFontSort(0, font->pattern, - 1, 0, &fcres); - fcsets[0] = font->set; - /* * Nothing was found in the cache. Now use * some dozen of Fontconfig calls to get the @@ -1334,8 +1325,32 @@ xmakeglyphfontspecs(XftGlyphFontSpec *specs, const Glyph *glyphs, int len, int x FcMatchPattern); FcDefaultSubstitute(fcpattern); - fontpattern = FcFontSetMatch(0, fcsets, 1, - fcpattern, &fcres); + /* FcFontSetMatch may return a font that doesn't contain + * the character we are looking for. Sort the font set + * instead and use the first one that contains the + * character or the first font if none contains it. + */ + candidatefonts = + FcFontSort(0, fcpattern, 1, 0, &fcres); + fontpattern = NULL; + for (j = 0; j < candidatefonts->nfont; j++) { + FcCharSet *charset = NULL; + fontpattern = candidatefonts->fonts[j]; + char contains_rune = + FcPatternGetCharSet( + fontpattern, FC_CHARSET, 0, + &charset) == FcResultMatch && + charset && + FcCharSetHasChar(charset, rune); + if (contains_rune) + break; + fontpattern = NULL; + } + if (!fontpattern) + fontpattern = candidatefonts->fonts[0]; + fontpattern = + FcFontRenderPrepare(0, fcpattern, fontpattern); + FcFontSetDestroy(candidatefonts); /* Allocate memory for the new cache entry. */ if (frclen >= frccap) { -- 2.43.0