// Copyright (C) 2026 Danilo M. // // 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. import Quickshell import Quickshell.Io import QtQuick import "../.." import "Fonts.js" as F import "../hyprland/Hypr.js" as H // One pixel size for the desktop, and where a system needs its own, an // override. Saved as ~/.config/keystone/fonts.json, then written into each // system in its own unit: Theme.fontSize reads the json itself; qt5ct/qt6ct // and the GTK settings files are edited in place, size only; kitty, rofi // and hyprlock read generated files under ~/.config/keystone that their // configs include over designed fallbacks; the groupbar is a keystone.lua // line. unified-desktop-theme no longer sets the Qt and GTK sizes, so a // re-install does not undo this page. Module { id: fm name: "fonts" label: "Fonts" // nf-fa-font, present in Inconsolata Nerd Font. icon: "" settings: true group: "Appearance" dirty: fm.touched keywords: ["font", "size", "text", "px", "pt", "qt6ct", "gtk", "kitty", "rofi", "hyprlock", "groupbar", "scale"] readonly property string home: Quickshell.env("HOME") readonly property string config: Quickshell.env("XDG_CONFIG_HOME") || fm.home + "/.config" readonly property var paths: ({ model: fm.config + "/keystone/fonts.json", qt6: fm.config + "/qt6ct/qt6ct.conf", qt5: fm.config + "/qt5ct/qt5ct.conf", gtk3: fm.config + "/gtk-3.0/settings.ini", gtk4: fm.config + "/gtk-4.0/settings.ini", gtk2: fm.home + "/.gtkrc-2.0", kittyConf: fm.config + "/kitty/kitty.conf", kitty: fm.config + "/keystone/kitty-fonts.conf", rofi: fm.config + "/keystone/rofi-fonts.rasi", hyprlockConf: fm.config + "/hypr/hyprlock.conf", hyprlock: fm.config + "/keystone/hyprlock-fonts.conf", keystone: fm.config + "/hypr/sections/keystone.lua", }) // key -> text as last read or written: FileView.text() lags setText(). property var texts: ({}) // Bumped on every load, so bindings over texts recompute. property int version: 0 property var rofiEntries: [] property real groupbarNow: 0 readonly property var hyprlockEntries: { fm.version; return F.hyprlockEntries(fm.texts.hyprlockConf ?? ""); } // What the files say now, in px, for the first model. function now() { const t = fm.texts; const pt = (v) => v === null ? null : F.toPx(v); return { quickshell: Theme.fontSize, qt: pt(F.qtPt(t.qt6 ?? "")), gtk: pt(F.gtkPt(t.gtk3 ?? "")), kitty: pt(F.kittyPt(t.kitty ?? "") ?? F.kittyPt(t.kittyConf ?? "")), groupbar: fm.groupbarNow || null, rofi: fm.rofiEntries.length ? F.toPx(Math.min.apply(null, fm.rofiEntries.map(e => e.pt))) : null, hyprlock: fm.hyprlockEntries.length ? F.toPx(Math.min.apply(null, fm.hyprlockEntries.map(e => e.pt))) : null, }; } readonly property var saved: { fm.version; return F.parseModel(fm.texts.model ?? "") ?? F.seed(fm.now()); } property var draft: ({ base: 16, override: {} }) // A reload replaces the draft only while it holds no edits. onSavedChanged: if (!fm.touched) fm.draft = fm.saved property bool touched: false property string notice: "" property string error: "" function setBase(px) { fm.touched = true; fm.draft = { base: px, override: fm.draft.override }; } function setOverride(id, px) { fm.touched = true; fm.draft = F.withOverride(fm.draft, id, px); } function discard() { fm.touched = false; fm.draft = fm.saved; fm.error = ""; } function refresh() { for (const v of views.instances) v.reload(); rofiProc.running = false; rofiProc.running = true; groupbarProc.running = false; groupbarProc.running = true; } property Variants views: Variants { model: Object.keys(fm.paths) FileView { required property string modelData path: fm.paths[modelData] atomicWrites: true watchChanges: true printErrors: false onFileChanged: reload() onLoaded: { fm.texts[modelData] = text(); fm.version++; } onLoadFailed: { fm.texts[modelData] = ""; fm.version++; } } } function write(key, t) { if (t === null || t === (fm.texts[key] ?? null)) return; const v = fm.views.instances.find(x => x.modelData === key); fm.texts[key] = t; v.setText(t); } // The designed sizes: every var(kf-..., "...") in the udt rofi files. property Process rofiProc: Process { command: ["sh", "-c", 'cat "$1"/*.rasi', "sh", fm.config + "/rofi/udt"] stdout: StdioCollector { onStreamFinished: { fm.rofiEntries = F.rofiEntries([this.text]); fm.version++; } } } property Process groupbarProc: Process { command: ["hyprctl", "getoption", "group:groupbar:font_size", "-j"] stdout: StdioCollector { onStreamFinished: { try { fm.groupbarNow = JSON.parse(this.text).int; } catch (e) {} } } } function save() { const m = fm.draft; const skipped = []; const edit = (key, f, what) => { const t = fm.texts[key] ?? ""; if (t === "") return; const n = f(t); if (n === null) skipped.push(what); else fm.write(key, n); }; const qt = F.toPt(F.size(m, "qt")), gtk = F.toPt(F.size(m, "gtk")); edit("qt6", t => F.setQt(t, qt), "qt6ct.conf"); edit("qt5", t => F.setQt(t, qt), "qt5ct.conf"); for (const k of ["gtk3", "gtk4", "gtk2"]) edit(k, t => F.setGtk(t, gtk), k); Quickshell.execDetached(["gsettings", "set", "org.gnome.desktop.interface", "font-name", F.gtkFamily(fm.texts.gtk3 ?? "") + " " + F.round(gtk)]); const kitty = F.kittyFile(m); const kittyChanged = kitty !== fm.texts.kitty; fm.write("kitty", kitty); // SIGUSR1 is kitty's reload-config, sent to the kitty processes only. if (kittyChanged) Quickshell.execDetached(["pkill", "-USR1", "-x", "kitty"]); if (fm.rofiEntries.length) fm.write("rofi", F.rofiFile(m, fm.rofiEntries)); if (fm.hyprlockEntries.length) fm.write("hyprlock", F.hyprlockFile(m, fm.hyprlockEntries)); // ponytail: keystone.lua is also the Options page's; a save on both at // the same moment could drop one line. Hyprland autoreloads on it. if (fm.texts.keystone !== undefined) fm.write("keystone", H.override(fm.texts.keystone, "group:groupbar:font_size", String(Math.round(F.size(m, "groupbar"))))); fm.write("model", JSON.stringify(m, null, 2) + "\n"); fm.touched = false; fm.version++; fm.error = skipped.length ? "No font size found in " + skipped.join(", ") + "; left alone." : ""; fm.notice = "Saved. Qt apps pick it up when they start."; } page: Component { Page { title: "Fonts" actions: Component { SaveBar { dirty: fm.dirty notice: fm.notice error: fm.error onSave: fm.save() onDiscard: fm.discard() } } FontsPage { width: parent.width; fonts: fm } } } }