| 1 | import QtQuick |
| 2 | import QtQuick.Layouts |
| 3 | import QtQuick.Effects |
| 4 | import Quickshell |
| 5 | import Quickshell.Io |
| 6 | import Quickshell.Wayland |
| 7 | import 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. |
| 15 | PanelWindow { |
| 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 | } |