1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-05-15 00:32:47 -04:00
Files
DankMaterialShell/quickshell/Services/DesktopService.qml
T
Iris be4ea71756 feat(settings): Added Default Apps page to settings (#2416)
* feat(settings): Added Default Apps page to settings

Added a new page to settings. This page relies on xdg-mime and gio, so their existence is checked to show the page.
This logic and the mime type stuff was added to DesktopService.qml.
Slightly reordered the settings sidebar to include an Applications category

* fix(settings): read xdg-terminals.list directly

read the file directly instead of using xdg-terminal-exec which might not be installed
2026-05-14 10:45:18 -04:00

292 lines
8.8 KiB
QML

pragma Singleton
pragma ComponentBehavior: Bound
import Quickshell.Io
import QtQuick
import Quickshell
Singleton {
id: root
property var _cache: ({})
property bool gioAvailable: false;
// For the queue that setDefaultApp uses
property var _setDefaultAppQueue: []
property bool _isProcessingQueue: false
Component.onCompleted: {
checkGioAndXdgMime.running = true;
}
Process {
id: checkGioAndXdgMime
command: ["sh", "-c", "which gio && which xdg-mime"]
running: false
onExited: (exitCode) => {
if (exitCode === 0) {
root.gioAvailable = true;
} else {
root.gioAvailable = false;
}
}
}
function resolveIconPath(moddedAppId) {
if (!moddedAppId)
return "";
if (_cache[moddedAppId] !== undefined)
return _cache[moddedAppId];
const result = (function () {
// 1. Try heuristic lookup (standard)
const entry = DesktopEntries.heuristicLookup(moddedAppId);
let icon = Quickshell.iconPath(entry?.icon, true);
if (icon && icon !== "")
return icon;
// 2. Try the appId itself as an icon name
icon = Quickshell.iconPath(moddedAppId, true);
if (icon && icon !== "")
return icon;
// 3. Try variations of the appId (lowercase, last part)
const appIds = [moddedAppId.toLowerCase()];
const lastPart = moddedAppId.split('.').pop();
if (lastPart && lastPart !== moddedAppId) {
appIds.push(lastPart);
appIds.push(lastPart.toLowerCase());
}
for (const id of appIds) {
icon = Quickshell.iconPath(id, true);
if (icon && icon !== "")
return icon;
}
// 4. Deep search in all desktop entries (if the above fail)
// This is slow-ish but only happens once for failed icons
const strippedId = moddedAppId.replace(/-bin$/, "").toLowerCase();
const allEntries = DesktopEntries.applications.values;
for (let i = 0; i < allEntries.length; i++) {
const e = allEntries[i];
const eId = (e.id || "").toLowerCase();
const eName = (e.name || "").toLowerCase();
const eExec = (e.execString || "").toLowerCase();
if (eId.includes(strippedId) || eName.includes(strippedId) || eExec.includes(strippedId)) {
icon = Quickshell.iconPath(e.icon, true);
if (icon && icon !== "")
return icon;
}
}
// 5. Nix/Guix specific store check (as a last resort)
for (const appId of appIds) {
let execPath = entry?.execString?.replace(/\/bin.*/, "");
if (!execPath)
continue;
if (execPath.startsWith("/nix/store/") || execPath.startsWith("/gnu/store/")) {
const basePath = execPath;
const sizes = ["256x256", "128x128", "64x64", "48x48", "32x32", "24x24", "16x16"];
let iconPath = `${basePath}/share/icons/hicolor/scalable/apps/${appId}.svg`;
icon = Quickshell.iconPath(iconPath, true);
if (icon && icon !== "")
return icon;
for (const size of sizes) {
iconPath = `${basePath}/share/icons/hicolor/${size}/apps/${appId}.png`;
icon = Quickshell.iconPath(iconPath, true);
if (icon && icon !== "")
return icon;
}
}
}
return "";
})();
_cache[moddedAppId] = result;
return result;
}
// Set default app for a MIME type
Component {
id: gioSetDefaultApp
Process {
property string targetMimeType: ""
property string targetDesktopFileId: ""
property string callbackId: ""
// Check if the app actually supports the MIME type before setting it as default
// This uses a shell script
command: ["sh", "-c", `
apps=$(gio mime "${targetMimeType}" 2>/dev/null | grep -v "^Default" | awk '{print $1}')
if echo "$apps" | grep -Fxq "${targetDesktopFileId}"; then
xdg-mime default "${targetDesktopFileId}" "${targetMimeType}"
gio mime "${targetMimeType}" "${targetDesktopFileId}"
fi
`]
onExited: (exitCode, exitStatus) => {
const success = (exitCode === 0)
if (!success) {
log.error("DesktopService: failed to set default app for", targetMimeType, "to", targetDesktopFileId, "(exit code:", exitCode + ")")
}
root._processDefaultAppQueue()
destroy()
}
}
}
function setDefaultApp(mimeType, desktopFileId, callbackId = "") {
// Add .desktop in case it's missing, xdg-mime needs it
if (!desktopFileId.endsWith(".desktop")) {
desktopFileId += ".desktop";
}
// Queue the request to avoid race conditions
_setDefaultAppQueue.push({
mimeType: mimeType,
desktopFileId: desktopFileId,
callbackId: callbackId
})
// Start processing the queue if not already running
if (!_isProcessingQueue) {
_processDefaultAppQueue()
}
}
function _processDefaultAppQueue() {
if (_setDefaultAppQueue.length === 0) {
_isProcessingQueue = false;
return;
}
_isProcessingQueue = true;
const request = _setDefaultAppQueue.shift();
const proc = gioSetDefaultApp.createObject(root, {
targetMimeType: request.mimeType,
targetDesktopFileId: request.desktopFileId,
callbackId: request.callbackId,
running: true
})
if (!proc) {
log.warn("DesktopService: couldn't create process for", request.mimeType, request.desktopFileId)
_processDefaultAppQueue()
}
}
// Get default app for a MIME type
Component {
id: xdgGetDefaultApp
Process {
property string targetMimeType: ""
property string callbackId: ""
stdout: StdioCollector {
onStreamFinished: {
const desktopFileId = text.trim();
root.getDefaultAppResult(targetMimeType, desktopFileId, callbackId);
}
}
stderr: StdioCollector {
onStreamFinished: {
if (text.trim().length > 0) {
log.error("DesktopService: xdg-mime query error:", text, "mime:", targetMimeType)
}
}
}
onExited: (exitCode, exitStatus) => { destroy() }
}
}
function getDefaultApp(mimeType, callbackId = "") {
const proc = xdgGetDefaultApp.createObject(root, {
targetMimeType: mimeType,
callbackId: callbackId,
command: ["xdg-mime", "query", "default", mimeType],
running: true
})
if (!proc) {
log.warn("DesktopService: couldn't create process for", mimeType)
}
}
signal getDefaultAppResult(string mimeType, string desktopFileId, string callbackId)
// Get apps that support a MIME type
Component {
id: gioGetAppsForMime
Process {
property string targetMimeType: ""
property string callbackId: ""
stdout: StdioCollector {
onStreamFinished: {
const lines = text.split("\n");
let appIds = [];
let seen = {};
for (let line of lines) {
const trimmed = line.trim();
if (
trimmed &&
trimmed.endsWith(".desktop") &&
!trimmed.startsWith("Default") &&
!trimmed.startsWith("default=")
) {
if (!seen[trimmed]) {
seen[trimmed] = true;
appIds.push(trimmed);
}
}
}
root.getAppsForMimeResult(targetMimeType, appIds, callbackId);
}
}
stderr: StdioCollector {
onStreamFinished: {
if (text.trim().length > 0) {
log.error("DesktopService: gio mime query error:", text, "command:", command, "mime:", targetMimeType);
}
}
}
onExited: (exitCode, exitStatus) => { destroy() }
}
}
function getAppsForMimeType(mimeType, callbackId = "") {
const proc = gioGetAppsForMime.createObject(root, {
targetMimeType: mimeType,
callbackId: callbackId,
command: ["gio", "mime", mimeType],
running: true
});
if (!proc) {
log.warn("DesktopService: couldn't create process for", mimeType)
}
}
signal getAppsForMimeResult(string mimeType, var appIds, string callbackId)
}