Combine the Omarchy menu and the launcher
Now that we can deep search, they don't need to be different
This commit is contained in:
@@ -7,7 +7,7 @@ hl.layer_rule({ match = { namespace = "omarchy-bar" }, no_anim = true, animation
|
|||||||
-- Launcher, image selector, emojis, clipboard overlays, and keyboard-driven
|
-- Launcher, image selector, emojis, clipboard overlays, and keyboard-driven
|
||||||
-- panels should pop without compositor layer fades. Panels keep their own
|
-- panels should pop without compositor layer fades. Panels keep their own
|
||||||
-- QML opacity transition for normal open/close, and skip it for panel handoff.
|
-- QML opacity transition for normal open/close, and skip it for panel handoff.
|
||||||
hl.layer_rule({ match = { namespace = "^(omarchy-menu|omarchy-launcher|omarchy-image-selector|omarchy-emojis|omarchy-clipboard|omarchy-keyboard-panel)$" }, no_anim = true, animation = "none" })
|
hl.layer_rule({ match = { namespace = "^(omarchy-menu|omarchy-image-selector|omarchy-emojis|omarchy-clipboard|omarchy-keyboard-panel)$" }, no_anim = true, animation = "none" })
|
||||||
|
|
||||||
-- Dev gallery is the main shell workbench; open it maximized like
|
-- Dev gallery is the main shell workbench; open it maximized like
|
||||||
-- SUPER+ALT+F so component previews have the whole workspace.
|
-- SUPER+ALT+F so component previews have the whole workspace.
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
o.bind("SUPER + SPACE", "Launch apps", "omarchy-shell shell toggle omarchy.launcher \"{}\"")
|
o.bind("SUPER + SPACE", "Omarchy menu", "omarchy-menu toggle")
|
||||||
o.bind("SUPER + CTRL + E", "Emojis", "omarchy-shell shell toggle omarchy.emojis")
|
o.bind("SUPER + CTRL + E", "Emojis", "omarchy-shell shell toggle omarchy.emojis")
|
||||||
o.bind("SUPER + CTRL + C", "Capture menu", "omarchy-menu toggle capture")
|
o.bind("SUPER + CTRL + C", "Capture menu", "omarchy-menu toggle capture")
|
||||||
o.bind("SUPER + CTRL + O", "Toggle menu", "omarchy-menu toggle toggle")
|
o.bind("SUPER + CTRL + O", "Toggle menu", "omarchy-menu toggle toggle")
|
||||||
o.bind("SUPER + CTRL + H", "Hardware menu", "omarchy-menu toggle hardware")
|
o.bind("SUPER + CTRL + H", "Hardware menu", "omarchy-menu toggle hardware")
|
||||||
o.bind("SUPER + ALT + SPACE", "Omarchy menu", "omarchy-menu toggle root")
|
|
||||||
o.bind("SUPER + SHIFT + code:201", "Omarchy menu", "omarchy-menu toggle root")
|
o.bind("SUPER + SHIFT + code:201", "Omarchy menu", "omarchy-menu toggle root")
|
||||||
o.bind("SUPER + ESCAPE", "System menu", "omarchy-menu toggle system")
|
o.bind("SUPER + ESCAPE", "System menu", "omarchy-menu toggle system")
|
||||||
o.bind("XF86PowerOff", "Power menu", "omarchy-menu toggle system", { locked = true })
|
o.bind("XF86PowerOff", "Power menu", "omarchy-menu toggle system", { locked = true })
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
// checked shell condition; append ✓ when it succeeds
|
// checked shell condition; append ✓ when it succeeds
|
||||||
|
|
||||||
// Root Menu
|
// Root Menu
|
||||||
"apps": {"icon":"","label":"Apps","aliases":["app","applications"],"action":"omarchy-shell shell summon omarchy.launcher"},
|
"apps": {"icon":"","label":"Apps","aliases":["app","applications"],"provider":"apps"},
|
||||||
"learn": {"icon":"","label":"Learn"},
|
"learn": {"icon":"","label":"Learn"},
|
||||||
"trigger": {"icon":"","label":"Trigger"},
|
"trigger": {"icon":"","label":"Trigger"},
|
||||||
"style": {"icon":"","label":"Style"},
|
"style": {"icon":"","label":"Style"},
|
||||||
|
|||||||
@@ -91,15 +91,6 @@ QtObject {
|
|||||||
property color border: root.composed("notifications.border", "notifications.border-alpha", root.accent, 1.0)
|
property color border: root.composed("notifications.border", "notifications.border-alpha", root.accent, 1.0)
|
||||||
property color countdown: root.pick("notifications.countdown", root.accent)
|
property color countdown: root.pick("notifications.countdown", root.accent)
|
||||||
}
|
}
|
||||||
readonly property QtObject launcher: QtObject {
|
|
||||||
property color background: root.composed("launcher.background", "launcher.background-alpha", root.background, 1.0)
|
|
||||||
property color text: root.pick("launcher.text", root.foreground)
|
|
||||||
property color border: root.composed("launcher.border", "launcher.border-alpha", root.foreground, 1.0)
|
|
||||||
property color scrim: root.composed("launcher.scrim", "launcher.scrim-alpha", root.background, 0.5)
|
|
||||||
property color selectedBackground: root.composed("launcher.selected-background", "launcher.selected-background-alpha", root.foreground, 0.08)
|
|
||||||
property color selectedText: root.pick("launcher.selected-text", root.accent)
|
|
||||||
property color selectedBorder: root.composed("launcher.selected-border", "launcher.selected-border-alpha", root.foreground, 0.0)
|
|
||||||
}
|
|
||||||
readonly property QtObject menu: QtObject {
|
readonly property QtObject menu: QtObject {
|
||||||
property color background: root.composed("menu.background", "menu.background-alpha", root.background, 1.0)
|
property color background: root.composed("menu.background", "menu.background-alpha", root.background, 1.0)
|
||||||
property color text: root.pick("menu.text", root.foreground)
|
property color text: root.pick("menu.text", root.foreground)
|
||||||
|
|||||||
@@ -309,7 +309,7 @@ QtObject {
|
|||||||
return fallback
|
return fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
// The launcher, menu, polkit, emojis, and clipboard surfaces honor an
|
// The menu, polkit, emojis, and clipboard surfaces honor an
|
||||||
// OMARCHY_MENU_FONT override for users who want a different family on the
|
// OMARCHY_MENU_FONT override for users who want a different family on the
|
||||||
// summoned popups than on the bar. Resolved once at startup; an empty env
|
// summoned popups than on the bar. Resolved once at startup; an empty env
|
||||||
// value falls back to the shared fontconfig alias.
|
// value falls back to the shared fontconfig alias.
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ shell/
|
|||||||
BarWidgetRegistry.qml unified registry for bar widgets (1p + 3p)
|
BarWidgetRegistry.qml unified registry for bar widgets (1p + 3p)
|
||||||
plugins/
|
plugins/
|
||||||
bar/ first-party plugins (see plugins/README.md)
|
bar/ first-party plugins (see plugins/README.md)
|
||||||
launcher/
|
|
||||||
image-picker/
|
image-picker/
|
||||||
menu/
|
menu/
|
||||||
notifications/
|
notifications/
|
||||||
|
|||||||
@@ -48,8 +48,10 @@ Item {
|
|||||||
|
|
||||||
BorderSurface {
|
BorderSurface {
|
||||||
id: card
|
id: card
|
||||||
width: Math.min(parent.width - Style.space(96), Style.space(370))
|
width: Math.min(parent.width - Style.space(32), Style.space(370))
|
||||||
height: Style.space(132)
|
// Grows with the wrapped message so narrow hosts (like the menu card)
|
||||||
|
// don't squeeze the text into the buttons.
|
||||||
|
height: card.contentTopInset + card.contentBottomInset + messageText.implicitHeight + Style.space(20) + Style.space(34)
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
color: root.background
|
color: root.background
|
||||||
borderSpec: Border.flat(root.selectedText, Style.normalBorderWidth)
|
borderSpec: Border.flat(root.selectedText, Style.normalBorderWidth)
|
||||||
@@ -66,6 +68,7 @@ Item {
|
|||||||
anchors.leftMargin: card.contentLeftInset
|
anchors.leftMargin: card.contentLeftInset
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
id: messageText
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ User-installed plugins live alongside these conceptually but on disk under
|
|||||||
| Plugin | id | kinds | entry point |
|
| Plugin | id | kinds | entry point |
|
||||||
|---------------|---------------------------|-------------------------|---------------------------------------|
|
|---------------|---------------------------|-------------------------|---------------------------------------|
|
||||||
| Bar | `omarchy.bar` | `bar` | `bar/Bar.qml` |
|
| Bar | `omarchy.bar` | `bar` | `bar/Bar.qml` |
|
||||||
| Launcher | `omarchy.launcher` | `overlay` | `launcher/Launcher.qml` |
|
|
||||||
| Image picker | `omarchy.image-picker` | `overlay` | `image-picker/ImagePicker.qml` |
|
| Image picker | `omarchy.image-picker` | `overlay` | `image-picker/ImagePicker.qml` |
|
||||||
| Emojis | `omarchy.emojis` | `overlay` | `emojis/Emojis.qml` |
|
| Emojis | `omarchy.emojis` | `overlay` | `emojis/Emojis.qml` |
|
||||||
| Clipboard mgr | `omarchy.clipboard` | `overlay` | `clipboard/Clipboard.qml` |
|
| Clipboard mgr | `omarchy.clipboard` | `overlay` | `clipboard/Clipboard.qml` |
|
||||||
@@ -49,14 +48,6 @@ providing [`config/omarchy/shell.json`](../../config/omarchy/shell.json) when
|
|||||||
the user has no file). See [`bar/README.md`](bar/README.md) for the widget catalogue
|
the user has no file). See [`bar/README.md`](bar/README.md) for the widget catalogue
|
||||||
and customization schema.
|
and customization schema.
|
||||||
|
|
||||||
## Launcher
|
|
||||||
|
|
||||||
Quickshell-powered launcher. It uses Quickshell's native
|
|
||||||
`DesktopEntries` model for discovery/activation and renders inside the
|
|
||||||
long-running shell with the legacy launcher card dimensions, colors, row
|
|
||||||
spacing, icon sizing, and keyboard behavior. Summoned directly over shell IPC
|
|
||||||
by the `SUPER + SPACE` binding and the Omarchy menu Apps row.
|
|
||||||
|
|
||||||
## Image picker
|
## Image picker
|
||||||
|
|
||||||
Fullscreen image-grid selector overlay. Used by `omarchy-menu-images`
|
Fullscreen image-grid selector overlay. Used by `omarchy-menu-images`
|
||||||
|
|||||||
@@ -1,665 +0,0 @@
|
|||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import QtQuick
|
|
||||||
import qs.Commons
|
|
||||||
import qs.Ui
|
|
||||||
import "LauncherSearch.js" as LauncherSearch
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property string omarchyPath: Quickshell.env("OMARCHY_PATH")
|
|
||||||
property var shell: null
|
|
||||||
property var manifest: null
|
|
||||||
|
|
||||||
property bool opened: false
|
|
||||||
property string placeholder: "\uf002 Search..."
|
|
||||||
property string filterText: ""
|
|
||||||
property int selectedIndex: 0
|
|
||||||
property bool cursorActive: true
|
|
||||||
property var filteredEntries: []
|
|
||||||
property int launchSerial: 0
|
|
||||||
property int launchToplevelCount: 0
|
|
||||||
property var launchActiveToplevel: null
|
|
||||||
property bool launchOsdOpen: false
|
|
||||||
property string launchOsdMessage: ""
|
|
||||||
property var configuredHiddenEntryIds: ({})
|
|
||||||
property var desktopHiddenEntryIds: ({})
|
|
||||||
property bool deleteConfirmOpen: false
|
|
||||||
property var deleteEntry: null
|
|
||||||
|
|
||||||
// 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: ({})
|
|
||||||
|
|
||||||
// Bound to the central [launcher] section in shell.toml via Color.qml.
|
|
||||||
// Each color already includes its alpha companion (composed in the
|
|
||||||
// singleton), so consumers can drop them straight into a Rectangle.
|
|
||||||
property color background: Color.launcher.background
|
|
||||||
property color foreground: Color.launcher.text
|
|
||||||
property color border: Color.launcher.border
|
|
||||||
property var borderSpec: Border.surfaceSpec("launcher", "border", border, 2)
|
|
||||||
property color scrim: Color.launcher.scrim
|
|
||||||
property color selectedBackground: Color.launcher.selectedBackground
|
|
||||||
property color selectedText: Color.launcher.selectedText
|
|
||||||
property color selectedBorder: Color.launcher.selectedBorder
|
|
||||||
property var selectedBorderSpec: Border.surfaceSpec("launcher", "selected-border", selectedBorder, 0)
|
|
||||||
readonly property real rowReservedBorderLeft: Border.left(selectedBorderSpec)
|
|
||||||
readonly property real rowReservedBorderRight: Border.right(selectedBorderSpec)
|
|
||||||
property string fontFamily: Style.font.menuFamily
|
|
||||||
|
|
||||||
property int cardWidth: 644
|
|
||||||
property int cardHeight: 400
|
|
||||||
property int contentMargin: 20
|
|
||||||
property int contentSpacing: 10
|
|
||||||
property int searchHeight: 44
|
|
||||||
property int rowHeight: 50
|
|
||||||
property int iconSlotWidth: 44
|
|
||||||
property int iconSize: 24
|
|
||||||
readonly property int listHeight: cardHeight - contentMargin * 2 - searchHeight - contentSpacing
|
|
||||||
|
|
||||||
function open(payloadJson) {
|
|
||||||
var payload = ({})
|
|
||||||
try { payload = JSON.parse(payloadJson || "{}") } catch (e) { payload = ({}) }
|
|
||||||
|
|
||||||
root.placeholder = payload.placeholder || "\uf002 Search..."
|
|
||||||
root.cardWidth = Math.max(300, Number(payload.width || 644))
|
|
||||||
var requestedListHeight = Number(payload.listHeight || payload.maxHeight || 0)
|
|
||||||
root.cardHeight = requestedListHeight > 0
|
|
||||||
? root.contentMargin * 2 + root.searchHeight + root.contentSpacing + requestedListHeight
|
|
||||||
: 400
|
|
||||||
|
|
||||||
root.filterText = payload.query || ""
|
|
||||||
root.selectedIndex = 0
|
|
||||||
root.cursorActive = true
|
|
||||||
root.disarmHover()
|
|
||||||
root.opened = true
|
|
||||||
root.rebuildDisplay()
|
|
||||||
// The shell may start before first-install packages have finished placing
|
|
||||||
// their icons. Refresh here even when the desktop entry list did not change.
|
|
||||||
if (!iconIndexScan.running) iconIndexScan.running = true
|
|
||||||
Qt.callLater(function() { keyCatcher.forceActiveFocus() })
|
|
||||||
}
|
|
||||||
|
|
||||||
function close() {
|
|
||||||
root.opened = false
|
|
||||||
}
|
|
||||||
|
|
||||||
function dismiss() {
|
|
||||||
root.deleteConfirmOpen = false
|
|
||||||
root.deleteEntry = null
|
|
||||||
root.opened = false
|
|
||||||
if (root.shell && typeof root.shell.hide === "function")
|
|
||||||
root.shell.hide((root.manifest && root.manifest.id) || "omarchy.launcher")
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
function entryName(entry) {
|
|
||||||
return LauncherSearch.entryName(entry)
|
|
||||||
}
|
|
||||||
|
|
||||||
function entrySubtext(entry) {
|
|
||||||
return LauncherSearch.entrySubtext(entry)
|
|
||||||
}
|
|
||||||
|
|
||||||
function entrySortKey(entry) {
|
|
||||||
return LauncherSearch.entrySortKey(entry)
|
|
||||||
}
|
|
||||||
|
|
||||||
function toplevelCount() {
|
|
||||||
try { return ToplevelManager.toplevels.values.length } catch (e) { return 0 }
|
|
||||||
}
|
|
||||||
|
|
||||||
function entrySearchText(entry) {
|
|
||||||
return LauncherSearch.entrySearchText(entry)
|
|
||||||
}
|
|
||||||
|
|
||||||
function disarmHover() {
|
|
||||||
pointerGate.reset()
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectFromPointer(index, item, mouse) {
|
|
||||||
if (!pointerGate.moved(item, mouse)) return
|
|
||||||
root.cursorActive = true
|
|
||||||
root.selectedIndex = index
|
|
||||||
}
|
|
||||||
|
|
||||||
function isHiddenEntry(entry) {
|
|
||||||
var id = String((entry && entry.id) || "")
|
|
||||||
return root.configuredHiddenEntryIds[id] === true || root.desktopHiddenEntryIds[id] === true
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
if (root.opened) root.rebuildDisplay()
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
if (root.opened) root.rebuildDisplay()
|
|
||||||
}
|
|
||||||
|
|
||||||
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/plugins/launcher/hidden-entries.sh"
|
|
||||||
return Util.shellQuote(script) + " " + Util.shellQuote(desktop)
|
|
||||||
}
|
|
||||||
|
|
||||||
function fuzzyScore(entry, query) {
|
|
||||||
return LauncherSearch.fuzzyScore(entry, query)
|
|
||||||
}
|
|
||||||
|
|
||||||
function sortedEntries(query) {
|
|
||||||
var values = DesktopEntries.applications.values || []
|
|
||||||
return LauncherSearch.sortedEntries(values, query, function(entry) { return root.isHiddenEntry(entry) })
|
|
||||||
}
|
|
||||||
|
|
||||||
function rebuildDisplay() {
|
|
||||||
displayModel.clear()
|
|
||||||
var rows = root.sortedEntries(root.filterText)
|
|
||||||
var entries = []
|
|
||||||
var count = Math.min(rows.length, 256)
|
|
||||||
for (var i = 0; i < count; i++) {
|
|
||||||
var entry = rows[i].entry
|
|
||||||
entries.push(entry)
|
|
||||||
displayModel.append({
|
|
||||||
name: root.entryName(entry),
|
|
||||||
subtext: root.entrySubtext(entry),
|
|
||||||
icon: String(entry.icon || "")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
root.filteredEntries = entries
|
|
||||||
|
|
||||||
if (displayModel.count === 0) root.selectedIndex = 0
|
|
||||||
else if (root.selectedIndex >= displayModel.count) root.selectedIndex = displayModel.count - 1
|
|
||||||
else if (root.selectedIndex < 0) root.selectedIndex = 0
|
|
||||||
|
|
||||||
Qt.callLater(function() {
|
|
||||||
if (displayModel.count > 0) resultList.positionViewAtIndex(root.selectedIndex, ListView.Contain)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function setFilter(nextFilter) {
|
|
||||||
root.filterText = nextFilter
|
|
||||||
root.selectedIndex = 0
|
|
||||||
root.cursorActive = true
|
|
||||||
root.disarmHover()
|
|
||||||
root.rebuildDisplay()
|
|
||||||
}
|
|
||||||
|
|
||||||
function select(delta) {
|
|
||||||
if (displayModel.count === 0) return
|
|
||||||
root.cursorActive = true
|
|
||||||
root.disarmHover()
|
|
||||||
root.selectedIndex = (root.selectedIndex + delta + displayModel.count) % displayModel.count
|
|
||||||
resultList.positionViewAtIndex(root.selectedIndex, ListView.Contain)
|
|
||||||
}
|
|
||||||
|
|
||||||
function activateIndex(index) {
|
|
||||||
if (root.deleteConfirmOpen) return
|
|
||||||
if (index < 0 || index >= root.filteredEntries.length) return
|
|
||||||
var entry = root.filteredEntries[index]
|
|
||||||
if (!entry) return
|
|
||||||
var desktopId = String(entry.id || "")
|
|
||||||
if (!desktopId) return
|
|
||||||
|
|
||||||
root.beginLaunchFeedback(entry)
|
|
||||||
root.dismiss()
|
|
||||||
Util.execDetached("gtk-launch " + Util.shellQuote(desktopId))
|
|
||||||
}
|
|
||||||
|
|
||||||
function requestDeleteIndex(index) {
|
|
||||||
if (index < 0 || index >= root.filteredEntries.length) return
|
|
||||||
var entry = root.filteredEntries[index]
|
|
||||||
if (!entry) return
|
|
||||||
root.deleteEntry = entry
|
|
||||||
deleteConfirm.selectedIndex = 1
|
|
||||||
root.deleteConfirmOpen = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function cancelDelete() {
|
|
||||||
root.deleteConfirmOpen = false
|
|
||||||
root.deleteEntry = null
|
|
||||||
deleteConfirm.selectedIndex = 1
|
|
||||||
root.disarmHover()
|
|
||||||
Qt.callLater(function() { keyCatcher.forceActiveFocus() })
|
|
||||||
}
|
|
||||||
|
|
||||||
function confirmDelete() {
|
|
||||||
var entry = root.deleteEntry
|
|
||||||
if (!entry) return
|
|
||||||
|
|
||||||
var desktopId = String(entry.id || "")
|
|
||||||
var name = root.entryName(entry)
|
|
||||||
var command = Util.shellQuote(root.omarchyPath + "/bin/omarchy-remove-launcher-entry") + " " + Util.shellQuote(desktopId) + " " + Util.shellQuote(name)
|
|
||||||
root.dismiss()
|
|
||||||
Util.execDetached(command)
|
|
||||||
}
|
|
||||||
|
|
||||||
function beginLaunchFeedback(entry) {
|
|
||||||
root.launchSerial++
|
|
||||||
root.launchToplevelCount = root.toplevelCount()
|
|
||||||
root.launchActiveToplevel = ToplevelManager.activeToplevel
|
|
||||||
root.launchOsdOpen = false
|
|
||||||
root.launchOsdMessage = "Launching " + root.entryName(entry) + "…"
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
ListModel { id: displayModel }
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
QtObject {
|
|
||||||
id: hiddenEntryOutput
|
|
||||||
property string text: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
FileView {
|
|
||||||
id: launcherHidesFile
|
|
||||||
path: root.omarchyPath + "/default/omarchy/launcher.hides"
|
|
||||||
watchChanges: true
|
|
||||||
printErrors: false
|
|
||||||
onLoaded: root.loadConfiguredHides(text())
|
|
||||||
onFileChanged: root.loadConfiguredHides(text())
|
|
||||||
onLoadFailed: root.loadConfiguredHides("")
|
|
||||||
}
|
|
||||||
|
|
||||||
PointerMoveGate {
|
|
||||||
id: pointerGate
|
|
||||||
referenceItem: card
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
if (root.opened) root.rebuildDisplay()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
hiddenEntryScan.running = true
|
|
||||||
iconIndexScan.running = true
|
|
||||||
}
|
|
||||||
|
|
||||||
PanelWindow {
|
|
||||||
id: panel
|
|
||||||
visible: root.opened
|
|
||||||
anchors { top: true; bottom: true; left: true; right: true }
|
|
||||||
color: "transparent"
|
|
||||||
WlrLayershell.namespace: "omarchy-launcher"
|
|
||||||
WlrLayershell.layer: WlrLayer.Overlay
|
|
||||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
|
||||||
exclusionMode: ExclusionMode.Ignore
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
color: root.scrim
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
onClicked: root.dismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
BorderSurface {
|
|
||||||
id: card
|
|
||||||
width: Math.min(root.cardWidth, panel.width - Style.gapsOut * 2)
|
|
||||||
height: Math.min(root.cardHeight, panel.height - Style.gapsOut * 2)
|
|
||||||
radius: Style.cornerRadius
|
|
||||||
anchors.centerIn: parent
|
|
||||||
color: root.background
|
|
||||||
borderSpec: root.borderSpec
|
|
||||||
padding: root.contentMargin
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
MouseArea { anchors.fill: parent; onClicked: {} }
|
|
||||||
|
|
||||||
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 (deleteConfirm.handleKey(event)) event.accepted = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === Qt.Key_Escape) {
|
|
||||||
if (root.filterText.length > 0) root.setFilter("")
|
|
||||||
else root.dismiss()
|
|
||||||
event.accepted = true
|
|
||||||
} else if (Util.editsFilter(event, root.filterText)) {
|
|
||||||
root.setFilter(Util.editedFilter(event, root.filterText))
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_Up) {
|
|
||||||
root.select(-1)
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_Down) {
|
|
||||||
root.select(1)
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_PageUp) {
|
|
||||||
root.select(-6)
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_PageDown) {
|
|
||||||
root.select(6)
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_Home) {
|
|
||||||
if (displayModel.count > 0) {
|
|
||||||
root.cursorActive = true
|
|
||||||
root.disarmHover()
|
|
||||||
root.selectedIndex = 0
|
|
||||||
resultList.positionViewAtIndex(root.selectedIndex, ListView.Contain)
|
|
||||||
}
|
|
||||||
event.accepted = true
|
|
||||||
} else if (event.key === Qt.Key_End) {
|
|
||||||
if (displayModel.count > 0) {
|
|
||||||
root.cursorActive = true
|
|
||||||
root.disarmHover()
|
|
||||||
root.selectedIndex = displayModel.count - 1
|
|
||||||
resultList.positionViewAtIndex(root.selectedIndex, ListView.Contain)
|
|
||||||
}
|
|
||||||
event.accepted = true
|
|
||||||
} 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ConfirmDialog {
|
|
||||||
id: deleteConfirm
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
opened: root.deleteConfirmOpen
|
|
||||||
z: 10
|
|
||||||
message: "Do you want to uninstall " + root.entryName(root.deleteEntry) + "?"
|
|
||||||
confirmText: "Uninstall"
|
|
||||||
background: root.background
|
|
||||||
foreground: root.foreground
|
|
||||||
scrim: root.scrim
|
|
||||||
selectedBackground: root.selectedBackground
|
|
||||||
selectedText: root.selectedText
|
|
||||||
fontFamily: root.fontFamily
|
|
||||||
cornerRadius: Style.cornerRadius
|
|
||||||
onCanceled: root.cancelDelete()
|
|
||||||
onConfirmed: root.confirmDelete()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.topMargin: card.contentTopInset
|
|
||||||
anchors.rightMargin: card.contentRightInset
|
|
||||||
anchors.bottomMargin: card.contentBottomInset
|
|
||||||
anchors.leftMargin: card.contentLeftInset
|
|
||||||
spacing: root.contentSpacing
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: root.searchHeight
|
|
||||||
radius: 0
|
|
||||||
color: root.background
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: 10
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.rightMargin: 10
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
text: root.filterText || root.placeholder
|
|
||||||
color: root.foreground
|
|
||||||
opacity: root.filterText ? 1 : 0.5
|
|
||||||
font.family: root.fontFamily
|
|
||||||
font.pixelSize: 18
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: parent.height - root.searchHeight - root.contentSpacing
|
|
||||||
|
|
||||||
ListView {
|
|
||||||
id: resultList
|
|
||||||
anchors.fill: parent
|
|
||||||
model: displayModel
|
|
||||||
clip: true
|
|
||||||
spacing: 0
|
|
||||||
boundsBehavior: Flickable.StopAtBounds
|
|
||||||
|
|
||||||
delegate: BorderSurface {
|
|
||||||
id: row
|
|
||||||
required property int index
|
|
||||||
required property string name
|
|
||||||
required property string subtext
|
|
||||||
required property string icon
|
|
||||||
|
|
||||||
readonly property bool hasCursor: root.cursorActive && row.index === root.selectedIndex
|
|
||||||
|
|
||||||
width: ListView.view.width
|
|
||||||
height: root.rowHeight
|
|
||||||
radius: 0
|
|
||||||
color: row.hasCursor ? root.selectedBackground : "transparent"
|
|
||||||
borderSpec: row.hasCursor ? root.selectedBorderSpec : Border.none()
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: iconSlot
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: root.rowReservedBorderLeft + 14
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: root.iconSlotWidth
|
|
||||||
height: parent.height
|
|
||||||
|
|
||||||
Image {
|
|
||||||
id: appIcon
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: root.iconSize
|
|
||||||
height: root.iconSize
|
|
||||||
fillMode: Image.PreserveAspectFit
|
|
||||||
// Decode at physical pixels: IconImage uses the logical size,
|
|
||||||
// which leaves PNG icons upscaled and blurry on HiDPI displays.
|
|
||||||
sourceSize.width: root.iconSize * Screen.devicePixelRatio
|
|
||||||
sourceSize.height: root.iconSize * Screen.devicePixelRatio
|
|
||||||
source: root.iconSource(row.icon)
|
|
||||||
asynchronous: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
visible: appIcon.status === Image.Error
|
|
||||||
text: "?"
|
|
||||||
color: row.hasCursor ? root.selectedText : root.foreground
|
|
||||||
font.family: root.fontFamily
|
|
||||||
font.pixelSize: 18
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.left: iconSlot.right
|
|
||||||
anchors.leftMargin: 14
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.rightMargin: root.rowReservedBorderRight + 14
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
text: row.name
|
|
||||||
color: row.hasCursor ? root.selectedText : root.foreground
|
|
||||||
font.family: root.fontFamily
|
|
||||||
font.pixelSize: 18
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: mouseArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onEntered: root.selectFromPointer(row.index, row, {
|
|
||||||
x: mouseArea.mouseX,
|
|
||||||
y: mouseArea.mouseY
|
|
||||||
})
|
|
||||||
onPositionChanged: function(mouse) {
|
|
||||||
root.selectFromPointer(row.index, row, mouse)
|
|
||||||
}
|
|
||||||
onClicked: {
|
|
||||||
root.cursorActive = true
|
|
||||||
root.selectedIndex = row.index
|
|
||||||
root.activateIndex(row.index)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: 14
|
|
||||||
visible: displayModel.count === 0
|
|
||||||
text: "No Results"
|
|
||||||
color: root.foreground
|
|
||||||
font.family: root.fontFamily
|
|
||||||
font.pixelSize: 18
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"schemaVersion": 1,
|
|
||||||
"id": "omarchy.launcher",
|
|
||||||
"name": "Launcher",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"author": "Omarchy",
|
|
||||||
"description": "A Quickshell-powered launcher",
|
|
||||||
"kinds": [
|
|
||||||
"overlay"
|
|
||||||
],
|
|
||||||
"keepLoaded": true,
|
|
||||||
"entryPoints": {
|
|
||||||
"overlay": "Launcher.qml"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+167
-3
@@ -11,6 +11,8 @@ Item {
|
|||||||
|
|
||||||
// Injected by omarchy-shell when this plugin is summoned.
|
// Injected by omarchy-shell when this plugin is summoned.
|
||||||
property string omarchyPath: Quickshell.env("OMARCHY_PATH")
|
property string omarchyPath: Quickshell.env("OMARCHY_PATH")
|
||||||
|
property var shell: null
|
||||||
|
property var manifest: null
|
||||||
|
|
||||||
// Plugin lifecycle hooks. The host calls open(payloadJson) after
|
// Plugin lifecycle hooks. The host calls open(payloadJson) after
|
||||||
// `omarchy-shell shell summon omarchy.menu ...` and close() when hidden.
|
// `omarchy-shell shell summon omarchy.menu ...` and close() when hidden.
|
||||||
@@ -72,6 +74,13 @@ Item {
|
|||||||
property var providersLoaded: ({})
|
property var providersLoaded: ({})
|
||||||
property var providerQueue: []
|
property var providerQueue: []
|
||||||
property int providerRevision: 0
|
property int providerRevision: 0
|
||||||
|
|
||||||
|
// Shared application engine (entries, hidden filters, icons, launch,
|
||||||
|
// removal), owned by the shell and also used by the standalone launcher.
|
||||||
|
readonly property var appLibrary: root.shell ? root.shell.appLibrary : null
|
||||||
|
property bool deleteConfirmOpen: false
|
||||||
|
property var deleteTarget: null
|
||||||
|
onOpenedChanged: if (!opened) { deleteConfirmOpen = false; deleteTarget = null }
|
||||||
// Bound to the central [menu] section in shell.toml via Color.qml.
|
// Bound to the central [menu] section in shell.toml via Color.qml.
|
||||||
// Each color already includes its alpha companion (composed in the
|
// Each color already includes its alpha companion (composed in the
|
||||||
// singleton), so consumers can drop them straight into a Rectangle.
|
// singleton), so consumers can drop them straight into a Rectangle.
|
||||||
@@ -231,9 +240,64 @@ Item {
|
|||||||
return MenuModel.slugify(value)
|
return MenuModel.slugify(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The apps provider is QML-native: rows come from the shared AppLibrary
|
||||||
|
// (DesktopEntries) instead of a bash enumeration, so they carry image
|
||||||
|
// icons, launch feedback, and uninstall support like the launcher.
|
||||||
|
function mergeAppRows() {
|
||||||
|
if (!root.appLibrary) return
|
||||||
|
|
||||||
|
var nextOrder = []
|
||||||
|
for (var i = 0; i < root.itemOrder.length; i++) {
|
||||||
|
var id = root.itemOrder[i]
|
||||||
|
var existing = root.items[id]
|
||||||
|
if (existing && existing.kind === "app") delete root.items[id]
|
||||||
|
else nextOrder.push(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows = root.appLibrary.sortedEntries("")
|
||||||
|
for (var j = 0; j < rows.length; j++) {
|
||||||
|
var entry = rows[j].entry
|
||||||
|
var appId = String(entry.id || "")
|
||||||
|
if (!appId) continue
|
||||||
|
var itemId = "apps." + appId
|
||||||
|
var subtext = root.appLibrary.entrySubtext(entry)
|
||||||
|
var aliases = subtext ? [subtext] : []
|
||||||
|
try {
|
||||||
|
if (entry.keywords && typeof entry.keywords.join === "function") aliases = aliases.concat(entry.keywords)
|
||||||
|
} catch (e) { }
|
||||||
|
root.items[itemId] = {
|
||||||
|
id: itemId,
|
||||||
|
parent: "apps",
|
||||||
|
kind: "app",
|
||||||
|
icon: "",
|
||||||
|
appIcon: String(entry.icon || ""),
|
||||||
|
appId: appId,
|
||||||
|
label: root.appLibrary.entryName(entry),
|
||||||
|
title: "",
|
||||||
|
target: "",
|
||||||
|
description: subtext,
|
||||||
|
action: "",
|
||||||
|
provider: "",
|
||||||
|
aliases: aliases,
|
||||||
|
when: "",
|
||||||
|
checked: "",
|
||||||
|
order: nextOrder.length
|
||||||
|
}
|
||||||
|
nextOrder.push(itemId)
|
||||||
|
}
|
||||||
|
|
||||||
|
root.itemOrder = nextOrder
|
||||||
|
if (root.opened) root.rebuildDisplay()
|
||||||
|
}
|
||||||
|
|
||||||
function startProviderForMenu(id) {
|
function startProviderForMenu(id) {
|
||||||
var entry = root.item(id)
|
var entry = root.item(id)
|
||||||
if (!entry || !entry.provider || root.providersLoaded[id]) return
|
if (!entry || !entry.provider || root.providersLoaded[id]) return
|
||||||
|
if (entry.provider === "apps") {
|
||||||
|
root.providersLoaded[id] = true
|
||||||
|
root.mergeAppRows()
|
||||||
|
return
|
||||||
|
}
|
||||||
var spec = root.providers[entry.provider]
|
var spec = root.providers[entry.provider]
|
||||||
if (!spec) return
|
if (!spec) return
|
||||||
|
|
||||||
@@ -301,6 +365,12 @@ Item {
|
|||||||
var entry = root.item(id)
|
var entry = root.item(id)
|
||||||
if (!entry || !entry.provider || root.providersLoaded[id]) return
|
if (!entry || !entry.provider || root.providersLoaded[id]) return
|
||||||
|
|
||||||
|
// Native providers don't touch providerProc, so they never need to queue.
|
||||||
|
if (entry.provider === "apps") {
|
||||||
|
root.startProviderForMenu(id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (providerProc.running) {
|
if (providerProc.running) {
|
||||||
if (root.providerQueue.indexOf(id) < 0) root.providerQueue = root.providerQueue.concat([id])
|
if (root.providerQueue.indexOf(id) < 0) root.providerQueue = root.providerQueue.concat([id])
|
||||||
return
|
return
|
||||||
@@ -403,6 +473,8 @@ Item {
|
|||||||
kind: "dmenu",
|
kind: "dmenu",
|
||||||
icon: "",
|
icon: "",
|
||||||
iconFont: "",
|
iconFont: "",
|
||||||
|
appIcon: "",
|
||||||
|
appId: "",
|
||||||
label: label,
|
label: label,
|
||||||
target: "",
|
target: "",
|
||||||
detail: "",
|
detail: "",
|
||||||
@@ -544,6 +616,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function activateIndex(index, fromPointer) {
|
function activateIndex(index, fromPointer) {
|
||||||
|
if (root.deleteConfirmOpen) return
|
||||||
if (root.dmenuActive) {
|
if (root.dmenuActive) {
|
||||||
if (root.mode === "input") {
|
if (root.mode === "input") {
|
||||||
root.applyDmenuSelection(root.filterText)
|
root.applyDmenuSelection(root.filterText)
|
||||||
@@ -559,11 +632,44 @@ Item {
|
|||||||
var row = displayModel.get(index)
|
var row = displayModel.get(index)
|
||||||
if (row.kind === "menu" || row.kind === "link") {
|
if (row.kind === "menu" || row.kind === "link") {
|
||||||
root.setActiveMenu(row.target || row.itemId, true, fromPointer)
|
root.setActiveMenu(row.target || row.itemId, true, fromPointer)
|
||||||
|
} else if (row.kind === "app") {
|
||||||
|
var appId = row.appId
|
||||||
|
var label = row.label
|
||||||
|
applySerial = requestSerial
|
||||||
|
opened = false
|
||||||
|
filterText = ""
|
||||||
|
if (root.appLibrary) root.appLibrary.launch(appId, label)
|
||||||
} else {
|
} else {
|
||||||
root.applySelected(row.itemId, row.action)
|
root.applySelected(row.itemId, row.action)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function requestDeleteSelected() {
|
||||||
|
if (!root.cursorActive || root.selectedIndex < 0 || root.selectedIndex >= displayModel.count) return
|
||||||
|
var row = displayModel.get(root.selectedIndex)
|
||||||
|
if (!row || row.kind !== "app") return
|
||||||
|
root.deleteTarget = { appId: row.appId, label: row.label }
|
||||||
|
deleteConfirm.selectedIndex = 1
|
||||||
|
root.deleteConfirmOpen = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelDelete() {
|
||||||
|
root.deleteConfirmOpen = false
|
||||||
|
root.deleteTarget = null
|
||||||
|
deleteConfirm.selectedIndex = 1
|
||||||
|
root.disarmPointer()
|
||||||
|
Qt.callLater(function() { keyCatcher.forceActiveFocus() })
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmDelete() {
|
||||||
|
var target = root.deleteTarget
|
||||||
|
root.deleteConfirmOpen = false
|
||||||
|
root.deleteTarget = null
|
||||||
|
if (!target) return
|
||||||
|
root.cancel()
|
||||||
|
if (root.appLibrary) root.appLibrary.remove(target.appId, target.label)
|
||||||
|
}
|
||||||
|
|
||||||
function applyDmenuSelection(value) {
|
function applyDmenuSelection(value) {
|
||||||
applySerial = requestSerial
|
applySerial = requestSerial
|
||||||
opened = false
|
opened = false
|
||||||
@@ -602,6 +708,9 @@ Item {
|
|||||||
opened = true
|
opened = true
|
||||||
rebuildDisplay()
|
rebuildDisplay()
|
||||||
loadProviderForMenu(activeMenu)
|
loadProviderForMenu(activeMenu)
|
||||||
|
// The shell may start before first-install packages have finished placing
|
||||||
|
// their icons. Refresh here even when the desktop entry list did not change.
|
||||||
|
if (root.appLibrary) root.appLibrary.refreshIcons()
|
||||||
|
|
||||||
Qt.callLater(function() { keyCatcher.forceActiveFocus() })
|
Qt.callLater(function() { keyCatcher.forceActiveFocus() })
|
||||||
}
|
}
|
||||||
@@ -709,6 +818,13 @@ Item {
|
|||||||
referenceItem: card
|
referenceItem: card
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: root.appLibrary
|
||||||
|
function onAppsChanged() {
|
||||||
|
if (root.providersLoaded["apps"]) root.mergeAppRows()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// The JSONC sources are watched so live edits to the default file (or the
|
// The JSONC sources are watched so live edits to the default file (or the
|
||||||
// user extension at ~/.config/omarchy/extensions/omarchy-menu.jsonc) take
|
// user extension at ~/.config/omarchy/extensions/omarchy-menu.jsonc) take
|
||||||
// effect without restarting the shell.
|
// effect without restarting the shell.
|
||||||
@@ -837,11 +953,20 @@ Item {
|
|||||||
Item {
|
Item {
|
||||||
id: keyCatcher
|
id: keyCatcher
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
z: root.deleteConfirmOpen ? 20 : 0
|
||||||
focus: true
|
focus: true
|
||||||
|
|
||||||
Keys.priority: Keys.BeforeItem
|
Keys.priority: Keys.BeforeItem
|
||||||
Keys.onPressed: function(event) {
|
Keys.onPressed: function(event) {
|
||||||
if (event.key === Qt.Key_Escape) {
|
if (root.deleteConfirmOpen) {
|
||||||
|
if (deleteConfirm.handleKey(event)) event.accepted = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === Qt.Key_Delete) {
|
||||||
|
root.requestDeleteSelected()
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_Escape) {
|
||||||
if (root.filterText) root.setFilter("")
|
if (root.filterText) root.setFilter("")
|
||||||
else root.cancel()
|
else root.cancel()
|
||||||
event.accepted = true
|
event.accepted = true
|
||||||
@@ -875,6 +1000,25 @@ Item {
|
|||||||
event.accepted = true
|
event.accepted = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ConfirmDialog {
|
||||||
|
id: deleteConfirm
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
opened: root.deleteConfirmOpen
|
||||||
|
z: 10
|
||||||
|
message: "Do you want to uninstall " + ((root.deleteTarget && root.deleteTarget.label) || "") + "?"
|
||||||
|
confirmText: "Uninstall"
|
||||||
|
background: root.background
|
||||||
|
foreground: root.foreground
|
||||||
|
scrim: root.scrim
|
||||||
|
selectedBackground: root.selectedBackground
|
||||||
|
selectedText: root.selectedText
|
||||||
|
fontFamily: root.fontFamily
|
||||||
|
cornerRadius: root.cornerRadius
|
||||||
|
onCanceled: root.cancelDelete()
|
||||||
|
onConfirmed: root.confirmDelete()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
@@ -944,6 +1088,8 @@ Item {
|
|||||||
required property string kind
|
required property string kind
|
||||||
required property string icon
|
required property string icon
|
||||||
required property string iconFont
|
required property string iconFont
|
||||||
|
required property string appIcon
|
||||||
|
required property string appId
|
||||||
required property string label
|
required property string label
|
||||||
required property string target
|
required property string target
|
||||||
required property string detail
|
required property string detail
|
||||||
@@ -952,7 +1098,8 @@ Item {
|
|||||||
required property int childCount
|
required property int childCount
|
||||||
|
|
||||||
readonly property bool hasCursor: root.cursorActive && row.index === root.selectedIndex
|
readonly property bool hasCursor: root.cursorActive && row.index === root.selectedIndex
|
||||||
readonly property bool hasIcon: row.icon.length > 0
|
readonly property bool isApp: row.kind === "app"
|
||||||
|
readonly property bool hasIcon: row.icon.length > 0 || row.isApp
|
||||||
|
|
||||||
width: ListView.view.width
|
width: ListView.view.width
|
||||||
height: root.rowHeightForDetail(row.detail)
|
height: root.rowHeightForDetail(row.detail)
|
||||||
@@ -973,7 +1120,7 @@ Item {
|
|||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: iconText
|
id: iconText
|
||||||
visible: row.hasIcon
|
visible: row.hasIcon && !row.isApp
|
||||||
text: row.icon
|
text: row.icon
|
||||||
color: row.hasCursor ? root.selectedText : root.foreground
|
color: row.hasCursor ? root.selectedText : root.foreground
|
||||||
font.family: row.iconFont.length > 0 ? row.iconFont : root.fontFamily
|
font.family: row.iconFont.length > 0 ? row.iconFont : root.fontFamily
|
||||||
@@ -986,6 +1133,23 @@ Item {
|
|||||||
y: contentColumn.y + labelText.y + (labelText.height - height) / 2
|
y: contentColumn.y + labelText.y + (labelText.height - height) / 2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Image {
|
||||||
|
id: appIconImage
|
||||||
|
visible: row.isApp
|
||||||
|
width: Style.font.iconLarge
|
||||||
|
height: Style.font.iconLarge
|
||||||
|
fillMode: Image.PreserveAspectFit
|
||||||
|
// Decode at physical pixels — a logical-size decode leaves
|
||||||
|
// PNG icons upscaled and blurry on HiDPI displays.
|
||||||
|
sourceSize.width: width * Screen.devicePixelRatio
|
||||||
|
sourceSize.height: height * Screen.devicePixelRatio
|
||||||
|
source: row.isApp && root.appLibrary ? root.appLibrary.iconSource(row.appIcon) : ""
|
||||||
|
asynchronous: true
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: root.rowReservedBorderLeft + Style.space(8) + (Style.space(36) - width) / 2
|
||||||
|
y: contentColumn.y + labelText.y + (labelText.height - height) / 2
|
||||||
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
id: contentColumn
|
id: contentColumn
|
||||||
anchors.left: row.hasIcon ? iconText.right : parent.left
|
anchors.left: row.hasIcon ? iconText.right : parent.left
|
||||||
|
|||||||
@@ -261,6 +261,8 @@ function displayRow(items, itemOrder, checkedResults, entry, detail, score, sect
|
|||||||
kind: entry.kind,
|
kind: entry.kind,
|
||||||
icon: entry.icon,
|
icon: entry.icon,
|
||||||
iconFont: entry.iconFont || "",
|
iconFont: entry.iconFont || "",
|
||||||
|
appIcon: entry.appIcon || "",
|
||||||
|
appId: entry.appId || "",
|
||||||
label: labelFor(entry, checkedResults),
|
label: labelFor(entry, checkedResults),
|
||||||
target: target,
|
target: target,
|
||||||
detail: detail || "",
|
detail: detail || "",
|
||||||
|
|||||||
@@ -0,0 +1,258 @@
|
|||||||
|
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
|
||||||
|
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.launchOsdOpen = false
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ ShellRoot {
|
|||||||
// own empty copies.
|
// own empty copies.
|
||||||
property PluginRegistry pluginRegistry: PluginRegistry { }
|
property PluginRegistry pluginRegistry: PluginRegistry { }
|
||||||
property BarWidgetRegistry barWidgetRegistry: BarWidgetRegistry { }
|
property BarWidgetRegistry barWidgetRegistry: BarWidgetRegistry { }
|
||||||
|
property AppLibrary appLibrary: AppLibrary { }
|
||||||
|
|
||||||
property string home: Quickshell.env("HOME")
|
property string home: Quickshell.env("HOME")
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ wait_until "omarchy-shell responds to ping" 60 omarchy-shell shell ping
|
|||||||
plugins=$(omarchy-shell shell listPlugins)
|
plugins=$(omarchy-shell shell listPlugins)
|
||||||
for plugin in \
|
for plugin in \
|
||||||
omarchy.audio omarchy.background omarchy.bar omarchy.bluetooth \
|
omarchy.audio omarchy.background omarchy.bar omarchy.bluetooth \
|
||||||
omarchy.clipboard omarchy.emojis omarchy.launcher omarchy.menu \
|
omarchy.clipboard omarchy.emojis omarchy.menu \
|
||||||
omarchy.monitor omarchy.network omarchy.notifications omarchy.power \
|
omarchy.monitor omarchy.network omarchy.notifications omarchy.power \
|
||||||
omarchy.reminders omarchy.weather; do
|
omarchy.reminders omarchy.weather; do
|
||||||
[[ $plugins == *"$plugin"* ]] || fail "shell plugin is loaded: $plugin" "loaded plugins: $plugins"
|
[[ $plugins == *"$plugin"* ]] || fail "shell plugin is loaded: $plugin" "loaded plugins: $plugins"
|
||||||
|
|||||||
@@ -94,23 +94,24 @@ screenshot "success-notification-popup"
|
|||||||
omarchy-shell notifications dismissAll >/dev/null
|
omarchy-shell notifications dismissAll >/dev/null
|
||||||
wait_until "notification popup closes" 15 layer_absent "omarchy-notifications"
|
wait_until "notification popup closes" 15 layer_absent "omarchy-notifications"
|
||||||
|
|
||||||
# The launcher does the full loop: open, search by typing, launch the top hit.
|
# The menu's Apps submenu does the full launcher loop: open, search by
|
||||||
|
# typing, launch the top hit.
|
||||||
if window_present "(?i)omawrite" >/dev/null 2>&1; then
|
if window_present "(?i)omawrite" >/dev/null 2>&1; then
|
||||||
fail "launcher test starts with no Omawrite window" "an Omawrite window is already open"
|
fail "app launch test starts with no Omawrite window" "an Omawrite window is already open"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
omarchy-shell shell summon omarchy.launcher >/dev/null
|
omarchy-menu summon apps >/dev/null
|
||||||
wait_until "launcher opens" 15 layer_present "omarchy-launcher"
|
wait_until "apps menu opens" 15 layer_present "omarchy-menu"
|
||||||
sleep 1
|
sleep 1
|
||||||
screenshot "success-launcher-open"
|
screenshot "success-apps-menu-open"
|
||||||
|
|
||||||
wtype "omawrite"
|
wtype "omawrite"
|
||||||
sleep 1
|
sleep 1
|
||||||
screenshot "success-launcher-search"
|
screenshot "success-apps-menu-search"
|
||||||
wtype -k Return
|
wtype -k Return
|
||||||
|
|
||||||
wait_until "launcher launches the top search hit" 60 window_present "(?i)omawrite"
|
wait_until "apps menu launches the top search hit" 60 window_present "(?i)omawrite"
|
||||||
wait_until "launcher closes after launching" 15 layer_absent "omarchy-launcher"
|
wait_until "apps menu closes after launching" 15 layer_absent "omarchy-menu"
|
||||||
|
|
||||||
close_windows "(?i)omawrite"
|
close_windows "(?i)omawrite"
|
||||||
wait_until "Omawrite window closes" 30 window_absent "(?i)omawrite"
|
wait_until "Omawrite window closes" 30 window_absent "(?i)omawrite"
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||||
|
|
||||||
|
run_node_test <<'JS'
|
||||||
|
const fs = require('fs')
|
||||||
|
const search = requireFromRoot('shell/services/AppSearch.js')
|
||||||
|
const menuQml = fs.readFileSync(path.join(root, 'shell/plugins/menu/Menu.qml'), 'utf8')
|
||||||
|
const appLibraryQml = fs.readFileSync(path.join(root, 'shell/services/AppLibrary.qml'), 'utf8')
|
||||||
|
|
||||||
|
const entries = [
|
||||||
|
{
|
||||||
|
name: 'Google Contacts',
|
||||||
|
genericName: 'Address Book',
|
||||||
|
comment: 'Manage contacts',
|
||||||
|
keywords: ['contacts', 'address book', 'people'],
|
||||||
|
id: 'google-contacts.desktop'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Calculator',
|
||||||
|
genericName: 'Calculator',
|
||||||
|
comment: 'Perform arithmetic, scientific or financial calculations',
|
||||||
|
keywords: ['calculation', 'arithmetic', 'scientific', 'financial'],
|
||||||
|
id: 'org.gnome.Calculator.desktop'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'OBS Studio',
|
||||||
|
genericName: 'Streaming/Recording Software',
|
||||||
|
comment: 'Free and Open Source Streaming/Recording Software',
|
||||||
|
keywords: ['streaming', 'recording', 'capture'],
|
||||||
|
id: 'com.obsproject.Studio.desktop'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Aether',
|
||||||
|
genericName: '',
|
||||||
|
comment: 'Minimal internet radio player',
|
||||||
|
keywords: ['audio', 'music', 'radio'],
|
||||||
|
id: 'io.github.taqi.aether.desktop'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Xournal++',
|
||||||
|
genericName: 'Notetaking',
|
||||||
|
comment: 'Take handwritten notes',
|
||||||
|
keywords: ['notes', 'pdf', 'annotation'],
|
||||||
|
id: 'com.github.xournalpp.xournalpp.desktop'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'RustDesk',
|
||||||
|
genericName: 'Remote Desktop',
|
||||||
|
comment: 'Remote desktop control',
|
||||||
|
keywords: ['remote', 'desktop', 'control'],
|
||||||
|
id: 'com.rustdesk.RustDesk.desktop'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const contactMatches = search.sortedEntries(entries, 'contact').map(row => search.entryName(row.entry))
|
||||||
|
assertDeepEqual(contactMatches, ['Google Contacts'], 'contact search only returns direct contact matches')
|
||||||
|
|
||||||
|
assert(
|
||||||
|
search.fuzzyScore(entries[1], 'contact') < 0,
|
||||||
|
'calculator does not match contact as a loose subsequence'
|
||||||
|
)
|
||||||
|
|
||||||
|
const acronymMatches = search.sortedEntries(entries, 'gc').map(row => search.entryName(row.entry))
|
||||||
|
assertEqual(acronymMatches[0], 'Google Contacts', 'short acronym matching still works')
|
||||||
|
|
||||||
|
const directMatches = search.sortedEntries(entries, 'obs').map(row => search.entryName(row.entry))
|
||||||
|
assertEqual(directMatches[0], 'OBS Studio', 'direct app-name matching still works')
|
||||||
|
|
||||||
|
// The menu's Apps submenu is the launcher now: app rows launch and uninstall
|
||||||
|
// through the shared app library instead of running commands themselves.
|
||||||
|
const activateMatch = menuQml.match(/function activateIndex\(index, fromPointer\) \{([\s\S]*?)\n \}/)
|
||||||
|
assert(activateMatch, 'menu activateIndex function exists')
|
||||||
|
assert(
|
||||||
|
activateMatch[1].includes('root.appLibrary.launch('),
|
||||||
|
'menu routes app launch through the shared app library'
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
!activateMatch[1].includes('entry.execute()'),
|
||||||
|
'menu does not execute desktop entries directly'
|
||||||
|
)
|
||||||
|
|
||||||
|
const confirmDeleteMatch = menuQml.match(/function confirmDelete\(\) \{([\s\S]*?)\n \}/)
|
||||||
|
assert(confirmDeleteMatch, 'menu confirmDelete function exists')
|
||||||
|
assert(
|
||||||
|
confirmDeleteMatch[1].includes('root.appLibrary.remove('),
|
||||||
|
'menu delete routes through the shared app library'
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
confirmDeleteMatch[1].includes('root.cancel()'),
|
||||||
|
'menu delete closes the menu after confirmation'
|
||||||
|
)
|
||||||
|
|
||||||
|
assert(
|
||||||
|
/function remove\(desktopId, name\) \{[\s\S]*?omarchy-remove-launcher-entry[\s\S]*?\n \}/.test(appLibraryQml),
|
||||||
|
'app library remove runs the remover through the shell'
|
||||||
|
)
|
||||||
|
|
||||||
|
assert(
|
||||||
|
/function launch\(desktopId, name\) \{[\s\S]*?gtk-launch[\s\S]*?\n \}/.test(appLibraryQml) &&
|
||||||
|
appLibraryQml.includes('Util.execDetached("gtk-launch "'),
|
||||||
|
'app library runs desktop entry launch through the shell'
|
||||||
|
)
|
||||||
|
|
||||||
|
assert(
|
||||||
|
/function iconIndexScanCommand\(\)[\s\S]*-path "\*\/apps\/\*" -o -path "\*\/devices\/\*"/.test(appLibraryQml),
|
||||||
|
'app library fallback icon index includes device icons'
|
||||||
|
)
|
||||||
|
|
||||||
|
const iconSourceMatch = appLibraryQml.match(/function iconSource\(icon\) \{([\s\S]*?)\n \}/)
|
||||||
|
assert(iconSourceMatch, 'app library iconSource function exists')
|
||||||
|
assert(
|
||||||
|
iconSourceMatch[1].indexOf('root.iconIndex[value]') < iconSourceMatch[1].indexOf('Quickshell.iconPath(value, true)'),
|
||||||
|
'app library prefers indexed app icons over ambiguous themed icons'
|
||||||
|
)
|
||||||
|
|
||||||
|
const openMatch = menuQml.match(/function openExistingMenu\(initialMenu\) \{([\s\S]*?)\n \}/)
|
||||||
|
assert(openMatch, 'menu openExistingMenu function exists')
|
||||||
|
assert(
|
||||||
|
openMatch[1].includes('root.appLibrary.refreshIcons()'),
|
||||||
|
'menu refreshes the shared icon index when opened'
|
||||||
|
)
|
||||||
|
JS
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
|
||||||
|
|
||||||
run_node_test <<'JS'
|
|
||||||
const fs = require('fs')
|
|
||||||
const search = requireFromRoot('shell/plugins/launcher/LauncherSearch.js')
|
|
||||||
const launcherQml = fs.readFileSync(path.join(root, 'shell/plugins/launcher/Launcher.qml'), 'utf8')
|
|
||||||
|
|
||||||
const entries = [
|
|
||||||
{
|
|
||||||
name: 'Google Contacts',
|
|
||||||
genericName: 'Address Book',
|
|
||||||
comment: 'Manage contacts',
|
|
||||||
keywords: ['contacts', 'address book', 'people'],
|
|
||||||
id: 'google-contacts.desktop'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Calculator',
|
|
||||||
genericName: 'Calculator',
|
|
||||||
comment: 'Perform arithmetic, scientific or financial calculations',
|
|
||||||
keywords: ['calculation', 'arithmetic', 'scientific', 'financial'],
|
|
||||||
id: 'org.gnome.Calculator.desktop'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'OBS Studio',
|
|
||||||
genericName: 'Streaming/Recording Software',
|
|
||||||
comment: 'Free and Open Source Streaming/Recording Software',
|
|
||||||
keywords: ['streaming', 'recording', 'capture'],
|
|
||||||
id: 'com.obsproject.Studio.desktop'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Aether',
|
|
||||||
genericName: '',
|
|
||||||
comment: 'Minimal internet radio player',
|
|
||||||
keywords: ['audio', 'music', 'radio'],
|
|
||||||
id: 'io.github.taqi.aether.desktop'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Xournal++',
|
|
||||||
genericName: 'Notetaking',
|
|
||||||
comment: 'Take handwritten notes',
|
|
||||||
keywords: ['notes', 'pdf', 'annotation'],
|
|
||||||
id: 'com.github.xournalpp.xournalpp.desktop'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'RustDesk',
|
|
||||||
genericName: 'Remote Desktop',
|
|
||||||
comment: 'Remote desktop control',
|
|
||||||
keywords: ['remote', 'desktop', 'control'],
|
|
||||||
id: 'com.rustdesk.RustDesk.desktop'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const contactMatches = search.sortedEntries(entries, 'contact').map(row => search.entryName(row.entry))
|
|
||||||
assertDeepEqual(contactMatches, ['Google Contacts'], 'contact search only returns direct contact matches')
|
|
||||||
|
|
||||||
assert(
|
|
||||||
search.fuzzyScore(entries[1], 'contact') < 0,
|
|
||||||
'calculator does not match contact as a loose subsequence'
|
|
||||||
)
|
|
||||||
|
|
||||||
const acronymMatches = search.sortedEntries(entries, 'gc').map(row => search.entryName(row.entry))
|
|
||||||
assertEqual(acronymMatches[0], 'Google Contacts', 'short acronym matching still works')
|
|
||||||
|
|
||||||
const directMatches = search.sortedEntries(entries, 'obs').map(row => search.entryName(row.entry))
|
|
||||||
assertEqual(directMatches[0], 'OBS Studio', 'direct app-name matching still works')
|
|
||||||
|
|
||||||
assert(
|
|
||||||
/function select\(delta\)[\s\S]*root\.disarmHover\(\)[\s\S]*root\.selectedIndex =/.test(launcherQml),
|
|
||||||
'launcher keyboard navigation disarms stale hover before moving selection'
|
|
||||||
)
|
|
||||||
assert(
|
|
||||||
/PointerMoveGate\s*\{[\s\S]*id: pointerGate[\s\S]*referenceItem: card[\s\S]*\}/.test(launcherQml),
|
|
||||||
'launcher uses shared pointer movement gate in card coordinates'
|
|
||||||
)
|
|
||||||
assert(
|
|
||||||
/function disarmHover\(\)[\s\S]*pointerGate\.reset\(\)/.test(launcherQml),
|
|
||||||
'launcher resets pointer movement gate when hover is disarmed'
|
|
||||||
)
|
|
||||||
const openMatch = launcherQml.match(/function open\(payloadJson\) \{([\s\S]*?)\n \}/)
|
|
||||||
assert(openMatch, 'launcher open function exists')
|
|
||||||
assert(
|
|
||||||
openMatch[1].indexOf('root.disarmHover()') < openMatch[1].indexOf('root.opened = true')
|
|
||||||
&& !openMatch[1].includes('pointerGate.allowInitialSample()'),
|
|
||||||
'launcher ignores a stale hidden-pointer position when becoming visible'
|
|
||||||
)
|
|
||||||
assert(
|
|
||||||
/function selectFromPointer\(index, item, mouse\)[\s\S]*pointerGate\.moved\(item, mouse\)[\s\S]*root\.selectedIndex = index/.test(launcherQml),
|
|
||||||
'launcher only selects from pointer after real movement'
|
|
||||||
)
|
|
||||||
assert(
|
|
||||||
/onPositionChanged: function\(mouse\) \{\s*root\.selectFromPointer\(row\.index, row, mouse\)\s*\}/.test(launcherQml),
|
|
||||||
'launcher row hover routes through pointer movement gate'
|
|
||||||
)
|
|
||||||
assert(
|
|
||||||
/onEntered: root\.selectFromPointer\(row\.index, row, \{\s*x: mouseArea\.mouseX,\s*y: mouseArea\.mouseY\s*\}\)/.test(launcherQml),
|
|
||||||
'launcher samples pointer movement immediately when entering a row'
|
|
||||||
)
|
|
||||||
assert(
|
|
||||||
!/onContainsMouseChanged:[\s\S]*root\.selectedIndex/.test(launcherQml),
|
|
||||||
'launcher does not select rows from containsMouse'
|
|
||||||
)
|
|
||||||
|
|
||||||
const confirmDeleteMatch = launcherQml.match(/function confirmDelete\(\) \{([\s\S]*?)\n \}/)
|
|
||||||
assert(confirmDeleteMatch, 'launcher confirmDelete function exists')
|
|
||||||
assert(
|
|
||||||
confirmDeleteMatch[1].includes('root.dismiss()'),
|
|
||||||
'launcher delete closes launcher after confirmation'
|
|
||||||
)
|
|
||||||
assert(
|
|
||||||
confirmDeleteMatch[1].includes('Util.execDetached(command)'),
|
|
||||||
'launcher delete runs remover through the shell'
|
|
||||||
)
|
|
||||||
|
|
||||||
const activateMatch = launcherQml.match(/function activateIndex\(index\) \{([\s\S]*?)\n \}/)
|
|
||||||
assert(activateMatch, 'launcher activateIndex function exists')
|
|
||||||
assert(
|
|
||||||
!activateMatch[1].includes('entry.execute()'),
|
|
||||||
'launcher does not execute desktop entries directly'
|
|
||||||
)
|
|
||||||
assert(
|
|
||||||
activateMatch[1].includes('gtk-launch') && activateMatch[1].includes('Util.execDetached'),
|
|
||||||
'launcher runs desktop entry launch through the shell'
|
|
||||||
)
|
|
||||||
|
|
||||||
assert(
|
|
||||||
/function iconIndexScanCommand\(\)[\s\S]*-path "\*\/apps\/\*" -o -path "\*\/devices\/\*"/.test(launcherQml),
|
|
||||||
'launcher fallback icon index includes device icons'
|
|
||||||
)
|
|
||||||
|
|
||||||
const iconSourceMatch = launcherQml.match(/function iconSource\(icon\) \{([\s\S]*?)\n \}/)
|
|
||||||
assert(iconSourceMatch, 'launcher iconSource function exists')
|
|
||||||
assert(
|
|
||||||
iconSourceMatch[1].indexOf('root.iconIndex[value]') < iconSourceMatch[1].indexOf('Quickshell.iconPath(value, true)'),
|
|
||||||
'launcher prefers indexed app icons over ambiguous themed icons'
|
|
||||||
)
|
|
||||||
|
|
||||||
assert(
|
|
||||||
openMatch[1].includes('if (!iconIndexScan.running) iconIndexScan.running = true'),
|
|
||||||
'launcher refreshes its icon index when opened'
|
|
||||||
)
|
|
||||||
JS
|
|
||||||
@@ -92,6 +92,8 @@ assertDeepEqual(
|
|||||||
kind: 'action',
|
kind: 'action',
|
||||||
icon: '',
|
icon: '',
|
||||||
iconFont: '',
|
iconFont: '',
|
||||||
|
appIcon: '',
|
||||||
|
appId: '',
|
||||||
label: 'Theme picker',
|
label: 'Theme picker',
|
||||||
target: 'style.theme',
|
target: 'style.theme',
|
||||||
detail: 'Style',
|
detail: 'Style',
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ run_node_test <<'JS'
|
|||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
|
|
||||||
const menuQml = fs.readFileSync(path.join(root, 'shell/plugins/menu/Menu.qml'), 'utf8')
|
const menuQml = fs.readFileSync(path.join(root, 'shell/plugins/menu/Menu.qml'), 'utf8')
|
||||||
const launcherQml = fs.readFileSync(path.join(root, 'shell/plugins/launcher/Launcher.qml'), 'utf8')
|
|
||||||
|
|
||||||
assert(
|
assert(
|
||||||
/rowReservedBorderLeft:\s*Border\.left\(selectedBorderSpec\)/.test(menuQml)
|
/rowReservedBorderLeft:\s*Border\.left\(selectedBorderSpec\)/.test(menuQml)
|
||||||
@@ -18,14 +17,4 @@ assert(
|
|||||||
'Menu row content does not depend on current selected border state'
|
'Menu row content does not depend on current selected border state'
|
||||||
)
|
)
|
||||||
|
|
||||||
assert(
|
|
||||||
/rowReservedBorderLeft:\s*Border\.left\(selectedBorderSpec\)/.test(launcherQml)
|
|
||||||
&& /rowReservedBorderRight:\s*Border\.right\(selectedBorderSpec\)/.test(launcherQml),
|
|
||||||
'Launcher rows reserve selected border insets'
|
|
||||||
)
|
|
||||||
|
|
||||||
assert(
|
|
||||||
!/anchors\.(left|right)Margin:[^\n]*\brow\.border(Left|Right)\b/.test(launcherQml),
|
|
||||||
'Launcher row content does not depend on current selected border state'
|
|
||||||
)
|
|
||||||
JS
|
JS
|
||||||
|
|||||||
@@ -127,8 +127,8 @@ jq -e '
|
|||||||
}
|
}
|
||||||
pass "shell IPC returns effective shell config"
|
pass "shell IPC returns effective shell config"
|
||||||
|
|
||||||
[[ $(shell_ipc shell summon omarchy.launcher '{"query":"term"}') == "ok" ]] || fail_with_log "shell IPC summons launcher overlay"
|
[[ $(shell_ipc shell summon omarchy.menu '{"menu":"apps"}') == "ok" ]] || fail_with_log "shell IPC summons menu apps overlay"
|
||||||
shell_ipc_quiet shell hide omarchy.launcher >/dev/null
|
shell_ipc_quiet shell hide omarchy.menu >/dev/null
|
||||||
[[ $(shell_ipc shell summon missing.plugin "{}") == "unknown" ]] || fail_with_log "shell IPC rejects unknown plugin"
|
[[ $(shell_ipc shell summon missing.plugin "{}") == "unknown" ]] || fail_with_log "shell IPC rejects unknown plugin"
|
||||||
pass "shell IPC summon and hide contract works"
|
pass "shell IPC summon and hide contract works"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user