Default to the system monospace font

The default was Berkeley Mono, which is one machine's licensed font and
reads as configuration rather than a default. It is now the system's
monospace font, so a fresh install looks right anywhere and --font is
there for anyone who wants their own.

Getting "the system monospace font" is less direct than it sounds.
Family::Name("monospace") resolves to Noto Sans — not monospaced at all.
cosmic-text's generic Family::Monospace goes through fontdb's built-in
preference, "FreeMono", which is usually absent, and then lands on an
arbitrary face (Adwaita Mono here). Enabling cosmic-text's fontconfig
feature changes nothing, because fontdb's config parser does not pick up
the alias files a distribution actually ships.

So fontconfig is asked directly: `fc-match -f %{family} monospace`, which
is the same answer every other application on the system gets — Noto Sans
Mono here. It costs 10ms on the worker thread that is already waiting for
the compositor, so nothing in wall clock. Without the fontconfig tools a
short list of common distribution defaults is tried instead.

Weight and stretch overrides went too: they were tuned for Berkeley Mono
Medium SemiCondensed, and a family name carries that anyway — the full
name resolves to exactly that face, verified by rendering both.

--verbose now reports the family the labels were shaped with, since the
default legitimately differs from machine to machine.
This commit is contained in:
Milad Alizadeh
2026-08-24 12:37:55 +01:00
parent 15b74d311d
commit 11dc30ddc1
5 changed files with 123 additions and 32 deletions
+2 -1
View File
@@ -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"),
);
}
+1 -1
View File
@@ -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]
+102 -21
View File
@@ -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<Buffer>,
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<String> {
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<String>, 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<String>, 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();
+5 -4
View File
@@ -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,