summaryrefslogtreecommitdiffstats
path: root/desktop/modules/fonts/FontsModule.qml
diff options
context:
space:
mode:
Diffstat (limited to 'desktop/modules/fonts/FontsModule.qml')
-rw-r--r--desktop/modules/fonts/FontsModule.qml180
1 files changed, 180 insertions, 0 deletions
diff --git a/desktop/modules/fonts/FontsModule.qml b/desktop/modules/fonts/FontsModule.qml
new file mode 100644
index 0000000..016d85d
--- /dev/null
+++ b/desktop/modules/fonts/FontsModule.qml
@@ -0,0 +1,180 @@
+// 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.
+
+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 }
+ }
+ }
+}