diff --git a/bin/omarchy-remove-launcher-entry b/bin/omarchy-remove-launcher-entry new file mode 100755 index 00000000..2e18f3a3 --- /dev/null +++ b/bin/omarchy-remove-launcher-entry @@ -0,0 +1,81 @@ +#!/bin/bash + +# omarchy:summary=Remove or uninstall the selected launcher entry +# omarchy:args= +# omarchy:hidden=true +# omarchy:requires-sudo=true + +set -e + +desktop_id="${1-}" +entry_name="${2-}" + +if [[ -z $desktop_id ]]; then + echo "Usage: omarchy-remove-launcher-entry " >&2 + exit 1 +fi + +desktop_file_name="$desktop_id" +if [[ $desktop_file_name != *.desktop ]]; then + desktop_file_name="$desktop_file_name.desktop" +fi + +desktop_dirs=() +if [[ -n ${XDG_DATA_HOME-} ]]; then + desktop_dirs+=("$XDG_DATA_HOME/applications") +else + desktop_dirs+=("$HOME/.local/share/applications") +fi + +if [[ -n ${XDG_DATA_DIRS-} ]]; then + IFS=: read -ra data_dirs <<<"$XDG_DATA_DIRS" + for data_dir in "${data_dirs[@]}"; do + desktop_dirs+=("$data_dir/applications") + done +else + desktop_dirs+=("/usr/local/share/applications" "/usr/share/applications") +fi + +desktop_file="" +for dir in "${desktop_dirs[@]}"; do + candidate="$dir/$desktop_file_name" + if [[ -f $candidate ]]; then + desktop_file="$candidate" + break + fi +done + +if [[ -z $desktop_file ]]; then + echo "Could not find launcher entry: $desktop_file_name" >&2 + exit 1 +fi + +desktop_name="${desktop_file##*/}" +desktop_name="${desktop_name%.desktop}" +exec_line="$(sed -n 's/^Exec=//p' "$desktop_file" | head -1)" + +if [[ $exec_line =~ omarchy-launch-webapp|omarchy-webapp-handler ]]; then + omarchy-webapp-remove "$desktop_name" + exit 0 +fi + +if [[ $exec_line =~ (^|[[:space:]])(\$TERMINAL|xdg-terminal-exec)[[:space:]].*-e([[:space:]]|$) ]]; then + omarchy-tui-remove "$desktop_name" + exit 0 +fi + +if package_name="$(pacman -Qqo "$desktop_file" 2>/dev/null | head -1)" && [[ -n $package_name ]]; then + display_name="${entry_name:-$desktop_name}" + quoted_package="$(printf '%q' "$package_name")" + quoted_display="$(printf '%q' "$display_name")" + exec omarchy-launch-floating-terminal-with-presentation "echo Uninstalling $quoted_display...; sudo pacman -Rns $quoted_package" +fi + +if command -v flatpak >/dev/null 2>&1 && flatpak info "${desktop_file_name%.desktop}" >/dev/null 2>&1; then + flatpak_id="${desktop_file_name%.desktop}" + quoted_flatpak="$(printf '%q' "$flatpak_id")" + exec omarchy-launch-floating-terminal-with-presentation "flatpak uninstall $quoted_flatpak" +fi + +echo "Don't know how to uninstall $desktop_file_name" >&2 +exit 1 diff --git a/shell/plugins/launcher/Launcher.qml b/shell/plugins/launcher/Launcher.qml index e2f107d6..d3e95692 100644 --- a/shell/plugins/launcher/Launcher.qml +++ b/shell/plugins/launcher/Launcher.qml @@ -27,6 +27,9 @@ Item { property string launchOsdMessage: "" property var configuredHiddenEntryIds: ({}) property var desktopHiddenEntryIds: ({}) + property bool deleteConfirmOpen: false + property var deleteEntry: null + property int deleteConfirmIndex: 1 // Bound to the central [launcher] section in shell.toml via Color.qml. // Each color already includes its alpha companion (composed in the @@ -75,6 +78,7 @@ Item { } function dismiss() { + root.deleteConfirmOpen = false root.opened = false if (root.shell && typeof root.shell.hide === "function") root.shell.hide((root.manifest && root.manifest.id) || "omarchy.launcher") @@ -196,6 +200,7 @@ Item { } function activateIndex(index) { + if (root.deleteConfirmOpen) return if (index < 0 || index >= root.filteredEntries.length) return var entry = root.filteredEntries[index] if (!entry) return @@ -204,6 +209,33 @@ Item { entry.execute() } + function requestDeleteIndex(index) { + if (index < 0 || index >= root.filteredEntries.length) return + var entry = root.filteredEntries[index] + if (!entry) return + root.deleteEntry = entry + root.deleteConfirmIndex = 1 + root.deleteConfirmOpen = true + } + + function cancelDelete() { + root.deleteConfirmOpen = false + root.deleteEntry = null + root.deleteConfirmIndex = 1 + Qt.callLater(function() { keyCatcher.forceActiveFocus() }) + } + + function confirmDelete() { + var entry = root.deleteEntry + if (!entry) return + + var desktopId = String(entry.id || "") + var name = root.entryName(entry) + root.cancelDelete() + root.dismiss() + Quickshell.execDetached([root.omarchyPath + "/bin/omarchy-remove-launcher-entry", desktopId, name]) + } + function beginLaunchFeedback(entry) { root.launchSerial++ root.launchToplevelCount = root.toplevelCount() @@ -327,10 +359,26 @@ Item { Item { id: keyCatcher anchors.fill: parent + z: root.deleteConfirmOpen ? 20 : 0 focus: true Keys.priority: Keys.BeforeItem Keys.onPressed: function(event) { + if (root.deleteConfirmOpen) { + if (event.key === Qt.Key_Escape) { + root.cancelDelete() + event.accepted = true + } else if (event.key === Qt.Key_Left || event.key === Qt.Key_Right || event.key === Qt.Key_Tab || event.key === Qt.Key_Backtab) { + root.deleteConfirmIndex = root.deleteConfirmIndex === 0 ? 1 : 0 + event.accepted = true + } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + if (root.deleteConfirmIndex === 0) root.cancelDelete() + else root.confirmDelete() + event.accepted = true + } + return + } + if (event.key === Qt.Key_Escape) { if (root.filterText.length > 0) root.setFilter("") else root.dismiss() @@ -367,11 +415,100 @@ Item { } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { root.activateIndex(root.selectedIndex) event.accepted = true + } else if (event.key === Qt.Key_Delete) { + root.requestDeleteIndex(root.selectedIndex) + event.accepted = true } else if (event.text && event.text.length === 1 && event.text.charCodeAt(0) >= 32 && event.text.charCodeAt(0) !== 127 && (event.modifiers === Qt.NoModifier || event.modifiers === Qt.ShiftModifier)) { root.setFilter(root.filterText + event.text) event.accepted = true } } + + Rectangle { + anchors.fill: parent + visible: root.deleteConfirmOpen + z: 10 + color: root.scrim + + MouseArea { anchors.fill: parent; onClicked: root.cancelDelete() } + + Rectangle { + width: Math.min(parent.width - 96, 370) + height: 132 + anchors.centerIn: parent + color: root.background + border.color: root.selectedText + border.width: 1 + radius: Style.cornerRadius + + MouseArea { anchors.fill: parent; onClicked: {} } + + Item { + anchors.fill: parent + anchors.margins: 18 + + Text { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + text: "Do you want to uninstall " + root.entryName(root.deleteEntry) + "?" + color: root.foreground + font.family: root.fontFamily + font.pixelSize: 16 + wrapMode: Text.WordWrap + } + + Row { + anchors.right: parent.right + anchors.bottom: parent.bottom + spacing: 10 + + Repeater { + model: ["Cancel", "Uninstall"] + + Rectangle { + required property int index + required property string modelData + + readonly property bool selected: root.deleteConfirmIndex === index + readonly property bool destructive: index === 1 + + width: 88 + height: 34 + color: selected + ? (destructive ? Util.alpha(Color.urgent, 0.22) : root.selectedBackground) + : "transparent" + border.color: destructive + ? (selected ? Color.urgent : Util.alpha(Color.urgent, 0.56)) + : (selected ? root.selectedText : Util.alpha(root.foreground, 0.38)) + border.width: 1 + radius: 0 + + Text { + id: buttonText + anchors.centerIn: parent + text: modelData + color: destructive ? (selected ? Color.urgent : root.foreground) : (selected ? root.selectedText : root.foreground) + font.family: root.fontFamily + font.pixelSize: 14 + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onEntered: root.deleteConfirmIndex = index + onClicked: { + if (index === 0) root.cancelDelete() + else root.confirmDelete() + } + } + } + } + } + } + } + } } Column { diff --git a/test/shell.d/launcher-remove-test.sh b/test/shell.d/launcher-remove-test.sh new file mode 100755 index 00000000..7eb93f6a --- /dev/null +++ b/test/shell.d/launcher-remove-test.sh @@ -0,0 +1,71 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +tmp_dir="$(mktemp -d)" +trap 'rm -rf "$tmp_dir"' EXIT + +mkdir -p "$tmp_dir/data/applications" "$tmp_dir/bin" + +write_fake_command() { + local name="$1" + local prefix="$2" + + cat >"$tmp_dir/bin/$name" <