diff options
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(""); +} |
