Krolyxon
Commit 84eb284

Branches

Refs →

aurora.configquickshellauroraChars.qml

235 lines · 11.0 KB · QML

1pragma Singleton
2import QtQuick
3import Quickshell
4import Quickshell.Io
5
6// Glyph data for CharPicker: chars.json (emoji, nerd font, unicode symbols, kaomoji; built by
7// chars-gen.py), use counts in ~/.cache/aurora/chars.json, fancy text styles, copy / type.
8// The 900K json is only parsed the first time the picker opens.
9Singleton {
10 id: root
11 readonly property string home: Quickshell.env("HOME")
12 property bool loaded: false
13 property var sets: [] // [{id, label, groups: [label]}]
14 property var items: [] // [{c, n, k, s, g, t, nl, kl}], t = 5 skin-tone variants or undefined
15 property var byChar: ({})
16 property var counts: ({}) // char -> {n: uses, t: last used ms}
17 property bool hasWtype: false
18
19 function load() { if (data.path === "") data.path = home + "/.config/quickshell/aurora/chars.json" }
20 // parse it in the background a bit after startup, so the first SUPER+grave is instant
21 Timer { interval: 5000; running: true; onTriggered: root.load() }
22
23 FileView {
24 id: data
25 printErrors: true
26 onLoaded: {
27 var d
28 try { d = JSON.parse(text()) } catch (e) { console.warn("chars.json:", e); return }
29 var out = [], by = {}, l = d.items
30 for (var i = 0; i < l.length; i++) {
31 var r = l[i]
32 var it = { i: i, c: r[0], n: r[1], k: r[2], s: r[3], g: r[4], t: r[5],
33 nl: r[1].toLowerCase(), kl: " " + r[2].toLowerCase() + " " + d.sets[r[3]].groups[r[4]].toLowerCase() + " " }
34 out.push(it)
35 if (by[it.c] === undefined) by[it.c] = it
36 }
37 root.sets = d.sets
38 root.byChar = by
39 root.items = out
40 root.loaded = true
41 }
42 }
43 FileView {
44 id: store
45 path: root.home + "/.cache/aurora/chars.json"
46 printErrors: false
47 onLoaded: { try { root.counts = JSON.parse(text()) } catch (e) { root.counts = {} } }
48 }
49 Process {
50 command: ["sh", "-c", "command -v wtype"]
51 running: true
52 onExited: code => root.hasWtype = code === 0
53 }
54
55 function setIndex(id) {
56 for (var i = 0; i < sets.length; i++) if (sets[i].id === id) return i
57 return -1
58 }
59 // the glyph to output for an item: skin tone applied when it has variants
60 function glyph(it, tone) { return it.t && tone > 0 ? it.t[tone - 1] : it.c }
61 function hex(c) {
62 var out = [], i = 0
63 while (i < c.length) {
64 var cp = c.codePointAt(i)
65 if (cp !== 0xFE0F && cp !== 0x200D) out.push("U+" + cp.toString(16).toUpperCase().padStart(4, "0"))
66 i += cp > 0xFFFF ? 2 : 1
67 }
68 return out.join(" ")
69 }
70
71 // ---- search ----
72 // sets: list of set indexes to search, group: -1 or a group of a single set.
73 // Every query word must match the name (fuzzy) or a keyword (word start / substring).
74 // Also: a pasted glyph, or a codepoint (u+f0001, 0xf0001, f0001).
75 function search(q, sl, group) {
76 var out = [], l = items
77 q = q.trim().toLowerCase()
78 var inSet = {}
79 for (var j = 0; j < sl.length; j++) inSet[sl[j]] = true
80 if (q === "") {
81 for (var i = 0; i < l.length; i++)
82 if (inSet[l[i].s] && (group < 0 || l[i].g === group)) out.push(l[i])
83 return out
84 }
85 var cpm = q.match(/^(?:u\+|0x|\\u)([0-9a-f]{2,6})$/), cpq = -1
86 if (cpm) cpq = parseInt(cpm[1], 16)
87 else if (/^[0-9a-f]{4,6}$/.test(q) && /[0-9]/.test(q)) cpq = parseInt(q, 16) // "face" is a word, "f001" a codepoint
88 var glyphq = /[^\x00-\x7f]/.test(q) ? q : ""
89 var words = q.split(/\s+/), scored = []
90 for (i = 0; i < l.length; i++) {
91 var it = l[i]
92 if (!inSet[it.s] || (group >= 0 && it.g !== group)) continue
93 var sc = 0
94 if (glyphq !== "") {
95 if (it.c === glyphq || it.c.replace("️", "") === glyphq.replace("️", "")) sc = 5000
96 } else if (cpq >= 0 && it.c.codePointAt(0) === cpq && it.c.length <= 2) {
97 sc = 5000
98 } else {
99 for (j = 0; j < words.length; j++) {
100 var w = words[j]
101 var s = Fuzzy.scoreLow(it.nl, w)
102 if (s < 300 && s < 99) s = 0 // scattered: one skipped letter at most ("thmbs"), not letters strewn over a name
103 if (s < 450 && it.kl.indexOf(" " + w) >= 0) s = 450
104 else if (s < 150 && w.length > 1 && it.kl.indexOf(w) >= 0) s = 150
105 if (s === 0) { sc = 0; break }
106 sc += s
107 }
108 if (sc === 0) continue
109 if (it.nl === q) sc += 800
110 sc -= it.nl.length * 0.5
111 if (sets[it.s].id === "emoji") sc += 40 // 11k nerd glyphs shouldn't drown the emoji
112 }
113 if (sc <= 0) continue
114 var u = counts[it.c]
115 if (u) sc += Math.min(u.n, 20) * 15
116 scored.push({ it: it, sc: sc })
117 }
118 scored.sort((a, b) => b.sc - a.sc || a.it.i - b.it.i) // ties keep file order (canonical ¯\_(ツ)_/¯ first)
119 for (i = 0; i < scored.length; i++) out.push(scored[i].it)
120 return out
121 }
122 // most recently used first; kaomoji excluded (they live in their own list view)
123 function recent(limit) {
124 var l = []
125 for (var c in counts) {
126 var it = byChar[c]
127 if (it && sets[it.s].id !== "kao") l.push({ it: it, t: counts[c].t })
128 }
129 l.sort((a, b) => b.t - a.t)
130 var out = []
131 for (var i = 0; i < l.length && i < limit; i++) out.push(l[i].it)
132 return out
133 }
134 // used kaomoji first (by count), rest in file order
135 function byUse(list) {
136 var u = [], rest = []
137 for (var i = 0; i < list.length; i++) (counts[list[i].c] ? u : rest).push(list[i])
138 u.sort((a, b) => counts[b.c].n - counts[a.c].n)
139 return u.concat(rest)
140 }
141 function bump(c) {
142 var n = {}
143 for (var k in counts) n[k] = counts[k]
144 n[c] = { n: (n[c] ? n[c].n : 0) + 1, t: Date.now() }
145 counts = n
146 store.setText(JSON.stringify(n))
147 }
148
149 // ---- output ----
150 function copy(s) {
151 Quickshell.execDetached(["sh", "-c", 'wl-copy -- "$1" && notify-send -a aurora -t 1500 "copied" "$1"', "sh", s])
152 }
153 // the picker closes first, so give the focus a moment to go back to the app
154 function type(s) {
155 Quickshell.execDetached(["sh", "-c", 'sleep 0.15; wtype -- "$1"', "sh", s])
156 }
157
158 // ---- fancy text ----
159 // [label, A, a, 0] start codepoints in Mathematical Alphanumeric Symbols, holes filled from Letterlike
160 readonly property var mathStyles: [
161 ["bold", 0x1D400, 0x1D41A, 0x1D7CE], ["italic", 0x1D434, 0x1D44E, 0], ["bold italic", 0x1D468, 0x1D482, 0],
162 ["script", 0x1D49C, 0x1D4B6, 0], ["bold script", 0x1D4D0, 0x1D4EA, 0],
163 ["fraktur", 0x1D504, 0x1D51E, 0], ["bold fraktur", 0x1D56C, 0x1D586, 0],
164 ["double-struck", 0x1D538, 0x1D552, 0x1D7D8], ["sans", 0x1D5A0, 0x1D5BA, 0x1D7E2],
165 ["sans bold", 0x1D5D4, 0x1D5EE, 0x1D7EC], ["sans italic", 0x1D608, 0x1D622, 0],
166 ["monospace", 0x1D670, 0x1D68A, 0x1D7F6]]
167 readonly property var holes: ({
168 "italic": { h: 0x210E },
169 "script": { B: 0x212C, E: 0x2130, F: 0x2131, H: 0x210B, I: 0x2110, L: 0x2112, M: 0x2133, R: 0x211B,
170 e: 0x212F, g: 0x210A, o: 0x2134 },
171 "fraktur": { C: 0x212D, H: 0x210C, I: 0x2111, R: 0x211C, Z: 0x2128 },
172 "double-struck": { C: 0x2102, H: 0x210D, N: 0x2115, P: 0x2119, Q: 0x211A, R: 0x211D, Z: 0x2124 }
173 })
174 readonly property string smallCaps: "ᴀʙᴄᴅᴇꜰɢʜɪᴊᴋʟᴍɴᴏᴘǫʀꜱᴛᴜᴠᴡxʏᴢ"
175 readonly property string flipLower: "ɐqɔpǝɟƃɥᴉɾʞlɯuodbɹsʇnʌʍxʎz"
176 readonly property var flipOther: ({ "0": "0", "1": "Ɩ", "2": "ᄅ", "3": "Ɛ", "4": "ㄣ", "5": "ϛ", "6": "9", "7": "ㄥ",
177 "8": "8", "9": "6", ".": "˙", ",": "'", "'": ",", "\"": "„", "?": "¿", "!": "¡", "(": ")", ")": "(",
178 "[": "]", "]": "[", "{": "}", "}": "{", "<": ">", ">": "<", "_": "‾", "&": "⅋" })
179
180 function mapLetters(t, f) {
181 var out = "", l = Array.from(t) // by codepoint, so emoji in the text survive
182 for (var i = 0; i < l.length; i++) {
183 var r = f(l[i], l[i].codePointAt(0))
184 out += r === null ? l[i] : r
185 }
186 return out
187 }
188 function fancy(t) {
189 var out = [], st = mathStyles
190 for (var i = 0; i < st.length; i++) {
191 var s = st[i], h = holes[s[0]] || {}
192 out.push({ c: mapLetters(t, (ch, code) => {
193 if (h[ch]) return String.fromCodePoint(h[ch])
194 if (code >= 65 && code <= 90) return String.fromCodePoint(s[1] + code - 65)
195 if (code >= 97 && code <= 122) return String.fromCodePoint(s[2] + code - 97)
196 if (code >= 48 && code <= 57 && s[3]) return String.fromCodePoint(s[3] + code - 48)
197 return null
198 }), n: s[0] })
199 }
200 out.push({ n: "small caps", c: mapLetters(t, (ch, code) => {
201 var c = ch.toLowerCase().charCodeAt(0)
202 return c >= 97 && c <= 122 ? smallCaps[c - 97] : null
203 }) })
204 out.push({ n: "full width", c: mapLetters(t, (ch, code) =>
205 code === 32 ? " " : (code > 32 && code < 127 ? String.fromCharCode(code + 0xFEE0) : null)) })
206 out.push({ n: "circled", c: mapLetters(t, (ch, code) => {
207 if (code >= 65 && code <= 90) return String.fromCodePoint(0x24B6 + code - 65)
208 if (code >= 97 && code <= 122) return String.fromCodePoint(0x24D0 + code - 97)
209 if (code === 48) return "⓪"
210 if (code >= 49 && code <= 57) return String.fromCodePoint(0x2460 + code - 49)
211 return null
212 }) })
213 out.push({ n: "negative circled", c: mapLetters(t, (ch, code) => {
214 var c = ch.toUpperCase().charCodeAt(0)
215 return c >= 65 && c <= 90 ? String.fromCodePoint(0x1F150 + c - 65) : null
216 }) })
217 out.push({ n: "squared", c: mapLetters(t, (ch, code) => {
218 var c = ch.toUpperCase().charCodeAt(0)
219 return c >= 65 && c <= 90 ? String.fromCodePoint(0x1F130 + c - 65) : null
220 }) })
221 out.push({ n: "negative squared", c: mapLetters(t, (ch, code) => {
222 var c = ch.toUpperCase().charCodeAt(0)
223 return c >= 65 && c <= 90 ? String.fromCodePoint(0x1F170 + c - 65) : null
224 }) })
225 out.push({ n: "strikethrough", c: mapLetters(t, (ch, code) => code === 32 ? null : ch + "̶") })
226 out.push({ n: "underline", c: mapLetters(t, (ch, code) => ch + "̲") })
227 out.push({ n: "upside down", c: Array.from(mapLetters(t, (ch, code) => {
228 var c = ch.toLowerCase().charCodeAt(0)
229 if (c >= 97 && c <= 122) return flipLower[c - 97]
230 return flipOther[ch] !== undefined ? flipOther[ch] : null
231 })).reverse().join("") })
232 out.push({ n: "spaced", c: Array.from(t).join(" ") })
233 return out
234 }
235}