diff options
Diffstat (limited to 'desktop/modules/fonts/FontsModule.qml')
| -rw-r--r-- | desktop/modules/fonts/FontsModule.qml | 180 |
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 } + } + } +} |
