Delete extra themes from the theme picker (#14713)
* Delete extra themes from the theme picker Delete on an extra (user-installed) theme opens the shared faded confirmation dialog; confirming runs omarchy-theme-remove and selection lands on the theme before the deleted one. Stock themes and user overrides of them are not offered for deletion, apply/re-ask are blocked while a removal runs, and row refreshes queue so a deletion never leaves the list stale. * Fail closed on theme deletion until stock themes are known Keep Delete disabled until the stock listing finishes successfully, so a stock override cannot look deletable in the startup window. Also hold theme apply (and further delete asks) from removal exit until the refreshed rows load, so Enter cannot apply the just-deleted theme in the gap.
This commit is contained in:
1 parent
fcf9eeb5c4
commit
988f44ea1a
3 files changed
+403
-5
No files matched your search
@@ -15,6 +15,7 @@ Item {
|
||||
// Injected by omarchy-shell; defaults to the session OMARCHY_PATH.
|
||||
property string omarchyPath: Quickshell.env("OMARCHY_PATH")
|
||||
property string stateHome: Quickshell.env("HOME") + "/.local/state"
|
||||
property string userThemesPath: Quickshell.env("HOME") + "/.config/omarchy/themes"
|
||||
property string imageDirs: Quickshell.env("OMARCHY_IMAGE_SELECTOR_DIRS") || Quickshell.env("OMARCHY_IMAGE_SELECTOR_DIR") || Quickshell.env("OMARCHY_STOCK_BACKGROUNDS_DIR") || (stateHome + "/omarchy/current/theme/backgrounds")
|
||||
property string imageRows: ""
|
||||
property string loadedImageRows: ""
|
||||
@@ -39,6 +40,13 @@ Item {
|
||||
property string themeRows: ""
|
||||
property bool themeMode: false
|
||||
property bool themeOpenPending: false
|
||||
property var extraThemeNames: []
|
||||
property var stockThemeNames: []
|
||||
property bool stockThemesKnown: false
|
||||
property bool awaitingDeleteRefresh: false
|
||||
property string pendingDeleteTheme: ""
|
||||
property string deleteSelectionPath: ""
|
||||
property bool deleteConfirmOpen: false
|
||||
// Bound to the central [image-picker] section in shell.toml via Commons.Color.qml.
|
||||
// `dimColor` tints unselected slices and text outlines against the desktop
|
||||
// behind the picker; it intentionally tracks the foundational background,
|
||||
@@ -58,7 +66,7 @@ Item {
|
||||
readonly property int previewRadius: Math.max(1, Math.min(16, Math.ceil((panel.width - expandedWidth) / (2 * (sliceWidth + sliceSpacing))) + 1))
|
||||
onPreviewRadiusChanged: updateVisibleItems()
|
||||
|
||||
onOpenedChanged: if (!opened) { layoutSettled = false; renderedFrames = 0 }
|
||||
onOpenedChanged: if (!opened) { layoutSettled = false; renderedFrames = 0; deleteConfirmOpen = false; pendingDeleteTheme = ""; deleteSelectionPath = ""; awaitingDeleteRefresh = false }
|
||||
|
||||
function scriptPath(name) {
|
||||
return omarchyPath + "/shell/plugins/image-picker/" + name
|
||||
@@ -179,6 +187,7 @@ Item {
|
||||
}
|
||||
|
||||
function applySelected() {
|
||||
if (themeMode && (deleteThemeProc.running || awaitingDeleteRefresh)) return
|
||||
var path = currentPath()
|
||||
|
||||
if (themeMode) {
|
||||
@@ -251,6 +260,10 @@ Item {
|
||||
}
|
||||
|
||||
function openSelector(nextImageDirs, nextImageRows, nextSelectedImage, nextSelectionFile, nextDoneFile, nextShowLabels, nextFilterable) {
|
||||
deleteConfirmOpen = false
|
||||
pendingDeleteTheme = ""
|
||||
deleteSelectionPath = ""
|
||||
awaitingDeleteRefresh = false
|
||||
if (requestActive && doneFile && doneFile !== nextDoneFile)
|
||||
finishDoneFile(doneFile)
|
||||
|
||||
@@ -356,6 +369,8 @@ Item {
|
||||
themeOpenPending = true
|
||||
}
|
||||
refreshThemeRows()
|
||||
refreshExtraThemes()
|
||||
refreshStockThemes()
|
||||
}
|
||||
|
||||
function openThemeRows() {
|
||||
@@ -363,8 +378,53 @@ Item {
|
||||
themeMode = true
|
||||
}
|
||||
|
||||
// A refresh asked mid-refresh (a deletion finishing behind the open-time
|
||||
// one) queues a second pass instead of leaving the list stale.
|
||||
function refreshThemeRows() {
|
||||
if (!themeRowsProc.running) themeRowsProc.running = true
|
||||
if (themeRowsProc.running) { themeRowsProc.queued = true; return }
|
||||
themeRowsProc.running = true
|
||||
}
|
||||
|
||||
function refreshExtraThemes() {
|
||||
if (extraThemesProc.running) { extraThemesProc.queued = true; return }
|
||||
extraThemesProc.running = true
|
||||
}
|
||||
|
||||
function refreshStockThemes() {
|
||||
if (!stockThemesProc.running) stockThemesProc.running = true
|
||||
}
|
||||
|
||||
function selectedThemeName() {
|
||||
return nameForPath(currentPath())
|
||||
}
|
||||
|
||||
function canDeleteSelectedTheme() {
|
||||
return themeMode && !deleteThemeProc.running && !awaitingDeleteRefresh && stockThemesKnown && ImagePickerModel.canDeleteTheme(selectedThemeName(), extraThemeNames, stockThemeNames)
|
||||
}
|
||||
|
||||
function requestDeleteSelectedTheme() {
|
||||
if (!canDeleteSelectedTheme()) return
|
||||
pendingDeleteTheme = selectedThemeName()
|
||||
deleteConfirm.selectedIndex = 1
|
||||
deleteConfirmOpen = true
|
||||
}
|
||||
|
||||
function cancelDeleteTheme() {
|
||||
deleteConfirmOpen = false
|
||||
pendingDeleteTheme = ""
|
||||
}
|
||||
|
||||
// Removal itself is omarchy-theme-remove's job (including its guards and
|
||||
// notification); the picker only refreshes its rows once it finishes.
|
||||
function confirmDeleteTheme() {
|
||||
var name = pendingDeleteTheme
|
||||
deleteConfirmOpen = false
|
||||
pendingDeleteTheme = ""
|
||||
if (!name || deleteThemeProc.running) return
|
||||
// Keep the user's place in the list once the deleted theme is gone.
|
||||
if (name === selectedThemeName()) deleteSelectionPath = ImagePickerModel.replacementSelectionPath(imageArray, selectedIndex, filterText)
|
||||
deleteThemeProc.command = ["omarchy-theme-remove", name]
|
||||
deleteThemeProc.running = true
|
||||
}
|
||||
|
||||
function updateThemeRows(rows) {
|
||||
@@ -376,8 +436,11 @@ Item {
|
||||
if (rows) openThemeRows()
|
||||
} else if (changed && rows && themeMode && opened) {
|
||||
// A theme was added or removed since the rows were last read. Keep the
|
||||
// user's place in the carousel rather than jumping back to the current.
|
||||
selectedImage = currentPath() || currentThemePreview()
|
||||
// user's place in the carousel rather than jumping back to the current;
|
||||
// a just-deleted selection lands on the theme before it.
|
||||
selectedImage = deleteSelectionPath || currentPath() || currentThemePreview()
|
||||
deleteSelectionPath = ""
|
||||
awaitingDeleteRefresh = false
|
||||
imageRows = rows
|
||||
loadRows(rows, false)
|
||||
}
|
||||
@@ -392,13 +455,62 @@ Item {
|
||||
|
||||
Process {
|
||||
id: themeRowsProc
|
||||
property bool queued: false
|
||||
command: [root.omarchyPath + "/bin/omarchy-theme-switcher", "--print-rows"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: root.updateThemeRows(String(text || "").trim())
|
||||
}
|
||||
onExited: {
|
||||
if (queued) {
|
||||
queued = false
|
||||
running = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: refreshThemeRows()
|
||||
// The same listing omarchy-theme-remove offers interactively: real
|
||||
// directories under the user themes path, never symlinked working copies.
|
||||
Process {
|
||||
id: extraThemesProc
|
||||
property bool queued: false
|
||||
command: ["find", root.userThemesPath, "-mindepth", "1", "-maxdepth", "1", "-type", "d", "!", "-xtype", "l", "-printf", "%f\n"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: root.extraThemeNames = ImagePickerModel.parseThemeNames(String(text || ""))
|
||||
}
|
||||
onExited: {
|
||||
if (queued) {
|
||||
queued = false
|
||||
running = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: stockThemesProc
|
||||
command: ["find", root.omarchyPath + "/themes", "-mindepth", "1", "-maxdepth", "1", "-type", "d", "-printf", "%f\n"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: root.stockThemeNames = ImagePickerModel.parseThemeNames(String(text || ""))
|
||||
}
|
||||
// Until this lands, stockThemeNames is empty and an override would look
|
||||
// deletable: deletion stays disabled unless the listing succeeded.
|
||||
onExited: function(exitCode) { if (exitCode === 0) root.stockThemesKnown = true }
|
||||
}
|
||||
|
||||
Process {
|
||||
id: deleteThemeProc
|
||||
onExited: function(exitCode) {
|
||||
if (exitCode === 0) root.awaitingDeleteRefresh = true
|
||||
else root.deleteSelectionPath = ""
|
||||
root.refreshExtraThemes()
|
||||
root.refreshThemeRows()
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
refreshThemeRows()
|
||||
refreshExtraThemes()
|
||||
refreshStockThemes()
|
||||
}
|
||||
|
||||
function startImageScan(serial, dirs) {
|
||||
if (loadImagesProc.running) {
|
||||
@@ -564,6 +676,10 @@ Item {
|
||||
|
||||
Keys.priority: Keys.BeforeItem
|
||||
Keys.onPressed: function(event) {
|
||||
if (root.deleteConfirmOpen) {
|
||||
if (deleteConfirm.handleKey(event)) event.accepted = true
|
||||
return
|
||||
}
|
||||
// Nothing is visible before the reveal: only let the user back
|
||||
// out, never filter into a dead end or apply an unseen pick.
|
||||
if (!root.layoutSettled) {
|
||||
@@ -583,6 +699,9 @@ Item {
|
||||
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||
root.applySelected()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Delete && !(event.modifiers & (Qt.ControlModifier | Qt.AltModifier | Qt.MetaModifier))) {
|
||||
root.requestDeleteSelectedTheme()
|
||||
event.accepted = true
|
||||
} else if (root.filterable && Util.editsFilter(event, root.filterText)) {
|
||||
root.updateFilter(Util.editedFilter(event, root.filterText))
|
||||
event.accepted = true
|
||||
@@ -755,5 +874,20 @@ Item {
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
// Unlike the picker itself, the confirmation keeps the faded backdrop:
|
||||
// a destructive choice should take over the screen.
|
||||
ConfirmDialog {
|
||||
id: deleteConfirm
|
||||
anchors.fill: parent
|
||||
opened: root.deleteConfirmOpen
|
||||
z: 10
|
||||
message: "Do you want to delete " + (root.pendingDeleteTheme ? root.labelForPath(root.pendingDeleteTheme) : "") + "?"
|
||||
confirmText: "Delete"
|
||||
background: root.dimColor
|
||||
foreground: root.foreground
|
||||
onCanceled: root.cancelDeleteTheme()
|
||||
onConfirmed: root.confirmDeleteTheme()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,41 @@ function labelForPath(path) {
|
||||
return nameForPath(path).replace(/[-_]+/g, " ").replace(/\b\w/g, function(match) { return match.toUpperCase() })
|
||||
}
|
||||
|
||||
// Extra (user-installed) theme names, one per line, as listed from the user
|
||||
// themes directory: the set omarchy-theme-remove can delete.
|
||||
function parseThemeNames(text) {
|
||||
var names = []
|
||||
var lines = String(text || "").split("\n")
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var name = lines[i].trim()
|
||||
if (name) names.push(name)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// A user directory shadowing a stock theme holds customizations, not an
|
||||
// extra theme: removing it from here would silently revert to stock (and a
|
||||
// shadow can even list twice, once per preview extension), so only themes
|
||||
// with no stock twin can be deleted from the picker.
|
||||
function canDeleteTheme(name, extraNames, stockNames) {
|
||||
if (!name || !Array.isArray(extraNames) || extraNames.indexOf(name) === -1) return false
|
||||
return !Array.isArray(stockNames) || stockNames.indexOf(name) === -1
|
||||
}
|
||||
|
||||
// Where selection lands when the selected image goes away: the previous
|
||||
// match in list order, or the next one when the first match is deleted.
|
||||
function replacementSelectionPath(images, selectedIndex, filterText) {
|
||||
var values = Array.isArray(images) ? images : []
|
||||
if (selectedIndex < 0 || selectedIndex >= values.length) return ""
|
||||
for (var i = selectedIndex - 1; i >= 0; i--) {
|
||||
if (itemMatches(values, i, filterText)) return values[i].filePath
|
||||
}
|
||||
for (var i = selectedIndex + 1; i < values.length; i++) {
|
||||
if (itemMatches(values, i, filterText)) return values[i].filePath
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
function loadRows(rows) {
|
||||
var images = []
|
||||
var seen = {}
|
||||
@@ -125,6 +160,9 @@ if (typeof module !== "undefined") {
|
||||
module.exports = {
|
||||
nameForPath: nameForPath,
|
||||
labelForPath: labelForPath,
|
||||
parseThemeNames: parseThemeNames,
|
||||
canDeleteTheme: canDeleteTheme,
|
||||
replacementSelectionPath: replacementSelectionPath,
|
||||
loadRows: loadRows,
|
||||
itemMatches: itemMatches,
|
||||
firstMatchingIndex: firstMatchingIndex,
|
||||
|
||||
Reference in new issue
Block a user