import Quickshell import Quickshell.Io import Quickshell.Wayland import QtQuick import qs.Commons import qs.Ui import "MenuModel.js" as MenuModel Item { id: root // Injected by omarchy-shell when this plugin is summoned. property string omarchyPath: Quickshell.env("OMARCHY_PATH") // Plugin lifecycle hooks. The host calls open(payloadJson) after // `omarchy-shell shell summon omarchy.menu ...` and close() when hidden. property string pendingInitialMenu: "root" function open(payloadJson) { var payload = ({}) try { payload = JSON.parse(payloadJson || "{}") } catch (e) { payload = ({}) } if (payload.fontFamily) root.fontFamily = payload.fontFamily if (payload.mode === "select" || payload.mode === "input") { root.openDmenu(payload) } else { root.openRoute(payload.initialMenu || payload.menu || "root") } } function close() { root.cancel() } function refresh() { defaultMenuFile.reload() userMenuFile.reload() return "ok" } function ping() { return "ok" } property string fontFamily: Style.font.menuFamily // JSONC menu definitions. The shell parses both at startup and merges // the user file on top of the defaults, so the keybind → IPC → visible // path doesn't have to shell out to bash + jq on every open. property string defaultMenuPath: omarchyPath + "/default/omarchy/omarchy-menu.jsonc" property string userMenuPath: Quickshell.env("HOME") + "/.config/omarchy/extensions/omarchy-menu.jsonc" property var defaultMenuItems: [] property var userMenuItems: [] property bool opened: false property string mode: "menu" readonly property bool dmenuActive: mode === "select" || mode === "input" property string dmenuPrompt: "" property var dmenuOptions: [] property string selectionFile: "" property string doneFile: "" property int dmenuWidth: 300 property int dmenuMaxHeight: 0 property bool requestActive: false property bool rowsLoaded: false property string activeMenu: "root" property string filterText: "" property int selectedIndex: 0 property bool cursorActive: false property int requestSerial: 0 property int applySerial: 0 property var items: ({}) property var itemOrder: [] property var navStack: [] property var providersLoaded: ({}) property var providerQueue: [] property int providerRevision: 0 // Bound to the central [menu] 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.menu.background property color foreground: Color.menu.text property color border: Color.menu.border property var borderSpec: Border.surfaceSpec("menu", "border", border, Math.max(1, Style.space(2))) property color scrim: Color.menu.scrim property color selectedBackground: Color.menu.selectedBackground property color selectedText: Color.menu.selectedText property color selectedBorder: Color.menu.selectedBorder property var selectedBorderSpec: Border.surfaceSpec("menu", "selected-border", selectedBorder, 0) readonly property int cornerRadius: Style.cornerRadius property int contentMargin: Style.spacing.panelPadding property int headerHeight: Math.max(Style.space(34), Style.font.title + Style.spacing.controlPaddingY * 2) property int contentSpacing: Style.spacing.md property int baseRowHeight: Math.max(Style.space(50), Style.font.body + Style.spacing.rowPaddingX * 2) property int detailRowHeight: Math.max(Style.space(58), Style.font.body + Style.font.caption + Style.spacing.rowPaddingX * 2) property int rowSpacing: Style.spacing.xs property int dividerHeight: Style.space(17) property bool searchDivider: false property int layoutSerial: 0 property int cardWidth: Math.min(root.dmenuActive ? Style.space(root.dmenuWidth) : ((root.activeMenu === "trigger.capture.screenrecord" || root.activeMenu === "style.font") ? Style.space(520) : Style.space(300)), panel.width - Style.gapsOut * 2) property int visibleRowsHeight: root.dmenuActive ? dmenuRowListHeight(layoutSerial, displayModel.count, filterText) : rowListHeight(layoutSerial, displayModel.count, filterText, searchDivider) property int cardHeight: root.dmenuActive ? Math.min(contentMargin * 2 + headerHeight + (mode === "input" ? 0 : contentSpacing + visibleRowsHeight), panel.height - Style.gapsOut * 2) : Math.min(Math.max(Style.space(220), contentMargin * 2 + headerHeight + contentSpacing + visibleRowsHeight), panel.height - Style.gapsOut * 2) function finishRequest(selection) { if (!root.requestActive || !root.doneFile) { root.opened = false return } var activeSelectionFile = root.selectionFile var activeDoneFile = root.doneFile root.requestActive = false root.selectionFile = "" root.doneFile = "" if (selection === null || selection === undefined) { resultProc.command = ["bash", "-lc", ": > " + Util.shellQuote(activeDoneFile)] } else { resultProc.command = ["bash", "-lc", "printf '%s\\n' " + Util.shellQuote(selection) + " > " + Util.shellQuote(activeSelectionFile) + "; : > " + Util.shellQuote(activeDoneFile)] } resultProc.running = true } function runAction(action) { var command = String(action || "") if (!command) return Quickshell.execDetached(Util.hyprExecCommand(command)) } function rowHeightForDetail(detail) { return root.filterText && detail ? root.detailRowHeight : root.baseRowHeight } function rowListHeight(_serial, _count, _filter, _divider) { if (displayModel.count === 0) return root.baseRowHeight var count = Math.min(displayModel.count, 10) var total = 0 var previousSection = "" for (var i = 0; i < count; i++) { var row = displayModel.get(i) if (i > 0) total += root.rowSpacing if (row.section === "drilldown" && previousSection !== "drilldown") total += root.dividerHeight total += root.rowHeightForDetail(row.detail) previousSection = row.section } return total } function dmenuRowListHeight(_serial, _count, _filter) { if (root.mode === "input") return 0 if (displayModel.count === 0) return root.baseRowHeight var count = Math.min(displayModel.count, 10) var total = 0 for (var i = 0; i < count; i++) { if (i > 0) total += root.rowSpacing total += root.baseRowHeight } return root.dmenuMaxHeight > 0 ? Math.min(total, Style.space(root.dmenuMaxHeight)) : total } function item(id) { return root.items[id] || null } // ------------------------------------------------------------------ // JSONC → normalized item array. Mirrors the bash bin's jq pipeline so // the on-disk authoring format stays untouched. // ------------------------------------------------------------------ function stripJsonc(raw) { return MenuModel.stripJsonc(raw) } function normalizeAliases(value) { return MenuModel.normalizeAliases(value) } function normalizeKeywords(id, aliases, raw) { return MenuModel.normalizeKeywords(id, aliases, raw) } function normalizeItem(id, raw) { return MenuModel.normalizeItem(id, raw) } function parseMenuJsonc(raw) { return MenuModel.parseMenuJsonc(raw) } // Merge defaults + user extension. Later entries override earlier ones // on a per-key basis (so the user can tweak label/icon/action without // re-declaring the whole row). function rebuildItemsFromSources() { var mergedMenu = MenuModel.mergeMenuSources(root.defaultMenuItems, root.userMenuItems) root.providerRevision += 1 root.providersLoaded = ({}) root.providerQueue = [] root.items = mergedMenu.items root.itemOrder = mergedMenu.itemOrder root.rowsLoaded = true root.evaluateGuards() if (root.opened) { root.rebuildDisplay() if (!root.dmenuActive) { if (root.filterText.trim()) root.loadProvidersForSearch() else root.loadProviderForMenu(root.activeMenu) } } } // Each known provider is a tiny bash one-liner that enumerates a list and // emits one tab-delimited row per item: `label\tvalue\tcurrent`. The shell // turns those into menu items children of `menuId`. readonly property var providers: ({ "fonts": { script: "current=$(omarchy-font-current 2>/dev/null); omarchy-font-list 2>/dev/null | while read -r f; do [[ -z $f ]] && continue; printf '%s\\t%s\\t%s\\n' \"$f\" \"$f\" \"$current\"; done", icon: "", actionFor: function(value) { return "omarchy-font-set '" + value.replace(/'/g, "'\\''") + "'" }, keywordsFor: function(value) { return value + " typeface" } }, "power-profiles": { script: "current=$(powerprofilesctl get 2>/dev/null); omarchy-powerprofiles-list 2>/dev/null | while read -r p; do [[ -z $p ]] && continue; printf '%s\\t%s\\t%s\\n' \"$p\" \"$p\" \"$current\"; done", icon: "\udb81\udc0b", actionFor: function(value) { return "powerprofilesctl set '" + value.replace(/'/g, "'\\''") + "'" }, keywordsFor: function(value) { return value + " power profile" } } }) function slugify(value) { return MenuModel.slugify(value) } function startProviderForMenu(id) { var entry = root.item(id) if (!entry || !entry.provider || root.providersLoaded[id]) return var spec = root.providers[entry.provider] if (!spec) return root.providersLoaded[id] = true providerProc.menuId = id providerProc.providerKey = entry.provider providerProc.revision = root.providerRevision providerProc.collected = "" providerProc.command = ["bash", "-lc", spec.script] providerProc.running = true } function mergeProviderRows(rows, menuId, providerKey) { var spec = root.providers[providerKey] if (!spec) return var changed = false var lines = String(rows || "").split("\n") var nextOrder = root.itemOrder.slice() for (var i = 0; i < lines.length; i++) { var line = lines[i].trim() if (!line) continue var parts = line.split("\t") var label = parts[0] || "" var value = parts[1] || parts[0] || "" var current = parts[2] || "" if (!label) continue var id = menuId + "." + root.slugify(value) if (!root.items[id]) nextOrder.push(id) root.items[id] = { id: id, parent: menuId, kind: "action", icon: (value === current) ? "✓" : (spec.icon || ""), label: label, target: "", keywords: spec.keywordsFor(value), description: "", action: spec.actionFor(value), provider: "", aliases: [], when: "", checked: "", order: nextOrder.indexOf(id) } changed = true } root.itemOrder = nextOrder if (changed && root.opened) root.rebuildDisplay() } function startNextProvider() { if (providerProc.running) return while (root.providerQueue.length > 0) { var id = root.providerQueue.shift() var entry = root.item(id) if (!entry || !entry.provider || root.providersLoaded[id]) continue root.startProviderForMenu(id) return } } function loadProviderForMenu(id) { var entry = root.item(id) if (!entry || !entry.provider || root.providersLoaded[id]) return if (providerProc.running) { if (root.providerQueue.indexOf(id) < 0) root.providerQueue = root.providerQueue.concat([id]) return } root.startProviderForMenu(id) } function loadProvidersForSearch() { var active = root.item(root.activeMenu) ? root.activeMenu : "root" for (var i = 0; i < root.itemOrder.length; i++) { var entry = root.item(root.itemOrder[i]) if (!entry || !entry.provider || root.providersLoaded[entry.id]) continue if (active !== "root" && entry.id !== active && !root.isDescendantOf(entry.id, active)) continue root.loadProviderForMenu(entry.id) } } function depthFor(id) { return MenuModel.depthFor(root.items, id) } function pathFor(id) { return MenuModel.pathFor(root.items, id) } function parentPathFor(id) { return MenuModel.parentPathFor(root.items, id) } function isDescendantOf(id, ancestorId) { return MenuModel.isDescendantOf(root.items, id, ancestorId) } function childCount(id) { return MenuModel.childCount(root.items, root.itemOrder, id) } // Items whose `when:` evaluated to false are hidden everywhere — nav, // drilldown, and search. Items with no `when:` are always visible. function isVisible(entry) { if (!entry) return false if (!entry.when) return true var result = root.whenResults[entry.id] return result === undefined ? true : result } // Label with the ✓ marker baked in when `checked:` evaluated truthy. function labelFor(entry) { return MenuModel.labelFor(entry, root.checkedResults) } function searchableToken(value) { return MenuModel.searchableToken(value) } function leafIdFor(id) { return MenuModel.leafIdFor(id) } function nameSearchText(entry) { return MenuModel.nameSearchText(entry) } function termInSearchWords(term, text) { return MenuModel.termInSearchWords(term, text) } function keywordTextMatches(query, text) { return MenuModel.keywordTextMatches(query, text) } function matchesQuery(entry, query) { return MenuModel.matchesQuery(entry, query, root.isVisible(entry)) } function searchScore(entry, query) { return MenuModel.searchScore(root.items, entry, query) } function displayRow(entry, detail, score, section) { return MenuModel.displayRow(root.items, root.itemOrder, root.checkedResults, entry, detail, score, section) } function rebuildDmenuDisplay() { displayModel.clear() root.searchDivider = false if (root.mode === "input") { layoutSerial += 1 return } var query = root.filterText.trim().toLowerCase() for (var i = 0; i < root.dmenuOptions.length; i++) { var label = String(root.dmenuOptions[i] || "") if (query && label.toLowerCase().indexOf(query) < 0) continue displayModel.append({ itemId: "dmenu." + i, kind: "dmenu", icon: "", label: label, target: "", detail: "", path: "", childCount: 0, action: "", provider: "", score: i, section: "" }) } layoutSerial += 1 if (displayModel.count === 0) selectedIndex = 0 else if (selectedIndex >= displayModel.count) selectedIndex = displayModel.count - 1 else if (selectedIndex < 0) selectedIndex = 0 Qt.callLater(function() { if (displayModel.count > 0) resultList.positionViewAtIndex(root.selectedIndex, ListView.Contain) }) } function rebuildDisplay() { if (root.dmenuActive) { root.rebuildDmenuDisplay() return } displayModel.clear() if (!root.rowsLoaded) return var active = root.item(root.activeMenu) ? root.activeMenu : "root" root.activeMenu = active var rows = [] var query = root.filterText.trim() root.searchDivider = false if (query) { var currentRows = [] var drilldownRows = [] for (var i = 0; i < root.itemOrder.length; i++) { var entry = root.item(root.itemOrder[i]) if (!entry || entry.id === "root") continue if (!root.isDescendantOf(entry.id, active)) continue if (!root.matchesQuery(entry, query)) continue var detail = root.parentPathFor(entry.id) var row = root.displayRow(entry, detail, root.searchScore(entry, query)) if (entry.parent === active) currentRows.push(row) else drilldownRows.push(row) } var searchSort = function(a, b) { if (a.score !== b.score) return a.score - b.score return a.path.localeCompare(b.path) } currentRows.sort(searchSort) drilldownRows.sort(searchSort) root.searchDivider = currentRows.length > 0 && drilldownRows.length > 0 if (root.searchDivider) { for (var d = 0; d < drilldownRows.length; d++) drilldownRows[d].section = "drilldown" } rows = currentRows.concat(drilldownRows) } else { for (var j = 0; j < root.itemOrder.length; j++) { var child = root.item(root.itemOrder[j]) if (!child || child.parent !== active) continue if (!root.isVisible(child)) continue rows.push(root.displayRow(child, child.description, child.order)) } } for (var k = 0; k < rows.length; k++) displayModel.append(rows[k]) layoutSerial += 1 if (displayModel.count === 0) selectedIndex = 0 else if (selectedIndex >= displayModel.count) selectedIndex = displayModel.count - 1 else if (selectedIndex < 0) selectedIndex = 0 Qt.callLater(function() { if (displayModel.count > 0) resultList.positionViewAtIndex(root.selectedIndex, ListView.Contain) }) } function select(delta) { if (displayModel.count === 0) return if (!cursorActive) { cursorActive = true selectedIndex = delta < 0 ? displayModel.count - 1 : 0 } else { selectedIndex = (selectedIndex + delta + displayModel.count) % displayModel.count } resultList.positionViewAtIndex(selectedIndex, ListView.Contain) } function setFilter(nextFilter) { root.filterText = nextFilter root.selectedIndex = 0 root.cursorActive = root.mode !== "input" if (!root.dmenuActive && root.filterText.trim()) root.loadProvidersForSearch() root.rebuildDisplay() } function setActiveMenu(id, pushHistory) { if (!root.item(id)) id = "root" if (pushHistory && id !== root.activeMenu) root.navStack = root.navStack.concat([root.activeMenu]) root.activeMenu = id root.filterText = "" root.selectedIndex = 0 root.cursorActive = true root.rebuildDisplay() root.loadProviderForMenu(id) } function goBack() { if (root.activeMenu === "root") return false if (root.navStack.length > 0) { var previous = root.navStack[root.navStack.length - 1] root.navStack = root.navStack.slice(0, root.navStack.length - 1) root.setActiveMenu(previous, false) return true } var active = root.item(root.activeMenu) root.setActiveMenu((active && active.parent) ? active.parent : "root", false) return true } function activateIndex(index) { if (root.dmenuActive) { if (root.mode === "input") { root.applyDmenuSelection(root.filterText) return } if (index < 0 || index >= displayModel.count) return root.applyDmenuSelection(displayModel.get(index).label) return } if (index < 0 || index >= displayModel.count) return var row = displayModel.get(index) if (row.kind === "menu" || row.kind === "link") { root.setActiveMenu(row.target || row.itemId, true) } else { root.applySelected(row.itemId, row.action) } } function applyDmenuSelection(value) { applySerial = requestSerial opened = false filterText = "" root.finishRequest(value) } function applySelected(id, action) { if (!id) { cancel(); return } applySerial = requestSerial opened = false filterText = "" root.runAction(action) } function cancel() { if (root.dmenuActive) root.finishRequest(null) opened = false filterText = "" } function openExistingMenu(initialMenu) { requestSerial += 1 mode = "menu" requestActive = false selectionFile = "" doneFile = "" activeMenu = root.item(initialMenu) ? initialMenu : "root" navStack = [] filterText = "" selectedIndex = 0 cursorActive = true root.evaluateGuards() opened = true rebuildDisplay() loadProviderForMenu(activeMenu) Qt.callLater(function() { keyCatcher.forceActiveFocus() }) } function openDmenu(payload) { requestSerial += 1 mode = payload.mode === "input" ? "input" : "select" dmenuPrompt = String(payload.prompt || (mode === "input" ? "Input" : "Select")) dmenuOptions = Array.isArray(payload.options) ? payload.options : [] selectionFile = String(payload.selectionFile || "") doneFile = String(payload.doneFile || "") requestActive = !!doneFile dmenuWidth = Math.max(1, Number(payload.width || 300)) dmenuMaxHeight = Math.max(0, Number(payload.maxHeight || 0)) activeMenu = "root" navStack = [] filterText = "" selectedIndex = 0 cursorActive = mode !== "input" opened = true rebuildDisplay() Qt.callLater(function() { keyCatcher.forceActiveFocus() }) } ListModel { id: displayModel } // ----------------------------------------------------------- route surface // // The menu is opened through the standard plugin lifecycle: // `omarchy-shell shell summon omarchy.menu '{"menu":"system"}'`. // Callers may pass a real id (`system`, `setup.power`) or an alias declared // in JSONC (`power`, `reminder-set`). Unknown strings fall through to the // id-as-route behavior so misspellings still attempt to open the literal id. function resolveRoute(input) { var raw = String(input || "").toLowerCase().replace(/_/g, "-") if (!raw || raw === "go" || raw === "menu") return "root" for (var i = 0; i < root.itemOrder.length; i++) { var entry = root.items[root.itemOrder[i]] if (!entry || !entry.aliases) continue for (var j = 0; j < entry.aliases.length; j++) { var alias = String(entry.aliases[j] || "").toLowerCase().replace(/_/g, "-") if (alias === raw) return entry.id } } return raw } function openRoute(initialMenu) { var id = root.resolveRoute(initialMenu) var entry = root.items[id] // If the resolved id is an action (i.e. the user invoked an alias for // a leaf, e.g. `omarchy menu summon screenrecord-stop`), run it directly // instead of opening an action with no children. if (entry && entry.kind === "action" && entry.action) { root.cancel() root.runAction(entry.action) return "ok" } // If it's a link (a redirect to another menu), follow the link. if (entry && entry.kind === "link" && entry.target) id = entry.target root.pendingInitialMenu = id root.openExistingMenu(id) return "ok" } Process { id: providerProc property string menuId: "" property string providerKey: "" property string collected: "" property int revision: 0 stdout: SplitParser { onRead: function(data) { providerProc.collected += data + "\n" } } onExited: { if (providerProc.revision === root.providerRevision) { root.mergeProviderRows(providerProc.collected, providerProc.menuId, providerProc.providerKey) if (root.filterText.trim()) root.loadProvidersForSearch() } root.startNextProvider() } } Process { id: resultProc onExited: { if (root.applySerial === root.requestSerial) root.opened = false } } // The JSONC sources are watched so live edits to the default file (or the // user extension at ~/.config/omarchy/extensions/omarchy-menu.jsonc) take // effect without restarting the shell. FileView { id: defaultMenuFile path: root.defaultMenuPath watchChanges: true printErrors: false onLoaded: { root.defaultMenuItems = root.parseMenuJsonc(text()); root.rebuildItemsFromSources() } onFileChanged: reload() } FileView { id: userMenuFile path: root.userMenuPath watchChanges: true printErrors: false onLoaded: { root.userMenuItems = root.parseMenuJsonc(text()); root.rebuildItemsFromSources() } onLoadFailed: { root.userMenuItems = []; root.rebuildItemsFromSources() } onFileChanged: reload() } // ---------------------------------------------------------------- guards // // `when:` (visibility) and `checked:` (✓ marker) are bash expressions the // shell wasn't allowed to evaluate before the perf rewrite. Now the shell // batches them into one bash subprocess per (re)load so the open path // never has to wait on them. property var whenResults: ({}) // id → true|false (allow visibility) property var checkedResults: ({}) // id → true|false (show ✓) function evaluateGuards() { var script = "" var ids = Object.keys(root.items) for (var i = 0; i < ids.length; i++) { var entry = root.items[ids[i]] if (!entry) continue if (entry.when) script += "if " + entry.when + " >/dev/null 2>&1; then echo " + ids[i] + ":w:1; else echo " + ids[i] + ":w:0; fi\n" if (entry.checked) script += "if " + entry.checked + " >/dev/null 2>&1; then echo " + ids[i] + ":c:1; else echo " + ids[i] + ":c:0; fi\n" } if (!script) { root.whenResults = ({}) root.checkedResults = ({}) return } guardProc.collected = "" guardProc.command = ["bash", "-lc", script] guardProc.running = true } Process { id: guardProc property string collected: "" stdout: SplitParser { onRead: function(data) { guardProc.collected += data + "\n" } } onExited: { var nextWhen = ({}) var nextChecked = ({}) var lines = guardProc.collected.split("\n") for (var i = 0; i < lines.length; i++) { var line = lines[i].trim() if (!line) continue var colon = line.lastIndexOf(":") if (colon < 0) continue var value = line.substring(colon + 1) === "1" var rest = line.substring(0, colon) var tagAt = rest.lastIndexOf(":") if (tagAt < 0) continue var id = rest.substring(0, tagAt) var tag = rest.substring(tagAt + 1) if (tag === "w") nextWhen[id] = value else if (tag === "c") nextChecked[id] = value } root.whenResults = nextWhen root.checkedResults = nextChecked if (root.opened) root.rebuildDisplay() } } PanelWindow { id: panel visible: root.opened && root.rowsLoaded anchors { top: true; bottom: true; left: true; right: true } color: "transparent" WlrLayershell.namespace: "omarchy-menu" WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive exclusionMode: ExclusionMode.Ignore Rectangle { anchors.fill: parent color: root.scrim } MouseArea { anchors.fill: parent onClicked: root.cancel() } BorderSurface { id: card width: root.cardWidth height: root.cardHeight radius: root.cornerRadius anchors.centerIn: parent color: root.background borderSpec: root.borderSpec padding: root.contentMargin MouseArea { anchors.fill: parent; onClicked: {} } Item { id: keyCatcher anchors.fill: parent focus: true Keys.priority: Keys.BeforeItem Keys.onPressed: function(event) { if (event.key === Qt.Key_Escape) { if (root.filterText) root.setFilter("") else root.cancel() event.accepted = true } else if (event.key === Qt.Key_Backspace) { if (root.filterText.length > 0) root.setFilter(root.filterText.slice(0, -1)) else root.goBack() 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_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_Right) { if (root.dmenuActive) { if (root.mode === "input") root.applyDmenuSelection(root.filterText) else if (displayModel.count > 0) root.activateIndex(root.cursorActive ? root.selectedIndex : 0) } else if (root.cursorActive) root.activateIndex(root.selectedIndex) else if (displayModel.count > 0) root.cursorActive = true 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 } } } 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.headerHeight radius: root.cornerRadius color: "transparent" Text { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter text: root.filterText || (root.dmenuActive ? (root.dmenuPrompt + "…") : ((root.item(root.activeMenu) ? root.item(root.activeMenu).label : "Go") + "…")) color: root.foreground opacity: root.filterText ? 1 : 0.58 font.family: root.fontFamily font.pixelSize: Style.font.heading elide: Text.ElideRight } } Item { width: parent.width height: root.visibleRowsHeight ListView { id: resultList anchors.fill: parent model: displayModel clip: true spacing: root.rowSpacing boundsBehavior: Flickable.StopAtBounds section.property: "section" section.criteria: ViewSection.FullString section.delegate: Item { required property string section width: ListView.view.width height: section === "drilldown" ? root.dividerHeight : 0 visible: section === "drilldown" Rectangle { anchors.left: parent.left anchors.leftMargin: Style.space(4) anchors.right: parent.right anchors.rightMargin: Style.space(4) anchors.verticalCenter: parent.verticalCenter height: Style.spacing.hairline color: Util.alpha(root.foreground, 0.2) } } delegate: BorderSurface { id: row required property int index required property string itemId required property string kind required property string icon required property string label required property string target required property string detail required property string path required property string action required property int childCount readonly property bool hasCursor: root.cursorActive && row.index === root.selectedIndex readonly property bool hasIcon: row.icon.length > 0 width: ListView.view.width height: root.rowHeightForDetail(row.detail) radius: root.cornerRadius color: row.hasCursor ? root.selectedBackground : "transparent" borderSpec: row.hasCursor ? root.selectedBorderSpec : Border.none() Rectangle { visible: false width: Style.space(4) height: parent.height - Style.space(18) radius: Math.min(root.cornerRadius, Style.space(4)) color: root.selectedBackground anchors.left: parent.left anchors.leftMargin: row.borderLeft + Style.space(8) anchors.verticalCenter: parent.verticalCenter } Text { id: iconText visible: row.hasIcon text: row.icon color: row.hasCursor ? root.selectedText : root.foreground font.family: root.fontFamily font.pixelSize: Style.font.iconLarge width: Style.space(36) horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter anchors.left: parent.left anchors.leftMargin: row.borderLeft + Style.space(8) y: contentColumn.y + labelText.y + (labelText.height - height) / 2 } Column { id: contentColumn anchors.left: row.hasIcon ? iconText.right : parent.left anchors.leftMargin: row.hasIcon ? Style.space(6) : row.borderLeft + Style.space(18) anchors.right: trail.left anchors.rightMargin: Style.space(6) anchors.verticalCenter: parent.verticalCenter spacing: Style.space(3) Text { id: labelText width: parent.width text: row.label color: row.hasCursor ? root.selectedText : root.foreground font.family: root.fontFamily font.pixelSize: Style.font.heading font.weight: Font.Medium elide: Text.ElideRight } Text { width: parent.width text: row.detail visible: root.filterText && row.detail.length > 0 color: root.foreground opacity: 0.52 font.family: root.fontFamily font.pixelSize: Style.font.bodySmall elide: Text.ElideRight } } Row { id: trail width: Style.space(14) anchors.right: parent.right anchors.rightMargin: row.borderRight + Style.space(8) y: contentColumn.y + labelText.y + (labelText.height - height) / 2 spacing: 0 Text { visible: false text: row.childCount color: root.foreground opacity: 0.45 font.family: root.fontFamily font.pixelSize: Style.font.body anchors.verticalCenter: parent.verticalCenter } Text { text: row.kind === "menu" || row.kind === "link" ? "›" : "" color: row.hasCursor ? root.selectedText : root.foreground opacity: row.kind === "menu" || row.kind === "link" ? 0.36 : 0 font.family: root.fontFamily font.pixelSize: Style.font.heading font.weight: Font.Normal anchors.verticalCenter: parent.verticalCenter } } MouseArea { id: mouseArea anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onPositionChanged: { root.cursorActive = true root.selectedIndex = row.index } onClicked: root.activateIndex(row.index) } } } Column { anchors.centerIn: parent spacing: Style.space(8) visible: displayModel.count === 0 && root.mode !== "input" Text { text: "󰈉" color: root.selectedText opacity: 0.8 font.family: root.fontFamily font.pixelSize: Style.font.displayLarge horizontalAlignment: Text.AlignHCenter width: Style.space(320) } Text { text: root.filterText ? "No matches for “" + root.filterText + "”" : "Nothing here yet" color: root.foreground opacity: 0.7 font.family: root.fontFamily font.pixelSize: Style.font.title horizontalAlignment: Text.AlignHCenter width: Style.space(320) } } } Item { width: parent.width height: 0 } } } } }