Krolyxon
Commit d48166e

Branches

Refs →

aurora.configquickshellauroraRunner.qml

564 lines · 25.8 KB · QML

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