diff options
| author | Danilo M. <danix@danix.xyz> | 2026-10-29 09:55:20 +0100 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-10-29 09:55:20 +0100 |
| commit | 28beb8a9600b6e74cab454401cc94c664d90bfe5 (patch) | |
| tree | c91a0731af8f3204e2c2b07a96a8bbfeaae68e35 /desktop/modules/fonts/Fonts.js | |
| parent | 38cda58f0969fbdda2c33a38356ab43b2869b955 (diff) | |
| download | quickshell-archive/companion.tar.gz quickshell-archive/companion.zip | |
fonts: one font size for the desktop, with per-system overridesarchive/companion
A settings page under Appearance > Fonts holds one pixel size for the
whole desktop and an override wherever a system needs its own, saved as
~/.config/keystone/fonts.json, all in px. At scale 1 the logical DPI is
96, so a point is 4/3 px, and each system is written in its own unit:
Theme.fontSize reads the json itself, so every shell follows live;
qt5ct/qt6ct and the GTK settings files are edited in place, size only,
plus gsettings; kitty, rofi and hyprlock read generated files under
~/.config/keystone that their configs include over designed fallbacks;
the groupbar is a keystone.lua line.
rofi and hyprlock are sections of designed sizes that scale together
from their body text, the smallest entry, each keeping its ratio unless
given its own size. The design is read from the fallbacks themselves,
var(kf-..., "Family N") in the rofi files and $kf_... = N in
hyprlock.conf, so a new entry appears on the page with no list to keep.
With no fonts.json the page starts from what the files say, base 16 and
an override per system at its current size, so the first save changes
nothing; seeded to 0.01px, since 0.1 scaled rofi's 72pt glyph to 72.2.
Each stepper shows px then pt in fixed columns, the system's own unit
highlighted.
Check: node desktop/modules/fonts/test_fonts.js
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'desktop/modules/fonts/Fonts.js')
| -rw-r--r-- | desktop/modules/fonts/Fonts.js | 152 |
1 files changed, 152 insertions, 0 deletions
diff --git a/desktop/modules/fonts/Fonts.js b/desktop/modules/fonts/Fonts.js new file mode 100644 index 0000000..5b7d303 --- /dev/null +++ b/desktop/modules/fonts/Fonts.js @@ -0,0 +1,152 @@ +.pragma library +// Copyright (C) 2026 Danilo M. <danix@danix.xyz> +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 2 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// Pure helpers for the fonts page, tested by test_fonts.js under node. +// +// The model is fonts.json: {base, override}. base is one pixel size for the +// whole desktop; override maps a system ("qt") or one entry of a section +// ("rofi:kf-launcher-entry") to its own pixel size. Everything is stored in +// px and converted where a system wants points: at scale 1 Wayland's logical +// DPI is 96 (gtk-xft-dpi reads 98304, 96 * 1024), so px = pt * 96 / 72. + +function toPx(pt) { return pt * 4 / 3; } +function toPt(px) { return px * 3 / 4; } +function round(x) { return Math.round(x * 10) / 10; } + +// The systems with one size each. unit is what the system's own config is +// written in; when says when a change is seen. +const SYSTEMS = [ + { id: "quickshell", label: "Quickshell", unit: "px", where: "every shell's Theme.fontSize", when: "at once" }, + { id: "qt", label: "Qt", unit: "pt", where: "qt6ct.conf, qt5ct.conf", when: "when an app starts" }, + { id: "gtk", label: "GTK", unit: "pt", where: "gtk-3.0 and gtk-4.0 settings.ini, .gtkrc-2.0, gsettings", when: "at once for GTK 3 and 4" }, + { id: "kitty", label: "kitty", unit: "pt", where: "kitty.conf, through an include", when: "at once" }, + { id: "groupbar", label: "Hyprland groupbar", unit: "px", where: "keystone.lua, group:groupbar:font_size", when: "at once" }, +]; + +// A section is a set of designed sizes that scale together: rofi's fonts, +// hyprlock's labels. Its own size is that of its smallest entry, its body +// text; every other entry keeps its ratio to it unless overridden. +const SECTIONS = [ + { id: "rofi", label: "rofi", unit: "pt", when: "next time rofi opens" }, + { id: "hyprlock", label: "hyprlock", unit: "pt", when: "next lock" }, +]; + +// A system's or a section's size in px. +function size(m, id) { + const o = m.override[id]; + return o !== undefined ? o : m.base; +} + +// A section's body size in its designed unit, pt for both. +function bodyPt(entries) { return Math.min.apply(null, entries.map(e => e.pt)); } + +// An entry's size in px: its override, else its designed size scaled by the +// section's size over the designed body. +function entryPx(m, section, entries, e) { + const o = m.override[section + ":" + e.name]; + if (o !== undefined) return o; + return size(m, section) * e.pt / bodyPt(entries); +} + +// The model with id set to px, or back to the base when px is null. +function withOverride(m, id, px) { + const o = {}; + for (const k in m.override) if (k !== id) o[k] = m.override[k]; + if (px !== null) o[id] = px; + return { base: m.base, override: o }; +} + +function parseModel(text) { + try { + const j = JSON.parse(text); + if (typeof j.base === "number") return { base: j.base, override: j.override || {} }; + } catch (e) {} + return null; +} + +// A first model from what the files say now, so the first save changes +// nothing: the base is Quickshell's 16px, and every system that differs +// from it is an override at its current size. Kept to 0.01px: at 0.1, an +// 11pt body stored as 14.7px scaled rofi's 72pt glyph to 72.2. +function seed(now) { + const m = { base: 16, override: {} }; + for (const id in now) if (now[id] !== null && Math.abs(now[id] - 16) > 0.05) m.override[id] = Math.round(now[id] * 100) / 100; + return m; +} + +// ---- reading ---- + +function qtPt(t) { const m = /^general="[^,]*,([\d.]+)/m.exec(t); return m ? Number(m[1]) : null; } +function gtkPt(t) { const m = /^gtk-font-name="?[^"\n]*?([\d.]+)"?\s*$/m.exec(t); return m ? Number(m[1]) : null; } +function kittyPt(t) { const m = /^font_size\s+([\d.]+)/m.exec(t); return m ? Number(m[1]) : null; } + +// rofi entries: every `var(kf-name, "Family Size")` in the .rasi texts, as +// {name, family, pt}. The fallback in the var is the designed size. +function rofiEntries(texts) { + const out = [], seen = {}; + const re = /var\(\s*(kf-[\w-]+)\s*,\s*"([^"]*?)\s+([\d.]+)"\s*\)/g; + for (const t of texts) { + let m; + while ((m = re.exec(t)) !== null) + if (!seen[m[1]]) { seen[m[1]] = true; out.push({ name: m[1], family: m[2], pt: Number(m[3]) }); } + } + return out; +} + +// hyprlock entries: every `$kf_name = size` in hyprlock.conf, the inline +// fallbacks the generated file overrides, as {name, pt}. +function hyprlockEntries(t) { + const out = []; + const re = /^\$(kf_\w+)\s*=\s*([\d.]+)\s*$/gm; + let m; + while ((m = re.exec(t)) !== null) out.push({ name: m[1], pt: Number(m[2]) }); + return out; +} + +// ---- writing ---- + +// qt5ct/qt6ct: the point size, the second field of general= and fixed=. +// null when neither key is there. +function setQt(t, pt) { + const re = /^((?:general|fixed)="[^,]*,)[\d.]+/gm; + return re.test(t) ? t.replace(re, "$1" + round(pt)) : null; +} + +// settings.ini and .gtkrc-2.0: the size at the end of gtk-font-name. +function setGtk(t, pt) { + const re = /^(gtk-font-name="?[^"\n]*?)[\d.]+("?\s*)$/m; + return re.test(t) ? t.replace(re, "$1" + round(pt) + "$2") : null; +} + +// The family gsettings is given, from the same key. +function gtkFamily(t) { + const m = /^gtk-font-name="?([^",\n]*?),?\s+[\d.]+"?\s*$/m.exec(t); + return m ? m[1] : "Noto Sans"; +} + +const HEAD = "Generated by the keystone settings window, Appearance > Fonts. Rewritten on every save."; + +function kittyFile(m) { + return "# " + HEAD + "\nfont_size " + round(toPt(size(m, "kitty"))) + "\n"; +} + +function rofiFile(m, entries) { + return "/* " + HEAD + " */\n* {\n" + + entries.map(e => " " + e.name + ": \"" + e.family + " " + round(toPt(entryPx(m, "rofi", entries, e))) + "\";\n").join("") + + "}\n"; +} + +// hyprlock's font_size is an integer, in points. +function hyprlockFile(m, entries) { + return "# " + HEAD + "\n" + + entries.map(e => "$" + e.name + " = " + Math.round(toPt(entryPx(m, "hyprlock", entries, e))) + "\n").join(""); +} |
