Krolyxon
Commit af47ed6

Branches

Refs →

aurora.configquickshellauroraRunner.qml

625 lines · 28.0 KB · QML

1import QtQuick
2import QtQuick.Layouts
3import QtQuick.Effects
4import Quickshell
5import Quickshell.Io
6import Quickshell.Wayland
7import Quickshell.Hyprland
8
9// App / command runner (replaced fuzzel on SUPER+R). One fuzzy list of desktop apps (+ their actions),
10// aurora commands and every executable on $PATH, ranked by match and launch count.
11// `=expr` calculator (Enter copies), `>cmd` runs a shell command. Ctrl+Enter runs in the terminal.
12// dmenu mode (`aurora-run -d`, `dmenu` IPC): plain list from the script's stdin, the choice goes back over a fifo.
13// Looks like glyph's launcher (github.com/Nebelung-cloud/glyph-rice, GPL-3.0, launcher/Launcher.qml): a card over the
14// dimmed screen, a search pill with the live caret, mode chips (all / run = `>` / calc = `=`), round icons in rows.
15PanelWindow {
16 id: win
17 property bool opened: false
18 property var shell // shell.qml root: toggleOverlay / openTab / barShown
19 signal closeRequested()
20 signal openRequested() // a dmenu request is loaded and wants the window
21
22 readonly property int rowH: 60
23 readonly property int rows: 10
24 readonly property int maxShown: 60
25 readonly property string home: Quickshell.env("HOME")
26
27 // ---- dmenu mode ----
28 property string mode: "run" // run | dmenu
29 property var dmItems: [] // [{kind:"dm", name, low, i}]
30 property string dmPrompt: ""
31 property string dmPlaceholder: ""
32 property int dmLines: -1 // -1 default, 0 = input only (no list)
33 property bool dmPassword: false
34 property bool dmIndex: false
35 property string dmFifo: "" // pending request's reply fifo, "" when none
36 readonly property bool dmenu: mode === "dmenu"
37 readonly property int rowsShown: !dmenu ? rows
38 : dmLines === 0 ? 0 : Math.min(dmLines > 0 ? dmLines : rows, 15, Math.max(1, dmItems.length))
39
40 // dir holds `opts` (key=value lines) and `items` (stdin of the script), see ~/.local/bin/aurora-run
41 function startDmenu(dir) {
42 if (dmFifo !== "") reply(false, "") // a newer request replaces a pending one
43 loadProc.dir = dir
44 loadProc.command = ["sh", "-c", 'cat "$1/opts"; printf "\\036\\n"; [ -f "$1/items" ] && cat "$1/items"', "sh", dir]
45 loadProc.running = true
46 }
47 Process {
48 id: loadProc
49 property string dir
50 stdout: StdioCollector {
51 onStreamFinished: {
52 var sep = text.indexOf("\x1e\n")
53 if (sep < 0) return
54 var o = {}, ol = text.substring(0, sep).split("\n")
55 for (var i = 0; i < ol.length; i++) {
56 var eq = ol[i].indexOf("=")
57 if (eq > 0) o[ol[i].substring(0, eq)] = ol[i].substring(eq + 1)
58 }
59 var items = [], l = text.substring(sep + 2).split("\n")
60 for (var j = 0; j < l.length; j++)
61 if (l[j] !== "") items.push({ kind: "dm", key: "", name: l[j], low: l[j].toLowerCase(), i: j,
62 sub: "", icon: "", glyph: "", tag: "" })
63 win.dmPrompt = o.prompt || ""
64 win.dmPlaceholder = o.placeholder || ""
65 win.dmLines = o.lines !== undefined && o.lines !== "" ? parseInt(o.lines) : -1
66 win.dmPassword = o.password === "1"
67 win.dmIndex = o.index === "1"
68 win.dmItems = items
69 win.dmFifo = loadProc.dir + "/reply"
70 win.mode = "dmenu"
71 filter.text = ""
72 win.current = 0
73 win.openRequested()
74 filter.forceActiveFocus()
75 }
76 }
77 }
78 // "<0|1>\n<value>\0" to the fifo through stdin, so a password never shows up in a process's argv
79 function reply(ok, value) {
80 replyProc.payload = (ok ? "0" : "1") + "\n" + value + "\0"
81 replyProc.command = ["sh", "-c", 'cat > "$1"', "sh", dmFifo]
82 replyProc.stdinEnabled = true
83 replyProc.running = true
84 dmFifo = ""
85 }
86 Process {
87 id: replyProc
88 property string payload
89 onStarted: { write(payload); payload = ""; stdinEnabled = false }
90 }
91 function endDmenu() {
92 mode = "run"
93 dmItems = []
94 dmPassword = false
95 filter.text = ""
96 }
97
98 property real t: opened ? 1 : 0
99 Behavior on t { NumberAnimation { duration: Theme.t; easing.type: Easing.BezierSpline; easing.bezierCurve: Theme.glide } }
100
101 visible: t > 0
102 anchors { top: true; bottom: true; left: true; right: true }
103 exclusionMode: ExclusionMode.Ignore
104 color: "transparent"
105 WlrLayershell.layer: WlrLayer.Overlay
106 WlrLayershell.namespace: "aurora-runner"
107 WlrLayershell.keyboardFocus: opened ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
108
109 property int current: 0
110 property string armed: ""
111 Timer { id: disarm; interval: 3000; onTriggered: win.armed = "" }
112
113 onOpenedChanged: {
114 if (!opened) {
115 // closed without a choice (esc, outside click, another overlay): the script gets "cancelled"
116 if (dmFifo !== "") reply(false, "")
117 if (dmenu) endDmenu()
118 return
119 }
120 armed = ""
121 filter.forceActiveFocus()
122 if (dmenu) return // startDmenu already reset the input
123 filter.text = ""
124 current = 0
125 if (Date.now() - binsAt > 60000) binProc.running = true
126 }
127
128 // ---- sources ----
129 function basename(p) { p = String(p); return p.substring(p.lastIndexOf("/") + 1) }
130
131 // desktop entries + their actions ("Librewolf: New Private Window")
132 property var apps: []
133 function loadApps() {
134 var out = [], l = DesktopEntries.applications.values
135 for (var i = 0; i < l.length; i++) {
136 var e = l[i]
137 if (e.noDisplay) continue
138 var ic = e.icon ? Quickshell.iconPath(e.icon, true) : ""
139 var bin = e.command && e.command.length ? basename(e.command[0]) : ""
140 out.push({ kind: "app", key: "app:" + e.id, name: e.name, sub: e.genericName || e.comment || "",
141 icon: ic, glyph: Theme.iApp, tag: "app", entry: e, action: null, bin: bin,
142 hay: [e.genericName || "", (e.keywords || []).join(" "), bin] })
143 var acts = e.actions || []
144 for (var j = 0; j < acts.length; j++)
145 out.push({ kind: "act", key: "act:" + e.id + ":" + acts[j].id, name: e.name + ": " + acts[j].name, sub: "",
146 icon: ic, glyph: Theme.iApp, tag: "app", entry: e, action: acts[j], bin: "", hay: [] })
147 }
148 apps = out
149 }
150 Component.onCompleted: loadApps()
151 Connections {
152 target: DesktopEntries
153 function onApplicationsChanged() { win.loadApps() }
154 }
155
156 // executables on $PATH (first one wins, like the shell). Apps whose binary is already listed are skipped.
157 property var bins: []
158 property double binsAt: 0
159 Process {
160 id: binProc
161 command: ["sh", "-c", 'set -f; IFS=:; for d in $PATH; do [ -d "$d" ] && find -L "$d" -maxdepth 1 -type f -executable -printf "%f\\t%p\\n" 2>/dev/null; done']
162 stdout: StdioCollector {
163 onStreamFinished: {
164 var seen = {}, out = [], l = text.split("\n")
165 for (var i = 0; i < l.length; i++) {
166 var t = l[i].indexOf("\t")
167 if (t <= 0) continue
168 var n = l[i].substring(0, t)
169 if (seen[n]) continue
170 seen[n] = true
171 out.push({ kind: "bin", key: "bin:" + n, name: n, sub: l[i].substring(t + 1),
172 icon: "", glyph: Theme.iConsole, tag: "bin", hay: [] })
173 }
174 win.bins = out
175 win.binsAt = Date.now()
176 }
177 }
178 }
179
180 // aurora's own actions. Session ones come from Session (same confirm rules as the power menu).
181 readonly property var commands: {
182 var c = [
183 { id: "panel", glyph: Theme.iHome, name: "control panel" },
184 { id: "audio", glyph: Theme.iVolHigh, name: "audio settings" },
185 { id: "net", glyph: Theme.iWifi, name: "network settings" },
186 { id: "bt", glyph: Theme.iBt, name: "bluetooth settings" },
187 { id: "sys", glyph: Theme.iChip, name: "system info" },
188 { id: "notif", glyph: Theme.iBell, name: "notifications" },
189 { id: "settings", glyph: Theme.iCog, name: "aurora settings" },
190 { id: "wall", glyph: Theme.iImage, name: "wallpaper picker" },
191 { id: "wallrnd", glyph: Theme.iShuffle, name: "random wallpaper" },
192 { id: "clip", glyph: Theme.iClipboard, name: "clipboard history" },
193 { id: "overview", glyph: Theme.iWindow, name: "window overview" },
194 { id: "cal", glyph: Theme.iCal, name: "calendar" },
195 { id: "power", glyph: Theme.iPower, name: "power menu" },
196 { id: "caffeine", glyph: Theme.iCoffee, name: "toggle caffeine" },
197 { id: "dnd", glyph: Theme.iBellOff, name: "toggle do not disturb" },
198 { id: "bar", glyph: Theme.iMonitor, name: "toggle bar" },
199 { id: "picker", glyph: Theme.iPicker, name: "colour picker" },
200 { id: "shot", glyph: Theme.iCrop, name: "screenshot region" },
201 { id: "ocr", glyph: Theme.iCrop, name: "screenshot ocr" }
202 ]
203 if (Rec.available) c.push({ id: "rec", glyph: Theme.iRec, name: Rec.busy ? "stop recording" : "record screen" })
204 var out = []
205 for (var i = 0; i < c.length; i++)
206 out.push({ kind: "aurora", key: "aur:" + c[i].id, id: c[i].id, name: c[i].name, sub: "", icon: "",
207 glyph: c[i].glyph, tag: "aurora", confirm: false, hay: [] })
208 var s = Session.actions
209 for (var j = 0; j < s.length; j++)
210 out.push({ kind: "session", key: "ses:" + s[j].id, id: s[j].id, name: s[j].label, sub: "session", icon: "",
211 glyph: s[j].icon, tag: "aurora", confirm: s[j].confirm, hay: [] })
212 return out
213 }
214
215 // ---- frecency: launch counts in ~/.cache/aurora/runner.json ----
216 property var counts: ({})
217 FileView {
218 id: store
219 path: win.home + "/.cache/aurora/runner.json"
220 printErrors: false
221 onLoaded: { try { win.counts = JSON.parse(text()) } catch (e) { win.counts = {} } }
222 }
223 function bump(key) {
224 var c = {}
225 for (var k in counts) c[k] = counts[k]
226 c[key] = (c[key] || 0) + 1
227 counts = c
228 store.setText(JSON.stringify(c))
229 }
230
231 // ---- calculator ----
232 readonly property var mathFns: ["sqrt", "cbrt", "abs", "sin", "cos", "tan", "asin", "acos", "atan",
233 "log", "log2", "log10", "exp", "floor", "ceil", "round", "min", "max", "pow"]
234 function calc(expr) {
235 if (!/^[0-9a-z\s.+\-*\/%^(),]+$/i.test(expr) || !/\d|pi/i.test(expr)) return ""
236 var bad = false
237 var js = expr.replace(/\^/g, "**").replace(/[a-z][a-z0-9]*/gi, function (w) {
238 var l = w.toLowerCase()
239 if (l === "pi") return "Math.PI"
240 if (l === "e") return "Math.E"
241 if (l === "ln") return "Math.log"
242 if (win.mathFns.indexOf(l) >= 0) return "Math." + l
243 bad = true
244 return w
245 })
246 if (bad) return ""
247 try {
248 var r = (new Function("return (" + js + ")"))()
249 if (typeof r !== "number" || !isFinite(r)) return ""
250 return String(Number(r.toPrecision(12)))
251 } catch (e) { return "" }
252 }
253
254 // ---- ranking ----
255 function rank(it, q) {
256 var s = Fuzzy.score(it.name, q)
257 for (var i = 0; i < it.hay.length; i++)
258 if (it.hay[i] !== "") s = Math.max(s, Fuzzy.score(it.hay[i], q) * 0.6)
259 if (s <= 0) return 0
260 var w = it.kind === "app" ? 1 : it.kind === "bin" ? 0.6 : 0.85
261 return s * w + 60 * Math.log(1 + (counts[it.key] || 0))
262 }
263 readonly property var shown: {
264 var raw = filter.text, q = raw.toLowerCase().trim(), res = []
265 if (dmenu) {
266 if (dmLines === 0) return res
267 var cap = 1000, dl = dmItems, di
268 if (q === "") return dl.length > cap ? dl.slice(0, cap) : dl
269 var hits = []
270 for (di = 0; di < dl.length; di++) {
271 var dsc = Fuzzy.scoreLow(dl[di].low, q)
272 if (dsc > 0) hits.push({ it: dl[di], sc: dsc })
273 }
274 hits.sort(function (x, y) { return y.sc - x.sc || x.it.i - y.it.i })
275 for (di = 0; di < hits.length && di < cap; di++) res.push(hits[di].it)
276 return res
277 }
278 if (q.charAt(0) === ">") {
279 var cmd = raw.trim().substring(1).trim()
280 if (cmd !== "") res.push({ kind: "sh", key: "", name: cmd, sub: "run in sh", icon: "", glyph: Theme.iShell, tag: "sh" })
281 return res
282 }
283 if (q.charAt(0) === "=") {
284 var r = calc(q.substring(1))
285 res.push({ kind: "calc", key: "", name: r !== "" ? r : "...", sub: q.substring(1).trim(), icon: "",
286 glyph: Theme.iCalc, tag: "=", value: r })
287 return res
288 }
289 if (/[\d)]\s*[+\-*\/%^]\s*[\d(.]/.test(q)) {
290 var v = calc(q)
291 if (v !== "") res.push({ kind: "calc", key: "", name: v, sub: q, icon: "", glyph: Theme.iCalc, tag: "=", value: v })
292 }
293
294 var all = [], i
295 var appBins = {}
296 for (i = 0; i < apps.length; i++) { all.push(apps[i]); if (apps[i].bin) appBins[apps[i].bin] = true }
297 for (i = 0; i < commands.length; i++) all.push(commands[i])
298 var scored = []
299 if (q === "") {
300 // most used first, then apps A-Z; no actions / bins / commands unless used before
301 for (i = 0; i < bins.length; i++) if (counts[bins[i].key]) all.push(bins[i])
302 for (i = 0; i < all.length; i++) {
303 var n = counts[all[i].key] || 0
304 if (n > 0 || all[i].kind === "app") scored.push({ it: all[i], sc: n })
305 }
306 scored.sort(function (x, y) { return y.sc - x.sc || x.it.name.localeCompare(y.it.name) })
307 } else {
308 for (i = 0; i < bins.length; i++) if (!appBins[bins[i].name]) all.push(bins[i])
309 for (i = 0; i < all.length; i++) {
310 var sc = rank(all[i], q)
311 if (sc > 0) scored.push({ it: all[i], sc: sc })
312 }
313 scored.sort(function (x, y) { return y.sc - x.sc || x.it.name.length - y.it.name.length })
314 }
315 for (i = 0; i < scored.length && res.length < maxShown; i++) res.push(scored[i].it)
316 // nothing matched: offer to run the text as a command, like fuzzel's execute-input
317 if (res.length === 0 && q !== "")
318 res.push({ kind: "sh", key: "", name: raw.trim(), sub: "run in sh", icon: "", glyph: Theme.iShell, tag: "sh" })
319 return res
320 }
321
322 // ---- launching ----
323 function exec(cmd, term) {
324 Session.launch(term ? ["foot", "-e"].concat(cmd) : cmd, home)
325 }
326 function trigger(i, term) {
327 if (dmenu) {
328 // like fuzzel: the selected line, or the typed text when nothing matches / Ctrl+Enter
329 var sel = term ? null : shown[i]
330 if (dmIndex) reply(true, sel ? String(sel.i) : "-1")
331 else reply(true, sel ? sel.name : filter.text)
332 closeRequested()
333 return
334 }
335 var it = shown[i]
336 if (!it) return
337 current = i
338 if (it.confirm && armed !== it.key) { armed = it.key; disarm.restart(); return }
339 armed = ""
340 if (it.key !== "") bump(it.key)
341 closeRequested()
342 switch (it.kind) {
343 case "app":
344 if (term || it.entry.runInTerminal) exec(it.entry.command, true)
345 else Session.launch(it.entry.command, it.entry.workingDirectory)
346 break
347 case "act": Session.launch(it.action.command); break
348 case "bin": exec([it.name], term); break
349 case "sh": exec(["sh", "-c", it.name], term); break
350 case "calc": if (it.value !== "") Quickshell.execDetached(["wl-copy", "--", it.value]); break
351 case "session": Session.run(it.id); break
352 case "aurora": command(it.id); break
353 }
354 }
355 function command(id) {
356 switch (id) {
357 case "panel": shell.openTab("home"); break
358 case "audio": case "net": case "bt": case "sys": case "notif": case "settings":
359 shell.openTab(id); break
360 case "wall": case "clip": case "overview": case "cal": case "power":
361 shell.toggleOverlay(id); break
362 case "wallrnd": Wall.random(); break
363 case "caffeine": Toggles.caffeine = !Toggles.caffeine; break
364 case "dnd": Notifs.dnd = !Notifs.dnd; break
365 case "bar": shell.barShown = !shell.barShown; break
366 case "picker": Toggles.pickColour(); break
367 case "shot": Shot.take("region"); break
368 case "ocr": Shot.take("ocr"); break
369 case "rec": Rec.toggle("screen"); break
370 }
371 }
372
373 // ---- UI ----
374 readonly property string modeNow: filter.text.charAt(0) === ">" ? "run" : filter.text.charAt(0) === "=" ? "calc" : "all"
375 function setMode(m) {
376 var t = filter.text.replace(/^[>=]\s*/, "")
377 filter.text = m === "run" ? ">" + t : m === "calc" ? "=" + t : t
378 filter.forceActiveFocus()
379 }
380
381 Rectangle {
382 anchors.fill: parent
383 color: Theme.scrim
384 opacity: win.t
385 MouseArea { anchors.fill: parent; onClicked: win.closeRequested() }
386 }
387
388 Rectangle {
389 id: card
390 anchors.horizontalCenter: parent.horizontalCenter
391 y: Math.round(parent.height * 0.13) + 24 * (1 - win.t)
392 width: Math.min(parent.width - 80, win.dmenu ? 760 : 860)
393 height: 30 + 66 + (win.rowsShown > 0 ? 20 + win.rowsShown * win.rowH : 0) + 30
394 opacity: win.t
395 radius: 44
396 color: Theme.fade(Theme.s1, Theme.panelAlpha)
397 border.width: 1
398 border.color: Theme.s2
399 MouseArea { anchors.fill: parent }
400
401 // ---- search pill ----
402 Rectangle {
403 id: search
404 x: 30
405 y: 30
406 width: parent.width - 60
407 height: 66
408 radius: 33
409 color: Theme.s2
410
411 Rectangle { // the dmenu prompt as a chip, else the lens / lock
412 id: promptBox
413 x: 12
414 anchors.verticalCenter: parent.verticalCenter
415 visible: win.dmenu && win.dmPrompt !== ""
416 width: visible ? promptText.implicitWidth + 28 : 0
417 height: 42
418 radius: 21
419 color: Theme.fill
420 Lb { id: promptText; anchors.centerIn: parent; text: win.dmPrompt.replace(/[:>]\s*$/, ""); color: Theme.fillFg }
421 }
422 Icon {
423 id: lens
424 x: 24
425 anchors.verticalCenter: parent.verticalCenter
426 visible: !promptBox.visible
427 text: win.dmPassword ? Theme.iLock : Theme.iSearch
428 color: Theme.grey
429 font.pixelSize: 22
430 }
431 TextInput {
432 id: filter
433 anchors.left: promptBox.visible ? promptBox.right : lens.right
434 anchors.leftMargin: 16
435 anchors.right: chips.visible ? chips.left : parent.right
436 anchors.rightMargin: 16
437 anchors.verticalCenter: parent.verticalCenter
438 echoMode: win.dmPassword ? TextInput.Password : TextInput.Normal
439 passwordCharacter: "•"
440 color: Theme.fg
441 selectionColor: Theme.fill
442 selectedTextColor: Theme.fillFg
443 font.family: win.dmPassword ? Theme.mono : Theme.ui
444 font.pixelSize: 22
445 font.letterSpacing: win.dmPassword ? 3 : 0
446 clip: true
447 cursorDelegate: Rectangle {
448 width: 2
449 color: Theme.live
450 visible: filter.activeFocus
451 Timer { running: filter.activeFocus; interval: 500; repeat: true; onTriggered: parent.opacity = 1 - parent.opacity }
452 }
453 Text {
454 visible: filter.text === ""
455 text: win.dmenu ? (win.dmPlaceholder !== "" ? win.dmPlaceholder : (win.dmPassword ? "Password" : "Type to filter"))
456 : "Search apps, commands, =calc, >run"
457 font.family: Theme.ui
458 font.pixelSize: 22
459 color: Theme.mute
460 }
461 onTextChanged: { win.current = 0; win.armed = "" }
462 Keys.onEscapePressed: win.closeRequested()
463 Keys.onReturnPressed: event => win.trigger(win.current, event.modifiers & Qt.ControlModifier)
464 Keys.onEnterPressed: event => win.trigger(win.current, event.modifiers & Qt.ControlModifier)
465 Keys.onPressed: event => {
466 var n = win.shown.length
467 if (!n) return
468 var step = 0
469 if (event.key === Qt.Key_Down || event.key === Qt.Key_Tab) step = 1
470 else if (event.key === Qt.Key_Up || event.key === Qt.Key_Backtab) step = -1
471 else if (event.key === Qt.Key_PageDown) step = Math.max(2, win.rowsShown)
472 else if (event.key === Qt.Key_PageUp) step = -Math.max(2, win.rowsShown)
473 else if ((event.modifiers & Qt.ControlModifier) && (event.key === Qt.Key_N || event.key === Qt.Key_J)) step = 1
474 else if ((event.modifiers & Qt.ControlModifier) && (event.key === Qt.Key_P || event.key === Qt.Key_K)) step = -1
475 if (step === 0) return
476 if (Math.abs(step) === 1) win.current = (win.current + step + n) % n
477 else win.current = Math.max(0, Math.min(n - 1, win.current + step))
478 win.armed = ""
479 event.accepted = true
480 }
481 }
482 Row {
483 id: chips
484 visible: !win.dmenu
485 anchors.right: parent.right
486 anchors.rightMargin: 12
487 anchors.verticalCenter: parent.verticalCenter
488 spacing: 6
489 Repeater {
490 model: ["all", "run", "calc"]
491 Chip {
492 required property string modelData
493 text: modelData
494 selected: win.modeNow === modelData
495 onClicked: win.setMode(modelData)
496 }
497 }
498 }
499 }
500
501 ListView {
502 id: list
503 x: 30
504 anchors.top: search.bottom
505 anchors.topMargin: 20
506 width: parent.width - 60
507 height: win.rowsShown * win.rowH
508 visible: win.rowsShown > 0
509 clip: true
510 spacing: 0
511 boundsBehavior: Flickable.StopAtBounds
512 model: win.shown
513 currentIndex: win.current
514 highlightFollowsCurrentItem: false
515 onCurrentIndexChanged: positionViewAtIndex(currentIndex, ListView.Contain)
516 delegate: Item {
517 id: row
518 required property var modelData
519 required property int index
520 readonly property bool selected: win.current === index
521 readonly property bool isArmed: win.armed !== "" && win.armed === modelData.key
522 readonly property bool isDm: modelData.kind === "dm"
523 readonly property color ink: isArmed ? Theme.errFg : selected ? Theme.fillFg : Theme.fg
524 width: list.width
525 height: win.rowH
526
527 Rectangle {
528 anchors.fill: parent
529 anchors.topMargin: 2
530 anchors.bottomMargin: 2
531 radius: height / 2
532 color: row.isArmed ? Theme.err : row.selected ? Theme.fill : rm.containsMouse ? Theme.s2 : "transparent"
533 }
534 Rectangle {
535 id: icoBox
536 x: 7
537 anchors.verticalCenter: parent.verticalCenter
538 visible: !row.isDm
539 width: visible ? 44 : 0
540 height: 44
541 radius: 22
542 color: row.selected || row.isArmed ? Theme.fade(row.ink, 0.14) : Theme.s2
543 Image {
544 id: img
545 anchors.centerIn: parent
546 width: 26
547 height: 26
548 visible: row.modelData.icon !== "" && status === Image.Ready
549 source: row.modelData.icon
550 sourceSize: Qt.size(52, 52)
551 asynchronous: true
552 smooth: true
553 layer.enabled: Settings.appIcons !== "colour"
554 layer.effect: Settings.appIcons === "mono" ? monoFx : dotFx
555 Component { id: dotFx; DotMask { step: 2 } }
556 Component { id: monoFx; MultiEffect { saturation: -1 } }
557 }
558 Icon {
559 anchors.centerIn: parent
560 visible: !img.visible
561 text: row.modelData.glyph
562 color: row.ink
563 font.pixelSize: 19
564 }
565 }
566 Lb {
567 id: tag
568 anchors.right: parent.right
569 anchors.rightMargin: 22
570 anchors.verticalCenter: parent.verticalCenter
571 px: 10
572 text: row.isArmed ? "↵ again · sure?" : row.modelData.tag
573 color: row.isArmed || row.selected ? row.ink : Theme.grey
574 }
575 Column {
576 anchors.left: icoBox.right
577 anchors.leftMargin: row.isDm ? 22 : 14
578 anchors.right: tag.left
579 anchors.rightMargin: 14
580 anchors.verticalCenter: parent.verticalCenter
581 Text {
582 width: parent.width
583 elide: Text.ElideRight
584 text: row.modelData.name
585 color: row.ink
586 font.family: row.modelData.kind === "calc" ? Theme.mono : Theme.ui
587 font.pixelSize: row.modelData.kind === "calc" ? 20 : 15
588 font.weight: row.modelData.kind === "calc" ? Font.Bold : Font.DemiBold
589 }
590 Lb {
591 visible: row.modelData.sub !== ""
592 width: parent.width
593 elide: Text.ElideRight
594 px: 10
595 text: row.modelData.sub
596 color: row.selected || row.isArmed ? row.ink : Theme.grey
597 opacity: row.selected ? 0.75 : 1
598 }
599 }
600 MouseArea {
601 id: rm
602 anchors.fill: parent
603 hoverEnabled: true
604 cursorShape: Qt.PointingHandCursor
605 onEntered: if (win.current !== row.index) { win.current = row.index; win.armed = "" }
606 onClicked: win.trigger(row.index, false)
607 }
608 }
609 }
610 }
611
612 Row {
613 anchors.horizontalCenter: parent.horizontalCenter
614 anchors.top: card.bottom
615 anchors.topMargin: 26
616 opacity: win.t
617 spacing: 34
618 Repeater {
619 model: !win.dmenu ? (win.shown.length ? ["↑↓ select", "↵ run", "ctrl ↵ terminal", "=calc", ">cmd", "esc close"] : ["no match", "esc close"])
620 : win.rowsShown === 0 ? ["↵ ok", "esc cancel"]
621 : win.shown.length ? ["↑↓ select", "↵ choose", "ctrl ↵ typed text", "esc cancel"] : ["no match", "↵ typed text", "esc cancel"]
622 Lb { required property string modelData; text: modelData }
623 }
624 }
625}