fedora
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property ListModel availableFonts: ListModel {}
|
||||
property ListModel monospaceFonts: ListModel {}
|
||||
property bool fontsLoaded: false
|
||||
property bool isLoading: false
|
||||
|
||||
function init() {
|
||||
if (fontsLoaded || isLoading)
|
||||
return;
|
||||
Logger.i("Font", "Service started");
|
||||
loadFontsViaFcList();
|
||||
}
|
||||
|
||||
// Load all fonts using fc-list in background - no main thread blocking
|
||||
function loadFontsViaFcList() {
|
||||
if (isLoading)
|
||||
return;
|
||||
isLoading = true;
|
||||
allFontsProcess.running = true;
|
||||
}
|
||||
|
||||
function populateModels(allFontsText, monoFontsText) {
|
||||
// Parse monospace fonts into a lookup set
|
||||
// fc-list returns comma-separated family names for fonts with multiple families
|
||||
var monoLookup = {};
|
||||
var monoLines = monoFontsText.split('\n');
|
||||
for (var i = 0; i < monoLines.length; i++) {
|
||||
var line = monoLines[i].trim();
|
||||
if (line) {
|
||||
var monoFamilies = line.split(',');
|
||||
for (var mi = 0; mi < monoFamilies.length; mi++) {
|
||||
var monoName = monoFamilies[mi].trim();
|
||||
if (monoName)
|
||||
monoLookup[monoName] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse all fonts - split comma-separated family names
|
||||
var allLines = allFontsText.split('\n');
|
||||
var fontSet = {}; // Deduplicate font families
|
||||
|
||||
for (var j = 0; j < allLines.length; j++) {
|
||||
var line = allLines[j].trim();
|
||||
if (line) {
|
||||
var families = line.split(',');
|
||||
for (var fi = 0; fi < families.length; fi++) {
|
||||
var fontName = families[fi].trim();
|
||||
if (fontName && !fontSet[fontName]) {
|
||||
fontSet[fontName] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort font names
|
||||
var sortedFonts = Object.keys(fontSet).sort(function (a, b) {
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
|
||||
// Build arrays for batch insert
|
||||
var allBatch = [];
|
||||
var monoBatch = [];
|
||||
|
||||
for (var k = 0; k < sortedFonts.length; k++) {
|
||||
var name = sortedFonts[k];
|
||||
var fontObj = {
|
||||
"key": name,
|
||||
"name": name
|
||||
};
|
||||
allBatch.push(fontObj);
|
||||
|
||||
// Check if monospace
|
||||
if (monoLookup[name] || name.toLowerCase().includes("mono")) {
|
||||
monoBatch.push(fontObj);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear and populate models (single batch operation)
|
||||
availableFonts.clear();
|
||||
monospaceFonts.clear();
|
||||
|
||||
availableFonts.append({
|
||||
"key": Qt.application.font.family,
|
||||
"name": I18n.tr("panels.indicator.system-default")
|
||||
});
|
||||
monospaceFonts.append({
|
||||
"key": "monospace",
|
||||
"name": I18n.tr("panels.indicator.system-default")
|
||||
});
|
||||
|
||||
for (var m = 0; m < allBatch.length; m++)
|
||||
availableFonts.append(allBatch[m]);
|
||||
for (var n = 0; n < monoBatch.length; n++)
|
||||
monospaceFonts.append(monoBatch[n]);
|
||||
|
||||
fontsLoaded = true;
|
||||
isLoading = false;
|
||||
Logger.i("Font", "Loaded", availableFonts.count, "fonts,", monospaceFonts.count, "monospace");
|
||||
}
|
||||
|
||||
// Temporary storage for process outputs
|
||||
property string _allFontsOutput: ""
|
||||
property string _monoFontsOutput: ""
|
||||
property bool _allFontsDone: false
|
||||
property bool _monoFontsDone: false
|
||||
|
||||
function checkBothProcessesDone() {
|
||||
if (_allFontsDone && _monoFontsDone) {
|
||||
populateModels(_allFontsOutput, _monoFontsOutput);
|
||||
// Clear temp storage
|
||||
_allFontsOutput = "";
|
||||
_monoFontsOutput = "";
|
||||
_allFontsDone = false;
|
||||
_monoFontsDone = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Process to get all font families (runs in background)
|
||||
Process {
|
||||
id: allFontsProcess
|
||||
command: ["fc-list", "--format", "%{family}\\n"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
root._allFontsOutput = this.text;
|
||||
root._allFontsDone = true;
|
||||
root.checkBothProcessesDone();
|
||||
}
|
||||
}
|
||||
|
||||
onRunningChanged: {
|
||||
if (running) {
|
||||
// Start mono fonts process in parallel
|
||||
monoFontsProcess.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
onExited: function (exitCode, exitStatus) {
|
||||
if (exitCode !== 0) {
|
||||
Logger.w("Font", "fc-list failed with exit code", exitCode);
|
||||
root._allFontsOutput = "";
|
||||
root._allFontsDone = true;
|
||||
root.checkBothProcessesDone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process to get monospace font families (runs in background, parallel)
|
||||
Process {
|
||||
id: monoFontsProcess
|
||||
command: ["fc-list", ":mono", "--format", "%{family}\\n"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
root._monoFontsOutput = this.text;
|
||||
root._monoFontsDone = true;
|
||||
root.checkBothProcessesDone();
|
||||
}
|
||||
}
|
||||
|
||||
onExited: function (exitCode, exitStatus) {
|
||||
if (exitCode !== 0) {
|
||||
root._monoFontsOutput = "";
|
||||
root._monoFontsDone = true;
|
||||
root.checkBothProcessesDone();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
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 {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property string rulesFilePath: Settings.configDir + "notification-rules.json"
|
||||
|
||||
property var rules: []
|
||||
|
||||
property FileView rulesFileView: FileView {
|
||||
id: rulesFileView
|
||||
path: root.rulesFilePath
|
||||
watchChanges: true
|
||||
printErrors: false
|
||||
|
||||
adapter: JsonAdapter {
|
||||
id: rulesAdapter
|
||||
property var rules: []
|
||||
}
|
||||
|
||||
onLoaded: {
|
||||
try {
|
||||
const parsed = JSON.parse(rulesFileView.text());
|
||||
const raw = Array.isArray(parsed.rules) ? parsed.rules : [];
|
||||
root.rules = raw.filter(r => (r.pattern || "").trim() !== "");
|
||||
} catch (e) {
|
||||
root.rules = [];
|
||||
}
|
||||
}
|
||||
|
||||
onLoadFailed: function (error) {
|
||||
root.rules = [];
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
}
|
||||
|
||||
function save() {
|
||||
Quickshell.execDetached(["mkdir", "-p", Settings.configDir]);
|
||||
root.rules = root.rules.filter(r => (r.pattern || "").trim() !== "");
|
||||
rulesAdapter.rules = root.rules;
|
||||
rulesFileView.writeAdapter();
|
||||
}
|
||||
|
||||
function evaluate(appName, summary, body) {
|
||||
const haystack = [appName || "", summary || "", body || ""].join(" ");
|
||||
for (let i = 0; i < root.rules.length; i++) {
|
||||
const r = root.rules[i];
|
||||
const pattern = (r.pattern || "").trim();
|
||||
if (pattern === "")
|
||||
continue;
|
||||
let matched = false;
|
||||
if (pattern.length >= 3 && pattern.startsWith("/") && pattern.endsWith("/")) {
|
||||
try {
|
||||
matched = new RegExp(pattern.slice(1, -1)).test(haystack);
|
||||
} catch (e) {
|
||||
Logger.w("NotificationRulesService", "Invalid regex:", pattern, e);
|
||||
}
|
||||
} else if (pattern.includes("*")) {
|
||||
try {
|
||||
const reStr = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
||||
matched = new RegExp(reStr, "i").test(haystack);
|
||||
} catch (e) {
|
||||
matched = haystack.toLowerCase().includes(pattern.toLowerCase());
|
||||
}
|
||||
} else {
|
||||
matched = haystack.toLowerCase().includes(pattern.toLowerCase());
|
||||
}
|
||||
if (matched) {
|
||||
const a = (r.action || "block").toLowerCase();
|
||||
if (a === "mute" || a === "hide")
|
||||
return a;
|
||||
if (a === "silence")
|
||||
return "hide";
|
||||
return "block";
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,337 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
import qs.Services.Theming
|
||||
|
||||
// Service to check if various programs are available on the system
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// Program availability properties
|
||||
property bool nmcliAvailable: false
|
||||
property bool bluetoothctlAvailable: false
|
||||
property bool wlsunsetAvailable: false
|
||||
property bool gnomeCalendarAvailable: false
|
||||
property bool pythonAvailable: false
|
||||
property bool wtypeAvailable: false
|
||||
|
||||
// Programs to check - maps property names to commands
|
||||
readonly property var programsToCheck: ({
|
||||
"bluetoothctlAvailable": ["sh", "-c", "command -v bluetoothctl"],
|
||||
"nmcliAvailable": ["sh", "-c", "command -v nmcli"],
|
||||
"wlsunsetAvailable": ["sh", "-c", "command -v wlsunset"],
|
||||
"gnomeCalendarAvailable": ["sh", "-c", "command -v gnome-calendar"],
|
||||
"wtypeAvailable": ["sh", "-c", "command -v wtype"],
|
||||
"pythonAvailable": ["sh", "-c", "command -v python3"]
|
||||
})
|
||||
|
||||
// Discord client auto-detection
|
||||
property var availableDiscordClients: []
|
||||
|
||||
// Code client auto-detection
|
||||
property var availableCodeClients: []
|
||||
|
||||
// Emacs client auto-detection
|
||||
property var availableEmacsClients: []
|
||||
|
||||
// Signal emitted when all checks are complete
|
||||
signal checksCompleted
|
||||
|
||||
// disable Night Light in settings if wlsunset is not available
|
||||
onChecksCompleted: {
|
||||
if (!wlsunsetAvailable && Settings.data.nightLight.enabled) {
|
||||
Settings.data.nightLight.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
onWlsunsetAvailableChanged: {
|
||||
if (!wlsunsetAvailable && Settings.data.nightLight.enabled) {
|
||||
Settings.data.nightLight.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Function to detect Discord client by checking config directories
|
||||
function detectDiscordClient() {
|
||||
// Build shell script to check each client
|
||||
var scriptParts = ["available_clients=\"\";"];
|
||||
|
||||
for (var i = 0; i < TemplateRegistry.discordClients.length; i++) {
|
||||
var client = TemplateRegistry.discordClients[i];
|
||||
var clientName = client.name;
|
||||
var configPath = client.configPath;
|
||||
|
||||
// Use the actual config path from the client, removing ~ prefix
|
||||
var checkPath = configPath.startsWith("~") ? configPath.substring(2) : configPath.substring(1);
|
||||
|
||||
scriptParts.push("if [ -d \"$HOME/" + checkPath + "\" ]; then available_clients=\"$available_clients " + clientName + "\"; fi;");
|
||||
}
|
||||
|
||||
scriptParts.push("echo \"$available_clients\"");
|
||||
|
||||
// Use a Process to check directory existence for all clients
|
||||
discordDetector.command = ["sh", "-c", scriptParts.join(" ")];
|
||||
discordDetector.running = true;
|
||||
}
|
||||
|
||||
// Process to detect Discord client directories
|
||||
Process {
|
||||
id: discordDetector
|
||||
running: false
|
||||
|
||||
onExited: function (exitCode) {
|
||||
availableDiscordClients = [];
|
||||
|
||||
if (exitCode === 0) {
|
||||
var detectedClients = stdout.text.trim().split(/\s+/).filter(function (client) {
|
||||
return client.length > 0;
|
||||
});
|
||||
|
||||
if (detectedClients.length > 0) {
|
||||
// Build list of available clients
|
||||
for (var i = 0; i < detectedClients.length; i++) {
|
||||
var clientName = detectedClients[i];
|
||||
for (var j = 0; j < TemplateRegistry.discordClients.length; j++) {
|
||||
var client = TemplateRegistry.discordClients[j];
|
||||
if (client.name === clientName) {
|
||||
availableDiscordClients.push(client);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Logger.d("ProgramChecker", "Detected Discord clients:", detectedClients.join(", "));
|
||||
}
|
||||
}
|
||||
|
||||
if (availableDiscordClients.length === 0) {
|
||||
Logger.d("ProgramChecker", "No Discord clients detected");
|
||||
}
|
||||
}
|
||||
|
||||
stdout: StdioCollector {}
|
||||
stderr: StdioCollector {}
|
||||
}
|
||||
|
||||
// Function to detect Code client by checking config directories
|
||||
function detectCodeClient() {
|
||||
// Build shell script to check each client
|
||||
var scriptParts = ["available_clients=\"\";"];
|
||||
|
||||
for (var i = 0; i < TemplateRegistry.codeClients.length; i++) {
|
||||
var client = TemplateRegistry.codeClients[i];
|
||||
var clientName = client.name;
|
||||
var configPath = client.configPath;
|
||||
|
||||
// Check if the config directory exists
|
||||
scriptParts.push("if [ -d \"$HOME" + configPath.substring(1) + "\" ]; then available_clients=\"$available_clients " + clientName + "\"; fi;");
|
||||
}
|
||||
|
||||
scriptParts.push("echo \"$available_clients\"");
|
||||
|
||||
// Use a Process to check directory existence for all clients
|
||||
codeDetector.command = ["sh", "-c", scriptParts.join(" ")];
|
||||
codeDetector.running = true;
|
||||
}
|
||||
|
||||
// Process to detect Code client directories
|
||||
Process {
|
||||
id: codeDetector
|
||||
running: false
|
||||
|
||||
onExited: function (exitCode) {
|
||||
availableCodeClients = [];
|
||||
|
||||
if (exitCode === 0) {
|
||||
var detectedClients = stdout.text.trim().split(/\s+/).filter(function (client) {
|
||||
return client.length > 0;
|
||||
});
|
||||
|
||||
if (detectedClients.length > 0) {
|
||||
// Build list of available clients
|
||||
for (var i = 0; i < detectedClients.length; i++) {
|
||||
var clientName = detectedClients[i];
|
||||
for (var j = 0; j < TemplateRegistry.codeClients.length; j++) {
|
||||
var client = TemplateRegistry.codeClients[j];
|
||||
if (client.name === clientName) {
|
||||
availableCodeClients.push(client);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Logger.d("ProgramChecker", "Detected Code clients:", detectedClients.join(", "));
|
||||
}
|
||||
}
|
||||
|
||||
if (availableCodeClients.length === 0) {
|
||||
Logger.d("ProgramChecker", "No Code clients detected");
|
||||
}
|
||||
}
|
||||
|
||||
stdout: StdioCollector {}
|
||||
stderr: StdioCollector {}
|
||||
}
|
||||
|
||||
// Function to detect Emacs client by checking config directories
|
||||
function detectEmacsClient() {
|
||||
// Build shell script to check each client
|
||||
var scriptParts = ["available_clients=\"\";"];
|
||||
|
||||
for (var i = 0; i < TemplateRegistry.emacsClients.length; i++) {
|
||||
var client = TemplateRegistry.emacsClients[i];
|
||||
var clientName = client.name;
|
||||
var configPath = client.path;
|
||||
|
||||
// Check if the config directory exists
|
||||
scriptParts.push("if [ -d \"$HOME" + configPath.substring(1) + "\" ]; then available_clients=\"$available_clients " + clientName + "\"; fi;");
|
||||
}
|
||||
|
||||
scriptParts.push("echo \"$available_clients\"");
|
||||
|
||||
// Use a Process to check directory existence for all clients
|
||||
emacsDetector.command = ["sh", "-c", scriptParts.join(" ")];
|
||||
emacsDetector.running = true;
|
||||
}
|
||||
|
||||
// Process to detect Emacs client directories
|
||||
Process {
|
||||
id: emacsDetector
|
||||
running: false
|
||||
|
||||
onExited: function (exitCode) {
|
||||
availableEmacsClients = [];
|
||||
|
||||
if (exitCode === 0) {
|
||||
var detectedClients = stdout.text.trim().split(/\s+/).filter(function (client) {
|
||||
return client.length > 0;
|
||||
});
|
||||
|
||||
if (detectedClients.length > 0) {
|
||||
// Build list of available clients
|
||||
for (var i = 0; i < detectedClients.length; i++) {
|
||||
var clientName = detectedClients[i];
|
||||
for (var j = 0; j < TemplateRegistry.emacsClients.length; j++) {
|
||||
var client = TemplateRegistry.emacsClients[j];
|
||||
if (client.name === clientName) {
|
||||
availableEmacsClients.push(client);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Logger.d("ProgramChecker", "Detected Emacs clients:", detectedClients.join(", "));
|
||||
}
|
||||
}
|
||||
|
||||
if (availableEmacsClients.length === 0) {
|
||||
Logger.d("ProgramChecker", "No Emacs clients detected");
|
||||
}
|
||||
}
|
||||
|
||||
stdout: StdioCollector {}
|
||||
stderr: StdioCollector {}
|
||||
}
|
||||
|
||||
// Internal tracking
|
||||
property int completedChecks: 0
|
||||
property int totalChecks: Object.keys(programsToCheck).length
|
||||
|
||||
// Single reusable Process object
|
||||
Process {
|
||||
id: checker
|
||||
running: false
|
||||
|
||||
property string currentProperty: ""
|
||||
|
||||
onExited: function (exitCode) {
|
||||
// Set the availability property
|
||||
root[currentProperty] = (exitCode === 0);
|
||||
|
||||
// Stop the process to free resources
|
||||
running = false;
|
||||
|
||||
// Track completion
|
||||
root.completedChecks++;
|
||||
|
||||
// Check next program or emit completion signal
|
||||
if (root.completedChecks >= root.totalChecks) {
|
||||
// Run Discord, Code and Emacs client detection after all checks are complete
|
||||
root.detectDiscordClient();
|
||||
root.detectCodeClient();
|
||||
root.detectEmacsClient();
|
||||
root.checksCompleted();
|
||||
} else {
|
||||
root.checkNextProgram();
|
||||
}
|
||||
}
|
||||
|
||||
stdout: StdioCollector {}
|
||||
stderr: StdioCollector {}
|
||||
}
|
||||
|
||||
// Queue of programs to check
|
||||
property var checkQueue: []
|
||||
property int currentCheckIndex: 0
|
||||
|
||||
// Function to check the next program in the queue
|
||||
function checkNextProgram() {
|
||||
if (currentCheckIndex >= checkQueue.length)
|
||||
return;
|
||||
var propertyName = checkQueue[currentCheckIndex];
|
||||
var command = programsToCheck[propertyName];
|
||||
|
||||
checker.currentProperty = propertyName;
|
||||
checker.command = command;
|
||||
checker.running = true;
|
||||
|
||||
currentCheckIndex++;
|
||||
}
|
||||
|
||||
// Function to run all program checks
|
||||
function checkAllPrograms() {
|
||||
// Reset state
|
||||
completedChecks = 0;
|
||||
currentCheckIndex = 0;
|
||||
checkQueue = Object.keys(programsToCheck);
|
||||
|
||||
// Start first check
|
||||
if (checkQueue.length > 0) {
|
||||
checkNextProgram();
|
||||
}
|
||||
}
|
||||
|
||||
// Function to check a specific program
|
||||
function checkProgram(programProperty) {
|
||||
if (!programsToCheck.hasOwnProperty(programProperty)) {
|
||||
Logger.w("ProgramChecker", "Unknown program property:", programProperty);
|
||||
return;
|
||||
}
|
||||
|
||||
checker.currentProperty = programProperty;
|
||||
checker.command = programsToCheck[programProperty];
|
||||
checker.running = true;
|
||||
}
|
||||
|
||||
// Manual function to test Discord detection (for debugging)
|
||||
function testDiscordDetection() {
|
||||
Logger.d("ProgramChecker", "Testing Discord detection...");
|
||||
Logger.d("ProgramChecker", "HOME:", Quickshell.env("HOME"));
|
||||
|
||||
// Test each client directory
|
||||
for (var i = 0; i < TemplateRegistry.discordClients.length; i++) {
|
||||
var client = TemplateRegistry.discordClients[i];
|
||||
var configDir = client.configPath.replace("~", Quickshell.env("HOME"));
|
||||
Logger.d("ProgramChecker", "Checking:", configDir);
|
||||
}
|
||||
|
||||
detectDiscordClient();
|
||||
}
|
||||
|
||||
// Initialize checks when service is created
|
||||
Component.onCompleted: {
|
||||
checkAllPrograms();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// Map to track active sound players: resolvedPath -> MediaPlayer instance
|
||||
property var activePlayers: ({})
|
||||
|
||||
// Check if QtMultimedia is available
|
||||
property bool multimediaAvailable: false
|
||||
|
||||
// Container for dynamically created players
|
||||
Item {
|
||||
id: playersContainer
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
// Test if QtMultimedia is available by trying to create a simple component
|
||||
try {
|
||||
var testComponent = Qt.createQmlObject(`
|
||||
import QtQuick
|
||||
import QtMultimedia
|
||||
Item {}
|
||||
`, root, "MultimediaTest");
|
||||
if (testComponent) {
|
||||
multimediaAvailable = true;
|
||||
testComponent.destroy();
|
||||
Logger.i("SoundService", "QtMultimedia found - sound playback enabled");
|
||||
}
|
||||
} catch (e) {
|
||||
multimediaAvailable = false;
|
||||
Logger.w("SoundService", "QtMultimedia not available - no audio will be played from noctalia-shell");
|
||||
}
|
||||
}
|
||||
|
||||
function resolvePath(soundPath) {
|
||||
if (!soundPath || soundPath === "") {
|
||||
return "";
|
||||
}
|
||||
|
||||
let resolvedPath = soundPath;
|
||||
|
||||
// If it's just a filename (no path separators), assume it's in Assets/Sounds/
|
||||
if (!soundPath.includes("/") && !soundPath.startsWith("file://")) {
|
||||
resolvedPath = Quickshell.shellDir + "/Assets/Sounds/" + soundPath;
|
||||
} else if (!soundPath.startsWith("/") && !soundPath.startsWith("file://")) {
|
||||
// Relative path - assume it's relative to shellDir
|
||||
resolvedPath = Quickshell.shellDir + "/" + soundPath;
|
||||
} else if (soundPath.startsWith("file://")) {
|
||||
resolvedPath = soundPath.substring(7); // Remove "file://" prefix
|
||||
}
|
||||
// Absolute paths are used as-is
|
||||
|
||||
return resolvedPath;
|
||||
}
|
||||
|
||||
function playSound(soundPath, options) {
|
||||
if (!soundPath || soundPath === "") {
|
||||
Logger.w("SoundService", "No sound path provided");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!multimediaAvailable) {
|
||||
Logger.d("SoundService", "QtMultimedia not available, cannot play sound:", soundPath);
|
||||
return;
|
||||
}
|
||||
|
||||
const opts = options || {};
|
||||
const volume = opts.volume !== undefined ? opts.volume : 1.0;
|
||||
const fallback = opts.fallback !== undefined ? opts.fallback : false;
|
||||
const repeat = opts.repeat !== undefined ? opts.repeat : false;
|
||||
|
||||
// Resolve path
|
||||
const resolvedPath = resolvePath(soundPath);
|
||||
|
||||
// Stop any existing player for this path if it's looping
|
||||
if (repeat && activePlayers[resolvedPath]) {
|
||||
stopSound(soundPath);
|
||||
}
|
||||
|
||||
// Create MediaPlayer instance dynamically with QtMultimedia import
|
||||
const loopsValue = repeat ? "MediaPlayer.Infinite" : "1";
|
||||
const escapedPath = resolvedPath.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
||||
const playerQml = `
|
||||
import QtQuick
|
||||
import QtMultimedia
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.System
|
||||
MediaPlayer {
|
||||
id: mediaPlayer
|
||||
property string resolvedPath: "${escapedPath}"
|
||||
property bool shouldFallback: ${fallback && !repeat}
|
||||
property real soundVolume: ${Math.max(0, Math.min(1, volume))}
|
||||
source: "file://${escapedPath}"
|
||||
loops: ${loopsValue}
|
||||
audioOutput: AudioOutput {
|
||||
volume: soundVolume
|
||||
}
|
||||
onErrorOccurred: {
|
||||
Logger.w("SoundService", "Error playing sound:", source, error, errorString);
|
||||
if (shouldFallback) {
|
||||
const fallbackPath = Quickshell.shellDir + "/Assets/Sounds/notification.mp3";
|
||||
if (fallbackPath !== resolvedPath) {
|
||||
SoundService.playSound(fallbackPath, {
|
||||
volume: soundVolume,
|
||||
fallback: false,
|
||||
repeat: false
|
||||
});
|
||||
}
|
||||
}
|
||||
if (SoundService.activePlayers[resolvedPath]) {
|
||||
delete SoundService.activePlayers[resolvedPath];
|
||||
}
|
||||
destroy();
|
||||
}
|
||||
onPlaybackStateChanged: function (state) {
|
||||
if (state === MediaPlayer.StoppedState && loops === 1) {
|
||||
if (SoundService.activePlayers[resolvedPath]) {
|
||||
delete SoundService.activePlayers[resolvedPath];
|
||||
}
|
||||
destroy();
|
||||
}
|
||||
}
|
||||
Component.onCompleted: {
|
||||
play();
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
try {
|
||||
const player = Qt.createQmlObject(playerQml, playersContainer, "MediaPlayer_" + resolvedPath.replace(/[^a-zA-Z0-9]/g, "_"));
|
||||
|
||||
if (!player) {
|
||||
Logger.w("SoundService", "Failed to create MediaPlayer for:", resolvedPath);
|
||||
// Try fallback if requested
|
||||
if (fallback && !repeat) {
|
||||
const defaultSound = Quickshell.shellDir + "/Assets/Sounds/notification.mp3";
|
||||
if (defaultSound !== resolvedPath) {
|
||||
playSound(defaultSound, {
|
||||
volume: volume,
|
||||
fallback: false,
|
||||
repeat: false
|
||||
});
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Store player in activePlayers map
|
||||
activePlayers[resolvedPath] = player;
|
||||
|
||||
Logger.d("SoundService", "Playing sound:", resolvedPath, `(volume: ${Math.round(volume * 100)}%)`, repeat ? "(repeat)" : "");
|
||||
} catch (e) {
|
||||
Logger.w("SoundService", "Failed to create MediaPlayer:", e);
|
||||
// Try fallback if requested
|
||||
if (fallback && !repeat) {
|
||||
const defaultSound = Quickshell.shellDir + "/Assets/Sounds/notification.mp3";
|
||||
if (defaultSound !== resolvedPath) {
|
||||
playSound(defaultSound, {
|
||||
volume: volume,
|
||||
fallback: false,
|
||||
repeat: false
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function stopSound(soundPath) {
|
||||
if (!multimediaAvailable) {
|
||||
// If multimedia isn't available, there are no active players to stop
|
||||
return;
|
||||
}
|
||||
|
||||
if (soundPath) {
|
||||
// Resolve path the same way as playSound
|
||||
const resolvedPath = resolvePath(soundPath);
|
||||
|
||||
// Stop and remove the player for this specific sound
|
||||
if (activePlayers[resolvedPath]) {
|
||||
const player = activePlayers[resolvedPath];
|
||||
player.stop();
|
||||
delete activePlayers[resolvedPath];
|
||||
player.destroy();
|
||||
Logger.d("SoundService", "Stopped sound:", resolvedPath);
|
||||
}
|
||||
} else {
|
||||
// Stop all active players (typically used for repeating sounds)
|
||||
const paths = Object.keys(activePlayers);
|
||||
for (let i = 0; i < paths.length; i++) {
|
||||
const path = paths[i];
|
||||
const player = activePlayers[path];
|
||||
player.stop();
|
||||
player.destroy();
|
||||
}
|
||||
activePlayers = {};
|
||||
Logger.d("SoundService", "Stopped all sounds");
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user