Unify font sizes through qs.Commons.Style

Themes now drive typography the same way they drive colors: one [font]
base-size in shell.toml is the rem root, and every Style.font.<token>
(caption, bodySmall, body, subtitle, title, heading, display,
displayLarge, iconSmall, icon, iconLarge) derives from it via a fixed
multiplier. Themes can also pin individual tokens for stylistic
emphasis. base-size is clamped 11..13 until row-height tokens exist.

Bar dimensions move to the same singleton: [bar] size-horizontal /
size-vertical replace the hardcoded 26/28 in Bar.qml, exposed as
Style.bar.sizeHorizontal / sizeVertical.

Style.qml also resolves the fontconfig 'monospace' alias via fc-match
and exposes Style.font.resolvedFamily so panels can display the
concrete family. Watches ~/.config/fontconfig/fonts.conf so it tracks
'omarchy font set <name>'.

The qs.Ui kit (PillButton, Dropdown, Toggle, TextField, etc.) and
every first-party plugin (bar widgets, settings, menu, clipboard,
emoji, polkit, notifications, osd, image-picker, dev-gallery) now
bind to Style.font.* instead of pixel literals. Only three deliberate
display-scale outliers remain: the notification empty-state glyph and
the weather flyout's hero temperature pair, all commented.

Background plugin's applyTheme IPC fast-path also pushes shell.toml to
Style so theme swaps update typography and bar size without waiting
for inotify debounce.

