Krolyxon
Branch master

Branches

Refs →

aurora.configquickshellauroraRunner.qml

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