summaryrefslogtreecommitdiffstats
path: root/desktop/modules/fonts/Fonts.js
blob: 5b7d303792ae9b8dfc145109f31eb94a65f46894 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
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("");
}