1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-06-08 04:09:15 -04:00

feat(settings): Added Settings Tab Autostart App (XDG Autostart) (#2535)

* feat(Autostart): add Autostart tab and application selection popup

* fix(AutoStartTab): update systemdUserDir property to use XDG_CONFIG_HOME

* fix(AutoStartTab): update autostartDir and systemdUserDir to use StandardPaths for config home

* refactor(AutoStartTab): use FileView & FolderListModel

* refactor(AutoStartTab): implement systemd override generation for autostart applications using FileView

* feat(AutoStartTab): add systemd check to determine environment and update tray icon visibility

* feat(SettingsSidebar, AutoStartTab, DesktopService): add autostart functionality and systemd checks

* feat(AutoStartTab): add hidden property support for desktop entries and toggle functionality

* feat(AutoStartTab): add initialize autostart directory and add toast if writer failed

* add(AutoStartTab): logging for scoped log tracking

---------
This commit is contained in:
arfan
2026-06-03 09:52:58 +07:00
committed by GitHub
parent 1ee42506b6
commit d2905072c0
5 changed files with 1144 additions and 0 deletions
@@ -570,5 +570,22 @@ FocusScope {
Qt.callLater(() => item.forceActiveFocus());
}
}
Loader {
id: autoStartLoader
anchors.fill: parent
active: root.currentIndex === 36
visible: active
focus: active
sourceComponent: AutoStartTab {
parentModal: root.parentModal
}
onActiveChanged: {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
}
}
@@ -245,6 +245,13 @@ Rectangle {
"icon": "app_registration",
"tabIndex": 19,
"hyprlandNiriOnly": true
},
{
"id": "autostart",
"text": I18n.tr("Autostart Apps"),
"icon": "line_start",
"tabIndex": 36,
"autostartOnly": true
}
]
},
@@ -369,6 +376,8 @@ Rectangle {
return false;
if (item.updaterOnly && !SystemUpdateService.sysupdateAvailable)
return false;
if (item.autostartOnly && !DesktopService.autostartAvailable)
return false;
return true;
}
@@ -0,0 +1,751 @@
import QtCore
import QtQuick
import Qt.labs.folderlistmodel
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 log: Log.scoped("AutoStartTab")
property var parentModal: null
property var entries: []
property var desktopApps: []
property string newEntryType: "desktop"
property string newEntryName: ""
property string newEntryExec: ""
property string newEntryDesktopId: ""
property string newEntryCommandWrapper: "%command%"
readonly property string autostartDir: {
const configHome = Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation));
return configHome + "/autostart";
}
function lookupDesktopIcon(name, exec, fileName) {
const appId = fileName ? fileName.replace(/\.desktop$/, "") : "";
let entry = appId ? DesktopEntries.heuristicLookup(appId) : null;
if (entry && entry.icon) return entry.icon;
if (exec) {
const cmdBase = exec.split(" ")[0].split("/").pop();
for (let i = 0; i < root.desktopApps.length; i++) {
const app = root.desktopApps[i];
if (app.icon) {
const appExec = (app.exec || app.execString || "").split(" ")[0].split("/").pop();
if (appExec === cmdBase) return app.icon;
}
}
}
return "";
}
function parseDesktopFile(content, filePath) {
if (!content || content.length === 0) return null;
const lines = content.split("\n");
let name = "";
let execCmd = "";
let icon = "";
let hidden = false;
let isDesktopEntry = false;
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (line === "[Desktop Entry]") {
isDesktopEntry = true;
} else if (isDesktopEntry) {
if (line.startsWith("[")) break;
const nameMatch = line.match(/^Name=(.+)$/);
if (nameMatch) name = nameMatch[1];
const execMatch = line.match(/^Exec=(.+)$/);
if (execMatch) execCmd = execMatch[1];
const iconMatch = line.match(/^Icon=(.+)$/);
if (iconMatch) icon = iconMatch[1];
const hiddenMatch = line.match(/^Hidden=(true|false)$/);
if (hiddenMatch) hidden = hiddenMatch[1] === "true";
}
}
if (!isDesktopEntry || !name || !execCmd) return null;
const fileName = filePath.split("/").pop();
if (!icon) icon = root.lookupDesktopIcon(name, execCmd, fileName);
return { name: name, exec: execCmd, icon: icon, hidden: hidden, filePath: filePath, fileName: fileName, content: content };
}
function addEntry() {
if (newEntryType === "desktop") {
if (!newEntryDesktopId) return;
const app = desktopApps.find(a => (a.id || a.execString) === newEntryDesktopId);
if (!app) return;
const entryName = app.name || newEntryDesktopId;
const appExec = app.exec || app.execString || "";
const execCmd = root.newEntryCommandWrapper.replace("%command%", appExec);
const appIcon = app.icon || "";
const fileName = entryName.toLowerCase().replace(/[^a-z0-9]/g, "-") + ".desktop";
writeDesktopFile(fileName, entryName, execCmd, appIcon);
} else {
if (!newEntryName || !newEntryExec) return;
const fileName = newEntryName.toLowerCase().replace(/[^a-z0-9]/g, "-") + ".desktop";
writeDesktopFile(fileName, newEntryName, newEntryExec, "");
}
}
function writeDesktopFile(fileName, name, execCmd, icon) {
let content = "[Desktop Entry]\nType=Application\nName=" + name + "\nExec=" + execCmd + "\n";
if (icon) content += "Icon=" + icon + "\n";
writerFileView.path = root.autostartDir + "/" + fileName;
writerFileView.setText(content);
root.resetNewEntry();
}
function setHidden(entry, hidden) {
if (!entry || !entry.content) return;
const lines = entry.content.split("\n");
const hiddenValue = hidden ? "true" : "false";
let found = false;
const merged = lines.map(line => {
const m = line.match(/^Hidden=(true|false)\s*$/);
if (m) {
found = true;
return "Hidden=" + hiddenValue;
}
return line;
});
if (!found) {
const idx = merged.findIndex(l => l.trim() === "[Desktop Entry]");
if (idx >= 0)
merged.splice(idx + 1, 0, "Hidden=" + hiddenValue);
else
merged.unshift("Hidden=" + hiddenValue);
}
writerFileView.path = entry.filePath;
writerFileView.setText(merged.join("\n"));
}
function removeEntry(filePath) {
const proc = removeFileComponent.createObject(root, {
targetPath: filePath,
running: true
});
}
function resetNewEntry() {
newEntryType = "desktop";
newEntryName = "";
newEntryExec = "";
newEntryDesktopId = "";
newEntryCommandWrapper = "%command%";
}
function addOrUpdateEntry(entry) {
var list = root.entries.slice();
for (var i = 0; i < list.length; i++) {
if (list[i].filePath === entry.filePath) {
list[i] = entry;
root.entries = list;
return;
}
}
list.push(entry);
list.sort((a, b) => a.fileName.localeCompare(b.fileName));
root.entries = list;
}
function removeEntryByPath(filePath) {
var list = root.entries.filter(e => e.filePath !== filePath);
root.entries = list;
}
FileView {
id: writerFileView
blockLoading: true
atomicWrites: true
onSaveFailed: error => {
ToastService.showError(I18n.tr("Failed to write autostart entry"))
log.warn("Failed to write autostart entry to " + writerFileView.path + ": " + error);
}
}
FolderListModel {
id: folderModel
nameFilters: ["*.desktop"]
showDirs: false
showDotAndDotDot: false
showHidden: false
sortField: FolderListModel.Name
onStatusChanged: {
if (status !== FolderListModel.Ready) return;
// rebuild entries
const validPaths = new Set();
for (let i = 0; i < folderModel.count; i++) {
const fp = folderModel.get(i, "filePath") || "";
validPaths.add(fp.startsWith("file://") ? fp.substring(7) : fp);
}
const filtered = root.entries.filter(e => validPaths.has(e.filePath));
if (filtered.length !== root.entries.length) {
root.entries = filtered;
}
}
onCountChanged: {
fileReaderRepeater.model = count;
}
}
Repeater {
id: fileReaderRepeater
model: 0
Item {
required property int index
readonly property string filePath: {
const fp = folderModel.get(index, "filePath") || "";
return fp.startsWith("file://") ? fp.substring(7) : fp;
}
FileView {
id: fileView
path: filePath ? "file://" + filePath : ""
watchChanges: true
onLoaded: {
const entry = root.parseDesktopFile(fileView.text(), filePath);
if (entry) {
root.addOrUpdateEntry(entry);
} else {
root.removeEntryByPath(filePath);
}
}
onFileChanged: reload()
onLoadFailed: {
root.removeEntryByPath(filePath);
}
}
}
}
Component {
id: removeFileComponent
Process {
property string targetPath: ""
command: ["rm", "-f", targetPath]
onExited: (exitCode, exitStatus) => {
root.removeEntryByPath(targetPath);
destroy();
}
}
}
function generateTrayIconFixSystemdOverride() {
const configHome = Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation));
const dir = configHome + "/systemd/user/app-@autostart.service.d";
const proc = systemdOverrideMkDirComp.createObject(root, { targetPath: dir, running: true });
}
FileView {
id: systemdOverrideWriter
atomicWrites: true
// make sure we don't overwrite an existing override with a default one, in case the user has already customized it
function buildOverrideContent(existing) {
if (!existing) return "[Unit]\nAfter=dms.service\n";
const lines = existing.split("\n");
const hasAfter = lines.some(l => l.trim() === "After=dms.service");
if (hasAfter) return existing;
const unitIdx = lines.findIndex(l => l.trim() === "[Unit]");
if (unitIdx >= 0) {
lines.splice(unitIdx + 1, 0, "After=dms.service");
} else {
lines.push("[Unit]", "After=dms.service");
}
return lines.join("\n");
}
onLoaded: {
const merged = buildOverrideContent(text());
if (merged !== text()) setText(merged);
ToastService.showInfo(I18n.tr("Systemd Override generated"));
}
onLoadFailed: {
setText("[Unit]\nAfter=dms.service\n");
ToastService.showInfo(I18n.tr("Systemd Override generated"));
}
onSaveFailed: error => {
ToastService.showError(I18n.tr("Failed to generate systemd override"));
log.warn("Failed to write systemd override to " + systemdOverrideWriter.path + ": " + error);
}
}
Component {
id: systemdOverrideMkDirComp
Process {
property string targetPath: ""
command: ["mkdir", "-p", targetPath]
onExited: (exitCode) => {
if (exitCode === 0) {
systemdOverrideWriter.path = targetPath + "/override.conf";
} else {
ToastService.showError(I18n.tr("Failed to generate systemd override"));
}
destroy();
}
}
}
Component {
id: autostartInitMkDirComp
Process {
command: ["mkdir", "-p", root.autostartDir]
onExited: (exitCode) => {
if (exitCode === 0) {
folderModel.folder = "file://" + root.autostartDir;
}
destroy();
}
}
}
Component.onCompleted: {
desktopApps = AppSearchService.getVisibleApplications() || [];
autostartInitMkDirComp.createObject(root, { running: true });
}
Component.onDestruction: {
desktopApps = [];
}
DankFlickable {
anchors.fill: parent
clip: true
contentHeight: mainColumn.height + Theme.spacingXL
contentWidth: width
AppBrowserPopup {
id: appBrowserPopup
appsModel: root.desktopApps
parentModal: root.parentModal
onAppSelected: appId => root.newEntryDesktopId = appId
}
Column {
id: mainColumn
topPadding: 4
width: Math.min(550, parent.width - Theme.spacingL * 2)
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingXL
visible: DesktopService.autostartAvailable
SettingsCard {
width: parent.width
iconName: "add_circle"
title: I18n.tr("Add Entry")
SettingsDropdownRow {
width: parent.width
text: I18n.tr("Entry Type")
description: I18n.tr("Choose whether to launch a desktop app or a command")
currentValue: root.newEntryType === "desktop" ? I18n.tr("Desktop Application") : I18n.tr("Command Line")
options: [I18n.tr("Desktop Application"), I18n.tr("Command Line")]
onValueChanged: val => {
root.newEntryType = val === I18n.tr("Desktop Application") ? "desktop" : "command";
}
}
Column {
width: parent.width
visible: root.newEntryType === "desktop"
spacing: Theme.spacingM
Item {
width: parent.width
height: appLabelColumn.height
Column {
id: appLabelColumn
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Application")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Select a desktop application")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
}
Row {
width: parent.width
spacing: Theme.spacingM
StyledRect {
height: 40
radius: Theme.cornerRadius
color: root.newEntryDesktopId ? Theme.surfaceContainerHigh : Theme.withAlpha(Theme.surfaceContainerHigh, 0.5)
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
readonly property string selectedName: {
if (!root.newEntryDesktopId) return "";
const app = root.desktopApps.find(a => (a.id || a.execString) === root.newEntryDesktopId);
return app ? (app.name || app.id || "") : root.newEntryDesktopId;
}
width: parent.width - browseButton.width - Theme.spacingM
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
visible: root.newEntryDesktopId !== ""
Image {
width: 24
height: 24
source: {
const app = root.desktopApps.find(a => (a.id || a.execString) === root.newEntryDesktopId);
return Paths.resolveIconUrl(app?.icon || "application-x-executable");
}
sourceSize.width: 24
sourceSize.height: 24
fillMode: Image.PreserveAspectFit
anchors.verticalCenter: parent.verticalCenter
onStatusChanged: {
if (status === Image.Error)
source = "image://icon/application-x-executable";
}
}
StyledText {
text: parent.parent.selectedName
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
text: I18n.tr("No application selected")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
visible: root.newEntryDesktopId === ""
}
}
DankButton {
id: browseButton
text: I18n.tr("Browse")
iconName: "search"
onClicked: appBrowserPopup.show()
}
}
Item {
width: parent.width
height: wrapperLabelColumn.height
Column {
id: wrapperLabelColumn
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Command")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Wrap the app command. %command% is replaced with the actual executable")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
}
DankTextField {
width: parent.width
placeholderText: I18n.tr("%command%")
text: root.newEntryCommandWrapper
onTextChanged: root.newEntryCommandWrapper = text
}
}
Column {
width: parent.width
visible: root.newEntryType === "command"
spacing: Theme.spacingM
Item {
width: parent.width
height: labelColumn.height
Column {
id: labelColumn
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Name")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Display name for this entry")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
}
DankTextField {
width: parent.width
placeholderText: I18n.tr("e.g. My Script")
text: root.newEntryName
onTextChanged: root.newEntryName = text
}
Item {
width: parent.width
height: labelColumn2.height
Column {
id: labelColumn2
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Command")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Full command to execute")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
}
DankTextField {
width: parent.width
placeholderText: I18n.tr("e.g. /usr/bin/my-script --flag")
text: root.newEntryExec
onTextChanged: root.newEntryExec = text
}
}
StyledText {
width: parent.width
text: I18n.tr("These add entries to the XDG autostart directory (~/.config/autostart/*.desktop)")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
}
Item {
width: parent.width
height: Theme.spacingM
}
DankButton {
anchors.horizontalCenter: parent.horizontalCenter
text: I18n.tr("Add to Autostart")
iconName: "add"
enabled: {
if (root.newEntryType === "desktop") return root.newEntryDesktopId !== "";
return root.newEntryName !== "" && root.newEntryExec !== "";
}
onClicked: root.addEntry()
}
}
SettingsCard {
id: entriesCard
width: parent.width
iconName: "line_start"
title: I18n.tr("Autostart Entries")
settingKey: "autostartEntries"
collapsible: true
expanded: true
Row {
width: parent.width
spacing: Theme.spacingM
StyledText {
width: parent.width - clearAllButton.width - Theme.spacingM
text: I18n.tr("Applications and commands to start automatically when you log in")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
anchors.verticalCenter: parent.verticalCenter
}
DankActionButton {
id: clearAllButton
iconName: "delete_sweep"
iconSize: Theme.iconSize - 2
iconColor: Theme.error
anchors.verticalCenter: parent.verticalCenter
onClicked: {
for (let i = 0; i < root.entries.length; i++) {
root.removeEntry(root.entries[i].filePath);
}
}
}
}
Column {
id: entriesList
width: parent.width
spacing: Theme.spacingS
Repeater {
model: root.entries
delegate: Rectangle {
width: entriesList.width
height: 48
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.3)
border.width: 0
Row {
width: parent.width
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
StyledText {
text: (index + 1).toString()
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.primary
width: 20
anchors.verticalCenter: parent.verticalCenter
}
Image {
width: 24
height: 24
source: Paths.resolveIconUrl(modelData.icon || "application-x-executable")
sourceSize.width: 24
sourceSize.height: 24
fillMode: Image.PreserveAspectFit
anchors.verticalCenter: parent.verticalCenter
onStatusChanged: {
if (status === Image.Error)
source = "image://icon/application-x-executable";
}
}
Column {
width: parent.width - 20 - Theme.spacingM - 24 - Theme.spacingM - Theme.spacingM - 60 - Theme.spacingM - 32 - Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: 2
StyledText {
width: parent.width
text: modelData.name
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: modelData.hidden ? Theme.surfaceVariantText : Theme.surfaceText
elide: Text.ElideRight
opacity: modelData.hidden ? 0.6 : 1.0
}
StyledText {
width: parent.width
text: modelData.hidden ? I18n.tr("Disabled") : modelData.exec
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
elide: Text.ElideRight
}
}
DankToggle {
anchors.verticalCenter: parent.verticalCenter
checked: !modelData.hidden
onToggled: checked => root.setHidden(modelData, !checked)
}
}
DankActionButton {
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
iconName: "close"
iconSize: 16
buttonSize: 32
circular: true
iconColor: Theme.error
onClicked: root.removeEntry(modelData.filePath)
}
}
}
StyledText {
width: parent.width
text: I18n.tr("No autostart entries")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
horizontalAlignment: Text.AlignHCenter
visible: root.entries.length === 0
}
}
}
SettingsCard {
width: parent.width
iconName: "system_tray"
title: I18n.tr("Tray Icon Fix")
visible: DesktopService.isSystemd
Column {
width: parent.width
spacing: Theme.spacingM
StyledText {
width: parent.width
text: I18n.tr("If autostart app icons don't appear in the system tray, generate a systemd override to ensure DMS starts before autostart apps")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
DankButton {
anchors.horizontalCenter: parent.horizontalCenter
text: I18n.tr("Generate Override")
iconName: "build"
onClicked: root.generateTrayIconFixSystemdOverride()
}
}
}
}
}
}
@@ -0,0 +1,332 @@
import QtQuick
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
FloatingWindow {
id: root
property bool disablePopupTransparency: true
property string searchQuery: ""
property var filteredApps: []
property int selectedIndex: -1
property bool keyboardNavigationActive: false
property var appsModel: []
property var parentModal: null
parentWindow: parentModal
signal appSelected(string appId)
objectName: "appBrowserPopup"
title: I18n.tr("Select Application")
minimumSize: Qt.size(400, 350)
implicitWidth: 500
implicitHeight: 550
color: "transparent"
visible: false
WindowBlur {
targetWindow: root
blurX: 0
blurY: 0
blurWidth: root.visible ? root.width : 0
blurHeight: root.visible ? root.height : 0
blurRadius: Theme.cornerRadius
}
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainer, 0.95)
border.color: Theme.outlineMedium
border.width: Theme.layerOutlineWidth
}
FocusScope {
anchors.fill: parent
focus: true
Keys.onPressed: event => {
switch (event.key) {
case Qt.Key_Escape:
root.hide();
event.accepted = true;
return;
case Qt.Key_Down:
root.selectNext();
event.accepted = true;
return;
case Qt.Key_Up:
root.selectPrevious();
event.accepted = true;
return;
case Qt.Key_Return:
case Qt.Key_Enter:
if (root.keyboardNavigationActive) {
root.selectApp();
} else if (root.filteredApps.length > 0) {
root.selectAppByIndex(0);
}
event.accepted = true;
return;
}
}
Column {
anchors.fill: parent
spacing: 0
Item {
width: parent.width
height: 48
MouseArea {
anchors.fill: parent
onPressed: windowControls.tryStartMove()
}
Rectangle {
anchors.fill: parent
color: Theme.withAlpha(Theme.surfaceContainerHigh, 0.5)
}
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingL
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "add_circle"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Select Application")
font.pixelSize: Theme.fontSizeXLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
DankActionButton {
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: root.hide()
}
}
}
Item {
width: parent.width
height: parent.height - 48
Column {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
DankTextField {
id: searchField
width: parent.width
height: 48
cornerRadius: Theme.cornerRadius
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHigh, 0.8)
normalBorderColor: Theme.outlineMedium
focusedBorderColor: Theme.primary
leftIconName: "search"
leftIconSize: Theme.iconSize
leftIconColor: Theme.surfaceVariantText
leftIconFocusedColor: Theme.primary
showClearButton: true
textColor: Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
placeholderText: I18n.tr("Search applications...")
text: root.searchQuery
onTextEdited: {
root.searchQuery = text;
root.updateFilteredApps();
}
}
DankListView {
id: appList
width: parent.width
height: parent.height - searchField.height - Theme.spacingM
spacing: Theme.spacingS
model: root.filteredApps
clip: true
delegate: Rectangle {
width: appList.width
height: 60
radius: Theme.cornerRadius
required property int index
required property var modelData
readonly property bool isSelected: root.keyboardNavigationActive && index === root.selectedIndex
color: isSelected ? Theme.withAlpha(Theme.primary, 0.16) : appArea.containsMouse ? Theme.withAlpha(Theme.primary, 0.08) : Theme.withAlpha(Theme.surfaceVariant, 0.3)
border.color: isSelected ? Theme.primary : Theme.outlineMedium
border.width: isSelected ? 2 : Theme.layerOutlineWidth
Row {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingM
Image {
width: 28
height: 28
source: Paths.resolveIconUrl(modelData.icon || "application-x-executable")
sourceSize.width: 28
sourceSize.height: 28
fillMode: Image.PreserveAspectFit
anchors.verticalCenter: parent.verticalCenter
onStatusChanged: {
if (status === Image.Error)
source = "image://icon/application-x-executable";
}
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 2
width: parent.width - 28 - Theme.spacingM * 3 - 24
StyledText {
text: modelData.name || modelData.id || ""
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
elide: Text.ElideRight
width: parent.width
}
StyledText {
text: modelData.comment || modelData.genericName || ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.outline
elide: Text.ElideRight
width: parent.width
}
}
DankIcon {
name: "add"
size: Theme.iconSize - 4
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: appArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
const appId = modelData.id || modelData.execString || "";
root.appSelected(appId);
root.hide();
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
}
}
FloatingWindowControls {
id: windowControls
targetWindow: root
}
}
function updateFilteredApps() {
const allApps = root.appsModel || [];
var filtered = [];
if (!searchQuery || searchQuery.length === 0) {
filtered = allApps.slice();
} else {
var query = searchQuery.toLowerCase();
for (var i = 0; i < allApps.length; i++) {
var app = allApps[i];
var name = (app.name || "").toLowerCase();
var id = (app.id || "").toLowerCase();
var comment = (app.comment || app.genericName || "").toLowerCase();
if (name.indexOf(query) !== -1 || id.indexOf(query) !== -1 || comment.indexOf(query) !== -1)
filtered.push(app);
}
}
filteredApps = filtered;
selectedIndex = -1;
keyboardNavigationActive = false;
}
function selectNext() {
if (filteredApps.length === 0) return;
keyboardNavigationActive = true;
selectedIndex = Math.min(selectedIndex + 1, filteredApps.length - 1);
}
function selectPrevious() {
if (filteredApps.length === 0) return;
keyboardNavigationActive = true;
selectedIndex = Math.max(selectedIndex - 1, -1);
if (selectedIndex === -1) keyboardNavigationActive = false;
}
function selectApp() {
if (selectedIndex < 0 || selectedIndex >= filteredApps.length) return;
selectAppByIndex(selectedIndex);
}
function selectAppByIndex(idx) {
const app = filteredApps[idx];
if (!app) return;
root.appSelected(app.id || app.execString || "");
hide();
}
function show() {
updateFilteredApps();
visible = true;
Qt.callLater(() => searchField.forceActiveFocus());
}
function hide() {
visible = false;
searchQuery = "";
filteredApps = [];
selectedIndex = -1;
keyboardNavigationActive = false;
}
onVisibleChanged: {
if (!visible) {
searchQuery = "";
filteredApps = [];
selectedIndex = -1;
keyboardNavigationActive = false;
}
}
}
+35
View File
@@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
import qs.Services
@@ -12,6 +13,40 @@ Singleton {
readonly property var log: Log.scoped("DesktopService")
property var _cache: ({})
property bool isSystemd: false
property bool systemdAutostartTargetActive: false
property bool systemdAutostartTargetChecked: false
readonly property bool autostartAvailable: root.systemdAutostartTargetChecked && (!root.isSystemd || root.systemdAutostartTargetActive)
Component.onCompleted: initSystemCheckProcess.running = true
Process {
id: initSystemCheckProcess
command: ["sh", "-c", "cat /proc/1/comm 2>/dev/null | tr -d '\\n'"]
running: false
stdout: StdioCollector {
onStreamFinished: {
root.isSystemd = (text || "").trim() === "systemd";
if (!root.isSystemd)
root.systemdAutostartTargetChecked = true;
else
systemdAutostartTargetCheck.running = true;
}
}
}
Process {
id: systemdAutostartTargetCheck
command: ["systemctl", "--user", "is-active", "xdg-desktop-autostart.target"]
running: false
stdout: StdioCollector {
onStreamFinished: {
root.systemdAutostartTargetActive = (text || "").trim() === "active";
root.systemdAutostartTargetChecked = true;
}
}
}
function resolveIconPath(moddedAppId) {
if (!moddedAppId)
return "";