Fonts: find fonts where Linux and Flatpak keep them; add and rescan commands (#277)

The font menu lists every family the scan of the system font folders finds,
but on Linux the scan only looked in /usr/share/fonts, /usr/local/share/fonts,
~/.fonts and ~/.local/share/fonts. Fonts under XDG data dirs, in folders
fontconfig is configured with (NixOS, Guix, custom setups) and, inside
Flatpak, the host's fonts never showed.

- system_font_dirs adds $XDG_DATA_HOME/fonts and each $XDG_DATA_DIRS/fonts,
  Flatpak's /run/host/{fonts,local-fonts,user-fonts}, and the <dir> entries
  of /etc/fonts/fonts.conf, local.conf and conf.d; duplicates are dropped.
- font.add {data} adds a font from its bytes for the session; font.rescan
  catalogs the installed fonts again, and the font menu has a
  "Rescan Installed Fonts" item for fonts installed while DeckCraft runs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Nikita Rakitin
2026-10-13 21:16:23 +03:00
co-authored by Cursor
parent 457406a7a5
commit eec67560df
4 changed files with 168 additions and 0 deletions
+25
View File
@@ -13,6 +13,8 @@ use crate::{Result, Session};
pub fn specs() -> Vec<CommandSpec> {
vec![
cmd!(noundo "font.add", "Add Font", [], None, "{data: base64 TTF/OTF/TTC} → {faces} added for this session", always, font_add),
cmd!(noundo "font.rescan", "Rescan Installed Fonts", ["Home", "Font"], None, "{} → {faces, families} (fonts installed since start-up)", always, font_rescan),
cmd!("format.bold", "Bold", ["Home", "Font"], Some("Cmd+B"), "{on?: bool}", has_text_or_shapes, bold),
cmd!("format.italic", "Italic", ["Home", "Font"], Some("Cmd+I"), "{on?: bool}", has_text_or_shapes, italic),
cmd!(
@@ -682,3 +684,26 @@ fn state(s: &mut Session, _p: &Value) -> Result<Value> {
"painter": s.painter.is_some(),
}))
}
/// Add a font from its bytes for this session (it then shows in the font menu and draws).
fn font_add(_s: &mut Session, p: &Value) -> Result<Value> {
let data = str_param(p, "data").and_then(base64_decode).ok_or_else(|| bad("font.add", "missing or invalid base64 `data`"))?;
if data.len() > 64 << 20 {
return Err(bad("font.add", "font file larger than 64 MB"));
}
let faces = deckcraft_fonts::FontDb::global().add_font(data);
if faces == 0 {
return Err(bad("font.add", "not a font DeckCraft can read (or already added)"));
}
Ok(json!({"faces": faces}))
}
/// Catalog the installed fonts again, so fonts installed while DeckCraft runs appear.
fn font_rescan(_s: &mut Session, _p: &Value) -> Result<Value> {
let db = deckcraft_fonts::FontDb::global();
#[cfg(not(target_arch = "wasm32"))]
let faces = db.load_system_fonts();
#[cfg(target_arch = "wasm32")]
let faces = 0usize;
Ok(json!({"faces": faces, "families": db.families().len()}))
}
+10
View File
@@ -835,3 +835,13 @@ fn slide_ranges_parse_like_powerpoints_print_box() {
let opts = cmd::file::pdf_options(&json!({"slides": [0, 7, 1]}), 2).unwrap();
assert_eq!(opts.slides, Some(vec![0, 1]), "indices past the end are dropped");
}
#[test]
fn font_add_refuses_non_fonts_and_rescan_reports_the_catalog() {
let mut s = session();
let not_font = crate::cmd::base64_encode(b"definitely not a font file");
assert!(s.execute("font.add", &json!({"data": not_font})).is_err());
assert!(s.execute("font.add", &json!({})).is_err());
let r = s.execute("font.rescan", &json!({})).unwrap();
assert!(r["families"].as_u64().unwrap() >= 1, "{r}");
}