Files

215 lines
6.4 KiB
QML

pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Commons
Singleton {
id: root
// Public properties
property string osPretty: ""
property string osLogo: ""
property bool isNixOS: false
property bool isReady: false
// User info
readonly property string username: (Quickshell.env("USER") || "")
readonly property string envRealName: (Quickshell.env("NOCTALIA_REALNAME") || "")
property string realName: ""
// Machine info
property string hostName: ""
// Internal: pending logo name for fallback after probe fails
property string pendingLogoName: ""
readonly property string displayName: {
// Explicit override
if (envRealName && envRealName.length > 0) {
return envRealName;
}
// Name from getent
if (realName && realName.length > 0) {
return realName;
}
// Fallback: $USER as-is (login names are case-sensitive on some systems)
if (username && username.length > 0) {
return username;
}
// Last resort: placeholder
return "User";
}
function init() {
Logger.i("HostService", "Service started");
}
// Internal helpers
function buildCandidates(name) {
const n = (name || "").trim();
if (!n)
return [];
const sizes = ["512x512", "256x256", "128x128", "64x64", "48x48", "32x32", "24x24", "22x22", "16x16"];
const exts = ["svg", "png"];
const candidates = [];
// pixmaps
for (const ext of exts) {
candidates.push(`/usr/share/pixmaps/${n}.${ext}`);
}
// hicolor scalable and raster sizes
candidates.push(`/usr/share/icons/hicolor/scalable/apps/${n}.svg`);
for (const s of sizes) {
for (const ext of exts) {
candidates.push(`/usr/share/icons/hicolor/${s}/apps/${n}.${ext}`);
}
}
// NixOS hicolor paths
candidates.push(`/run/current-system/sw/share/icons/hicolor/scalable/apps/${n}.svg`);
for (const s of sizes) {
for (const ext of exts) {
candidates.push(`/run/current-system/sw/share/icons/hicolor/${s}/apps/${n}.${ext}`);
}
}
// Generic icon themes under /usr/share/icons (common cases)
for (const ext of exts) {
candidates.push(`/usr/share/icons/${n}.${ext}`);
candidates.push(`/usr/share/icons/${n}/${n}.${ext}`);
candidates.push(`/usr/share/icons/${n}/apps/${n}.${ext}`);
}
return candidates;
}
function resolveLogo(name) {
const n = (name || "").trim();
if (!n)
return;
// First try Quickshell's icon lookup for direct file paths
try {
const path = Quickshell.iconPath(n, "");
if (path && path !== "" && !path.startsWith("image://")) {
// Got a direct file path - use it
const finalPath = path.startsWith("file://") ? path : "file://" + path;
root.osLogo = finalPath;
Logger.d("HostService", "Found logo via icon theme:", root.osLogo);
return;
}
} catch (e) {
// Ignore and continue to manual probe
}
// Try manual probing for hicolor/pixmaps paths
// Store name for fallback to image:// URI if probe fails
root.pendingLogoName = n;
const all = buildCandidates(n);
if (all.length === 0) {
// No candidates, try image:// URI directly
root.osLogo = `image://icon/${n}`;
Logger.d("HostService", "Using theme icon URI:", root.osLogo);
return;
}
const script = all.map(p => `if [ -f "${p}" ]; then echo "${p}"; exit 0; fi`).join("; ") + "; exit 1";
probe.command = ["sh", "-c", script];
probe.running = true;
}
// Read /etc/os-release and trigger resolution
FileView {
id: osInfo
path: "/etc/os-release"
onLoaded: {
try {
const lines = text().split("\n");
const val = k => {
const l = lines.find(x => x.startsWith(k + "="));
return l ? l.split("=")[1].replace(/"/g, "") : "";
};
root.osPretty = val("PRETTY_NAME") || val("NAME");
Logger.i("HostService", "Detected", root.osPretty);
const osId = (val("ID") || "").toLowerCase();
root.isNixOS = osId === "nixos" || (root.osPretty || "").toLowerCase().includes("nixos");
const logoName = val("LOGO");
Logger.i("HostService", "Looking for logo icon:", logoName);
if (logoName) {
resolveLogo(logoName);
}
root.isReady = true;
} catch (e) {
Logger.w("HostService", "failed to read os-release", e);
}
}
}
Process {
id: probe
onExited: code => {
const p = String(stdout.text || "").trim();
if (code === 0 && p) {
root.osLogo = `file://${p}`;
root.pendingLogoName = "";
Logger.d("HostService", "Found", root.osLogo);
} else if (root.pendingLogoName) {
// Manual probe failed, fallback to image:// URI (theme icon)
root.osLogo = `image://icon/${root.pendingLogoName}`;
root.pendingLogoName = "";
Logger.d("HostService", "Using theme icon URI:", root.osLogo);
} else {
root.osLogo = "";
Logger.w("HostService", "No distro logo found");
}
}
stdout: StdioCollector {}
stderr: StdioCollector {}
}
// Resolve GECOS real name once on startup
Process {
id: realNameProcess
command: ["sh", "-c", "getent passwd \"$USER\" | cut -d: -f5 | cut -d, -f1"]
running: true
stdout: StdioCollector {
onStreamFinished: {
const name = String(text || "").trim();
if (name.length > 0) {
root.realName = name;
Logger.i("HostService", "resolved real name", name);
}
}
}
stderr: StdioCollector {}
}
// Resolve hostname from distro-specific locations.
// Prefer /etc/hostname, fallback to Gentoo's /etc/conf.d/hostname.
Process {
id: hostNameProcess
command: ["sh", "-c",
"if [ -r /etc/hostname ]; then sed -n '1p' /etc/hostname; exit 0; fi; if [ -r /etc/conf.d/hostname ]; then v=$(sed -n -E 's/^[[:space:]]*[Hh][Oo][Ss][Tt][Nn][Aa][Mm][Ee][[:space:]]*=[[:space:]]*//p' /etc/conf.d/hostname | sed -n '1p'); v=$(printf '%s' \"$v\" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//; s/^\"//; s/\"$//; s/^\x27//; s/\x27$//'); printf '%s\n' \"$v\"; exit 0; fi; exit 0"]
running: true
stdout: StdioCollector {
onStreamFinished: {
const name = String(text || "").trim();
if (name.length > 0) {
root.hostName = name;
Logger.i("HostService", "resolved hostname", name);
}
}
}
stderr: StdioCollector {}
}
}