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

Reply via email to