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
+13 -5
View File
@@ -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
+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]
+98 -17
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 !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"));
}
let found = db
.faces()
.any(|f| f.families.iter().any(|(name, _)| name == family));
if !found {
db.load_system_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);
}
// 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,