Dev gallery (omarchy dev ui-preview) now ships a Typography section
that renders the full scale and theme tokens live, and its summon
command is fixed (omarchy-shell-ipc -> omarchy-shell).
This commit is contained in:
Ryan Hughes
2026-05-18 11:33:19 -04:00
parent c26cf49f7d
commit 648bc54db1
41 changed files with 762 additions and 285 deletions
+158 -9
View File
@@ -4,9 +4,9 @@ import Quickshell
import Quickshell.Io
// Shared structural style tokens for the shell. Color is the palette
// singleton; Style holds the *shape* and *focus-affordance* tokens that
// every panel surface and qs.Ui component should bind to so they stay
// in sync as the user toggles round/sharp corners or as themes change.
// singleton; Style holds everything else themes can influence — corner
// rounding, focus affordances, typography scale, and bar dimensions —
// so panels and qs.Ui components have a single source of truth.
//
// `cornerRadius` mirrors Hyprland's `decoration:rounding`. Themes ship
// their own rounding via theme/hyprland.lua; the user toggle via
@@ -15,9 +15,11 @@ import Quickshell.Io
// up the change here by re-running `hyprctl getoption` whenever either
// of those input files changes.
//
// Single source of truth lives in Hyprland; the shell follows. That
// means a theme that ships `rounding = 10` gives us 10px panels by
// default, and the round/sharp user toggle still works on top of it.
// Typography and bar size come from `theme/shell.toml`. `[font] base-size`
// is the rem root; every `Style.font.<token>` derives from it via the
// scale multipliers below unless the theme pins that specific token.
// `[bar] size-horizontal` / `size-vertical` set the cross-axis dimension
// for top/bottom and left/right bars respectively.
QtObject {
id: root
@@ -36,6 +38,69 @@ QtObject {
readonly property color hotFill: Qt.rgba(Color.foreground.r, Color.foreground.g, Color.foreground.b, 0.08)
readonly property color selectedFill: Qt.rgba(Color.foreground.r, Color.foreground.g, Color.foreground.b, 0.18)
// ---------------------------------------------------------- typography
//
// `fontFamily` defaults to "monospace" so the bar and every qs.Ui
// component follows the fontconfig alias `omarchy-font-set` writes.
// Themes can override per-token via [font] in shell.toml, but the
// family stays system-wide.
property string fontFamily: "monospace"
// The concrete family `monospace` resolves to right now, e.g.
// "JetBrainsMono Nerd Font". Bind `font.family` to `fontFamily` (so the
// alias path keeps working when the user runs `omarchy font set`), but
// read `resolvedFontFamily` when you want to *display* what's drawing.
property string resolvedFontFamily: "monospace"
// Clamped 11..13 by loadShell — bar height and row heights are fixed
// until we ship matching spacing tokens, so unbounded growth clips.
property int fontBaseSize: 12
// Parsed maps populated by loadShell. Keep them as plain dicts so
// reassigning the whole property fires reactive bindings.
property var fontOverrides: ({})
property var barOverrides: ({})
function fontPx(mult) {
return Math.max(1, Math.round(fontBaseSize * mult))
}
function fontToken(key, fallback) {
var v = fontOverrides[key]
var n = Number(v)
return (isFinite(n) && n > 0) ? Math.round(n) : fallback
}
function barToken(key, fallback) {
var v = barOverrides[key]
var n = Number(v)
return (isFinite(n) && n > 0) ? Math.round(n) : fallback
}
readonly property QtObject font: QtObject {
readonly property string family: root.fontFamily
readonly property string resolvedFamily: root.resolvedFontFamily
readonly property int baseSize: root.fontBaseSize
readonly property int caption: root.fontToken("caption", root.fontPx(0.833)) // 10
readonly property int bodySmall: root.fontToken("body-small", root.fontPx(0.917)) // 11
readonly property int body: root.fontToken("body", root.fontPx(1.0)) // 12
readonly property int subtitle: root.fontToken("subtitle", root.fontPx(1.083)) // 13
readonly property int title: root.fontToken("title", root.fontPx(1.167)) // 14
readonly property int heading: root.fontToken("heading", root.fontPx(1.333)) // 16
readonly property int display: root.fontToken("display", root.fontPx(2.0)) // 24
readonly property int displayLarge: root.fontToken("display-large", root.fontPx(2.333)) // 28
readonly property int iconSmall: root.fontToken("icon-small", bodySmall)
readonly property int icon: root.fontToken("icon", title)
readonly property int iconLarge: root.fontToken("icon-large", root.fontPx(1.5)) // 18
}
readonly property QtObject bar: QtObject {
readonly property int sizeHorizontal: root.barToken("size-horizontal", 26)
readonly property int sizeVertical: root.barToken("size-vertical", 28)
}
function refresh() {
hyprctlProc.running = true
}
@@ -50,6 +115,43 @@ QtObject {
}
}
// Parse [font] base-size + per-token overrides and [bar] size-* keys
// out of shell.toml. Color.qml owns the quoted-string side of the same
// file; we only care about the unquoted integer values here.
function loadShell(raw) {
var fontOut = {}
var barOut = {}
var nextBase = 12
var text = String(raw || "")
if (text) {
var lines = text.split("\n")
var section = ""
for (var i = 0; i < lines.length; i++) {
var line = lines[i].replace(/^\s+|\s+$/g, "")
if (!line || line.charAt(0) === "#") continue
var sectionMatch = line.match(/^\[([A-Za-z0-9_-]+)\]\s*(#.*)?$/)
if (sectionMatch) { section = sectionMatch[1]; continue }
var kv = line.match(/^([A-Za-z0-9_-]+)\s*=\s*(-?\d+)\s*(#.*)?$/)
if (!kv) continue
var key = kv[1]
var val = parseInt(kv[2], 10)
if (section === "font") {
if (key === "base-size") nextBase = val
else fontOut[key] = val
} else if (section === "bar" && (key === "size-horizontal" || key === "size-vertical")) {
barOut[key] = val
}
}
}
// Clamp the rem root. Per-token overrides aren't clamped — a theme
// that wants display-large = 64 should be allowed to ship it.
if (nextBase < 11) nextBase = 11
if (nextBase > 13) nextBase = 13
fontBaseSize = nextBase
fontOverrides = fontOut
barOverrides = barOut
}
property Process hyprctlProc: Process {
id: hyprctlProc
command: ["hyprctl", "-j", "getoption", "decoration:rounding"]
@@ -59,6 +161,34 @@ QtObject {
}
}
// Resolve the fontconfig alias to a concrete family name. `omarchy font
// set <name>` rewrites ~/.config/fontconfig/fonts.conf and restarts the
// shell, but rerun on file change anyway so manual edits propagate too.
function resolveFontFamily() {
fcMatchProc.running = true
}
property Process fcMatchProc: Process {
id: fcMatchProc
command: ["fc-match", "-f", "%{family[0]}", "monospace"]
stdout: StdioCollector {
waitForEnd: true
onStreamFinished: {
var name = String(text || "").trim()
if (name.length > 0) root.resolvedFontFamily = name
}
}
}
property FileView fontconfigFile: FileView {
path: Quickshell.env("HOME") + "/.config/fontconfig/fonts.conf"
watchChanges: true
printErrors: false
onFileChanged: root.resolveFontFamily()
onLoaded: root.resolveFontFamily()
onLoadFailed: root.resolveFontFamily()
}
// Re-poll Hyprland a beat after either input file changes. Hyprland's
// auto-reload runs asynchronously when its sourced .lua files change,
// so racing it with an immediate hyprctl gives the old value. 200ms is
@@ -71,12 +201,18 @@ QtObject {
}
// The theme name flips whenever `omarchy-theme-set` swaps the theme/
// symlink; that's when theme/hyprland.lua's `rounding` value changes.
// symlink; that's when theme/hyprland.lua's `rounding` value changes
// and the new theme/shell.toml drops into place. Force a reload of
// shell.toml here so we don't wait on the inotify watch — Color.qml
// uses the same tripwire for the same reason.
property FileView themeNameFile: FileView {
path: Quickshell.env("HOME") + "/.config/omarchy/current/theme.name"
watchChanges: true
printErrors: false
onFileChanged: refreshTimer.restart()
onFileChanged: {
refreshTimer.restart()
shellTomlFile.reload()
}
}
// `omarchy style corners <round|sharp>` creates or removes this flag file.
@@ -91,5 +227,18 @@ QtObject {
onLoadFailed: refreshTimer.restart()
}
Component.onCompleted: refresh()
property FileView shellTomlFile: FileView {
id: shellTomlFile
path: Quickshell.env("HOME") + "/.config/omarchy/current/theme/shell.toml"
watchChanges: true
printErrors: false
onLoaded: root.loadShell(text())
onLoadFailed: root.loadShell("")
onFileChanged: reload()
}
Component.onCompleted: {
refresh()
resolveFontFamily()
}
}