Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
261 lines
9.0 KiB
QML
261 lines
9.0 KiB
QML
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import Quickshell.Wayland
|
|
import qs.Commons
|
|
import "AppSearch.js" as AppSearch
|
|
|
|
// Shared desktop-application library: the sorted entry list with hidden-entry
|
|
// filtering, the icon fallback index, launch feedback, and entry removal.
|
|
// Injected as shell.appLibrary; the menu's Apps submenu is the consumer.
|
|
Item {
|
|
id: root
|
|
|
|
property string omarchyPath: Quickshell.env("OMARCHY_PATH")
|
|
|
|
property var configuredHiddenEntryIds: ({})
|
|
property var desktopHiddenEntryIds: ({})
|
|
|
|
// Maps an icon name to a file on disk (e.g. "omacut" -> ".../apps/omacut.svg").
|
|
// Used as a fallback for icons that Qt's themed lookup misses because they were
|
|
// installed after this process started (its icon cache never re-scans). Refreshed
|
|
// whenever the app list changes, so newly installed apps get their icon live.
|
|
property var iconIndex: ({})
|
|
property var pendingIconIndex: ({})
|
|
|
|
property int launchSerial: 0
|
|
property int launchToplevelCount: 0
|
|
property var launchActiveToplevel: null
|
|
// True while the launch OSD is on screen. It outlives the launch that opened
|
|
// it: the OSD shows with duration 0, so only closeLaunchFeedback() takes it
|
|
// down.
|
|
property bool launchOsdOpen: false
|
|
property string launchOsdMessage: ""
|
|
|
|
// Emitted whenever the visible application set may have changed: desktop
|
|
// entries appeared or vanished, or the hidden-entry filters reloaded.
|
|
signal appsChanged()
|
|
|
|
function entryName(entry) {
|
|
return AppSearch.entryName(entry)
|
|
}
|
|
|
|
function entrySubtext(entry) {
|
|
return AppSearch.entrySubtext(entry)
|
|
}
|
|
|
|
function isHiddenEntry(entry) {
|
|
var id = String((entry && entry.id) || "")
|
|
return root.configuredHiddenEntryIds[id] === true || root.desktopHiddenEntryIds[id] === true
|
|
}
|
|
|
|
function sortedEntries(query) {
|
|
var values = DesktopEntries.applications.values || []
|
|
return AppSearch.sortedEntries(values, query, function(entry) { return root.isHiddenEntry(entry) })
|
|
}
|
|
|
|
function iconSource(icon) {
|
|
var value = String(icon || "")
|
|
if (value.length === 0) return Quickshell.iconPath("application-x-executable", true)
|
|
if (value.indexOf("file://") === 0 || value.indexOf("image://") === 0) return value
|
|
if (value.charAt(0) === "/") return Util.fileUrl(value)
|
|
// Prefer the context-limited app/device index. An unconstrained themed
|
|
// lookup can resolve an app name such as "zoom" to an action icon instead.
|
|
var found = root.iconIndex[value]
|
|
if (found) return Util.fileUrl(found)
|
|
var themed = Quickshell.iconPath(value, true)
|
|
if (themed.length > 0) return themed
|
|
return Quickshell.iconPath("application-x-executable", true)
|
|
}
|
|
|
|
// The shell may start before first-install packages have finished placing
|
|
// their icons; consumers call this when they open so icons appear live.
|
|
function refreshIcons() {
|
|
if (!iconIndexScan.running) iconIndexScan.running = true
|
|
}
|
|
|
|
function launch(desktopId, name) {
|
|
var id = String(desktopId || "")
|
|
if (!id) return
|
|
root.beginLaunchFeedback(name)
|
|
Util.execDetached("gtk-launch " + Util.shellQuote(id))
|
|
}
|
|
|
|
function remove(desktopId, name) {
|
|
var id = String(desktopId || "")
|
|
if (!id) return
|
|
Util.execDetached(Util.shellQuote(root.omarchyPath + "/bin/omarchy-remove-launcher-entry") + " " + Util.shellQuote(id) + " " + Util.shellQuote(String(name || id)))
|
|
}
|
|
|
|
function normalizeDesktopId(id) {
|
|
var value = String(id || "").trim()
|
|
if (value.slice(-8) === ".desktop") value = value.slice(0, -8)
|
|
return value
|
|
}
|
|
|
|
function loadConfiguredHides(rawText) {
|
|
var next = ({})
|
|
var lines = String(rawText || "").split(/\n/)
|
|
for (var i = 0; i < lines.length; i++) {
|
|
var id = root.normalizeDesktopId(lines[i])
|
|
if (id.length > 0) next[id] = true
|
|
}
|
|
root.configuredHiddenEntryIds = next
|
|
root.appsChanged()
|
|
}
|
|
|
|
function loadDesktopHiddenEntries(rawText) {
|
|
var next = ({})
|
|
var lines = String(rawText || "").split(/\n/)
|
|
for (var i = 0; i < lines.length; i++) {
|
|
var id = root.normalizeDesktopId(lines[i])
|
|
if (id.length > 0) next[id] = true
|
|
}
|
|
root.desktopHiddenEntryIds = next
|
|
root.appsChanged()
|
|
}
|
|
|
|
function iconIndexScanCommand() {
|
|
// List app/device icons across the XDG icon dirs and /usr/share/pixmaps as
|
|
// "<path>" lines. Some desktop entries, such as Print Settings, use device
|
|
// icons like "printer" instead of app icons. SVGs are emitted before PNGs
|
|
// so the parser, which keeps the first hit per name, prefers scalable icons.
|
|
return [
|
|
'dirs="$HOME/.icons $HOME/.local/share/icons";',
|
|
'IFS=":"; for d in ${XDG_DATA_DIRS:-/usr/local/share:/usr/share}; do dirs="$dirs $d/icons"; done; unset IFS;',
|
|
'for ext in svg png; do',
|
|
' for base in $dirs; do',
|
|
' [[ -d $base ]] && find "$base" \\( -path "*/apps/*" -o -path "*/devices/*" \\) -name "*.$ext" 2>/dev/null;',
|
|
' done;',
|
|
' find /usr/share/pixmaps -maxdepth 1 -name "*.$ext" 2>/dev/null;',
|
|
'done'
|
|
].join(' ')
|
|
}
|
|
|
|
function indexIconLine(path) {
|
|
var value = String(path || "").trim()
|
|
if (value.length === 0) return
|
|
var slash = value.lastIndexOf("/")
|
|
var file = slash >= 0 ? value.slice(slash + 1) : value
|
|
var dot = file.lastIndexOf(".")
|
|
var name = dot > 0 ? file.slice(0, dot) : file
|
|
if (name.length > 0 && root.pendingIconIndex[name] === undefined)
|
|
root.pendingIconIndex[name] = value
|
|
}
|
|
|
|
function hiddenEntryScanCommand() {
|
|
var desktop = [Quickshell.env("XDG_CURRENT_DESKTOP"), Quickshell.env("XDG_SESSION_DESKTOP"), Quickshell.env("DESKTOP_SESSION")].filter(function(v) { return String(v || "").length > 0 }).join(":")
|
|
var script = root.omarchyPath + "/shell/services/hidden-entries.sh"
|
|
return Util.shellQuote(script) + " " + Util.shellQuote(desktop)
|
|
}
|
|
|
|
function toplevelCount() {
|
|
try { return ToplevelManager.toplevels.values.length } catch (e) { return 0 }
|
|
}
|
|
|
|
function beginLaunchFeedback(name) {
|
|
root.launchSerial++
|
|
root.launchToplevelCount = root.toplevelCount()
|
|
root.launchActiveToplevel = ToplevelManager.activeToplevel
|
|
root.launchOsdMessage = "Launching " + String(name || "application") + "…"
|
|
launchDelay.restart()
|
|
launchTimeout.restart()
|
|
}
|
|
|
|
function closeLaunchFeedback(serial) {
|
|
if (serial !== root.launchSerial) return
|
|
launchDelay.stop()
|
|
launchTimeout.stop()
|
|
if (root.launchOsdOpen) {
|
|
Quickshell.execDetached(["omarchy-shell", "osd", "close"])
|
|
root.launchOsdOpen = false
|
|
}
|
|
}
|
|
|
|
function maybeFinishLaunchFeedback() {
|
|
if (!launchDelay.running && !launchTimeout.running && !root.launchOsdOpen) return
|
|
if (root.toplevelCount() <= root.launchToplevelCount && ToplevelManager.activeToplevel === root.launchActiveToplevel) return
|
|
root.closeLaunchFeedback(root.launchSerial)
|
|
}
|
|
|
|
QtObject {
|
|
id: hiddenEntryOutput
|
|
property string text: ""
|
|
}
|
|
|
|
Process {
|
|
id: hiddenEntryScan
|
|
command: ["bash", "-lc", root.hiddenEntryScanCommand()]
|
|
stdout: SplitParser { onRead: function(line) { hiddenEntryOutput.text += line + "\n" } }
|
|
onStarted: hiddenEntryOutput.text = ""
|
|
onExited: root.loadDesktopHiddenEntries(hiddenEntryOutput.text)
|
|
}
|
|
|
|
Process {
|
|
id: iconIndexScan
|
|
command: ["bash", "-lc", root.iconIndexScanCommand()]
|
|
stdout: SplitParser { onRead: function(line) { root.indexIconLine(line) } }
|
|
onStarted: root.pendingIconIndex = ({})
|
|
// Swapping the property re-evaluates every iconSource() binding, so
|
|
// newly found icons appear without rebuilding the list.
|
|
onExited: root.iconIndex = root.pendingIconIndex
|
|
}
|
|
|
|
// Coalesces bursts of app-list changes (a package install touches many
|
|
// entries) into a single rescan.
|
|
Timer {
|
|
id: iconIndexDebounce
|
|
interval: 750
|
|
onTriggered: if (!iconIndexScan.running) iconIndexScan.running = true
|
|
}
|
|
|
|
FileView {
|
|
path: root.omarchyPath + "/default/omarchy/launcher.hides"
|
|
watchChanges: true
|
|
printErrors: false
|
|
onLoaded: root.loadConfiguredHides(text())
|
|
onFileChanged: root.loadConfiguredHides(text())
|
|
onLoadFailed: root.loadConfiguredHides("")
|
|
}
|
|
|
|
Connections {
|
|
target: ToplevelManager.toplevels
|
|
function onValuesChanged() { root.maybeFinishLaunchFeedback() }
|
|
}
|
|
|
|
Connections {
|
|
target: ToplevelManager
|
|
function onActiveToplevelChanged() { root.maybeFinishLaunchFeedback() }
|
|
}
|
|
|
|
Timer {
|
|
id: launchDelay
|
|
interval: 2000
|
|
onTriggered: {
|
|
if (root.toplevelCount() > root.launchToplevelCount || ToplevelManager.activeToplevel !== root.launchActiveToplevel) return
|
|
root.launchOsdOpen = true
|
|
Quickshell.execDetached(["omarchy-shell", "osd", "show", JSON.stringify({ icon: "", message: root.launchOsdMessage, duration: 0 })])
|
|
}
|
|
}
|
|
|
|
Timer {
|
|
id: launchTimeout
|
|
interval: 15000
|
|
onTriggered: root.closeLaunchFeedback(root.launchSerial)
|
|
}
|
|
|
|
Connections {
|
|
target: DesktopEntries.applications
|
|
function onValuesChanged() {
|
|
hiddenEntryScan.running = true
|
|
iconIndexDebounce.restart()
|
|
root.appsChanged()
|
|
}
|
|
}
|
|
|
|
Component.onCompleted: {
|
|
hiddenEntryScan.running = true
|
|
iconIndexScan.running = true
|
|
}
|
|
}
|