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

dock: add trash bin button (#2277)

* dock: add trash bin button

- icon reflects content- filled/empty
- multiple file manager support with nautilus as default, builtin as
fallback
- settingsspec at dock tab
- context menu

* fix: remove support for builtin filebrowser

needs specific adaptors at FB adhering the trash freedesktop spec

* fix: suppress auto-hide dock with trash context menu open

* feat: allow for custom file manager command

* feat: switch runner to proc.runcommand with toasts on command failures
This commit is contained in:
Kangheng Liu
2026-04-27 09:55:00 -04:00
committed by GitHub
parent e805f6b5ac
commit 536e654b5e
9 changed files with 763 additions and 5 deletions

View File

@@ -545,6 +545,9 @@ Singleton {
property int dockMaxVisibleApps: 0
property int dockMaxVisibleRunningApps: 0
property bool dockShowOverflowBadge: true
property bool dockShowTrash: false
property string dockTrashFileManager: "nautilus"
property string dockTrashCustomCommand: ""
property bool notificationOverlayEnabled: false
property bool notificationPopupShadowEnabled: true

View File

@@ -350,6 +350,9 @@ var SPEC = {
dockMaxVisibleApps: { def: 0 },
dockMaxVisibleRunningApps: { def: 0 },
dockShowOverflowBadge: { def: true },
dockShowTrash: { def: false },
dockTrashFileManager: { def: "nautilus" },
dockTrashCustomCommand: { def: "" },
notificationOverlayEnabled: { def: false },
notificationPopupShadowEnabled: { def: true },

View File

@@ -4,6 +4,7 @@ import qs.Common
import qs.Modals
import qs.Modals.Changelog
import qs.Modals.Clipboard
import qs.Modals.Common
import qs.Modals.Greeter
import qs.Modals.Settings
import qs.Modals.DankLauncherV2
@@ -284,11 +285,15 @@ Item {
sourceComponent: Dock {
contextMenu: dockContextMenuLoader.item ? dockContextMenuLoader.item : null
trashContextMenu: dockTrashContextMenuLoader.item ? dockTrashContextMenuLoader.item : null
}
onLoaded: {
if (item) {
dockContextMenuLoader.active = true;
if (SettingsData.dockShowTrash) {
dockTrashContextMenuLoader.active = true;
}
}
}
@@ -340,6 +345,43 @@ Item {
}
}
LazyLoader {
id: dockTrashContextMenuLoader
active: false
DockTrashContextMenu {
id: dockTrashContextMenu
}
}
Connections {
target: SettingsData
function onDockShowTrashChanged() {
if (SettingsData.dockShowTrash) {
dockTrashContextMenuLoader.active = true;
}
}
}
ConfirmModal {
id: emptyTrashConfirm
}
Connections {
target: TrashService
function onEmptyTrashConfirmRequested(itemCount) {
emptyTrashConfirm.showWithOptions({
title: I18n.tr("Empty Trash?"),
message: I18n.tr("Permanently delete %1 item(s)? This cannot be undone.").arg(itemCount),
confirmText: I18n.tr("Empty"),
cancelText: I18n.tr("Cancel"),
confirmColor: Theme.error,
onConfirm: () => TrashService.emptyTrash()
});
}
}
LazyLoader {
id: notificationCenterLoader

View File

@@ -13,6 +13,7 @@ Variants {
model: SettingsData.getFilteredScreens("dock")
property var contextMenu
property var trashContextMenu
delegate: PanelWindow {
id: dock
@@ -120,7 +121,7 @@ Variants {
return Math.round(v * _dpr) / _dpr;
}
property bool contextMenuOpen: (dockVariants.contextMenu && dockVariants.contextMenu.visible && dockVariants.contextMenu.screen === modelData)
property bool contextMenuOpen: (dockVariants.contextMenu && dockVariants.contextMenu.visible && dockVariants.contextMenu.screen === modelData) || (dockVariants.trashContextMenu && dockVariants.trashContextMenu.visible && dockVariants.trashContextMenu.screen === modelData)
property bool revealSticky: false
readonly property bool shouldHideForWindows: {
@@ -659,6 +660,7 @@ Variants {
anchors.rightMargin: dock.isVertical ? SettingsData.dockSpacing : 0
contextMenu: dockVariants.contextMenu
trashContextMenu: dockVariants.trashContextMenu
groupByApp: dock.groupByApp
isVertical: dock.isVertical
dockScreen: dock.screen

View File

@@ -8,6 +8,7 @@ Item {
id: root
property var contextMenu: null
property var trashContextMenu: null
property bool requestDockShow: false
property int pinnedAppCount: 0
property bool groupByApp: false
@@ -460,19 +461,32 @@ Item {
function updateModel() {
const baseResult = buildBaseItems();
dockItems = applyOverflow(baseResult);
let finalItems = applyOverflow(baseResult);
if (SettingsData.dockShowTrash) {
finalItems.push({
uniqueKey: "trash_button",
type: "trash",
appId: "__TRASH__",
toplevel: null,
isPinned: false,
isRunning: false,
isInOverflow: false
});
}
dockItems = finalItems;
}
delegate: Item {
id: delegateItem
property var dockButton: itemData.type === "launcher" ? launcherButton : button
property var dockButton: itemData.type === "launcher" ? launcherButton : (itemData.type === "trash" ? trashButton : button)
property var itemData: modelData
readonly property bool isOverflowToggle: itemData.type === "overflow-toggle"
readonly property bool isTrash: itemData.type === "trash"
readonly property bool isInOverflow: itemData.isInOverflow === true
clip: false
z: (itemData.type === "launcher" ? launcherButton.dragging : button.dragging) ? 100 : 0
z: (itemData.type === "launcher" ? launcherButton.dragging : (itemData.type === "trash" ? false : button.dragging)) ? 100 : 0
visible: !isInOverflow || root.overflowExpanded
opacity: (isInOverflow && !root.overflowExpanded) ? 0 : 1
scale: (isInOverflow && !root.overflowExpanded) ? 0.8 : 1
@@ -568,9 +582,21 @@ Item {
index: model.index
}
DockTrashButton {
id: trashButton
visible: itemData.type === "trash"
anchors.centerIn: parent
width: delegateItem.width
height: delegateItem.height
actualIconSize: root.iconSize
dockApps: root
contextMenu: root.trashContextMenu
parentDockScreen: root.dockScreen
}
DockAppButton {
id: button
visible: !isOverflowToggle && itemData.type !== "separator" && itemData.type !== "launcher"
visible: !isOverflowToggle && itemData.type !== "separator" && itemData.type !== "launcher" && itemData.type !== "trash"
anchors.centerIn: parent
width: delegateItem.width
height: delegateItem.height
@@ -640,6 +666,9 @@ Item {
function onDockMaxVisibleRunningAppsChanged() {
repeater.updateModel();
}
function onDockShowTrashChanged() {
repeater.updateModel();
}
}
onGroupByAppChanged: repeater.updateModel()

View File

@@ -0,0 +1,137 @@
import QtQuick
import Quickshell
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
clip: false
property var dockApps: null
property var contextMenu: null
property var parentDockScreen: null
property real actualIconSize: 40
property real hoverAnimOffset: 0
property bool isHovered: mouseArea.containsMouse
property bool showTooltip: mouseArea.containsMouse
readonly property string tooltipText: TrashService.isEmpty ? I18n.tr("Trash") : (I18n.tr("Trash") + " (" + TrashService.count + ")")
readonly property bool isVertical: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right
readonly property real animationDistance: actualIconSize
readonly property real animationDirection: {
if (SettingsData.dockPosition === SettingsData.Position.Bottom)
return -1;
if (SettingsData.dockPosition === SettingsData.Position.Top)
return 1;
if (SettingsData.dockPosition === SettingsData.Position.Right)
return -1;
if (SettingsData.dockPosition === SettingsData.Position.Left)
return 1;
return -1;
}
onIsHoveredChanged: {
if (mouseArea.pressed)
return;
if (isHovered) {
exitAnimation.stop();
if (!bounceAnimation.running)
bounceAnimation.restart();
} else {
bounceAnimation.stop();
exitAnimation.restart();
}
}
SequentialAnimation {
id: bounceAnimation
running: false
NumberAnimation {
target: root
property: "hoverAnimOffset"
to: animationDirection * animationDistance * 0.25
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasizedAccel
}
NumberAnimation {
target: root
property: "hoverAnimOffset"
to: animationDirection * animationDistance * 0.2
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasizedDecel
}
}
NumberAnimation {
id: exitAnimation
running: false
target: root
property: "hoverAnimOffset"
to: 0
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasizedDecel
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
TrashService.openTrash();
} else if (mouse.button === Qt.RightButton) {
if (contextMenu) {
contextMenu.showForButton(root, root.height, parentDockScreen, dockApps);
}
}
}
}
Item {
id: visualContent
anchors.fill: parent
transform: Translate {
x: !isVertical ? 0 : hoverAnimOffset
y: !isVertical ? hoverAnimOffset : 0
}
Item {
anchors.centerIn: parent
width: actualIconSize
height: actualIconSize
IconImage {
id: trashIcon
anchors.centerIn: parent
width: actualIconSize - 4
height: actualIconSize - 4
smooth: true
asynchronous: true
source: Quickshell.iconPath(TrashService.isEmpty ? "user-trash" : "user-trash-full", "user-trash")
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Easing.OutCubic
}
}
}
}
}
}

View File

@@ -0,0 +1,377 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
PanelWindow {
id: root
WindowBlur {
targetWindow: root
blurX: menuContainer.x
blurY: menuContainer.y
blurWidth: root.visible ? menuContainer.width : 0
blurHeight: root.visible ? menuContainer.height : 0
blurRadius: Theme.cornerRadius
}
WlrLayershell.namespace: "dms:dock-trash-context-menu"
property var anchorItem: null
property real dockVisibleHeight: 40
property int margin: 10
property var dockApps: null
function showForButton(button, dockHeight, dockScreen, parentDockApps) {
if (dockScreen) {
root.screen = dockScreen;
}
anchorItem = button;
dockVisibleHeight = dockHeight || 40;
dockApps = parentDockApps || null;
visible = true;
}
function close() {
visible = false;
}
screen: null
visible: false
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors {
top: true
left: true
right: true
bottom: true
}
property point anchorPos: Qt.point(screen ? screen.width / 2 : 0, screen ? screen.height - 100 : 0)
onAnchorItemChanged: updatePosition()
onVisibleChanged: {
if (visible) {
updatePosition();
}
}
function updatePosition() {
if (!anchorItem || !screen) {
anchorPos = Qt.point(screen ? screen.width / 2 : 0, screen ? screen.height - 100 : 0);
return;
}
const dockWindow = anchorItem.Window.window;
if (!dockWindow) {
anchorPos = Qt.point(screen.width / 2, screen.height - 100);
return;
}
const buttonPosInDock = anchorItem.mapToItem(dockWindow.contentItem, 0, 0);
let actualDockHeight = root.dockVisibleHeight;
function findDockBackground(item) {
if (item.objectName === "dockBackground") {
return item;
}
for (var i = 0; i < item.children.length; i++) {
const found = findDockBackground(item.children[i]);
if (found) {
return found;
}
}
return null;
}
const dockBackground = findDockBackground(dockWindow.contentItem);
let actualDockWidth = dockWindow.width;
if (dockBackground) {
actualDockHeight = dockBackground.height;
actualDockWidth = dockBackground.width;
}
const isVertical = SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right;
const dockMargin = SettingsData.dockMargin + 16;
let buttonScreenX, buttonScreenY;
if (isVertical) {
const dockContentHeight = dockWindow.height;
const screenHeight = root.screen.height;
const dockTopMargin = Math.round((screenHeight - dockContentHeight) / 2);
buttonScreenY = dockTopMargin + buttonPosInDock.y + anchorItem.height / 2;
if (SettingsData.dockPosition === SettingsData.Position.Right) {
buttonScreenX = root.screen.width - actualDockWidth - dockMargin - 20;
} else {
buttonScreenX = actualDockWidth + dockMargin + 20;
}
} else {
const isDockAtBottom = SettingsData.dockPosition === SettingsData.Position.Bottom;
if (isDockAtBottom) {
buttonScreenY = root.screen.height - actualDockHeight - dockMargin - 20;
} else {
buttonScreenY = actualDockHeight + dockMargin + 20;
}
const dockContentWidth = dockWindow.width;
const screenWidth = root.screen.width;
const dockLeftMargin = Math.round((screenWidth - dockContentWidth) / 2);
buttonScreenX = dockLeftMargin + buttonPosInDock.x + anchorItem.width / 2;
}
anchorPos = Qt.point(buttonScreenX, buttonScreenY);
}
Rectangle {
id: menuContainer
x: {
const isVertical = SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right;
if (isVertical) {
const isDockAtRight = SettingsData.dockPosition === SettingsData.Position.Right;
if (isDockAtRight) {
return Math.max(10, root.anchorPos.x - width + 30);
} else {
return Math.min(root.width - width - 10, root.anchorPos.x - 30);
}
} else {
const left = 10;
const right = root.width - width - 10;
const want = root.anchorPos.x - width / 2;
return Math.max(left, Math.min(right, want));
}
}
y: {
const isVertical = SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right;
if (isVertical) {
const top = 10;
const bottom = root.height - height - 10;
const want = root.anchorPos.y - height / 2;
return Math.max(top, Math.min(bottom, want));
} else {
const isDockAtBottom = SettingsData.dockPosition === SettingsData.Position.Bottom;
if (isDockAtBottom) {
return Math.max(10, root.anchorPos.y - height + 30);
} else {
return Math.min(root.height - height - 10, root.anchorPos.y - 30);
}
}
}
width: Math.min(400, Math.max(180, menuColumn.implicitWidth + Theme.spacingS * 2))
height: menuColumn.implicitHeight + Theme.spacingS * 2
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius
border.color: BlurService.enabled ? BlurService.borderColor : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: BlurService.enabled ? BlurService.borderWidth : 1
opacity: root.visible ? 1 : 0
visible: opacity > 0
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
Rectangle {
anchors.fill: parent
anchors.topMargin: 4
anchors.leftMargin: 2
anchors.rightMargin: -2
anchors.bottomMargin: -4
radius: parent.radius
color: Qt.rgba(0, 0, 0, 0.15)
z: -1
}
Column {
id: menuColumn
width: parent.width - Theme.spacingS * 2
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: Theme.spacingS
spacing: 1
Rectangle {
width: parent.width
height: 28
radius: Theme.cornerRadius
color: openArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
DankIcon {
anchors.verticalCenter: parent.verticalCenter
name: "folder_open"
size: 14
color: Theme.surfaceText
opacity: 0.7
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: I18n.tr("Open Trash")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
}
DankRipple {
id: openRipple
rippleColor: Theme.surfaceText
cornerRadius: Theme.cornerRadius
}
MouseArea {
id: openArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: mouse => openRipple.trigger(mouse.x, mouse.y)
onClicked: {
TrashService.openTrash();
root.close();
}
}
}
Rectangle {
width: parent.width
height: 28
radius: Theme.cornerRadius
enabled: !TrashService.isEmpty
opacity: enabled ? 1 : 0.4
color: emptyArea.containsMouse && enabled ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
DankIcon {
anchors.verticalCenter: parent.verticalCenter
name: "delete_forever"
size: 14
color: emptyArea.containsMouse && parent.parent.enabled ? Theme.error : Theme.surfaceText
opacity: 0.7
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: TrashService.isEmpty ? I18n.tr("Empty Trash") : I18n.tr("Empty Trash (%1)").arg(TrashService.count)
font.pixelSize: Theme.fontSizeSmall
color: emptyArea.containsMouse && parent.parent.enabled ? Theme.error : Theme.surfaceText
font.weight: Font.Normal
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
}
DankRipple {
id: emptyRipple
rippleColor: Theme.error
cornerRadius: Theme.cornerRadius
}
MouseArea {
id: emptyArea
anchors.fill: parent
hoverEnabled: true
enabled: parent.enabled
cursorShape: Qt.PointingHandCursor
onPressed: mouse => emptyRipple.trigger(mouse.x, mouse.y)
onClicked: {
TrashService.requestEmptyTrash();
root.close();
}
}
}
Rectangle {
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
Rectangle {
width: parent.width
height: 28
radius: Theme.cornerRadius
color: settingsArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
DankIcon {
anchors.verticalCenter: parent.verticalCenter
name: "settings"
size: 14
color: Theme.surfaceText
opacity: 0.7
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: I18n.tr("Settings")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
}
DankRipple {
id: settingsRipple
rippleColor: Theme.surfaceText
cornerRadius: Theme.cornerRadius
}
MouseArea {
id: settingsArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: mouse => settingsRipple.trigger(mouse.x, mouse.y)
onClicked: {
PopoutService.focusOrToggleSettingsWithTab("dock");
root.close();
}
}
}
}
}
MouseArea {
anchors.fill: parent
z: -1
onClicked: root.close()
}
}

View File

@@ -506,6 +506,79 @@ Item {
}
}
SettingsCard {
width: parent.width
iconName: "delete"
title: I18n.tr("Trash")
settingKey: "dockTrash"
SettingsToggleRow {
settingKey: "dockShowTrash"
tags: ["dock", "trash", "bin", "recycle"]
text: I18n.tr("Show Trash in Dock")
description: I18n.tr("Place a trash bin at the end of the dock")
checked: SettingsData.dockShowTrash
onToggled: checked => SettingsData.set("dockShowTrash", checked)
}
SettingsDropdownRow {
id: trashFmDropdown
settingKey: "dockTrashFileManager"
tags: ["dock", "trash", "file", "manager", "nautilus", "thunar", "dolphin", "custom"]
text: I18n.tr("Open Trash With")
description: I18n.tr("File manager used to open the trash. Pick \"custom\" to enter your own command.")
visible: SettingsData.dockShowTrash
currentValue: SettingsData.dockTrashFileManager
options: TrashService.availableFileManagers || []
onValueChanged: value => SettingsData.set("dockTrashFileManager", value)
}
FocusScope {
width: parent.width - Theme.spacingM * 2
height: visible ? trashCustomCommandColumn.implicitHeight : 0
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
visible: SettingsData.dockShowTrash && SettingsData.dockTrashFileManager === "custom"
Column {
id: trashCustomCommandColumn
width: parent.width
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("Custom open-trash command")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
DankTextField {
id: trashCustomCommandField
width: parent.width
placeholderText: "pcmanfm trash:///"
backgroundColor: Theme.surfaceContainerHighest
normalBorderColor: Theme.outlineMedium
focusedBorderColor: Theme.primary
Component.onCompleted: {
if (SettingsData.dockTrashCustomCommand) {
text = SettingsData.dockTrashCustomCommand;
}
}
onTextEdited: SettingsData.set("dockTrashCustomCommand", text.trim())
MouseArea {
anchors.fill: parent
onPressed: mouse => {
trashCustomCommandField.forceActiveFocus();
mouse.accepted = false;
}
}
}
}
}
}
SettingsCard {
width: parent.width
iconName: "photo_size_select_large"

View File

@@ -0,0 +1,92 @@
pragma Singleton
import QtQuick
import Qt.labs.folderlistmodel
import Quickshell
import Quickshell.Io
import qs.Common
Singleton {
id: root
readonly property string _homeDir: Quickshell.env("HOME") || ""
readonly property string _xdgDataHome: Quickshell.env("XDG_DATA_HOME") || (_homeDir + "/.local/share")
readonly property string trashFilesDir: _xdgDataHome + "/Trash/files"
readonly property int count: trashModel.count
readonly property bool isEmpty: count === 0
property var availableFileManagers: []
signal emptyTrashConfirmRequested(int itemCount)
FolderListModel {
id: trashModel
folder: "file://" + root.trashFilesDir
showDirs: true
showFiles: true
showHidden: true
showDotAndDotDot: false
sortField: FolderListModel.Name
nameFilters: ["*"]
}
Process {
id: detectProc
running: false
command: ["sh", "-c", "for fm in nautilus thunar dolphin; do command -v $fm >/dev/null 2>&1 && echo $fm; done"]
stdout: StdioCollector {
onStreamFinished: {
const detected = (text || "").split("\n").map(s => s.trim()).filter(s => s.length > 0);
detected.push("custom");
root.availableFileManagers = detected;
}
}
}
Component.onCompleted: {
detectProc.running = true;
}
function openTrash() {
const choice = SettingsData.dockTrashFileManager || "nautilus";
if (choice === "custom") {
const cmd = (SettingsData.dockTrashCustomCommand || "").trim();
if (!cmd) {
ToastService.showInfo(I18n.tr("Cannot open trash: no custom command set"), I18n.tr("Configure one in Settings → Dock → Trash."));
return;
}
Proc.runCommand(null, ["sh", "-c", cmd], (output, exitCode) => {
if (exitCode !== 0) {
ToastService.showError(I18n.tr("Trash command failed (exit %1)").arg(exitCode), I18n.tr("Check your custom command in Settings → Dock → Trash."));
}
}, 0, Proc.noTimeout);
return;
}
if (availableFileManagers.indexOf(choice) < 0) {
ToastService.showInfo(I18n.tr("Cannot open trash: '%1' is not installed").arg(choice), I18n.tr("Pick a different file manager in Settings → Dock → Trash."));
return;
}
switch (choice) {
case "nautilus":
Quickshell.execDetached(["nautilus", "trash:///"]);
break;
case "thunar":
Quickshell.execDetached(["thunar", "trash:///"]);
break;
case "dolphin":
Quickshell.execDetached(["dolphin", "trash:///"]);
break;
}
}
function requestEmptyTrash() {
if (isEmpty)
return;
emptyTrashConfirmRequested(count);
}
function emptyTrash() {
Quickshell.execDetached(["gio", "trash", "--empty"]);
}
}