summaryrefslogtreecommitdiffstats
path: root/desktop/modules/fonts/Fonts.js
diff options
context:
space:
mode:
Diffstat (limited to 'desktop/modules/fonts/Fonts.js')
-rw-r--r--desktop/modules/fonts/Fonts.js152
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("");
+}