diff options
| author | Danilo M. <danix@danix.xyz> | 2026-10-29 09:55:20 +0100 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-10-29 09:55:20 +0100 |
| commit | 28beb8a9600b6e74cab454401cc94c664d90bfe5 (patch) | |
| tree | c91a0731af8f3204e2c2b07a96a8bbfeaae68e35 /desktop/modules/fonts/FontsModule.qml | |
| parent | 38cda58f0969fbdda2c33a38356ab43b2869b955 (diff) | |
| download | quickshell-archive/companion.tar.gz quickshell-archive/companion.zip | |
fonts: one font size for the desktop, with per-system overridesarchive/companion
A settings page under Appearance > Fonts holds one pixel size for the
whole desktop and an override wherever a system needs its own, saved as
~/.config/keystone/fonts.json, all in px. At scale 1 the logical DPI is
96, so a point is 4/3 px, and each system is written in its own unit:
Theme.fontSize reads the json itself, so every shell follows live;
qt5ct/qt6ct and the GTK settings files are edited in place, size only,
plus gsettings; kitty, rofi and hyprlock read generated files under
~/.config/keystone that their configs include over designed fallbacks;
the groupbar is a keystone.lua line.
rofi and hyprlock are sections of designed sizes that scale together
from their body text, the smallest entry, each keeping its ratio unless
given its own size. The design is read from the fallbacks themselves,
var(kf-..., "Family N") in the rofi files and $kf_... = N in
hyprlock.conf, so a new entry appears on the page with no list to keep.
With no fonts.json the page starts from what the files say, base 16 and
an override per system at its current size, so the first save changes
nothing; seeded to 0.01px, since 0.1 scaled rofi's 72pt glyph to 72.2.
Each stepper shows px then pt in fixed columns, the system's own unit
highlighted.
Check: node desktop/modules/fonts/test_fonts.js
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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 } + } + } +} |
