gardesk/garterm / 290229c

Browse files

dynamic fontconfig fallback for missing glyphs

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
290229cb1bd810deb8bd61be9846fe2063fd6c92
Parents
bcc33bb
Tree
48dac50

3 changed files

StatusFile+-
M .gitignore 2 0
M Cargo.lock 2 3
M garterm/src/render/font.rs 80 4
.gitignoremodified
@@ -2,3 +2,5 @@
22
 docs/
33
 .fackr/
44
 .vscode/
5
+CLAUDE.md
6
+AGENTS.md
Cargo.lockmodified
@@ -470,7 +470,7 @@ dependencies = [
470470
 
471471
 [[package]]
472472
 name = "gartk-core"
473
-version = "0.1.0"
473
+version = "0.3.0"
474474
 dependencies = [
475475
  "serde",
476476
  "thiserror 2.0.18",
@@ -478,7 +478,7 @@ dependencies = [
478478
 
479479
 [[package]]
480480
 name = "gartk-x11"
481
-version = "0.1.0"
481
+version = "0.3.0"
482482
 dependencies = [
483483
  "gartk-core",
484484
  "thiserror 2.0.18",
@@ -1447,7 +1447,6 @@ version = "0.13.1"
14471447
 source = "registry+https://github.com/rust-lang/crates.io-index"
14481448
 checksum = "9a0b683b20ef64071ff03745b14391751f6beab06a54347885459b77a3f2caa5"
14491449
 dependencies = [
1450
- "arrayvec",
14511450
  "utf8parse",
14521451
  "vte_generate_state_changes",
14531452
 ]
garterm/src/render/font.rsmodified
@@ -1,5 +1,6 @@
11
 use fontdue::{Font, FontSettings};
2
-use std::collections::HashMap;
2
+use std::cell::RefCell;
3
+use std::collections::{HashMap, HashSet};
34
 use std::process::Command;
45
 use std::sync::Arc;
56
 use thiserror::Error;
@@ -26,6 +27,10 @@ pub struct FontCache {
2627
     fonts: HashMap<FontStyle, Arc<Font>>,
2728
     /// Fallback fonts for missing glyphs (symbols, icons, etc.)
2829
     fallback_fonts: Vec<Arc<Font>>,
30
+    /// Fonts discovered dynamically via fontconfig charset queries
31
+    dynamic_fallbacks: RefCell<Vec<Arc<Font>>>,
32
+    /// Codepoints we've already attempted fontconfig discovery for
33
+    tried_codepoints: RefCell<HashSet<char>>,
2934
     size: f32,
3035
     cell_width: f32,
3136
     cell_height: f32,
@@ -143,6 +148,8 @@ impl FontCache {
143148
         Ok(Self {
144149
             fonts,
145150
             fallback_fonts,
151
+            dynamic_fallbacks: RefCell::new(Vec::new()),
152
+            tried_codepoints: RefCell::new(HashSet::new()),
146153
             size,
147154
             cell_width,
148155
             cell_height,
@@ -254,6 +261,8 @@ impl FontCache {
254261
         Self {
255262
             fonts: self.fonts.clone(),
256263
             fallback_fonts: self.fallback_fonts.clone(),
264
+            dynamic_fallbacks: RefCell::new(self.dynamic_fallbacks.borrow().clone()),
265
+            tried_codepoints: RefCell::new(self.tried_codepoints.borrow().clone()),
257266
             size: new_size,
258267
             cell_width,
259268
             cell_height,
@@ -268,7 +277,9 @@ impl FontCache {
268277
         })
269278
     }
270279
 
271
-    /// Rasterize a character, using fallback fonts if needed
280
+    /// Rasterize a character, using fallback fonts if needed.
281
+    /// Falls back through static fallbacks, then dynamically-discovered fonts,
282
+    /// and finally queries fontconfig for a font containing the codepoint.
272283
     pub fn rasterize(&self, c: char, style: FontStyle) -> (fontdue::Metrics, Vec<u8>) {
273284
         let primary_font = self.font(style);
274285
 
@@ -277,23 +288,88 @@ impl FontCache {
277288
             return primary_font.rasterize(c, self.size);
278289
         }
279290
 
280
-        // Try fallback fonts
291
+        // Try static fallback fonts
281292
         for fallback in &self.fallback_fonts {
282293
             if fallback.lookup_glyph_index(c) != 0 {
283294
                 return fallback.rasterize(c, self.size);
284295
             }
285296
         }
286297
 
298
+        // Try already-discovered dynamic fallbacks
299
+        for fallback in self.dynamic_fallbacks.borrow().iter() {
300
+            if fallback.lookup_glyph_index(c) != 0 {
301
+                return fallback.rasterize(c, self.size);
302
+            }
303
+        }
304
+
305
+        // Ask fontconfig to find a font for this codepoint (once per codepoint)
306
+        if !self.tried_codepoints.borrow().contains(&c) {
307
+            self.tried_codepoints.borrow_mut().insert(c);
308
+            if let Some(font) = self.discover_font_for_char(c) {
309
+                let result = font.rasterize(c, self.size);
310
+                self.dynamic_fallbacks.borrow_mut().push(font);
311
+                return result;
312
+            }
313
+        }
314
+
287315
         // No font has this glyph - return from primary (will be placeholder/tofu)
288316
         primary_font.rasterize(c, self.size)
289317
     }
290318
 
319
+    /// Query fontconfig for a font that contains the given character
320
+    fn discover_font_for_char(&self, c: char) -> Option<Arc<Font>> {
321
+        let codepoint = c as u32;
322
+        let query = format!(":charset={:04x}", codepoint);
323
+
324
+        let output = Command::new("fc-list")
325
+            .args(["-f", "%{file}\n", &query])
326
+            .output()
327
+            .ok()?;
328
+
329
+        if !output.status.success() {
330
+            return None;
331
+        }
332
+
333
+        let stdout = String::from_utf8_lossy(&output.stdout);
334
+        let mut seen = HashSet::new();
335
+
336
+        for line in stdout.lines() {
337
+            let path = line.trim();
338
+            if path.is_empty() || !seen.insert(path.to_string()) {
339
+                continue;
340
+            }
341
+
342
+            // Skip color emoji fonts (fontdue can't rasterize bitmap/COLR fonts)
343
+            if path.contains("ColorEmoji") {
344
+                continue;
345
+            }
346
+
347
+            if let Ok(data) = std::fs::read(path) {
348
+                if let Ok(font) = Font::from_bytes(data, FontSettings::default()) {
349
+                    if font.lookup_glyph_index(c) != 0 {
350
+                        tracing::debug!(
351
+                            "Dynamic font fallback: U+{:04X} '{}' -> {}",
352
+                            codepoint, c, path
353
+                        );
354
+                        return Some(Arc::new(font));
355
+                    }
356
+                }
357
+            }
358
+        }
359
+
360
+        tracing::debug!("No font found for U+{:04X} '{}'", codepoint, c);
361
+        None
362
+    }
363
+
291364
     /// Check if any font can render this character
292365
     pub fn has_glyph(&self, c: char) -> bool {
293366
         let primary = self.font(FontStyle::Regular);
294367
         if primary.lookup_glyph_index(c) != 0 {
295368
             return true;
296369
         }
297
-        self.fallback_fonts.iter().any(|f| f.lookup_glyph_index(c) != 0)
370
+        if self.fallback_fonts.iter().any(|f| f.lookup_glyph_index(c) != 0) {
371
+            return true;
372
+        }
373
+        self.dynamic_fallbacks.borrow().iter().any(|f| f.lookup_glyph_index(c) != 0)
298374
     }
299375
 }