1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-05-15 00:32:47 -04:00

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
This commit is contained in:
Iris
2026-05-14 17:45:18 +03:00
committed by GitHub
parent 39b90bb140
commit be4ea71756
4 changed files with 630 additions and 8 deletions
@@ -0,0 +1,391 @@
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.Settings.Widgets
Item {
id: root
readonly property var appCategory: ({
WebBrowser: 0,
FileManager: 1,
TextEditor: 2,
ImageViewer: 3,
VideoPlayer: 4,
MusicPlayer: 5,
PDFReader: 6,
Mail: 7,
Terminal: 8,
Calendar: 9
})
property string currentWebBrowserAppId: ""
property string currentFileManagerAppId: ""
property string currentTextEditorAppId: ""
property string currentImageViewerAppId: ""
property string currentVideoPlayerAppId: ""
property string currentMusicPlayerAppId: ""
property string currentPDFReaderAppId: ""
property string currentMailAppId: ""
property string currentTerminalAppId: ""
property string currentCalendarAppId: ""
property var categoryModels: ({})
// A curated list of MIME types for each category.
// The first one is used for fetching the apps list and current default,
// the rest are for setting the default app.
readonly property var mimeMapping: ({
[root.appCategory.WebBrowser]: [
"x-scheme-handler/https",
"x-scheme-handler/http",
"text/html",
"application/xhtml+xml"
],
[root.appCategory.FileManager]: [
"inode/directory",
"x-scheme-handler/file"
],
[root.appCategory.TextEditor]: [
"text/plain",
"application/x-zerosize",
"text/x-c++src",
"text/x-csrc",
"text/x-python",
"text/x-shellscript",
"application/json"
],
[root.appCategory.ImageViewer]: [
"image/png",
"image/jpeg",
"image/gif",
"image/bmp",
"image/webp",
"image/avif",
"image/svg+xml"
],
[root.appCategory.VideoPlayer]: [
"video/mp4",
"video/x-matroska",
"video/webm",
"video/avi",
"video/mpeg",
"video/quicktime",
"video/x-msvideo"
],
[root.appCategory.MusicPlayer]: [
"audio/mpeg",
"audio/x-flac",
"audio/wav",
"audio/ogg",
"audio/aac",
"audio/webm"
],
[root.appCategory.PDFReader]: [
"application/pdf",
"application/x-ext-pdf",
"application/x-bzpdf",
"application/x-gzpdf",
"application/vnd.comicbook-rar",
"application/vnd.comicbook+zip"
],
[root.appCategory.Mail]: ["x-scheme-handler/mailto"],
[root.appCategory.Calendar]: ["x-scheme-handler/calendar"],
[root.appCategory.Terminal]: ["terminal"] // Special
})
function propertyName(type) {
const names = Object.keys(root.appCategory);
return "current" + names[type] + "AppId";
}
function loadAppSearchCategory(categoryName) {
const apps = AppSearchService.getVisibleApplications() || [];
return apps.filter(app => {
const categories = app.categories || [];
return categories.includes(categoryName);
});
}
function getAppDisplayName(appId) {
let entry = DesktopEntries.heuristicLookup(appId);
if (entry && entry.name) {
return entry.name;
}
// If the appname can't be found, show the appID
const withoutSuffix = appId.replace(/\.desktop$/, "");
if (withoutSuffix !== appId) {
entry = DesktopEntries.heuristicLookup(withoutSuffix);
if (entry && entry.name) {
return entry.name;
}
}
return appId;
}
function loadCategoryModel(categoryKey, categorySearchName) {
const apps = loadAppSearchCategory(categorySearchName);
const appIds = apps.map(app => app.id || app.execString || "").filter(id => id);
let models = Object.assign({}, root.categoryModels);
models[categoryKey] = appIds.map(id => ({
text: root.getAppDisplayName(id),
value: id
}));
root.categoryModels = models;
}
Component.onCompleted: {
const categories = Object.values(root.appCategory);
categories.forEach(category => {
switch (category) {
case root.appCategory.Terminal:
// Terminals don't have a MIME type
loadCategoryModel(root.appCategory.Terminal, "TerminalEmulator");
getDefaultTerminal();
break;
case root.appCategory.WebBrowser:
// When using the MIME type, stuff like dms-run shows up.
// It's probably better to use the category.
loadCategoryModel(root.appCategory.WebBrowser, "WebBrowser");
DesktopService.getDefaultApp(mimeMapping[category][0], category.toString());
break;
case root.appCategory.FileManager:
// Use categories for file managers instead,
// you don't want Kate as your file manager just because it can open folders
loadCategoryModel(root.appCategory.FileManager, "FileManager");
DesktopService.getDefaultApp(mimeMapping[category][0], category.toString());
break;
default:
const mimeType = mimeMapping[category][0];
DesktopService.getDefaultApp(mimeType, category.toString());
DesktopService.getAppsForMimeType(mimeType, category.toString());
break;
}
});
}
function getDefaultTerminal() {
// Run xdg-terminal-exec to get the default terminal
const proc = xdgGetDefaultTerminal.createObject(root, {
running: true
});
}
function setDefaultTerminal(terminalId) {
// Write to xdg-terminals.list
const proc = xdgSetDefaultTerminal.createObject(root, {
terminalId: terminalId,
running: true
});
}
Component {
id: xdgSetDefaultTerminal
Process {
property string terminalId: ""
property string configPath: Quickshell.env("XDG_CONFIG_HOME") || (Quickshell.env("HOME") + "/.config")
command: ["sh", "-c", `echo "${terminalId}.desktop" > "${configPath}/xdg-terminals.list"`]
onExited: (exitCode, exitStatus) => {
if (exitCode != 0) {
log.error("Failed to write xdg-terminals.list, exit code:", exitCode);
}
destroy();
}
}
}
Component {
id: xdgGetDefaultTerminal
Process {
property string configPath: Quickshell.env("XDG_CONFIG_HOME") || (Quickshell.env("HOME") + "/.config")
command: ["sh", "-c", `cat '${configPath}/xdg-terminals.list'`]
stdout: StdioCollector {
onStreamFinished: {
const defaultTerminal = text.trim();
if (defaultTerminal) {
root.currentTerminalAppId = defaultTerminal;
} else {
log.warn("No default terminal found");
}
}
}
stderr: StdioCollector {
onStreamFinished: {
if (text.trim().length > 0) {
log.error("Error getting default terminal:", text);
}
}
}
onExited: (exitCode, exitStatus) => {
destroy();
}
}
}
Connections {
target: DesktopService
function onGetAppsForMimeResult(mimeType, appIds, callbackId) {
if (!appIds || appIds.length === 0) {
log.info("No apps found for MIME type:", mimeType);
return;
}
let categoryIndex = parseInt(callbackId);
let models = Object.assign({}, root.categoryModels);
models[categoryIndex] = appIds.map(id => {
return {
text: root.getAppDisplayName(id),
value: id
};
});
root.categoryModels = models;
}
function onGetDefaultAppResult(mimeType, desktopFileId, callbackId) {
if (!desktopFileId) {
log.info("No default app found for MIME type:", mimeType);
return
}
root[propertyName(parseInt(callbackId))] = desktopFileId;
}
}
component AppSelector: SettingsDropdownRow {
property int category: -1
options: (root.categoryModels[category] || []).map(opt => opt.text)
enabled: options.length > 0
emptyText: options.length > 0 ? I18n.tr("Unset", "Unset") : ""
opacity: options.length > 0 ? 1 : 0.5
currentValue: {
let id = root[propertyName(category)];
if (!id || id.length === 0) {
return ""
}
return root.getAppDisplayName(id);
}
onValueChanged: val => {
let model = root.categoryModels[category] || [];
let found = model.find(opt => opt.text === val);
if (found) {
if (category === root.appCategory.Terminal) {
root.setDefaultTerminal(found.value);
} else {
// Set the default app for all MIME types in the category
// If the app doesn't support a MIME type, it will be ignored
root.mimeMapping[category].forEach(mimeType => {
DesktopService.setDefaultApp(mimeType, found.value, category.toString());
});
}
}
}
}
// Dropdowns
DankFlickable {
anchors.fill: parent
clip: true
contentHeight: mainColumn.height + Theme.spacingXL
contentWidth: width
Column {
id: mainColumn
topPadding: 4
width: Math.min(550, parent.width - Theme.spacingL * 2)
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingXL
SettingsCard {
title: I18n.tr("Internet", "Internet")
iconName: "public"
AppSelector {
text: I18n.tr("Web Browser", "Web Browser")
tags: ["web", "browser", "internet"]
category: root.appCategory.WebBrowser
description: I18n.tr("Handles links and opens HTML files", "Handles links and opens HTML files")
}
AppSelector {
text: I18n.tr("Mail", "Mail")
category: root.appCategory.Mail
tags: ["mail", "email"]
description: I18n.tr("Handles mailto links", "Handles mailto links")
}
}
SettingsCard {
title: I18n.tr("Utilities", "Utilities")
iconName: "terminal"
AppSelector {
text: I18n.tr("File Manager", "File Manager")
tags: ["file", "manager"]
category: root.appCategory.FileManager
description: I18n.tr("Manages files and directories", "Manages files and directories")
}
AppSelector {
text: I18n.tr("Terminal", "Terminal")
category: root.appCategory.Terminal
tags: ["terminal", "console"]
description: I18n.tr("Used for xdg-terminal-exec", "Used for xdg-terminal-exec")
}
AppSelector {
text: I18n.tr("Calendar", "Calendar")
category: root.appCategory.Calendar
tags: ["calendar", "events"]
description: I18n.tr("Manages calendar events", "Manages calendar events")
}
}
SettingsCard {
title: I18n.tr("Documents", "Documents")
iconName: "edit_document"
AppSelector {
text: I18n.tr("Text Editor", "Text Editor")
category: root.appCategory.TextEditor
tags: ["text", "editor"]
description: I18n.tr("For editing plain text files", "For editing plain text files")
}
AppSelector {
text: I18n.tr("PDF Reader", "PDF Reader")
category: root.appCategory.PDFReader
tags: ["pdf", "reader"]
description: I18n.tr("For reading PDF files", "For reading PDF files")
}
}
SettingsCard {
title: I18n.tr("Multimedia", "Multimedia")
iconName: "movie"
AppSelector {
text: I18n.tr("Image Viewer", "Image Viewer")
category: root.appCategory.ImageViewer
tags: ["image", "viewer"]
description: I18n.tr("Opens image files", "Opens image files")
}
AppSelector {
text: I18n.tr("Video Player", "Video Player")
category: root.appCategory.VideoPlayer
tags: ["video", "player"]
description: I18n.tr("Plays video files", "Plays video files")
}
AppSelector {
text: I18n.tr("Music Player", "Music Player")
category: root.appCategory.MusicPlayer
tags: ["music", "player"]
description: I18n.tr("Plays audio files", "Plays audio files")
}
}
}
}
}