diff --git a/README.md b/README.md index a7012b0..6baced6 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ wl-pick [--format tsv|json|portal] [--live all|current|none] [--fps N] - `--fps N` cap on live updates per tile per second (default 12) - `--no-outputs` windows only; displays are included as tiles by default - `--hide-labels` draws an icon-only grid -- `--font FAMILY` label font family (default `Berkeley Mono`) +- `--font FAMILY` label font family (default: the system monospace font) - `--font-size PX` label size in logical px - `--timeout SECS` exits after a deadline, in case the keyboard grab ever traps you @@ -143,10 +143,18 @@ columns capped at 4, 16:9 tiles, `title · app` centred underneath) and live in `src/theme.rs`, which is the one place to change them. They are not configurable at runtime beyond the font flags. -The font is looked up by family name. Your own font directories are scanned -first because they are small; the full system scan (~37ms) happens only if the -family isn't found there, and an unknown family then falls back to whatever -cosmic-text picks rather than failing. Long titles are ellipsised to the cell. +The label font defaults to the system monospace font — whatever `fc-match +monospace` answers, which is what the rest of the desktop uses. (cosmic-text's +own generic resolves through a built-in preference that is usually not +installed, and then lands on an arbitrary face, so it is asked directly +instead; if fontconfig isn't available, a short list of common distribution +defaults is tried.) `--font` names a family instead, and `--verbose` reports +which family the labels were actually shaped with. + +Naming a family scans your own font directories first because they are small; +the full system scan (~37ms) happens only if it isn't found there. An unknown +family falls back to whatever cosmic-text picks rather than failing. Long titles +are ellipsised to the cell. ## Requirements diff --git a/src/app.rs b/src/app.rs index 89ed710..8723aa0 100644 --- a/src/app.rs +++ b/src/app.rs @@ -221,7 +221,7 @@ impl App { } eprintln!( "wl-pick: {} tile(s), {} captured; grid {}x{}, surface {}x{} logical \ - at scale {}, {} MB of capture buffers", + at scale {}, {} MB of capture buffers, labels in {:?}", self.tiles.len(), self.tiles.iter().filter(|t| t.ready).count(), self.layout.cols, @@ -230,6 +230,7 @@ impl App { self.layout.height, self.scale, self.stats.pool_bytes >> 20, + self.labels.as_ref().map(|l| l.family()).unwrap_or("none"), ); } diff --git a/src/cli.rs b/src/cli.rs index ed22101..9768a5c 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -18,7 +18,7 @@ usage: wl-pick [options] --fps N cap on live updates per tile per second [12] --no-outputs windows only; displays are included by default --hide-labels draw an icon-only grid - --font FAMILY label font family [Berkeley Mono] + --font FAMILY label font family [the system monospace font] --font-size PX label size in logical px [13.3] --timeout SECS exit anyway after SECS, in case the keyboard grab ever traps you [off] diff --git a/src/text.rs b/src/text.rs index 6615183..a97f8e1 100644 --- a/src/text.rs +++ b/src/text.rs @@ -12,8 +12,7 @@ use std::thread::{self, JoinHandle}; use cosmic_text::{ - Align, Attrs, Buffer, Color, Family, FontSystem, Metrics, Shaping, Stretch, SwashCache, Weight, - Wrap, fontdb, + Align, Attrs, Buffer, Color, Family, FontSystem, Metrics, Shaping, SwashCache, Wrap, fontdb, }; use crate::shm::Painter; @@ -23,6 +22,7 @@ pub struct Labels { fs: FontSystem, cache: SwashCache, lines: Vec, + family: String, } /// Shape `texts` into one centred single line each, at most `box_w` wide. @@ -36,37 +36,94 @@ pub fn spawn( thread::spawn(move || build(texts, family, font_px, line_h, box_w)) } +/// The default family: whatever this system calls its monospace font. +pub const SYSTEM_MONO: &str = "monospace"; + +/// Families to try when fontconfig cannot be asked, roughly in order of how +/// likely a distribution is to ship one as its default monospace. +const MONO_CANDIDATES: &[&str] = &[ + "Noto Sans Mono", + "DejaVu Sans Mono", + "Liberation Mono", + "Adwaita Mono", + "Source Code Pro", + "Hack", + "Fira Mono", + "Courier New", +]; + /// Load the smallest font database that can render `family`. /// /// `FontSystem::new()` scans every system font, which costs ~37ms — most of the /// startup budget. A user's own font directories are tiny by comparison, so try -/// those first and only pay for the full scan when the family really isn't -/// there (which is also what makes an unknown family fall back gracefully). +/// those first and only pay for the full scan when the family really isn't there +/// (which is also what makes an unknown family fall back gracefully). The +/// generic default lives among the system fonts, so it skips that shortcut. fn font_db(family: &str) -> FontSystem { let mut db = fontdb::Database::new(); - if let Ok(home) = std::env::var("HOME") { - db.load_fonts_dir(format!("{home}/.fonts")); - db.load_fonts_dir(format!("{home}/.local/share/fonts")); + if !is_generic(family) { + if let Ok(home) = std::env::var("HOME") { + db.load_fonts_dir(format!("{home}/.fonts")); + db.load_fonts_dir(format!("{home}/.local/share/fonts")); + } + if has_family(&db, family) { + // The locale only orders CJK fallbacks; labels are ids and titles. + return FontSystem::new_with_locale_and_db("en-US".to_string(), db); + } } - let found = db - .faces() - .any(|f| f.families.iter().any(|(name, _)| name == family)); - if !found { - db.load_system_fonts(); - } - // The locale only orders CJK fallbacks; labels here are app ids and titles. + db.load_system_fonts(); FontSystem::new_with_locale_and_db("en-US".to_string(), db) } +fn is_generic(family: &str) -> bool { + family.eq_ignore_ascii_case(SYSTEM_MONO) +} + +fn has_family(db: &fontdb::Database, family: &str) -> bool { + db.faces() + .any(|f| f.families.iter().any(|(name, _)| name == family)) +} + +/// Turn the generic default into a real family name. +/// +/// cosmic-text's own generic resolves through fontdb's built-in preference +/// ("FreeMono"), which is usually absent and then lands on an arbitrary face — so +/// ask fontconfig instead, since that is what the rest of the desktop uses. A +/// named family passes through untouched; if it turns out to be missing, +/// cosmic-text falls back on its own. +fn resolve_family(db: &fontdb::Database, family: &str) -> String { + if !is_generic(family) { + return family.to_string(); + } + fc_match_mono() + .filter(|name| has_family(db, name)) + .or_else(|| { + MONO_CANDIDATES + .iter() + .find(|name| has_family(db, name)) + .map(|name| name.to_string()) + }) + .unwrap_or_else(|| family.to_string()) +} + +/// What fontconfig says "monospace" means here. A system without the fontconfig +/// tools is not an error: the candidate list covers the common defaults. +fn fc_match_mono() -> Option { + let out = std::process::Command::new("fc-match") + .args(["-f", "%{family}", SYSTEM_MONO]) + .output() + .ok()?; + let text = String::from_utf8(out.stdout).ok()?; + // fc-match can answer with several comma-separated aliases for one face. + let first = text.split(',').next()?.trim().to_string(); + (!first.is_empty()).then_some(first) +} + fn build(texts: Vec, family: String, font_px: f32, line_h: f32, box_w: f32) -> Labels { let mut fs = font_db(&family); let mut cache = SwashCache::new(); - // An unknown family is not an error: cosmic-text falls back to a system - // face, which is the whole reason the font is named rather than pathed. - let attrs = Attrs::new() - .family(Family::Name(&family)) - .weight(Weight(500)) - .stretch(Stretch::SemiCondensed); + let family = resolve_family(fs.db(), &family); + let attrs = Attrs::new().family(Family::Name(&family)); let metrics = Metrics::new(font_px, line_h); let mut lines = Vec::with_capacity(texts.len()); @@ -80,7 +137,12 @@ fn build(texts: Vec, family: String, font_px: f32, line_h: f32, box_w: f buf.draw(&mut fs, &mut cache, Color::rgb(0, 0, 0), |_, _, _, _, _| {}); lines.push(buf); } - Labels { fs, cache, lines } + Labels { + fs, + cache, + lines, + family, + } } /// Shorten `text` until it fits in `box_w`, ending with an ellipsis — window @@ -124,6 +186,12 @@ fn ellipsize( } impl Labels { + /// The family the labels were actually shaped with, which for the generic + /// default depends on what this system has installed. + pub fn family(&self) -> &str { + &self.family + } + /// Draw label `i` inside `at` (physical px), clipped to it. pub fn draw(&mut self, p: &mut Painter, i: usize, at: Rect, color: Argb) { let Some(buf) = self.lines.get_mut(i) else { @@ -171,6 +239,19 @@ mod tests { assert!(touched > 20, "only {touched} pixels were painted"); } + #[test] + fn the_generic_default_resolves_to_a_real_monospace_family() { + let fs = font_db(SYSTEM_MONO); + let resolved = resolve_family(fs.db(), SYSTEM_MONO); + assert_ne!(resolved, SYSTEM_MONO, "should have named a real family"); + assert!( + has_family(fs.db(), &resolved), + "{resolved:?} is not in the database" + ); + // A named family passes through, present or not. + assert_eq!(resolve_family(fs.db(), "Some Font"), "Some Font"); + } + #[test] fn ellipsizes_long_titles() { let mut fs = FontSystem::new(); diff --git a/src/theme.rs b/src/theme.rs index 08ae88a..a7632c9 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -29,9 +29,10 @@ pub struct Theme { pub max_cols: i32, /// Gap between a thumbnail and its label (rasi `element { spacing }`). pub spacing: i32, - /// Label font: a family name resolved against system fonts, with whatever - /// cosmic-text falls back to if it is missing. Size and line height are - /// logical px — rofi's "Berkeley Mono 12" at pango size="small". + /// Label font family, resolved against the system's fonts. The default is + /// the generic "monospace", which becomes whatever fontconfig says that is + /// here. Size and line height are logical px, matching the rofi theme the + /// look came from (12pt at pango size="small"). pub font: String, pub font_px: f32, pub line_h: i32, @@ -55,7 +56,7 @@ impl Default for Theme { margin: 12, max_cols: 4, spacing: 10, - font: "Berkeley Mono".to_string(), + font: crate::text::SYSTEM_MONO.to_string(), font_px: 13.3, line_h: 17, labels: true,