diff --git a/Backend.qml b/Backend.qml index 8895670..978b036 100644 --- a/Backend.qml +++ b/Backend.qml @@ -12,6 +12,7 @@ import "core/Frecency.js" as Frecency import "core/OmarchyMenu.js" as OmarchyMenu import "core/RootSearch.js" as RootSearch import "core/Calculator.js" as Calculator +import "core/Emoji.js" as Emoji import "services" Item { @@ -26,7 +27,7 @@ Item { property var config: Schema.withConfigDefaults(null) property bool directoriesReady: false - readonly property bool ready: directoriesReady && settingsStore.loaded && frecencyStore.loaded && calculatorStore.loaded + readonly property bool ready: directoriesReady && settingsStore.loaded && frecencyStore.loaded && calculatorStore.loaded && emojiStore.loaded readonly property var registry: Rpc.createRegistry() readonly property double startedAt: Date.now() @@ -35,7 +36,7 @@ Item { property var candidates: [] property var candidatesById: ({}) property int candidatesRevision: 0 - readonly property bool loading: menuSource.loading || nativeSearch.pending !== null + readonly property bool loading: menuSource.loading || nativeSearch.pending !== null || emojiSearch.pending !== null // Bumped when exchange rates or zone offsets arrive, so an open // calculator row re-evaluates. readonly property int calculatorRevision: (calculatorData.rates ? 1 : 0) + calculatorData.ratesFetchedAt + calculatorData.zonesAt @@ -45,12 +46,18 @@ Item { function call(method, data) { return Rpc.call(backend.registry, method, data) } function callSync(method, data) { return Rpc.callSync(backend.registry, method, data) } - function cancelSearch() { nativeSearch.cancel() } + function cancelSearch() { nativeSearch.cancel(); emojiSearch.cancel() } function searchAsync(data, callback) { nativeSearch.cancel() var query = String(data && data.query || "").trim() var view = data && data.view + if (query && view && view.kind === "emoji") { + emojiSearch.search(Ranker.transliterate(query.toLowerCase()), "medium", function(matches) { + callback(backend.callSync("rootSearch.search", Object.assign({}, data, { fleaHits: matches }))) + }) + return + } if (!query || (view && view.kind !== "root")) { callback(backend.callSync("rootSearch.search", data)) return @@ -64,6 +71,14 @@ Item { id: nativeSearch onFailed: function(message) { backend.log("warning", "search", message) } } + + // A second flea helper over the emoji list; null hits fall back to Emoji.matchScore. + NativeSearch { + id: emojiSearch + ids: backend.emoji.list.map(function(e) { return e.symbol }) + entries: FleaSearch.entries(Emoji.searchEntries(backend.emoji.list), Ranker.transliterate) + onFailed: function(message) { backend.log("warning", "emoji-search", message) } + } function subscribe(method, callback) { return Rpc.subscribe(backend.registry, method, callback) } function emit(method, data) { @@ -202,6 +217,79 @@ Item { return null } + // ---------------------------------------------------------------- emoji + + property string emojiRaw: "" + property string emojiKeywordsRaw: "" + readonly property var emoji: Emoji.parse(backend.emojiRaw, backend.emojiKeywordsRaw) + + function emojiEntry(id) { + var symbol = String(id).slice("emoji:".length) + var list = backend.emoji.list + for (var i = 0; i < list.length; i++) if (list[i].symbol === symbol) return list[i] + return null + } + + function emojiState(filter) { + var doc = emojiStore.loaded ? emojiStore.doc : { items: {}, pinned: [], skinTone: "none" } + var nowSec = Date.now() / 1000 + var recent = Object.keys(doc.items).sort(function(a, b) { + return Frecency.score(doc.items[b].anchor, nowSec) - Frecency.score(doc.items[a].anchor, nowSec) + }) + return { pinned: doc.pinned, recent: recent, tone: doc.skinTone, filter: filter || "all" } + } + + function emojiCandidate(id) { + var e = backend.emojiEntry(id) + if (!e) throw new Error("Unknown emoji " + id) + var state = backend.emojiState() + return { entry: e, candidate: Emoji.candidate(e, state.tone, state.pinned) } + } + + function setPinned(symbol, pin) { + emojiStore.update(function(doc) { + doc.pinned = doc.pinned.filter(function(s) { return s !== symbol }) + if (pin) doc.pinned.push(symbol) + }) + backend.emit("emojiPicker.stateChanged", null) + } + + function emojiAction(action, id, query) { + var found = backend.emojiCandidate(id) + var e = found.entry, c = found.candidate + function uprank() { + emojiStore.update(function(doc) { doc.items[e.symbol] = Frecency.recordVisit(doc.items[e.symbol] || null, Date.now(), query || "") }) + } + switch (action) { + case "open": + case "paste": + uprank() + clipboard.paste(c.symbol) + return { close: true } + case "copy": + uprank() + clipboard.copy(c.symbol) + return { close: true, hud: "Copied to Clipboard" } + case "copy-name": + clipboard.copy(c.title) + return { close: true, hud: "Copied to Clipboard" } + case "pin": + case "unpin": + backend.setPinned(e.symbol, action === "pin") + return { toast: { style: "success", title: action === "pin" ? "Pinned" : "Unpinned" } } + case "remove-recent": + emojiStore.update(function(doc) { delete doc.items[e.symbol] }) + return { toast: { style: "success", title: "Removed from Recently Used" } } + case "tone:none": case "tone:light": case "tone:medium-light": case "tone:medium": case "tone:medium-dark": case "tone:dark": + backend.callSync("emojiPicker.setSkinTone", { tone: action.slice(5) }) + return { toast: { style: "success", title: "Skin tone set" } } + case "clear-recent": + emojiStore.update(function(doc) { doc.items = {} }) + return { toast: { style: "success", title: "Recently Used cleared" } } + } + throw new Error("Unknown action " + action) + } + // ------------------------------------------------------------- execution function summonPlugin(id, payload) { @@ -428,12 +516,14 @@ Item { if (data && data.fleaHits) ctx.fleaHits = data.fleaHits var sections if (view && view.kind === "menu") sections = RootSearch.submenuSections(backend.candidates, view.menuId, query, ctx, backend.deps) + else if (view && view.kind === "emoji") sections = Emoji.sections(backend.emoji.list, query, Object.assign(backend.emojiState(), { fleaHits: data.fleaHits || null })) else if (view && view.kind === "calculator-history") sections = RootSearch.historySections(calculatorStore.doc.history, query, ctx.nowMs, Calculator.filterHistory) else { ctx.filter = String(data && data.filter || "all") if (query.trim() && ctx.filter === "all") ctx.calculation = backend.calculate(query) sections = RootSearch.rootSections(backend.candidates, query, ctx, backend.deps) } + if (view && view.kind === "emoji") return sections return sections.map(function(s) { return { id: s.id, title: s.title, rows: s.rows.map(function(r) { return r.candidate }) } }) @@ -442,6 +532,7 @@ Item { var rowId = String(data && data.id || "") if (rowId === RootSearch.CALCULATOR_ID) return RootSearch.actionsFor({ kind: "calculator" }) if (rowId.indexOf("calculator-history:") === 0) return RootSearch.actionsFor({ kind: "calculator-history" }) + if (rowId.indexOf("emoji:") === 0) return Emoji.actionsFor(backend.emojiCandidate(rowId).candidate, backend.emojiState().tone) var candidate = backend.decorated(rowId) if (!candidate) { var fallback = RootSearch.FALLBACKS.filter(function(f) { return f.id === data.id })[0] @@ -461,6 +552,10 @@ Item { var entry = backend.historyEntry(rowId) return entry ? RootSearch.aboutFor(RootSearch.historyCandidate(entry, nowMs), nowMs) : [] } + if (rowId.indexOf("emoji:") === 0) { + var found = backend.emojiCandidate(rowId) + return Emoji.aboutFor(found.candidate, found.entry) + } var candidate = backend.decorated(rowId) if (!candidate) { var f = RootSearch.FALLBACKS.filter(function(x) { return x.id === rowId })[0] @@ -491,6 +586,11 @@ Item { if (!entry) throw new Error("No such history entry") return backend.calculatorAction(String(data.action || "open"), entry, entry.id) } + if (data && String(data.id).indexOf("emoji:") === 0) { + var confirmClear = String(data.action) === "clear-recent" && !data.confirmed + if (confirmClear) return { confirm: { title: "Clear Recently Used", message: "This forgets every emoji you have pasted or copied.", primary: "Clear", destructive: true } } + return backend.emojiAction(String(data.action || "open"), data.id, String(data.query || "")) + } return backend.performAction(data) }, getSearchHistory: function() { return frecencyStore.loaded ? frecencyStore.doc.searchHistory : [] } @@ -520,6 +620,25 @@ Item { } }) + // Names from Raycast's emojiPicker namespace (ref 02 §2); setSkinTone and + // getSkinTone stand in for its preference [D]. + Rpc.register(backend.registry, "emojiPicker", { + search: function(data) { return Emoji.sections(backend.emoji.list, String(data && data.query || ""), backend.emojiState(data && data.filter)) }, + get: function(data) { return backend.emojiCandidate("emoji:" + String(data && data.symbol || "")).candidate }, + pin: function(data) { backend.emojiCandidate("emoji:" + data.symbol); backend.setPinned(String(data.symbol), true); return null }, + unpin: function(data) { backend.setPinned(String(data && data.symbol || ""), false); return null }, + uprank: function(data) { backend.emojiAction("open", "emoji:" + String(data && data.symbol || ""), ""); return null }, + resetRanking: function(data) { return backend.emojiAction("remove-recent", "emoji:" + String(data && data.symbol || ""), "") }, + getSkinTone: function() { return emojiStore.loaded ? emojiStore.doc.skinTone : "none" }, + setSkinTone: function(data) { + var tone = String(data && data.tone || "") + if (!Emoji.TONES.some(function(t) { return t.id === tone })) throw new Error("Skin tone must be one of " + Emoji.TONES.map(function(t) { return t.id }).join(", ")) + emojiStore.update(function(doc) { doc.skinTone = tone }) + backend.emit("emojiPicker.stateChanged", null) + return tone + } + }) + Rpc.register(backend.registry, "frecency", { visit: function(data) { backend.recordVisit(String(data.id), String(data.query || "")); return null }, get: function(data) { @@ -548,7 +667,7 @@ Item { function onValuesChanged() { rebuildTimer.restart() } } - Component.onDestruction: { settingsStore.flush(); frecencyStore.flush(); calculatorStore.flush() } + Component.onDestruction: { settingsStore.flush(); frecencyStore.flush(); calculatorStore.flush(); emojiStore.flush() } onNotification: function(method, data) { if (method === "window.visibilityChanged" && data && data.visible) { menuSource.refresh() @@ -574,7 +693,7 @@ Item { id: mkdirs // Moves directories left under the old name, then creates everything // owner-only and repairs modes left readable by older builds. - command: Paths.setupArgv(backend.paths, [Schema.documents.settings.file, Schema.documents.frecency.file, Schema.documents.calculator.file] + command: Paths.setupArgv(backend.paths, [Schema.documents.settings.file, Schema.documents.frecency.file, Schema.documents.calculator.file, Schema.documents.emoji.file] .map(function(file) { return backend.paths.db + "/" + file })) onExited: function(exitCode) { if (exitCode !== 0) backend.log("error", "paths", "could not create data directories", { exitCode: exitCode }) @@ -612,6 +731,28 @@ Item { onMessage: function(level, text, metadata) { backend.log(level, "store", text, metadata) } } + JsonStore { + id: emojiStore + ready: backend.directoriesReady + path: backend.paths.db + "/" + Schema.documents.emoji.file + migrations: Schema.documents.emoji.migrations + onMessage: function(level, text, metadata) { backend.log(level, "store", text, metadata) } + } + + FileView { + path: decodeURIComponent(Qt.resolvedUrl("data/emoji.json").toString().replace(/^file:\/\//, "")) + printErrors: false + onLoaded: backend.emojiRaw = text() + onLoadFailed: backend.log("error", "emoji", "could not read " + path) + } + + // Omarchy's emoji keywords (MIT, read in place) make searches like "lol" work. + FileView { + path: Quickshell.env("OMARCHY_PATH") ? Quickshell.env("OMARCHY_PATH") + "/shell/plugins/emojis/emojis.json" : "" + printErrors: false + onLoaded: backend.emojiKeywordsRaw = text() + } + CalculatorData { id: calculatorData cacheDir: backend.directoriesReady ? backend.paths.cache : "" @@ -624,6 +765,7 @@ Item { Connections { target: settingsStore; function onChanged() { backend.storeRevision++ } } Connections { target: frecencyStore; function onChanged() { backend.storeRevision++ } } Connections { target: calculatorStore; function onChanged() { backend.storeRevision++ } } + Connections { target: emojiStore; function onChanged() { backend.storeRevision++ } } FileView { path: backend.directoriesReady ? backend.paths.configFile : "" diff --git a/CLAUDE.md b/CLAUDE.md index b9d6019..aaa2cc7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -149,6 +149,7 @@ Works: config.json sets `remToPxRate` (16), `workHoursPerDay` (8) and `clockFormat` (`auto` follows the Qt locale, or `12h` / `24h`). ↵ copies the answer (HUD), Ctrl+↵ pastes it (wl-copy on stdin, then Shift+Insert), Ctrl+Shift+C copies "question = answer". Each one saves to **Calculator History** (`db/calculator.json`, 4096 rows / 90 days): a built-in command and pushed view, searchable, Ctrl+D deletes an entry, Ctrl+Shift+D deletes all. RPC: `calculator.calculate|historyGetAll|historyDelete|historyDeleteAll|refreshCurrencies|getCurrencies`. A bare number, word or quantity (`42`, `firefox`, `5 kg`) never produces a row, because the section takes ↵. +- **Emoji & Symbols** (`builtin:emoji-picker`, `core/Emoji.js`): an 8-column grid with sections for Pinned, Recently Used (frecency) and the Unicode categories, and the selected emoji's name in the footer. Arrow keys move around the grid. ↵ pastes, Ctrl+↵ copies, Ctrl+Shift+C copies the name, Ctrl+Shift+P pins, Ctrl+D / Ctrl+Shift+D remove recents. Skin tone is set from the action panel. Search uses flea's engine (a second `omacast-search` helper over the emoji list), and falls back to `Emoji.matchScore` when the helper isn't built. Global shortcut `omacast:emoji` (bound to Super+Ctrl+Space in this machine's `omacast.lua`, replacing Omarchy's background switcher). It opens in ~70 ms. Each color-emoji glyph is decoded again on every show, because hiding the window drops its render-thread glyph cache, so the grid turns off the ListView `cacheBuffer` to decode only the lines on screen. The emoji list is `data/emoji.json`, built from Unicode 17 `emoji-test.txt` by `bin/build-emoji`. Search keywords are read in place from Omarchy's `shell/plugins/emojis/emojis.json`. State lives in `db/emoji.json`. RPC: `emojiPicker.search|get|pin|unpin|uprank|resetRanking|getSkinTone|setSkinTone`. Not done: Unicode symbols, custom keywords, moving pins up and down, and a category dropdown (removed on request). - **Latency:** show → mapped in 59–66 ms, hide in 24–42 ms on Omarchy 4.0.6 (`tests/smoke.sh`; it was 37–44 ms on the old `OverlayWindow`). - **Tests:** root search, calculator and privacy coverage (`node --test tests/*.test.js`). diff --git a/Launcher.qml b/Launcher.qml index f14679f..036a508 100644 --- a/Launcher.qml +++ b/Launcher.qml @@ -92,6 +92,14 @@ Item { onPressed: root.toggle() } + // Opens Emoji & Symbols directly: hl.dsp.global("omacast:emoji"). + GlobalShortcut { + appid: "omacast" + name: "emoji" + description: "Open omacast Emoji & Symbols" + onPressed: root.run("builtin:emoji-picker") + } + IpcHandler { target: "omacast" diff --git a/bin/build-emoji b/bin/build-emoji new file mode 100755 index 0000000..9434b26 --- /dev/null +++ b/bin/build-emoji @@ -0,0 +1,32 @@ +#!/usr/bin/env node +// Builds data/emoji.json from Unicode's emoji-test.txt (Unicode License v3): +// bin/build-emoji path/to/emoji-test.txt +// Each row is [symbol, name, group index, skin-toneable 0/1]. Toned variants +// and the Component group are left out; tones are applied at paste time. +const fs = require("fs") +const path = require("path") +const src = process.argv[2] +if (!src) { console.error("usage: bin/build-emoji emoji-test.txt"); process.exit(2) } +const text = fs.readFileSync(src, "utf8") +const version = (text.match(/^# Version: (\S+)/m) || [])[1] || "?" +const TONE = /[\u{1F3FB}-\u{1F3FF}]/u +const all = new Set() +const rows = [] +const groups = [] +let group = "" +for (const line of text.split("\n")) { + const g = line.match(/^# group: (.+)/) + if (g) { group = g[1]; continue } + const m = line.match(/^[0-9A-F ]+;\s*fully-qualified\s*#\s*(\S+)\s+E\S+\s+(.+)$/) + if (!m) continue + all.add(m[1]) + if (group === "Component" || TONE.test(m[1])) continue + if (groups.indexOf(group) < 0) groups.push(group) + rows.push([m[1], m[2], groups.indexOf(group)]) +} +// Same rule as core/Emoji.js applyTone: the modifier follows the first code point. +const toned = (s) => { const cps = Array.from(s); return cps[0] + "\u{1F3FB}" + cps.slice(cps[1] === "️" ? 2 : 1).join("") } +for (const r of rows) r.push(all.has(toned(r[0])) ? 1 : 0) +const out = path.join(__dirname, "..", "data", "emoji.json") +fs.writeFileSync(out, JSON.stringify({ source: "Unicode emoji-test.txt " + version, groups, emoji: rows }) + "\n") +console.log(`${rows.length} emoji, ${rows.filter((r) => r[3]).length} toneable -> ${out}`) diff --git a/core/Emoji.js b/core/Emoji.js new file mode 100644 index 0000000..06ae4b9 --- /dev/null +++ b/core/Emoji.js @@ -0,0 +1,186 @@ +// Emoji & Symbols (ref 04 §1 emoji-picker): data parsing, search, sections, +// skin tones and grid navigation. Pure JS, shared by QML and node tests. + +var COLUMNS = 8 // ref 04 §1 columnCount = 8 [O] +var RECENT_LIMIT = 16 // two grid rows [I] + +// Raycast's skin tone setting (ref 04 App. B emojiSkinTone) [O]; ids [D]. +var TONES = [ + { id: "none", title: "Default", modifier: "" }, + { id: "light", title: "Light", modifier: "🏻" }, + { id: "medium-light", title: "Medium-Light", modifier: "🏼" }, + { id: "medium", title: "Medium", modifier: "🏽" }, + { id: "medium-dark", title: "Medium-Dark", modifier: "🏾" }, + { id: "dark", title: "Dark", modifier: "🏿" } +] + +// data: data/emoji.json; keywordsRaw: Omarchy's emojis.json ([{e, k}]), +// whose keywords are merged in when it is present. +function parse(dataRaw, keywordsRaw) { + var data + try { data = JSON.parse(String(dataRaw || "")) } catch (e) { return { groups: [], list: [] } } + var extra = {} + try { + var omarchy = JSON.parse(String(keywordsRaw || "[]")) + for (var i = 0; i < omarchy.length; i++) if (omarchy[i] && omarchy[i].e) extra[omarchy[i].e] = String(omarchy[i].k || "") + } catch (e) {} + var groups = data.groups || [] + var list = (data.emoji || []).map(function(r) { + var name = String(r[1]) + return { + symbol: r[0], name: name, title: titleCase(name), group: groups[r[2]] || "", toneable: r[3] === 1, + // Omarchy's keywords lack the emoji-variation selector on some symbols. + words: (name + " " + (extra[r[0]] || extra[r[0].replace(/\uFE0F/g, "")] || "")).toLowerCase() + } + }) + return { groups: groups, list: list } +} + +function titleCase(name) { + return name.replace(/(^|[\s:(-])([a-z])/g, function(_, pre, ch) { return pre + ch.toUpperCase() }) +} + +function applyTone(symbol, tone) { + var t = TONES.filter(function(x) { return x.id === tone })[0] + if (!t || !t.modifier) return symbol + // Not Array.from: QML's engine splits strings into UTF-16 units there. + var first = codePoints(symbol)[0] || "" + var rest = symbol.slice(first.length) + return first + t.modifier + (rest.charAt(0) === "\uFE0F" ? rest.slice(1) : rest) +} + +function codePoints(s) { + var out = [] + for (var i = 0; i < s.length; i++) { + var c = s.charCodeAt(i) + var pair = c >= 0xD800 && c <= 0xDBFF && i + 1 < s.length + out.push(pair ? s.slice(i, i + 2) : s.charAt(i)) + if (pair) i++ + } + return out +} + +// 0 no match; higher is better. Every query word must start a word of the +// name or keywords. Whole words beat prefixes ("lol" finds 😂 before 🍭), +// the name beats keywords, and the exact name wins. [I] +function matchScore(item, words) { + var name = item.name.toLowerCase() + var score = 0 + for (var i = 0; i < words.length; i++) { + var w = words[i].replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + var start = "(^|[\\s:_-])" + w, whole = start + "($|[\\s:_-])" + if (new RegExp(whole).test(name)) score += 4 + else if (new RegExp(whole).test(item.words)) score += 3 + else if (new RegExp(start).test(name)) score += 2 + else if (new RegExp(start).test(item.words)) score += 1 + else return 0 + } + if (name === words.join(" ")) score += 10 + return score +} + +// state: { pinned: [symbol], recent: [symbol by frecency], tone, filter } +function sections(list, query, state) { + var words = String(query || "").toLowerCase().split(/\s+/).filter(Boolean) + var filter = state.filter && state.filter !== "all" ? state.filter : "" + var bySymbol = {} + for (var i = 0; i < list.length; i++) bySymbol[list[i].symbol] = list[i] + function rows(items) { return items.map(function(e) { return candidate(e, state.tone, state.pinned) }) } + + if (words.length) { + var hits = [], recentAt = {} + ;(state.recent || []).forEach(function(sym, n) { recentAt[sym] = n }) + for (var j = 0; j < list.length; j++) { + if (filter && list[j].group !== filter) continue + var e = list[j], r = recentAt[e.symbol] === undefined ? Infinity : recentAt[e.symbol] + if (state.fleaHits) { + // Flea's engine (bin/omacast-search, SPEC §2.0): the best field wins, + // then the name match, then recently used, then Unicode order. + var h = state.fleaHits[e.symbol] + if (h) hits.push({ e: e, s: Math.max(h.title || 0, h.keyword || 0), r: r, t: h.title || 0, i: j }) + } else { + var s = matchScore(e, words) + if (s) hits.push({ e: e, s: s, r: r, t: 0, i: j }) + } + } + hits.sort(function(a, b) { return b.s - a.s || b.t - a.t || (a.r === b.r ? 0 : a.r < b.r ? -1 : 1) || a.i - b.i }) + return hits.length ? [{ id: "results", title: "Results", rows: rows(hits.map(function(h) { return h.e })) }] : [] + } + + var out = [] + if (!filter) { + var pinned = (state.pinned || []).map(function(s) { return bySymbol[s] }).filter(Boolean) + var recent = (state.recent || []).filter(function(s) { return (state.pinned || []).indexOf(s) < 0 }) + .map(function(s) { return bySymbol[s] }).filter(Boolean).slice(0, RECENT_LIMIT) + if (pinned.length) out.push({ id: "pinned", title: "Pinned", rows: rows(pinned) }) + if (recent.length) out.push({ id: "recent", title: "Recently Used", rows: rows(recent) }) + } + var byGroup = {}, order = [] + for (var k = 0; k < list.length; k++) { + var g = list[k].group + if (filter && g !== filter) continue + if (!byGroup[g]) { byGroup[g] = []; order.push(g) } + byGroup[g].push(list[k]) + } + for (var n = 0; n < order.length; n++) out.push({ id: "group-" + n, title: order[n], rows: rows(byGroup[order[n]]) }) + return out +} + +function candidate(e, tone, pinned) { + var symbol = e.toneable ? applyTone(e.symbol, tone) : e.symbol + return { + id: "emoji:" + e.symbol, type: "emoji", kind: "emoji", title: e.title, subtitle: e.group, + glyph: symbol, symbol: symbol, pinned: (pinned || []).indexOf(e.symbol) >= 0 + } +} + +// tone: the current skin tone, offered as "Skin Tone: …" actions. +function actionsFor(c, tone) { + return [ + { id: "paste", title: "Paste to Active App", shortcut: "↵", section: "primary", icon: "" }, + { id: "copy", title: "Copy to Clipboard", shortcut: "Ctrl ↵", section: "primary", icon: "" }, + { id: "copy-name", title: "Copy Name", shortcut: "Ctrl Shift C", section: "copy", icon: "" }, + { id: c.pinned ? "unpin" : "pin", title: c.pinned ? "Unpin Emoji" : "Pin Emoji", shortcut: "Ctrl Shift P", section: "pin", icon: "" }, + { id: "remove-recent", title: "Remove from Recently Used", shortcut: "Ctrl D", section: "delete", icon: "" }, + { id: "clear-recent", title: "Clear Recently Used", shortcut: "Ctrl Shift D", section: "delete", icon: "" } + ].concat(TONES.map(function(t) { + return { id: "tone:" + t.id, title: "Skin Tone: " + t.title, shortcut: "", section: "tone", icon: t.id === tone ? "✓" : "" } + })) +} + +function aboutFor(c, e) { + return [ + { label: "Emoji", value: c.symbol }, + { label: "Name", value: c.title }, + { label: "Category", value: c.subtitle }, + { label: "Code Points", value: codePoints(c.symbol).map(function(ch) { return "U+" + ch.codePointAt(0).toString(16).toUpperCase() }).join(" ") }, + { label: "Keywords", value: e ? e.words.slice(e.name.length).trim().split(/\s+/).filter(Boolean).join(", ") : "" } + ].filter(function(r) { return r.value }) +} + +// Flea search entries (FleaSearch.entries input): the name as title and +// the extra keywords; ids are the symbols. +function searchEntries(list) { + return list.map(function(e) { + return { title: e.name, keywords: e.words.slice(e.name.length).split(/\s+/).filter(Boolean) } + }) +} + +// Packs sections into grid lines: [{section}, {cells: [candidate…]}]. +function gridLines(sectionList, columns) { + var out = [] + for (var s = 0; s < sectionList.length; s++) { + out.push({ section: sectionList[s] }) + var rows = sectionList[s].rows + for (var i = 0; i < rows.length; i += columns) out.push({ cells: rows.slice(i, i + columns) }) + } + return out +} + +if (typeof module !== "undefined") { + module.exports = { + COLUMNS: COLUMNS, RECENT_LIMIT: RECENT_LIMIT, TONES: TONES, + parse: parse, titleCase: titleCase, applyTone: applyTone, codePoints: codePoints, matchScore: matchScore, + sections: sections, searchEntries: searchEntries, candidate: candidate, actionsFor: actionsFor, aboutFor: aboutFor, gridLines: gridLines + } +} diff --git a/core/RootSearch.js b/core/RootSearch.js index b424a03..064053d 100644 --- a/core/RootSearch.js +++ b/core/RootSearch.js @@ -31,6 +31,15 @@ var BUILTINS = [ run: { summon: "omarchy.menu", payload: "{\"menu\":\"root\"}" }, rootSearch: { when: "always" } }, + { + id: "builtin:emoji-picker", + title: "Search Emoji & Symbols", + subtitle: "Emoji & Symbols", + glyph: "\uf118", // nf-fa-smile_o + keywords: ["emoji", "emoticon", "smiley", "symbols", "picker"], + run: { push: { kind: "emoji", title: "Search Emoji & Symbols" } }, + rootSearch: { when: "always" } + }, { id: "builtin:calculator-history", title: "Calculator History", diff --git a/core/Schema.js b/core/Schema.js index cdac573..06da080 100644 --- a/core/Schema.js +++ b/core/Schema.js @@ -46,6 +46,21 @@ var calculatorMigrations = [ } ] +// Emoji & Symbols: frecency per base emoji (Frecency.recordVisit entries, +// ref 02 §4.6 "emoji recents use the same formula"), pins and skin tone. +var emojiMigrations = [ + { + version: 1, + name: "emoji", + up: function(doc) { + doc.items = {} // symbol -> frecency entry + doc.pinned = [] // symbols, in pin order + doc.skinTone = "none" // Emoji.TONES id + return doc + } + } +] + var SEARCH_HISTORY_LIMIT = 20 function pushSearchHistory(history, query) { @@ -60,7 +75,8 @@ function pushSearchHistory(history, query) { var documents = { settings: { file: "settings.json", migrations: settingsMigrations }, frecency: { file: "frecency.json", migrations: frecencyMigrations }, - calculator: { file: "calculator.json", migrations: calculatorMigrations } + calculator: { file: "calculator.json", migrations: calculatorMigrations }, + emoji: { file: "emoji.json", migrations: emojiMigrations } } var configDefaults = { @@ -111,6 +127,7 @@ if (typeof module !== "undefined") { settingsMigrations: settingsMigrations, frecencyMigrations: frecencyMigrations, calculatorMigrations: calculatorMigrations, + emojiMigrations: emojiMigrations, pushSearchHistory: pushSearchHistory, SEARCH_HISTORY_LIMIT: SEARCH_HISTORY_LIMIT, documents: documents, diff --git a/data/emoji.json b/data/emoji.json new file mode 100644 index 0000000..48626f2 --- /dev/null +++ b/data/emoji.json @@ -0,0 +1 @@ +{"source":"Unicode emoji-test.txt 17.0","groups":["Smileys & Emotion","People & Body","Animals & Nature","Food & Drink","Travel & Places","Activities","Objects","Symbols","Flags"],"emoji":[["😀","grinning face",0,0],["😃","grinning face with big eyes",0,0],["😄","grinning face with smiling eyes",0,0],["😁","beaming face with smiling eyes",0,0],["😆","grinning squinting face",0,0],["😅","grinning face with sweat",0,0],["🤣","rolling on the floor laughing",0,0],["😂","face with tears of joy",0,0],["🙂","slightly smiling face",0,0],["🙃","upside-down face",0,0],["🫠","melting face",0,0],["😉","winking face",0,0],["😊","smiling face with smiling eyes",0,0],["😇","smiling face with halo",0,0],["🥰","smiling face with hearts",0,0],["😍","smiling face with heart-eyes",0,0],["🤩","star-struck",0,0],["😘","face blowing a kiss",0,0],["😗","kissing face",0,0],["☺️","smiling face",0,0],["😚","kissing face with closed eyes",0,0],["😙","kissing face with smiling eyes",0,0],["🥲","smiling face with tear",0,0],["😋","face savoring food",0,0],["😛","face with tongue",0,0],["😜","winking face with tongue",0,0],["🤪","zany face",0,0],["😝","squinting face with tongue",0,0],["🤑","money-mouth face",0,0],["🤗","smiling face with open hands",0,0],["🤭","face with hand over mouth",0,0],["🫢","face with open eyes and hand over mouth",0,0],["🫣","face with peeking eye",0,0],["🤫","shushing face",0,0],["🤔","thinking face",0,0],["🫡","saluting face",0,0],["🤐","zipper-mouth face",0,0],["🤨","face with raised eyebrow",0,0],["😐","neutral face",0,0],["😑","expressionless face",0,0],["😶","face without mouth",0,0],["🫥","dotted line face",0,0],["😶‍🌫️","face in clouds",0,0],["😏","smirking face",0,0],["😒","unamused face",0,0],["🙄","face with rolling eyes",0,0],["😬","grimacing face",0,0],["😮‍💨","face exhaling",0,0],["🤥","lying face",0,0],["🫨","shaking face",0,0],["🙂‍↔️","head shaking horizontally",0,0],["🙂‍↕️","head shaking vertically",0,0],["😌","relieved face",0,0],["😔","pensive face",0,0],["😪","sleepy face",0,0],["🤤","drooling face",0,0],["😴","sleeping face",0,0],["🫩","face with bags under eyes",0,0],["😷","face with medical mask",0,0],["🤒","face with thermometer",0,0],["🤕","face with head-bandage",0,0],["🤢","nauseated face",0,0],["🤮","face vomiting",0,0],["🤧","sneezing face",0,0],["🥵","hot face",0,0],["🥶","cold face",0,0],["🥴","woozy face",0,0],["😵","face with crossed-out eyes",0,0],["😵‍💫","face with spiral eyes",0,0],["🤯","exploding head",0,0],["🤠","cowboy hat face",0,0],["🥳","partying face",0,0],["🥸","disguised face",0,0],["😎","smiling face with sunglasses",0,0],["🤓","nerd face",0,0],["🧐","face with monocle",0,0],["😕","confused face",0,0],["🫤","face with diagonal mouth",0,0],["😟","worried face",0,0],["🙁","slightly frowning face",0,0],["☹️","frowning face",0,0],["😮","face with open mouth",0,0],["😯","hushed face",0,0],["😲","astonished face",0,0],["😳","flushed face",0,0],["🫪","distorted face",0,0],["🥺","pleading face",0,0],["🥹","face holding back tears",0,0],["😦","frowning face with open mouth",0,0],["😧","anguished face",0,0],["😨","fearful face",0,0],["😰","anxious face with sweat",0,0],["😥","sad but relieved face",0,0],["😢","crying face",0,0],["😭","loudly crying face",0,0],["😱","face screaming in fear",0,0],["😖","confounded face",0,0],["😣","persevering face",0,0],["😞","disappointed face",0,0],["😓","downcast face with sweat",0,0],["😩","weary face",0,0],["😫","tired face",0,0],["🥱","yawning face",0,0],["😤","face with steam from nose",0,0],["😡","enraged face",0,0],["😠","angry face",0,0],["🤬","face with symbols on mouth",0,0],["😈","smiling face with horns",0,0],["👿","angry face with horns",0,0],["💀","skull",0,0],["☠️","skull and crossbones",0,0],["💩","pile of poo",0,0],["🤡","clown face",0,0],["👹","ogre",0,0],["👺","goblin",0,0],["👻","ghost",0,0],["👽","alien",0,0],["👾","alien monster",0,0],["🤖","robot",0,0],["😺","grinning cat",0,0],["😸","grinning cat with smiling eyes",0,0],["😹","cat with tears of joy",0,0],["😻","smiling cat with heart-eyes",0,0],["😼","cat with wry smile",0,0],["😽","kissing cat",0,0],["🙀","weary cat",0,0],["😿","crying cat",0,0],["😾","pouting cat",0,0],["🙈","see-no-evil monkey",0,0],["🙉","hear-no-evil monkey",0,0],["🙊","speak-no-evil monkey",0,0],["💌","love letter",0,0],["💘","heart with arrow",0,0],["💝","heart with ribbon",0,0],["💖","sparkling heart",0,0],["💗","growing heart",0,0],["💓","beating heart",0,0],["💞","revolving hearts",0,0],["💕","two hearts",0,0],["💟","heart decoration",0,0],["❣️","heart exclamation",0,0],["💔","broken heart",0,0],["❤️‍🔥","heart on fire",0,0],["❤️‍🩹","mending heart",0,0],["❤️","red heart",0,0],["🩷","pink heart",0,0],["🧡","orange heart",0,0],["💛","yellow heart",0,0],["💚","green heart",0,0],["💙","blue heart",0,0],["🩵","light blue heart",0,0],["💜","purple heart",0,0],["🤎","brown heart",0,0],["🖤","black heart",0,0],["🩶","grey heart",0,0],["🤍","white heart",0,0],["💋","kiss mark",0,0],["💯","hundred points",0,0],["💢","anger symbol",0,0],["🫯","fight cloud",0,0],["💥","collision",0,0],["💫","dizzy",0,0],["💦","sweat droplets",0,0],["💨","dashing away",0,0],["🕳️","hole",0,0],["💬","speech balloon",0,0],["👁️‍🗨️","eye in speech bubble",0,0],["🗨️","left speech bubble",0,0],["🗯️","right anger bubble",0,0],["💭","thought balloon",0,0],["💤","ZZZ",0,0],["👋","waving hand",1,1],["🤚","raised back of hand",1,1],["🖐️","hand with fingers splayed",1,1],["✋","raised hand",1,1],["🖖","vulcan salute",1,1],["🫱","rightwards hand",1,1],["🫲","leftwards hand",1,1],["🫳","palm down hand",1,1],["🫴","palm up hand",1,1],["🫷","leftwards pushing hand",1,1],["🫸","rightwards pushing hand",1,1],["👌","OK hand",1,1],["🤌","pinched fingers",1,1],["🤏","pinching hand",1,1],["✌️","victory hand",1,1],["🤞","crossed fingers",1,1],["🫰","hand with index finger and thumb crossed",1,1],["🤟","love-you gesture",1,1],["🤘","sign of the horns",1,1],["🤙","call me hand",1,1],["👈","backhand index pointing left",1,1],["👉","backhand index pointing right",1,1],["👆","backhand index pointing up",1,1],["🖕","middle finger",1,1],["👇","backhand index pointing down",1,1],["☝️","index pointing up",1,1],["🫵","index pointing at the viewer",1,1],["👍","thumbs up",1,1],["👎","thumbs down",1,1],["✊","raised fist",1,1],["👊","oncoming fist",1,1],["🤛","left-facing fist",1,1],["🤜","right-facing fist",1,1],["👏","clapping hands",1,1],["🙌","raising hands",1,1],["🫶","heart hands",1,1],["👐","open hands",1,1],["🤲","palms up together",1,1],["🤝","handshake",1,1],["🙏","folded hands",1,1],["✍️","writing hand",1,1],["💅","nail polish",1,1],["🤳","selfie",1,1],["💪","flexed biceps",1,1],["🦾","mechanical arm",1,0],["🦿","mechanical leg",1,0],["🦵","leg",1,1],["🦶","foot",1,1],["👂","ear",1,1],["🦻","ear with hearing aid",1,1],["👃","nose",1,1],["🧠","brain",1,0],["🫀","anatomical heart",1,0],["🫁","lungs",1,0],["🦷","tooth",1,0],["🦴","bone",1,0],["👀","eyes",1,0],["👁️","eye",1,0],["👅","tongue",1,0],["👄","mouth",1,0],["🫦","biting lip",1,0],["👶","baby",1,1],["🧒","child",1,1],["👦","boy",1,1],["👧","girl",1,1],["🧑","person",1,1],["👱","person: blond hair",1,1],["👨","man",1,1],["🧔","person: beard",1,1],["🧔‍♂️","man: beard",1,1],["🧔‍♀️","woman: beard",1,1],["👨‍🦰","man: red hair",1,1],["👨‍🦱","man: curly hair",1,1],["👨‍🦳","man: white hair",1,1],["👨‍🦲","man: bald",1,1],["👩","woman",1,1],["👩‍🦰","woman: red hair",1,1],["🧑‍🦰","person: red hair",1,1],["👩‍🦱","woman: curly hair",1,1],["🧑‍🦱","person: curly hair",1,1],["👩‍🦳","woman: white hair",1,1],["🧑‍🦳","person: white hair",1,1],["👩‍🦲","woman: bald",1,1],["🧑‍🦲","person: bald",1,1],["👱‍♀️","woman: blond hair",1,1],["👱‍♂️","man: blond hair",1,1],["🧓","older person",1,1],["👴","old man",1,1],["👵","old woman",1,1],["🙍","person frowning",1,1],["🙍‍♂️","man frowning",1,1],["🙍‍♀️","woman frowning",1,1],["🙎","person pouting",1,1],["🙎‍♂️","man pouting",1,1],["🙎‍♀️","woman pouting",1,1],["🙅","person gesturing NO",1,1],["🙅‍♂️","man gesturing NO",1,1],["🙅‍♀️","woman gesturing NO",1,1],["🙆","person gesturing OK",1,1],["🙆‍♂️","man gesturing OK",1,1],["🙆‍♀️","woman gesturing OK",1,1],["💁","person tipping hand",1,1],["💁‍♂️","man tipping hand",1,1],["💁‍♀️","woman tipping hand",1,1],["🙋","person raising hand",1,1],["🙋‍♂️","man raising hand",1,1],["🙋‍♀️","woman raising hand",1,1],["🧏","deaf person",1,1],["🧏‍♂️","deaf man",1,1],["🧏‍♀️","deaf woman",1,1],["🙇","person bowing",1,1],["🙇‍♂️","man bowing",1,1],["🙇‍♀️","woman bowing",1,1],["🤦","person facepalming",1,1],["🤦‍♂️","man facepalming",1,1],["🤦‍♀️","woman facepalming",1,1],["🤷","person shrugging",1,1],["🤷‍♂️","man shrugging",1,1],["🤷‍♀️","woman shrugging",1,1],["🧑‍⚕️","health worker",1,1],["👨‍⚕️","man health worker",1,1],["👩‍⚕️","woman health worker",1,1],["🧑‍🎓","student",1,1],["👨‍🎓","man student",1,1],["👩‍🎓","woman student",1,1],["🧑‍🏫","teacher",1,1],["👨‍🏫","man teacher",1,1],["👩‍🏫","woman teacher",1,1],["🧑‍⚖️","judge",1,1],["👨‍⚖️","man judge",1,1],["👩‍⚖️","woman judge",1,1],["🧑‍🌾","farmer",1,1],["👨‍🌾","man farmer",1,1],["👩‍🌾","woman farmer",1,1],["🧑‍🍳","cook",1,1],["👨‍🍳","man cook",1,1],["👩‍🍳","woman cook",1,1],["🧑‍🔧","mechanic",1,1],["👨‍🔧","man mechanic",1,1],["👩‍🔧","woman mechanic",1,1],["🧑‍🏭","factory worker",1,1],["👨‍🏭","man factory worker",1,1],["👩‍🏭","woman factory worker",1,1],["🧑‍💼","office worker",1,1],["👨‍💼","man office worker",1,1],["👩‍💼","woman office worker",1,1],["🧑‍🔬","scientist",1,1],["👨‍🔬","man scientist",1,1],["👩‍🔬","woman scientist",1,1],["🧑‍💻","technologist",1,1],["👨‍💻","man technologist",1,1],["👩‍💻","woman technologist",1,1],["🧑‍🎤","singer",1,1],["👨‍🎤","man singer",1,1],["👩‍🎤","woman singer",1,1],["🧑‍🎨","artist",1,1],["👨‍🎨","man artist",1,1],["👩‍🎨","woman artist",1,1],["🧑‍✈️","pilot",1,1],["👨‍✈️","man pilot",1,1],["👩‍✈️","woman pilot",1,1],["🧑‍🚀","astronaut",1,1],["👨‍🚀","man astronaut",1,1],["👩‍🚀","woman astronaut",1,1],["🧑‍🚒","firefighter",1,1],["👨‍🚒","man firefighter",1,1],["👩‍🚒","woman firefighter",1,1],["👮","police officer",1,1],["👮‍♂️","man police officer",1,1],["👮‍♀️","woman police officer",1,1],["🕵️","detective",1,1],["🕵️‍♂️","man detective",1,1],["🕵️‍♀️","woman detective",1,1],["💂","guard",1,1],["💂‍♂️","man guard",1,1],["💂‍♀️","woman guard",1,1],["🥷","ninja",1,1],["👷","construction worker",1,1],["👷‍♂️","man construction worker",1,1],["👷‍♀️","woman construction worker",1,1],["🫅","person with crown",1,1],["🤴","prince",1,1],["👸","princess",1,1],["👳","person wearing turban",1,1],["👳‍♂️","man wearing turban",1,1],["👳‍♀️","woman wearing turban",1,1],["👲","person with skullcap",1,1],["🧕","woman with headscarf",1,1],["🤵","person in tuxedo",1,1],["🤵‍♂️","man in tuxedo",1,1],["🤵‍♀️","woman in tuxedo",1,1],["👰","person with veil",1,1],["👰‍♂️","man with veil",1,1],["👰‍♀️","woman with veil",1,1],["🤰","pregnant woman",1,1],["🫃","pregnant man",1,1],["🫄","pregnant person",1,1],["🤱","breast-feeding",1,1],["👩‍🍼","woman feeding baby",1,1],["👨‍🍼","man feeding baby",1,1],["🧑‍🍼","person feeding baby",1,1],["👼","baby angel",1,1],["🎅","Santa Claus",1,1],["🤶","Mrs. Claus",1,1],["🧑‍🎄","Mx Claus",1,1],["🦸","superhero",1,1],["🦸‍♂️","man superhero",1,1],["🦸‍♀️","woman superhero",1,1],["🦹","supervillain",1,1],["🦹‍♂️","man supervillain",1,1],["🦹‍♀️","woman supervillain",1,1],["🧙","mage",1,1],["🧙‍♂️","man mage",1,1],["🧙‍♀️","woman mage",1,1],["🧚","fairy",1,1],["🧚‍♂️","man fairy",1,1],["🧚‍♀️","woman fairy",1,1],["🧛","vampire",1,1],["🧛‍♂️","man vampire",1,1],["🧛‍♀️","woman vampire",1,1],["🧜","merperson",1,1],["🧜‍♂️","merman",1,1],["🧜‍♀️","mermaid",1,1],["🧝","elf",1,1],["🧝‍♂️","man elf",1,1],["🧝‍♀️","woman elf",1,1],["🧞","genie",1,0],["🧞‍♂️","man genie",1,0],["🧞‍♀️","woman genie",1,0],["🧟","zombie",1,0],["🧟‍♂️","man zombie",1,0],["🧟‍♀️","woman zombie",1,0],["🧌","troll",1,0],["🫈","hairy creature",1,0],["💆","person getting massage",1,1],["💆‍♂️","man getting massage",1,1],["💆‍♀️","woman getting massage",1,1],["💇","person getting haircut",1,1],["💇‍♂️","man getting haircut",1,1],["💇‍♀️","woman getting haircut",1,1],["🚶","person walking",1,1],["🚶‍♂️","man walking",1,1],["🚶‍♀️","woman walking",1,1],["🚶‍➡️","person walking facing right",1,1],["🚶‍♀️‍➡️","woman walking facing right",1,1],["🚶‍♂️‍➡️","man walking facing right",1,1],["🧍","person standing",1,1],["🧍‍♂️","man standing",1,1],["🧍‍♀️","woman standing",1,1],["🧎","person kneeling",1,1],["🧎‍♂️","man kneeling",1,1],["🧎‍♀️","woman kneeling",1,1],["🧎‍➡️","person kneeling facing right",1,1],["🧎‍♀️‍➡️","woman kneeling facing right",1,1],["🧎‍♂️‍➡️","man kneeling facing right",1,1],["🧑‍🦯","person with white cane",1,1],["🧑‍🦯‍➡️","person with white cane facing right",1,1],["👨‍🦯","man with white cane",1,1],["👨‍🦯‍➡️","man with white cane facing right",1,1],["👩‍🦯","woman with white cane",1,1],["👩‍🦯‍➡️","woman with white cane facing right",1,1],["🧑‍🦼","person in motorized wheelchair",1,1],["🧑‍🦼‍➡️","person in motorized wheelchair facing right",1,1],["👨‍🦼","man in motorized wheelchair",1,1],["👨‍🦼‍➡️","man in motorized wheelchair facing right",1,1],["👩‍🦼","woman in motorized wheelchair",1,1],["👩‍🦼‍➡️","woman in motorized wheelchair facing right",1,1],["🧑‍🦽","person in manual wheelchair",1,1],["🧑‍🦽‍➡️","person in manual wheelchair facing right",1,1],["👨‍🦽","man in manual wheelchair",1,1],["👨‍🦽‍➡️","man in manual wheelchair facing right",1,1],["👩‍🦽","woman in manual wheelchair",1,1],["👩‍🦽‍➡️","woman in manual wheelchair facing right",1,1],["🏃","person running",1,1],["🏃‍♂️","man running",1,1],["🏃‍♀️","woman running",1,1],["🏃‍➡️","person running facing right",1,1],["🏃‍♀️‍➡️","woman running facing right",1,1],["🏃‍♂️‍➡️","man running facing right",1,1],["🧑‍🩰","ballet dancer",1,1],["💃","woman dancing",1,1],["🕺","man dancing",1,1],["🕴️","person in suit levitating",1,1],["👯","people with bunny ears",1,1],["👯‍♂️","men with bunny ears",1,1],["👯‍♀️","women with bunny ears",1,1],["🧖","person in steamy room",1,1],["🧖‍♂️","man in steamy room",1,1],["🧖‍♀️","woman in steamy room",1,1],["🧗","person climbing",1,1],["🧗‍♂️","man climbing",1,1],["🧗‍♀️","woman climbing",1,1],["🤺","person fencing",1,0],["🏇","horse racing",1,1],["⛷️","skier",1,0],["🏂","snowboarder",1,1],["🏌️","person golfing",1,1],["🏌️‍♂️","man golfing",1,1],["🏌️‍♀️","woman golfing",1,1],["🏄","person surfing",1,1],["🏄‍♂️","man surfing",1,1],["🏄‍♀️","woman surfing",1,1],["🚣","person rowing boat",1,1],["🚣‍♂️","man rowing boat",1,1],["🚣‍♀️","woman rowing boat",1,1],["🏊","person swimming",1,1],["🏊‍♂️","man swimming",1,1],["🏊‍♀️","woman swimming",1,1],["⛹️","person bouncing ball",1,1],["⛹️‍♂️","man bouncing ball",1,1],["⛹️‍♀️","woman bouncing ball",1,1],["🏋️","person lifting weights",1,1],["🏋️‍♂️","man lifting weights",1,1],["🏋️‍♀️","woman lifting weights",1,1],["🚴","person biking",1,1],["🚴‍♂️","man biking",1,1],["🚴‍♀️","woman biking",1,1],["🚵","person mountain biking",1,1],["🚵‍♂️","man mountain biking",1,1],["🚵‍♀️","woman mountain biking",1,1],["🤸","person cartwheeling",1,1],["🤸‍♂️","man cartwheeling",1,1],["🤸‍♀️","woman cartwheeling",1,1],["🤼","people wrestling",1,1],["🤼‍♂️","men wrestling",1,1],["🤼‍♀️","women wrestling",1,1],["🤽","person playing water polo",1,1],["🤽‍♂️","man playing water polo",1,1],["🤽‍♀️","woman playing water polo",1,1],["🤾","person playing handball",1,1],["🤾‍♂️","man playing handball",1,1],["🤾‍♀️","woman playing handball",1,1],["🤹","person juggling",1,1],["🤹‍♂️","man juggling",1,1],["🤹‍♀️","woman juggling",1,1],["🧘","person in lotus position",1,1],["🧘‍♂️","man in lotus position",1,1],["🧘‍♀️","woman in lotus position",1,1],["🛀","person taking bath",1,1],["🛌","person in bed",1,1],["🧑‍🤝‍🧑","people holding hands",1,0],["👭","women holding hands",1,1],["👫","woman and man holding hands",1,1],["👬","men holding hands",1,1],["💏","kiss",1,1],["👩‍❤️‍💋‍👨","kiss: woman, man",1,0],["👨‍❤️‍💋‍👨","kiss: man, man",1,0],["👩‍❤️‍💋‍👩","kiss: woman, woman",1,0],["💑","couple with heart",1,1],["👩‍❤️‍👨","couple with heart: woman, man",1,0],["👨‍❤️‍👨","couple with heart: man, man",1,0],["👩‍❤️‍👩","couple with heart: woman, woman",1,0],["👨‍👩‍👦","family: man, woman, boy",1,0],["👨‍👩‍👧","family: man, woman, girl",1,0],["👨‍👩‍👧‍👦","family: man, woman, girl, boy",1,0],["👨‍👩‍👦‍👦","family: man, woman, boy, boy",1,0],["👨‍👩‍👧‍👧","family: man, woman, girl, girl",1,0],["👨‍👨‍👦","family: man, man, boy",1,0],["👨‍👨‍👧","family: man, man, girl",1,0],["👨‍👨‍👧‍👦","family: man, man, girl, boy",1,0],["👨‍👨‍👦‍👦","family: man, man, boy, boy",1,0],["👨‍👨‍👧‍👧","family: man, man, girl, girl",1,0],["👩‍👩‍👦","family: woman, woman, boy",1,0],["👩‍👩‍👧","family: woman, woman, girl",1,0],["👩‍👩‍👧‍👦","family: woman, woman, girl, boy",1,0],["👩‍👩‍👦‍👦","family: woman, woman, boy, boy",1,0],["👩‍👩‍👧‍👧","family: woman, woman, girl, girl",1,0],["👨‍👦","family: man, boy",1,0],["👨‍👦‍👦","family: man, boy, boy",1,0],["👨‍👧","family: man, girl",1,0],["👨‍👧‍👦","family: man, girl, boy",1,0],["👨‍👧‍👧","family: man, girl, girl",1,0],["👩‍👦","family: woman, boy",1,0],["👩‍👦‍👦","family: woman, boy, boy",1,0],["👩‍👧","family: woman, girl",1,0],["👩‍👧‍👦","family: woman, girl, boy",1,0],["👩‍👧‍👧","family: woman, girl, girl",1,0],["🗣️","speaking head",1,0],["👤","bust in silhouette",1,0],["👥","busts in silhouette",1,0],["🫂","people hugging",1,0],["👪","family",1,0],["🧑‍🧑‍🧒","family: adult, adult, child",1,0],["🧑‍🧑‍🧒‍🧒","family: adult, adult, child, child",1,0],["🧑‍🧒","family: adult, child",1,0],["🧑‍🧒‍🧒","family: adult, child, child",1,0],["👣","footprints",1,0],["🫆","fingerprint",1,0],["🐵","monkey face",2,0],["🐒","monkey",2,0],["🦍","gorilla",2,0],["🦧","orangutan",2,0],["🐶","dog face",2,0],["🐕","dog",2,0],["🦮","guide dog",2,0],["🐕‍🦺","service dog",2,0],["🐩","poodle",2,0],["🐺","wolf",2,0],["🦊","fox",2,0],["🦝","raccoon",2,0],["🐱","cat face",2,0],["🐈","cat",2,0],["🐈‍⬛","black cat",2,0],["🦁","lion",2,0],["🐯","tiger face",2,0],["🐅","tiger",2,0],["🐆","leopard",2,0],["🐴","horse face",2,0],["🫎","moose",2,0],["🫏","donkey",2,0],["🐎","horse",2,0],["🦄","unicorn",2,0],["🦓","zebra",2,0],["🦌","deer",2,0],["🦬","bison",2,0],["🐮","cow face",2,0],["🐂","ox",2,0],["🐃","water buffalo",2,0],["🐄","cow",2,0],["🐷","pig face",2,0],["🐖","pig",2,0],["🐗","boar",2,0],["🐽","pig nose",2,0],["🐏","ram",2,0],["🐑","ewe",2,0],["🐐","goat",2,0],["🐪","camel",2,0],["🐫","two-hump camel",2,0],["🦙","llama",2,0],["🦒","giraffe",2,0],["🐘","elephant",2,0],["🦣","mammoth",2,0],["🦏","rhinoceros",2,0],["🦛","hippopotamus",2,0],["🐭","mouse face",2,0],["🐁","mouse",2,0],["🐀","rat",2,0],["🐹","hamster",2,0],["🐰","rabbit face",2,0],["🐇","rabbit",2,0],["🐿️","chipmunk",2,0],["🦫","beaver",2,0],["🦔","hedgehog",2,0],["🦇","bat",2,0],["🐻","bear",2,0],["🐻‍❄️","polar bear",2,0],["🐨","koala",2,0],["🐼","panda",2,0],["🦥","sloth",2,0],["🦦","otter",2,0],["🦨","skunk",2,0],["🦘","kangaroo",2,0],["🦡","badger",2,0],["🐾","paw prints",2,0],["🦃","turkey",2,0],["🐔","chicken",2,0],["🐓","rooster",2,0],["🐣","hatching chick",2,0],["🐤","baby chick",2,0],["🐥","front-facing baby chick",2,0],["🐦","bird",2,0],["🐧","penguin",2,0],["🕊️","dove",2,0],["🦅","eagle",2,0],["🦆","duck",2,0],["🦢","swan",2,0],["🦉","owl",2,0],["🦤","dodo",2,0],["🪶","feather",2,0],["🦩","flamingo",2,0],["🦚","peacock",2,0],["🦜","parrot",2,0],["🪽","wing",2,0],["🐦‍⬛","black bird",2,0],["🪿","goose",2,0],["🐦‍🔥","phoenix",2,0],["🐸","frog",2,0],["🐊","crocodile",2,0],["🐢","turtle",2,0],["🦎","lizard",2,0],["🐍","snake",2,0],["🐲","dragon face",2,0],["🐉","dragon",2,0],["🦕","sauropod",2,0],["🦖","T-Rex",2,0],["🐳","spouting whale",2,0],["🐋","whale",2,0],["🐬","dolphin",2,0],["🫍","orca",2,0],["🦭","seal",2,0],["🐟","fish",2,0],["🐠","tropical fish",2,0],["🐡","blowfish",2,0],["🦈","shark",2,0],["🐙","octopus",2,0],["🐚","spiral shell",2,0],["🪸","coral",2,0],["🪼","jellyfish",2,0],["🦀","crab",2,0],["🦞","lobster",2,0],["🦐","shrimp",2,0],["🦑","squid",2,0],["🦪","oyster",2,0],["🐌","snail",2,0],["🦋","butterfly",2,0],["🐛","bug",2,0],["🐜","ant",2,0],["🐝","honeybee",2,0],["🪲","beetle",2,0],["🐞","lady beetle",2,0],["🦗","cricket",2,0],["🪳","cockroach",2,0],["🕷️","spider",2,0],["🕸️","spider web",2,0],["🦂","scorpion",2,0],["🦟","mosquito",2,0],["🪰","fly",2,0],["🪱","worm",2,0],["🦠","microbe",2,0],["💐","bouquet",2,0],["🌸","cherry blossom",2,0],["💮","white flower",2,0],["🪷","lotus",2,0],["🏵️","rosette",2,0],["🌹","rose",2,0],["🥀","wilted flower",2,0],["🌺","hibiscus",2,0],["🌻","sunflower",2,0],["🌼","blossom",2,0],["🌷","tulip",2,0],["🪻","hyacinth",2,0],["🌱","seedling",2,0],["🪴","potted plant",2,0],["🌲","evergreen tree",2,0],["🌳","deciduous tree",2,0],["🌴","palm tree",2,0],["🌵","cactus",2,0],["🌾","sheaf of rice",2,0],["🌿","herb",2,0],["☘️","shamrock",2,0],["🍀","four leaf clover",2,0],["🍁","maple leaf",2,0],["🍂","fallen leaf",2,0],["🍃","leaf fluttering in wind",2,0],["🪹","empty nest",2,0],["🪺","nest with eggs",2,0],["🍄","mushroom",2,0],["🪾","leafless tree",2,0],["🍇","grapes",3,0],["🍈","melon",3,0],["🍉","watermelon",3,0],["🍊","tangerine",3,0],["🍋","lemon",3,0],["🍋‍🟩","lime",3,0],["🍌","banana",3,0],["🍍","pineapple",3,0],["🥭","mango",3,0],["🍎","red apple",3,0],["🍏","green apple",3,0],["🍐","pear",3,0],["🍑","peach",3,0],["🍒","cherries",3,0],["🍓","strawberry",3,0],["🫐","blueberries",3,0],["🥝","kiwi fruit",3,0],["🍅","tomato",3,0],["🫒","olive",3,0],["🥥","coconut",3,0],["🥑","avocado",3,0],["🍆","eggplant",3,0],["🥔","potato",3,0],["🥕","carrot",3,0],["🌽","ear of corn",3,0],["🌶️","hot pepper",3,0],["🫑","bell pepper",3,0],["🥒","cucumber",3,0],["🥬","leafy green",3,0],["🥦","broccoli",3,0],["🧄","garlic",3,0],["🧅","onion",3,0],["🥜","peanuts",3,0],["🫘","beans",3,0],["🌰","chestnut",3,0],["🫚","ginger root",3,0],["🫛","pea pod",3,0],["🍄‍🟫","brown mushroom",3,0],["🫜","root vegetable",3,0],["🍞","bread",3,0],["🥐","croissant",3,0],["🥖","baguette bread",3,0],["🫓","flatbread",3,0],["🥨","pretzel",3,0],["🥯","bagel",3,0],["🥞","pancakes",3,0],["🧇","waffle",3,0],["🧀","cheese wedge",3,0],["🍖","meat on bone",3,0],["🍗","poultry leg",3,0],["🥩","cut of meat",3,0],["🥓","bacon",3,0],["🍔","hamburger",3,0],["🍟","french fries",3,0],["🍕","pizza",3,0],["🌭","hot dog",3,0],["🥪","sandwich",3,0],["🌮","taco",3,0],["🌯","burrito",3,0],["🫔","tamale",3,0],["🥙","stuffed flatbread",3,0],["🧆","falafel",3,0],["🥚","egg",3,0],["🍳","cooking",3,0],["🥘","shallow pan of food",3,0],["🍲","pot of food",3,0],["🫕","fondue",3,0],["🥣","bowl with spoon",3,0],["🥗","green salad",3,0],["🍿","popcorn",3,0],["🧈","butter",3,0],["🧂","salt",3,0],["🥫","canned food",3,0],["🍱","bento box",3,0],["🍘","rice cracker",3,0],["🍙","rice ball",3,0],["🍚","cooked rice",3,0],["🍛","curry rice",3,0],["🍜","steaming bowl",3,0],["🍝","spaghetti",3,0],["🍠","roasted sweet potato",3,0],["🍢","oden",3,0],["🍣","sushi",3,0],["🍤","fried shrimp",3,0],["🍥","fish cake with swirl",3,0],["🥮","moon cake",3,0],["🍡","dango",3,0],["🥟","dumpling",3,0],["🥠","fortune cookie",3,0],["🥡","takeout box",3,0],["🍦","soft ice cream",3,0],["🍧","shaved ice",3,0],["🍨","ice cream",3,0],["🍩","doughnut",3,0],["🍪","cookie",3,0],["🎂","birthday cake",3,0],["🍰","shortcake",3,0],["🧁","cupcake",3,0],["🥧","pie",3,0],["🍫","chocolate bar",3,0],["🍬","candy",3,0],["🍭","lollipop",3,0],["🍮","custard",3,0],["🍯","honey pot",3,0],["🍼","baby bottle",3,0],["🥛","glass of milk",3,0],["☕","hot beverage",3,0],["🫖","teapot",3,0],["🍵","teacup without handle",3,0],["🍶","sake",3,0],["🍾","bottle with popping cork",3,0],["🍷","wine glass",3,0],["🍸","cocktail glass",3,0],["🍹","tropical drink",3,0],["🍺","beer mug",3,0],["🍻","clinking beer mugs",3,0],["🥂","clinking glasses",3,0],["🥃","tumbler glass",3,0],["🫗","pouring liquid",3,0],["🥤","cup with straw",3,0],["🧋","bubble tea",3,0],["🧃","beverage box",3,0],["🧉","mate",3,0],["🧊","ice",3,0],["🥢","chopsticks",3,0],["🍽️","fork and knife with plate",3,0],["🍴","fork and knife",3,0],["🥄","spoon",3,0],["🔪","kitchen knife",3,0],["🫙","jar",3,0],["🏺","amphora",3,0],["🌍","globe showing Europe-Africa",4,0],["🌎","globe showing Americas",4,0],["🌏","globe showing Asia-Australia",4,0],["🌐","globe with meridians",4,0],["🗺️","world map",4,0],["🗾","map of Japan",4,0],["🧭","compass",4,0],["🏔️","snow-capped mountain",4,0],["⛰️","mountain",4,0],["🛘","landslide",4,0],["🌋","volcano",4,0],["🗻","mount fuji",4,0],["🏕️","camping",4,0],["🏖️","beach with umbrella",4,0],["🏜️","desert",4,0],["🏝️","desert island",4,0],["🏞️","national park",4,0],["🏟️","stadium",4,0],["🏛️","classical building",4,0],["🏗️","building construction",4,0],["🧱","brick",4,0],["🪨","rock",4,0],["🪵","wood",4,0],["🛖","hut",4,0],["🏘️","houses",4,0],["🏚️","derelict house",4,0],["🏠","house",4,0],["🏡","house with garden",4,0],["🏢","office building",4,0],["🏣","Japanese post office",4,0],["🏤","post office",4,0],["🏥","hospital",4,0],["🏦","bank",4,0],["🏨","hotel",4,0],["🏩","love hotel",4,0],["🏪","convenience store",4,0],["🏫","school",4,0],["🏬","department store",4,0],["🏭","factory",4,0],["🏯","Japanese castle",4,0],["🏰","castle",4,0],["💒","wedding",4,0],["🗼","Tokyo tower",4,0],["🗽","Statue of Liberty",4,0],["⛪","church",4,0],["🕌","mosque",4,0],["🛕","hindu temple",4,0],["🕍","synagogue",4,0],["⛩️","shinto shrine",4,0],["🕋","kaaba",4,0],["⛲","fountain",4,0],["⛺","tent",4,0],["🌁","foggy",4,0],["🌃","night with stars",4,0],["🏙️","cityscape",4,0],["🌄","sunrise over mountains",4,0],["🌅","sunrise",4,0],["🌆","cityscape at dusk",4,0],["🌇","sunset",4,0],["🌉","bridge at night",4,0],["♨️","hot springs",4,0],["🎠","carousel horse",4,0],["🛝","playground slide",4,0],["🎡","ferris wheel",4,0],["🎢","roller coaster",4,0],["💈","barber pole",4,0],["🎪","circus tent",4,0],["🚂","locomotive",4,0],["🚃","railway car",4,0],["🚄","high-speed train",4,0],["🚅","bullet train",4,0],["🚆","train",4,0],["🚇","metro",4,0],["🚈","light rail",4,0],["🚉","station",4,0],["🚊","tram",4,0],["🚝","monorail",4,0],["🚞","mountain railway",4,0],["🚋","tram car",4,0],["🚌","bus",4,0],["🚍","oncoming bus",4,0],["🚎","trolleybus",4,0],["🚐","minibus",4,0],["🚑","ambulance",4,0],["🚒","fire engine",4,0],["🚓","police car",4,0],["🚔","oncoming police car",4,0],["🚕","taxi",4,0],["🚖","oncoming taxi",4,0],["🚗","automobile",4,0],["🚘","oncoming automobile",4,0],["🚙","sport utility vehicle",4,0],["🛻","pickup truck",4,0],["🚚","delivery truck",4,0],["🚛","articulated lorry",4,0],["🚜","tractor",4,0],["🏎️","racing car",4,0],["🏍️","motorcycle",4,0],["🛵","motor scooter",4,0],["🦽","manual wheelchair",4,0],["🦼","motorized wheelchair",4,0],["🛺","auto rickshaw",4,0],["🚲","bicycle",4,0],["🛴","kick scooter",4,0],["🛹","skateboard",4,0],["🛼","roller skate",4,0],["🚏","bus stop",4,0],["🛣️","motorway",4,0],["🛤️","railway track",4,0],["🛢️","oil drum",4,0],["⛽","fuel pump",4,0],["🛞","wheel",4,0],["🚨","police car light",4,0],["🚥","horizontal traffic light",4,0],["🚦","vertical traffic light",4,0],["🛑","stop sign",4,0],["🚧","construction",4,0],["⚓","anchor",4,0],["🛟","ring buoy",4,0],["⛵","sailboat",4,0],["🛶","canoe",4,0],["🚤","speedboat",4,0],["🛳️","passenger ship",4,0],["⛴️","ferry",4,0],["🛥️","motor boat",4,0],["🚢","ship",4,0],["✈️","airplane",4,0],["🛩️","small airplane",4,0],["🛫","airplane departure",4,0],["🛬","airplane arrival",4,0],["🪂","parachute",4,0],["💺","seat",4,0],["🚁","helicopter",4,0],["🚟","suspension railway",4,0],["🚠","mountain cableway",4,0],["🚡","aerial tramway",4,0],["🛰️","satellite",4,0],["🚀","rocket",4,0],["🛸","flying saucer",4,0],["🛎️","bellhop bell",4,0],["🧳","luggage",4,0],["⌛","hourglass done",4,0],["⏳","hourglass not done",4,0],["⌚","watch",4,0],["⏰","alarm clock",4,0],["⏱️","stopwatch",4,0],["⏲️","timer clock",4,0],["🕰️","mantelpiece clock",4,0],["🕛","twelve o’clock",4,0],["🕧","twelve-thirty",4,0],["🕐","one o’clock",4,0],["🕜","one-thirty",4,0],["🕑","two o’clock",4,0],["🕝","two-thirty",4,0],["🕒","three o’clock",4,0],["🕞","three-thirty",4,0],["🕓","four o’clock",4,0],["🕟","four-thirty",4,0],["🕔","five o’clock",4,0],["🕠","five-thirty",4,0],["🕕","six o’clock",4,0],["🕡","six-thirty",4,0],["🕖","seven o’clock",4,0],["🕢","seven-thirty",4,0],["🕗","eight o’clock",4,0],["🕣","eight-thirty",4,0],["🕘","nine o’clock",4,0],["🕤","nine-thirty",4,0],["🕙","ten o’clock",4,0],["🕥","ten-thirty",4,0],["🕚","eleven o’clock",4,0],["🕦","eleven-thirty",4,0],["🌑","new moon",4,0],["🌒","waxing crescent moon",4,0],["🌓","first quarter moon",4,0],["🌔","waxing gibbous moon",4,0],["🌕","full moon",4,0],["🌖","waning gibbous moon",4,0],["🌗","last quarter moon",4,0],["🌘","waning crescent moon",4,0],["🌙","crescent moon",4,0],["🌚","new moon face",4,0],["🌛","first quarter moon face",4,0],["🌜","last quarter moon face",4,0],["🌡️","thermometer",4,0],["☀️","sun",4,0],["🌝","full moon face",4,0],["🌞","sun with face",4,0],["🪐","ringed planet",4,0],["⭐","star",4,0],["🌟","glowing star",4,0],["🌠","shooting star",4,0],["🌌","milky way",4,0],["☁️","cloud",4,0],["⛅","sun behind cloud",4,0],["⛈️","cloud with lightning and rain",4,0],["🌤️","sun behind small cloud",4,0],["🌥️","sun behind large cloud",4,0],["🌦️","sun behind rain cloud",4,0],["🌧️","cloud with rain",4,0],["🌨️","cloud with snow",4,0],["🌩️","cloud with lightning",4,0],["🌪️","tornado",4,0],["🌫️","fog",4,0],["🌬️","wind face",4,0],["🌀","cyclone",4,0],["🌈","rainbow",4,0],["🌂","closed umbrella",4,0],["☂️","umbrella",4,0],["☔","umbrella with rain drops",4,0],["⛱️","umbrella on ground",4,0],["⚡","high voltage",4,0],["❄️","snowflake",4,0],["☃️","snowman",4,0],["⛄","snowman without snow",4,0],["☄️","comet",4,0],["🔥","fire",4,0],["💧","droplet",4,0],["🌊","water wave",4,0],["🎃","jack-o-lantern",5,0],["🎄","Christmas tree",5,0],["🎆","fireworks",5,0],["🎇","sparkler",5,0],["🧨","firecracker",5,0],["✨","sparkles",5,0],["🎈","balloon",5,0],["🎉","party popper",5,0],["🎊","confetti ball",5,0],["🎋","tanabata tree",5,0],["🎍","pine decoration",5,0],["🎎","Japanese dolls",5,0],["🎏","carp streamer",5,0],["🎐","wind chime",5,0],["🎑","moon viewing ceremony",5,0],["🧧","red envelope",5,0],["🎀","ribbon",5,0],["🎁","wrapped gift",5,0],["🎗️","reminder ribbon",5,0],["🎟️","admission tickets",5,0],["🎫","ticket",5,0],["🎖️","military medal",5,0],["🏆","trophy",5,0],["🏅","sports medal",5,0],["🥇","1st place medal",5,0],["🥈","2nd place medal",5,0],["🥉","3rd place medal",5,0],["⚽","soccer ball",5,0],["⚾","baseball",5,0],["🥎","softball",5,0],["🏀","basketball",5,0],["🏐","volleyball",5,0],["🏈","american football",5,0],["🏉","rugby football",5,0],["🎾","tennis",5,0],["🥏","flying disc",5,0],["🎳","bowling",5,0],["🏏","cricket game",5,0],["🏑","field hockey",5,0],["🏒","ice hockey",5,0],["🥍","lacrosse",5,0],["🏓","ping pong",5,0],["🏸","badminton",5,0],["🥊","boxing glove",5,0],["🥋","martial arts uniform",5,0],["🥅","goal net",5,0],["⛳","flag in hole",5,0],["⛸️","ice skate",5,0],["🎣","fishing pole",5,0],["🤿","diving mask",5,0],["🎽","running shirt",5,0],["🎿","skis",5,0],["🛷","sled",5,0],["🥌","curling stone",5,0],["🎯","bullseye",5,0],["🪀","yo-yo",5,0],["🪁","kite",5,0],["🔫","water pistol",5,0],["🎱","pool 8 ball",5,0],["🔮","crystal ball",5,0],["🪄","magic wand",5,0],["🎮","video game",5,0],["🕹️","joystick",5,0],["🎰","slot machine",5,0],["🎲","game die",5,0],["🧩","puzzle piece",5,0],["🧸","teddy bear",5,0],["🪅","piñata",5,0],["🪩","mirror ball",5,0],["🪆","nesting dolls",5,0],["♠️","spade suit",5,0],["♥️","heart suit",5,0],["♦️","diamond suit",5,0],["♣️","club suit",5,0],["♟️","chess pawn",5,0],["🃏","joker",5,0],["🀄","mahjong red dragon",5,0],["🎴","flower playing cards",5,0],["🎭","performing arts",5,0],["🖼️","framed picture",5,0],["🎨","artist palette",5,0],["🧵","thread",5,0],["🪡","sewing needle",5,0],["🧶","yarn",5,0],["🪢","knot",5,0],["👓","glasses",6,0],["🕶️","sunglasses",6,0],["🥽","goggles",6,0],["🥼","lab coat",6,0],["🦺","safety vest",6,0],["👔","necktie",6,0],["👕","t-shirt",6,0],["👖","jeans",6,0],["🧣","scarf",6,0],["🧤","gloves",6,0],["🧥","coat",6,0],["🧦","socks",6,0],["👗","dress",6,0],["👘","kimono",6,0],["🥻","sari",6,0],["🩱","one-piece swimsuit",6,0],["🩲","briefs",6,0],["🩳","shorts",6,0],["👙","bikini",6,0],["👚","woman’s clothes",6,0],["🪭","folding hand fan",6,0],["👛","purse",6,0],["👜","handbag",6,0],["👝","clutch bag",6,0],["🛍️","shopping bags",6,0],["🎒","backpack",6,0],["🩴","thong sandal",6,0],["👞","man’s shoe",6,0],["👟","running shoe",6,0],["🥾","hiking boot",6,0],["🥿","flat shoe",6,0],["👠","high-heeled shoe",6,0],["👡","woman’s sandal",6,0],["🩰","ballet shoes",6,0],["👢","woman’s boot",6,0],["🪮","hair pick",6,0],["👑","crown",6,0],["👒","woman’s hat",6,0],["🎩","top hat",6,0],["🎓","graduation cap",6,0],["🧢","billed cap",6,0],["🪖","military helmet",6,0],["⛑️","rescue worker’s helmet",6,0],["📿","prayer beads",6,0],["💄","lipstick",6,0],["💍","ring",6,0],["💎","gem stone",6,0],["🔇","muted speaker",6,0],["🔈","speaker low volume",6,0],["🔉","speaker medium volume",6,0],["🔊","speaker high volume",6,0],["📢","loudspeaker",6,0],["📣","megaphone",6,0],["📯","postal horn",6,0],["🔔","bell",6,0],["🔕","bell with slash",6,0],["🎼","musical score",6,0],["🎵","musical note",6,0],["🎶","musical notes",6,0],["🎙️","studio microphone",6,0],["🎚️","level slider",6,0],["🎛️","control knobs",6,0],["🎤","microphone",6,0],["🎧","headphone",6,0],["📻","radio",6,0],["🎷","saxophone",6,0],["🎺","trumpet",6,0],["🪊","trombone",6,0],["🪗","accordion",6,0],["🎸","guitar",6,0],["🎹","musical keyboard",6,0],["🎻","violin",6,0],["🪕","banjo",6,0],["🥁","drum",6,0],["🪘","long drum",6,0],["🪇","maracas",6,0],["🪈","flute",6,0],["🪉","harp",6,0],["📱","mobile phone",6,0],["📲","mobile phone with arrow",6,0],["☎️","telephone",6,0],["📞","telephone receiver",6,0],["📟","pager",6,0],["📠","fax machine",6,0],["🔋","battery",6,0],["🪫","low battery",6,0],["🔌","electric plug",6,0],["💻","laptop",6,0],["🖥️","desktop computer",6,0],["🖨️","printer",6,0],["⌨️","keyboard",6,0],["🖱️","computer mouse",6,0],["🖲️","trackball",6,0],["💽","computer disk",6,0],["💾","floppy disk",6,0],["💿","optical disk",6,0],["📀","dvd",6,0],["🧮","abacus",6,0],["🎥","movie camera",6,0],["🎞️","film frames",6,0],["📽️","film projector",6,0],["🎬","clapper board",6,0],["📺","television",6,0],["📷","camera",6,0],["📸","camera with flash",6,0],["📹","video camera",6,0],["📼","videocassette",6,0],["🔍","magnifying glass tilted left",6,0],["🔎","magnifying glass tilted right",6,0],["🕯️","candle",6,0],["💡","light bulb",6,0],["🔦","flashlight",6,0],["🏮","red paper lantern",6,0],["🪔","diya lamp",6,0],["📔","notebook with decorative cover",6,0],["📕","closed book",6,0],["📖","open book",6,0],["📗","green book",6,0],["📘","blue book",6,0],["📙","orange book",6,0],["📚","books",6,0],["📓","notebook",6,0],["📒","ledger",6,0],["📃","page with curl",6,0],["📜","scroll",6,0],["📄","page facing up",6,0],["📰","newspaper",6,0],["🗞️","rolled-up newspaper",6,0],["📑","bookmark tabs",6,0],["🔖","bookmark",6,0],["🏷️","label",6,0],["🪙","coin",6,0],["💰","money bag",6,0],["🪎","treasure chest",6,0],["💴","yen banknote",6,0],["💵","dollar banknote",6,0],["💶","euro banknote",6,0],["💷","pound banknote",6,0],["💸","money with wings",6,0],["💳","credit card",6,0],["🧾","receipt",6,0],["💹","chart increasing with yen",6,0],["✉️","envelope",6,0],["📧","e-mail",6,0],["📨","incoming envelope",6,0],["📩","envelope with arrow",6,0],["📤","outbox tray",6,0],["📥","inbox tray",6,0],["📦","package",6,0],["📫","closed mailbox with raised flag",6,0],["📪","closed mailbox with lowered flag",6,0],["📬","open mailbox with raised flag",6,0],["📭","open mailbox with lowered flag",6,0],["📮","postbox",6,0],["🗳️","ballot box with ballot",6,0],["✏️","pencil",6,0],["✒️","black nib",6,0],["🖋️","fountain pen",6,0],["🖊️","pen",6,0],["🖌️","paintbrush",6,0],["🖍️","crayon",6,0],["📝","memo",6,0],["💼","briefcase",6,0],["📁","file folder",6,0],["📂","open file folder",6,0],["🗂️","card index dividers",6,0],["📅","calendar",6,0],["📆","tear-off calendar",6,0],["🗒️","spiral notepad",6,0],["🗓️","spiral calendar",6,0],["📇","card index",6,0],["📈","chart increasing",6,0],["📉","chart decreasing",6,0],["📊","bar chart",6,0],["📋","clipboard",6,0],["📌","pushpin",6,0],["📍","round pushpin",6,0],["📎","paperclip",6,0],["🖇️","linked paperclips",6,0],["📏","straight ruler",6,0],["📐","triangular ruler",6,0],["✂️","scissors",6,0],["🗃️","card file box",6,0],["🗄️","file cabinet",6,0],["🗑️","wastebasket",6,0],["🔒","locked",6,0],["🔓","unlocked",6,0],["🔏","locked with pen",6,0],["🔐","locked with key",6,0],["🔑","key",6,0],["🗝️","old key",6,0],["🔨","hammer",6,0],["🪓","axe",6,0],["⛏️","pick",6,0],["⚒️","hammer and pick",6,0],["🛠️","hammer and wrench",6,0],["🗡️","dagger",6,0],["⚔️","crossed swords",6,0],["💣","bomb",6,0],["🪃","boomerang",6,0],["🏹","bow and arrow",6,0],["🛡️","shield",6,0],["🪚","carpentry saw",6,0],["🔧","wrench",6,0],["🪛","screwdriver",6,0],["🔩","nut and bolt",6,0],["⚙️","gear",6,0],["🗜️","clamp",6,0],["⚖️","balance scale",6,0],["🦯","white cane",6,0],["🔗","link",6,0],["⛓️‍💥","broken chain",6,0],["⛓️","chains",6,0],["🪝","hook",6,0],["🧰","toolbox",6,0],["🧲","magnet",6,0],["🪜","ladder",6,0],["🪏","shovel",6,0],["⚗️","alembic",6,0],["🧪","test tube",6,0],["🧫","petri dish",6,0],["🧬","dna",6,0],["🔬","microscope",6,0],["🔭","telescope",6,0],["📡","satellite antenna",6,0],["💉","syringe",6,0],["🩸","drop of blood",6,0],["💊","pill",6,0],["🩹","adhesive bandage",6,0],["🩼","crutch",6,0],["🩺","stethoscope",6,0],["🩻","x-ray",6,0],["🚪","door",6,0],["🛗","elevator",6,0],["🪞","mirror",6,0],["🪟","window",6,0],["🛏️","bed",6,0],["🛋️","couch and lamp",6,0],["🪑","chair",6,0],["🚽","toilet",6,0],["🪠","plunger",6,0],["🚿","shower",6,0],["🛁","bathtub",6,0],["🪤","mouse trap",6,0],["🪒","razor",6,0],["🧴","lotion bottle",6,0],["🧷","safety pin",6,0],["🧹","broom",6,0],["🧺","basket",6,0],["🧻","roll of paper",6,0],["🪣","bucket",6,0],["🧼","soap",6,0],["🫧","bubbles",6,0],["🪥","toothbrush",6,0],["🧽","sponge",6,0],["🧯","fire extinguisher",6,0],["🛒","shopping cart",6,0],["🚬","cigarette",6,0],["⚰️","coffin",6,0],["🪦","headstone",6,0],["⚱️","funeral urn",6,0],["🧿","nazar amulet",6,0],["🪬","hamsa",6,0],["🗿","moai",6,0],["🪧","placard",6,0],["🪪","identification card",6,0],["🏧","ATM sign",7,0],["🚮","litter in bin sign",7,0],["🚰","potable water",7,0],["♿","wheelchair symbol",7,0],["🚹","men’s room",7,0],["🚺","women’s room",7,0],["🚻","restroom",7,0],["🚼","baby symbol",7,0],["🚾","water closet",7,0],["🛂","passport control",7,0],["🛃","customs",7,0],["🛄","baggage claim",7,0],["🛅","left luggage",7,0],["⚠️","warning",7,0],["🚸","children crossing",7,0],["⛔","no entry",7,0],["🚫","prohibited",7,0],["🚳","no bicycles",7,0],["🚭","no smoking",7,0],["🚯","no littering",7,0],["🚱","non-potable water",7,0],["🚷","no pedestrians",7,0],["📵","no mobile phones",7,0],["🔞","no one under eighteen",7,0],["☢️","radioactive",7,0],["☣️","biohazard",7,0],["⬆️","up arrow",7,0],["↗️","up-right arrow",7,0],["➡️","right arrow",7,0],["↘️","down-right arrow",7,0],["⬇️","down arrow",7,0],["↙️","down-left arrow",7,0],["⬅️","left arrow",7,0],["↖️","up-left arrow",7,0],["↕️","up-down arrow",7,0],["↔️","left-right arrow",7,0],["↩️","right arrow curving left",7,0],["↪️","left arrow curving right",7,0],["⤴️","right arrow curving up",7,0],["⤵️","right arrow curving down",7,0],["🔃","clockwise vertical arrows",7,0],["🔄","counterclockwise arrows button",7,0],["🔙","BACK arrow",7,0],["🔚","END arrow",7,0],["🔛","ON! arrow",7,0],["🔜","SOON arrow",7,0],["🔝","TOP arrow",7,0],["🛐","place of worship",7,0],["⚛️","atom symbol",7,0],["🕉️","om",7,0],["✡️","star of David",7,0],["☸️","wheel of dharma",7,0],["☯️","yin yang",7,0],["✝️","latin cross",7,0],["☦️","orthodox cross",7,0],["☪️","star and crescent",7,0],["☮️","peace symbol",7,0],["🕎","menorah",7,0],["🔯","dotted six-pointed star",7,0],["🪯","khanda",7,0],["♈","Aries",7,0],["♉","Taurus",7,0],["♊","Gemini",7,0],["♋","Cancer",7,0],["♌","Leo",7,0],["♍","Virgo",7,0],["♎","Libra",7,0],["♏","Scorpio",7,0],["♐","Sagittarius",7,0],["♑","Capricorn",7,0],["♒","Aquarius",7,0],["♓","Pisces",7,0],["⛎","Ophiuchus",7,0],["🔀","shuffle tracks button",7,0],["🔁","repeat button",7,0],["🔂","repeat single button",7,0],["▶️","play button",7,0],["⏩","fast-forward button",7,0],["⏭️","next track button",7,0],["⏯️","play or pause button",7,0],["◀️","reverse button",7,0],["⏪","fast reverse button",7,0],["⏮️","last track button",7,0],["🔼","upwards button",7,0],["⏫","fast up button",7,0],["🔽","downwards button",7,0],["⏬","fast down button",7,0],["⏸️","pause button",7,0],["⏹️","stop button",7,0],["⏺️","record button",7,0],["⏏️","eject button",7,0],["🎦","cinema",7,0],["🔅","dim button",7,0],["🔆","bright button",7,0],["📶","antenna bars",7,0],["🛜","wireless",7,0],["📳","vibration mode",7,0],["📴","mobile phone off",7,0],["♀️","female sign",7,0],["♂️","male sign",7,0],["⚧️","transgender symbol",7,0],["✖️","multiply",7,0],["➕","plus",7,0],["➖","minus",7,0],["➗","divide",7,0],["🟰","heavy equals sign",7,0],["♾️","infinity",7,0],["‼️","double exclamation mark",7,0],["⁉️","exclamation question mark",7,0],["❓","red question mark",7,0],["❔","white question mark",7,0],["❕","white exclamation mark",7,0],["❗","red exclamation mark",7,0],["〰️","wavy dash",7,0],["💱","currency exchange",7,0],["💲","heavy dollar sign",7,0],["⚕️","medical symbol",7,0],["♻️","recycling symbol",7,0],["⚜️","fleur-de-lis",7,0],["🔱","trident emblem",7,0],["📛","name badge",7,0],["🔰","Japanese symbol for beginner",7,0],["⭕","hollow red circle",7,0],["✅","check mark button",7,0],["☑️","check box with check",7,0],["✔️","check mark",7,0],["❌","cross mark",7,0],["❎","cross mark button",7,0],["➰","curly loop",7,0],["➿","double curly loop",7,0],["〽️","part alternation mark",7,0],["✳️","eight-spoked asterisk",7,0],["✴️","eight-pointed star",7,0],["❇️","sparkle",7,0],["©️","copyright",7,0],["®️","registered",7,0],["™️","trade mark",7,0],["🫟","splatter",7,0],["#️⃣","keycap: #",7,0],["*️⃣","keycap: *",7,0],["0️⃣","keycap: 0",7,0],["1️⃣","keycap: 1",7,0],["2️⃣","keycap: 2",7,0],["3️⃣","keycap: 3",7,0],["4️⃣","keycap: 4",7,0],["5️⃣","keycap: 5",7,0],["6️⃣","keycap: 6",7,0],["7️⃣","keycap: 7",7,0],["8️⃣","keycap: 8",7,0],["9️⃣","keycap: 9",7,0],["🔟","keycap: 10",7,0],["🔠","input latin uppercase",7,0],["🔡","input latin lowercase",7,0],["🔢","input numbers",7,0],["🔣","input symbols",7,0],["🔤","input latin letters",7,0],["🅰️","A button (blood type)",7,0],["🆎","AB button (blood type)",7,0],["🅱️","B button (blood type)",7,0],["🆑","CL button",7,0],["🆒","COOL button",7,0],["🆓","FREE button",7,0],["ℹ️","information",7,0],["🆔","ID button",7,0],["Ⓜ️","circled M",7,0],["🆕","NEW button",7,0],["🆖","NG button",7,0],["🅾️","O button (blood type)",7,0],["🆗","OK button",7,0],["🅿️","P button",7,0],["🆘","SOS button",7,0],["🆙","UP! button",7,0],["🆚","VS button",7,0],["🈁","Japanese “here” button",7,0],["🈂️","Japanese “service charge” button",7,0],["🈷️","Japanese “monthly amount” button",7,0],["🈶","Japanese “not free of charge” button",7,0],["🈯","Japanese “reserved” button",7,0],["🉐","Japanese “bargain” button",7,0],["🈹","Japanese “discount” button",7,0],["🈚","Japanese “free of charge” button",7,0],["🈲","Japanese “prohibited” button",7,0],["🉑","Japanese “acceptable” button",7,0],["🈸","Japanese “application” button",7,0],["🈴","Japanese “passing grade” button",7,0],["🈳","Japanese “vacancy” button",7,0],["㊗️","Japanese “congratulations” button",7,0],["㊙️","Japanese “secret” button",7,0],["🈺","Japanese “open for business” button",7,0],["🈵","Japanese “no vacancy” button",7,0],["🔴","red circle",7,0],["🟠","orange circle",7,0],["🟡","yellow circle",7,0],["🟢","green circle",7,0],["🔵","blue circle",7,0],["🟣","purple circle",7,0],["🟤","brown circle",7,0],["⚫","black circle",7,0],["⚪","white circle",7,0],["🟥","red square",7,0],["🟧","orange square",7,0],["🟨","yellow square",7,0],["🟩","green square",7,0],["🟦","blue square",7,0],["🟪","purple square",7,0],["🟫","brown square",7,0],["⬛","black large square",7,0],["⬜","white large square",7,0],["◼️","black medium square",7,0],["◻️","white medium square",7,0],["◾","black medium-small square",7,0],["◽","white medium-small square",7,0],["▪️","black small square",7,0],["▫️","white small square",7,0],["🔶","large orange diamond",7,0],["🔷","large blue diamond",7,0],["🔸","small orange diamond",7,0],["🔹","small blue diamond",7,0],["🔺","red triangle pointed up",7,0],["🔻","red triangle pointed down",7,0],["💠","diamond with a dot",7,0],["🔘","radio button",7,0],["🔳","white square button",7,0],["🔲","black square button",7,0],["🏁","chequered flag",8,0],["🚩","triangular flag",8,0],["🎌","crossed flags",8,0],["🏴","black flag",8,0],["🏳️","white flag",8,0],["🏳️‍🌈","rainbow flag",8,0],["🏳️‍⚧️","transgender flag",8,0],["🏴‍☠️","pirate flag",8,0],["🇦🇨","flag: Ascension Island",8,0],["🇦🇩","flag: Andorra",8,0],["🇦🇪","flag: United Arab Emirates",8,0],["🇦🇫","flag: Afghanistan",8,0],["🇦🇬","flag: Antigua & Barbuda",8,0],["🇦🇮","flag: Anguilla",8,0],["🇦🇱","flag: Albania",8,0],["🇦🇲","flag: Armenia",8,0],["🇦🇴","flag: Angola",8,0],["🇦🇶","flag: Antarctica",8,0],["🇦🇷","flag: Argentina",8,0],["🇦🇸","flag: American Samoa",8,0],["🇦🇹","flag: Austria",8,0],["🇦🇺","flag: Australia",8,0],["🇦🇼","flag: Aruba",8,0],["🇦🇽","flag: Åland Islands",8,0],["🇦🇿","flag: Azerbaijan",8,0],["🇧🇦","flag: Bosnia & Herzegovina",8,0],["🇧🇧","flag: Barbados",8,0],["🇧🇩","flag: Bangladesh",8,0],["🇧🇪","flag: Belgium",8,0],["🇧🇫","flag: Burkina Faso",8,0],["🇧🇬","flag: Bulgaria",8,0],["🇧🇭","flag: Bahrain",8,0],["🇧🇮","flag: Burundi",8,0],["🇧🇯","flag: Benin",8,0],["🇧🇱","flag: St. Barthélemy",8,0],["🇧🇲","flag: Bermuda",8,0],["🇧🇳","flag: Brunei",8,0],["🇧🇴","flag: Bolivia",8,0],["🇧🇶","flag: Caribbean Netherlands",8,0],["🇧🇷","flag: Brazil",8,0],["🇧🇸","flag: Bahamas",8,0],["🇧🇹","flag: Bhutan",8,0],["🇧🇻","flag: Bouvet Island",8,0],["🇧🇼","flag: Botswana",8,0],["🇧🇾","flag: Belarus",8,0],["🇧🇿","flag: Belize",8,0],["🇨🇦","flag: Canada",8,0],["🇨🇨","flag: Cocos (Keeling) Islands",8,0],["🇨🇩","flag: Congo - Kinshasa",8,0],["🇨🇫","flag: Central African Republic",8,0],["🇨🇬","flag: Congo - Brazzaville",8,0],["🇨🇭","flag: Switzerland",8,0],["🇨🇮","flag: Côte d’Ivoire",8,0],["🇨🇰","flag: Cook Islands",8,0],["🇨🇱","flag: Chile",8,0],["🇨🇲","flag: Cameroon",8,0],["🇨🇳","flag: China",8,0],["🇨🇴","flag: Colombia",8,0],["🇨🇵","flag: Clipperton Island",8,0],["🇨🇶","flag: Sark",8,0],["🇨🇷","flag: Costa Rica",8,0],["🇨🇺","flag: Cuba",8,0],["🇨🇻","flag: Cape Verde",8,0],["🇨🇼","flag: Curaçao",8,0],["🇨🇽","flag: Christmas Island",8,0],["🇨🇾","flag: Cyprus",8,0],["🇨🇿","flag: Czechia",8,0],["🇩🇪","flag: Germany",8,0],["🇩🇬","flag: Diego Garcia",8,0],["🇩🇯","flag: Djibouti",8,0],["🇩🇰","flag: Denmark",8,0],["🇩🇲","flag: Dominica",8,0],["🇩🇴","flag: Dominican Republic",8,0],["🇩🇿","flag: Algeria",8,0],["🇪🇦","flag: Ceuta & Melilla",8,0],["🇪🇨","flag: Ecuador",8,0],["🇪🇪","flag: Estonia",8,0],["🇪🇬","flag: Egypt",8,0],["🇪🇭","flag: Western Sahara",8,0],["🇪🇷","flag: Eritrea",8,0],["🇪🇸","flag: Spain",8,0],["🇪🇹","flag: Ethiopia",8,0],["🇪🇺","flag: European Union",8,0],["🇫🇮","flag: Finland",8,0],["🇫🇯","flag: Fiji",8,0],["🇫🇰","flag: Falkland Islands",8,0],["🇫🇲","flag: Micronesia",8,0],["🇫🇴","flag: Faroe Islands",8,0],["🇫🇷","flag: France",8,0],["🇬🇦","flag: Gabon",8,0],["🇬🇧","flag: United Kingdom",8,0],["🇬🇩","flag: Grenada",8,0],["🇬🇪","flag: Georgia",8,0],["🇬🇫","flag: French Guiana",8,0],["🇬🇬","flag: Guernsey",8,0],["🇬🇭","flag: Ghana",8,0],["🇬🇮","flag: Gibraltar",8,0],["🇬🇱","flag: Greenland",8,0],["🇬🇲","flag: Gambia",8,0],["🇬🇳","flag: Guinea",8,0],["🇬🇵","flag: Guadeloupe",8,0],["🇬🇶","flag: Equatorial Guinea",8,0],["🇬🇷","flag: Greece",8,0],["🇬🇸","flag: South Georgia & South Sandwich Islands",8,0],["🇬🇹","flag: Guatemala",8,0],["🇬🇺","flag: Guam",8,0],["🇬🇼","flag: Guinea-Bissau",8,0],["🇬🇾","flag: Guyana",8,0],["🇭🇰","flag: Hong Kong SAR China",8,0],["🇭🇲","flag: Heard & McDonald Islands",8,0],["🇭🇳","flag: Honduras",8,0],["🇭🇷","flag: Croatia",8,0],["🇭🇹","flag: Haiti",8,0],["🇭🇺","flag: Hungary",8,0],["🇮🇨","flag: Canary Islands",8,0],["🇮🇩","flag: Indonesia",8,0],["🇮🇪","flag: Ireland",8,0],["🇮🇱","flag: Israel",8,0],["🇮🇲","flag: Isle of Man",8,0],["🇮🇳","flag: India",8,0],["🇮🇴","flag: British Indian Ocean Territory",8,0],["🇮🇶","flag: Iraq",8,0],["🇮🇷","flag: Iran",8,0],["🇮🇸","flag: Iceland",8,0],["🇮🇹","flag: Italy",8,0],["🇯🇪","flag: Jersey",8,0],["🇯🇲","flag: Jamaica",8,0],["🇯🇴","flag: Jordan",8,0],["🇯🇵","flag: Japan",8,0],["🇰🇪","flag: Kenya",8,0],["🇰🇬","flag: Kyrgyzstan",8,0],["🇰🇭","flag: Cambodia",8,0],["🇰🇮","flag: Kiribati",8,0],["🇰🇲","flag: Comoros",8,0],["🇰🇳","flag: St. Kitts & Nevis",8,0],["🇰🇵","flag: North Korea",8,0],["🇰🇷","flag: South Korea",8,0],["🇰🇼","flag: Kuwait",8,0],["🇰🇾","flag: Cayman Islands",8,0],["🇰🇿","flag: Kazakhstan",8,0],["🇱🇦","flag: Laos",8,0],["🇱🇧","flag: Lebanon",8,0],["🇱🇨","flag: St. Lucia",8,0],["🇱🇮","flag: Liechtenstein",8,0],["🇱🇰","flag: Sri Lanka",8,0],["🇱🇷","flag: Liberia",8,0],["🇱🇸","flag: Lesotho",8,0],["🇱🇹","flag: Lithuania",8,0],["🇱🇺","flag: Luxembourg",8,0],["🇱🇻","flag: Latvia",8,0],["🇱🇾","flag: Libya",8,0],["🇲🇦","flag: Morocco",8,0],["🇲🇨","flag: Monaco",8,0],["🇲🇩","flag: Moldova",8,0],["🇲🇪","flag: Montenegro",8,0],["🇲🇫","flag: St. Martin",8,0],["🇲🇬","flag: Madagascar",8,0],["🇲🇭","flag: Marshall Islands",8,0],["🇲🇰","flag: North Macedonia",8,0],["🇲🇱","flag: Mali",8,0],["🇲🇲","flag: Myanmar (Burma)",8,0],["🇲🇳","flag: Mongolia",8,0],["🇲🇴","flag: Macao SAR China",8,0],["🇲🇵","flag: Northern Mariana Islands",8,0],["🇲🇶","flag: Martinique",8,0],["🇲🇷","flag: Mauritania",8,0],["🇲🇸","flag: Montserrat",8,0],["🇲🇹","flag: Malta",8,0],["🇲🇺","flag: Mauritius",8,0],["🇲🇻","flag: Maldives",8,0],["🇲🇼","flag: Malawi",8,0],["🇲🇽","flag: Mexico",8,0],["🇲🇾","flag: Malaysia",8,0],["🇲🇿","flag: Mozambique",8,0],["🇳🇦","flag: Namibia",8,0],["🇳🇨","flag: New Caledonia",8,0],["🇳🇪","flag: Niger",8,0],["🇳🇫","flag: Norfolk Island",8,0],["🇳🇬","flag: Nigeria",8,0],["🇳🇮","flag: Nicaragua",8,0],["🇳🇱","flag: Netherlands",8,0],["🇳🇴","flag: Norway",8,0],["🇳🇵","flag: Nepal",8,0],["🇳🇷","flag: Nauru",8,0],["🇳🇺","flag: Niue",8,0],["🇳🇿","flag: New Zealand",8,0],["🇴🇲","flag: Oman",8,0],["🇵🇦","flag: Panama",8,0],["🇵🇪","flag: Peru",8,0],["🇵🇫","flag: French Polynesia",8,0],["🇵🇬","flag: Papua New Guinea",8,0],["🇵🇭","flag: Philippines",8,0],["🇵🇰","flag: Pakistan",8,0],["🇵🇱","flag: Poland",8,0],["🇵🇲","flag: St. Pierre & Miquelon",8,0],["🇵🇳","flag: Pitcairn Islands",8,0],["🇵🇷","flag: Puerto Rico",8,0],["🇵🇸","flag: Palestinian Territories",8,0],["🇵🇹","flag: Portugal",8,0],["🇵🇼","flag: Palau",8,0],["🇵🇾","flag: Paraguay",8,0],["🇶🇦","flag: Qatar",8,0],["🇷🇪","flag: Réunion",8,0],["🇷🇴","flag: Romania",8,0],["🇷🇸","flag: Serbia",8,0],["🇷🇺","flag: Russia",8,0],["🇷🇼","flag: Rwanda",8,0],["🇸🇦","flag: Saudi Arabia",8,0],["🇸🇧","flag: Solomon Islands",8,0],["🇸🇨","flag: Seychelles",8,0],["🇸🇩","flag: Sudan",8,0],["🇸🇪","flag: Sweden",8,0],["🇸🇬","flag: Singapore",8,0],["🇸🇭","flag: St. Helena",8,0],["🇸🇮","flag: Slovenia",8,0],["🇸🇯","flag: Svalbard & Jan Mayen",8,0],["🇸🇰","flag: Slovakia",8,0],["🇸🇱","flag: Sierra Leone",8,0],["🇸🇲","flag: San Marino",8,0],["🇸🇳","flag: Senegal",8,0],["🇸🇴","flag: Somalia",8,0],["🇸🇷","flag: Suriname",8,0],["🇸🇸","flag: South Sudan",8,0],["🇸🇹","flag: São Tomé & Príncipe",8,0],["🇸🇻","flag: El Salvador",8,0],["🇸🇽","flag: Sint Maarten",8,0],["🇸🇾","flag: Syria",8,0],["🇸🇿","flag: Eswatini",8,0],["🇹🇦","flag: Tristan da Cunha",8,0],["🇹🇨","flag: Turks & Caicos Islands",8,0],["🇹🇩","flag: Chad",8,0],["🇹🇫","flag: French Southern Territories",8,0],["🇹🇬","flag: Togo",8,0],["🇹🇭","flag: Thailand",8,0],["🇹🇯","flag: Tajikistan",8,0],["🇹🇰","flag: Tokelau",8,0],["🇹🇱","flag: Timor-Leste",8,0],["🇹🇲","flag: Turkmenistan",8,0],["🇹🇳","flag: Tunisia",8,0],["🇹🇴","flag: Tonga",8,0],["🇹🇷","flag: Türkiye",8,0],["🇹🇹","flag: Trinidad & Tobago",8,0],["🇹🇻","flag: Tuvalu",8,0],["🇹🇼","flag: Taiwan",8,0],["🇹🇿","flag: Tanzania",8,0],["🇺🇦","flag: Ukraine",8,0],["🇺🇬","flag: Uganda",8,0],["🇺🇲","flag: U.S. Outlying Islands",8,0],["🇺🇳","flag: United Nations",8,0],["🇺🇸","flag: United States",8,0],["🇺🇾","flag: Uruguay",8,0],["🇺🇿","flag: Uzbekistan",8,0],["🇻🇦","flag: Vatican City",8,0],["🇻🇨","flag: St. Vincent & Grenadines",8,0],["🇻🇪","flag: Venezuela",8,0],["🇻🇬","flag: British Virgin Islands",8,0],["🇻🇮","flag: U.S. Virgin Islands",8,0],["🇻🇳","flag: Vietnam",8,0],["🇻🇺","flag: Vanuatu",8,0],["🇼🇫","flag: Wallis & Futuna",8,0],["🇼🇸","flag: Samoa",8,0],["🇽🇰","flag: Kosovo",8,0],["🇾🇪","flag: Yemen",8,0],["🇾🇹","flag: Mayotte",8,0],["🇿🇦","flag: South Africa",8,0],["🇿🇲","flag: Zambia",8,0],["🇿🇼","flag: Zimbabwe",8,0],["🏴󠁧󠁢󠁥󠁮󠁧󠁿","flag: England",8,0],["🏴󠁧󠁢󠁳󠁣󠁴󠁿","flag: Scotland",8,0],["🏴󠁧󠁢󠁷󠁬󠁳󠁿","flag: Wales",8,0]]} diff --git a/tests/emoji.test.js b/tests/emoji.test.js new file mode 100644 index 0000000..b0d4eba --- /dev/null +++ b/tests/emoji.test.js @@ -0,0 +1,59 @@ +const test = require("node:test") +const assert = require("node:assert/strict") +const fs = require("fs") +const path = require("path") +const Emoji = require("../core/Emoji.js") + +const raw = fs.readFileSync(path.join(__dirname, "..", "data", "emoji.json"), "utf8") +const keywords = JSON.stringify([{ e: "😂", k: "face with tears of joy lol haha" }, { e: "❤", k: "red heart love" }]) +const { groups, list } = Emoji.parse(raw, keywords) +const state = { pinned: [], recent: [], tone: "none", filter: "all" } +const symbols = (sections) => sections.flatMap((s) => s.rows.map((r) => r.symbol)) + +test("parses the bundled data into groups without components or toned variants", () => { + assert.equal(groups[0], "Smileys & Emotion") + assert.ok(!groups.includes("Component")) + assert.ok(list.length > 1800) + assert.ok(!list.some((e) => /[\u{1F3FB}-\u{1F3FF}]/u.test(e.symbol))) + assert.equal(list[0].title, "Grinning Face") +}) + +test("search ranks name matches over keyword matches and merges Omarchy keywords", () => { + assert.equal(symbols(Emoji.sections(list, "lol", state))[0], "😂") + assert.equal(symbols(Emoji.sections(list, "love", state)).includes("❤️"), true) // keyword without FE0F still merges + const hearts = symbols(Emoji.sections(list, "red heart", state)) + assert.equal(hearts[0], "❤️") + assert.deepEqual(Emoji.sections(list, "zzqqxx", state), []) +}) + +test("empty query shows pinned, recent, then categories; filter narrows", () => { + const s = Emoji.sections(list, "", { ...state, pinned: ["🔥"], recent: ["🔥", "👍"] }) + assert.deepEqual(s.slice(0, 3).map((x) => x.title), ["Pinned", "Recently Used", "Smileys & Emotion"]) + assert.deepEqual(s[1].rows.map((r) => r.symbol), ["👍"]) + const flags = Emoji.sections(list, "", { ...state, filter: "Flags" }) + assert.deepEqual(flags.map((x) => x.title), ["Flags"]) +}) + +test("skin tone applies to toneable emoji only", () => { + assert.equal(Emoji.applyTone("👋", "medium"), "👋🏽") + assert.equal(Emoji.applyTone("🖐️", "dark"), "🖐🏿") + assert.equal(Emoji.applyTone("🏃‍♂️", "light"), "🏃🏻‍♂️") + const wave = Emoji.sections(list, "waving hand", { ...state, tone: "dark" })[0].rows[0] + assert.equal(wave.symbol, "👋🏿") + assert.equal(wave.id, "emoji:👋") + const smile = Emoji.sections(list, "grinning face", { ...state, tone: "dark" })[0].rows[0] + assert.equal(smile.symbol, "😀") +}) + +test("gridLines packs sections into lines of columns", () => { + const lines = Emoji.gridLines([{ id: "a", title: "A", rows: [1, 2, 3, 4, 5] }], 2) + assert.deepEqual(lines.map((l) => (l.section ? "S" : l.cells.length)), ["S", 2, 2, 1]) +}) + +test("flea hits rank by best field, then name, then recently used", () => { + const T = 4000000 + const hits = { "🔥": { title: 5000000, keyword: null }, "🚒": { title: T, keyword: 5000000 }, "🧯": { title: T, keyword: 5000000 } } + const s = Emoji.sections(list, "fire", { ...state, recent: ["🧯"], fleaHits: hits }) + assert.deepEqual(symbols(s), ["🔥", "🧯", "🚒"]) + assert.deepEqual(Emoji.searchEntries([{ name: "fire", words: "fire flame hot" }]), [{ title: "fire", keywords: ["flame", "hot"] }]) +}) diff --git a/ui/LauncherWindow.qml b/ui/LauncherWindow.qml index 618a8ad..a7297a2 100644 --- a/ui/LauncherWindow.qml +++ b/ui/LauncherWindow.qml @@ -6,6 +6,7 @@ import qs.Commons as Commons import qs.Ui import "../core/RootSearch.js" as RootSearch import "../core/Rows.js" as Rows +import "../core/Emoji.js" as Emoji // Omarchy 4.0.x dropped qs.Ui.OverlayWindow; this is the same full-screen // overlay layer the shell's own emoji picker uses. @@ -73,6 +74,9 @@ PanelWindow { // Ctrl+P root filter (RootSearch.FILTERS), Ctrl+T toast actions, // Ctrl+Shift+K About panel and the confirm alert. property string rootFilter: "all" + // Emoji & Symbols: the cell selected in a grid line. + property int selectedColumn: 0 + readonly property bool grid: window.view.kind === "emoji" property bool dropdownOpen: false property bool toastPanelOpen: false property bool aboutOpen: false @@ -98,6 +102,7 @@ PanelWindow { readonly property string placeholder: window.view.kind === "alias" ? "Type an alias, then press ↵" : window.view.kind === "menu" ? "Search " + window.view.title + "…" : window.view.kind === "calculator-history" ? "Search calculations…" + : window.grid ? "Search Emoji & Symbols…" : "Search for apps and commands…" readonly property int cardWidth: Math.min(px(750), width - Style.gapsOut * 2) @@ -195,8 +200,19 @@ PanelWindow { window.select(-1) return } - var keepIndex = -1, next = [] - for (var s = 0; s < result.data.length; s++) { + var keepIndex = -1, keepColumn = 0, next = [] + if (window.grid) { + var lines = Emoji.gridLines(result.data, Emoji.COLUMNS) + for (var l = 0; l < lines.length; l++) { + if (lines[l].section) { next.push(window.rowData({ id: "section:" + lines[l].section.id, title: lines[l].section.title }, "section")); continue } + var cells = lines[l].cells + for (var k = 0; k < cells.length; k++) if (keepIndex < 0 && cells[k].id === keepId) { keepIndex = next.length; keepColumn = k } + var line = window.rowData({ id: "line:" + l }, "emoji-row") + line.cellsJson = JSON.stringify(cells.map(function(c) { return { id: c.id, symbol: c.symbol, title: c.title } })) + next.push(line) + } + } + for (var s = 0; s < (window.grid ? 0 : result.data.length); s++) { var section = result.data[s] if (section.title || s > 0) next.push(window.rowData({ id: "section:" + section.id, title: section.title }, "section")) for (var r = 0; r < section.rows.length; r++) { @@ -207,7 +223,7 @@ PanelWindow { } Rows.update(rows, next) if (restoreIndex >= 0 && restoreIndex < rows.count && window.isSelectable(restoreIndex)) window.select(restoreIndex) - else window.select(keepIndex >= 0 ? keepIndex : window.nextSelectable(-1, 1)) + else window.select(keepIndex >= 0 ? keepIndex : window.nextSelectable(-1, 1), keepIndex >= 0 ? keepColumn : 0) } function rowData(c, kind) { @@ -228,13 +244,14 @@ PanelWindow { calcResult: String(c.calcResult || ""), calcInputCaption: String(c.calcInputCaption || ""), calcResultCaption: String(c.calcResultCaption || ""), - argsJson: JSON.stringify(c.arguments || []) + argsJson: JSON.stringify(c.arguments || []), + cellsJson: "" } } function isSelectable(index) { var kind = rows.get(index).rowKind - return kind === "item" || kind === "calculator" + return kind === "item" || kind === "calculator" || kind === "emoji-row" } function nextSelectable(from, direction) { @@ -243,7 +260,7 @@ PanelWindow { return -1 } - function select(index) { + function select(index, column) { if (index !== window.selectedIndex) { var previous = list.itemAtIndex(window.selectedIndex) if (previous) previous.selected = false @@ -251,7 +268,17 @@ PanelWindow { window.selectedIndex = index var current = list.itemAtIndex(index) if (current) current.selected = true - window.selectedRow = Rows.snapshot(rows, index) + var snap = Rows.snapshot(rows, index) + // A grid line stands for its selected cell: actions, About and the + // footer all see that emoji. + if (snap && snap.rowKind === "emoji-row") { + var cells = JSON.parse(snap.cellsJson) + var col = Math.max(0, Math.min(cells.length - 1, column === undefined ? window.selectedColumn : column)) + window.selectedColumn = col + snap.itemId = cells[col].id + snap.title = cells[col].title + } + window.selectedRow = snap // Argument values belong to one command; a different row starts empty. var id = window.selectedRow ? window.selectedRow.itemId : "" if (id !== window.argsForId) { @@ -277,6 +304,17 @@ PanelWindow { window.select(target) } + // ← → in the emoji grid, wrapping onto the neighbouring line. + function moveColumn(delta) { + if (!window.selectedRow || window.selectedRow.rowKind !== "emoji-row") return + pointerGate.reset() + var col = window.selectedColumn + delta + var count = JSON.parse(window.selectedRow.cellsJson).length + if (col >= 0 && col < count) { window.select(window.selectedIndex, col); return } + var line = window.nextSelectable(window.selectedIndex, delta) + if (line >= 0) window.select(line, delta > 0 ? 0 : Emoji.COLUMNS - 1) + } + function jumpSection(direction) { var i = window.selectedIndex for (i += direction; i >= 0 && i < rows.count; i += direction) @@ -622,12 +660,14 @@ PanelWindow { if (window.historyIndex >= 0) { window.recallHistory(-1); return true } window.move(1); return true } + if (window.grid && !ctrl && !alt && (key === Qt.Key_Left || key === Qt.Key_Right)) { window.moveColumn(key === Qt.Key_Left ? -1 : 1); return true } + if (ctrl && shift && key === Qt.Key_P) { window.performIfOffered(["pin", "unpin"]); return true } if (key === Qt.Key_PageUp) { window.move(-8); return true } if (key === Qt.Key_PageDown) { window.move(8); return true } if (ctrl && shift && key === Qt.Key_F) { window.performIfOffered(["favorite-add", "favorite-remove"]); return true } - if (ctrl && shift && key === Qt.Key_C) { window.performIfOffered(["copy-command", "copy-id", "copy-question-answer"]); return true } - if (ctrl && shift && key === Qt.Key_D) { window.performIfOffered(["delete-all-entries"]); return true } - if (ctrl && key === Qt.Key_D) { window.performIfOffered(["delete-entry"]); return true } + if (ctrl && shift && key === Qt.Key_C) { window.performIfOffered(["copy-command", "copy-id", "copy-question-answer", "copy-name"]); return true } + if (ctrl && shift && key === Qt.Key_D) { window.performIfOffered(["delete-all-entries", "clear-recent"]); return true } + if (ctrl && key === Qt.Key_D) { window.performIfOffered(["delete-entry", "remove-recent"]); return true } if (alt && shift && key === Qt.Key_D) { window.performIfOffered(["disable"]); return true } if (ctrl && key >= Qt.Key_1 && key <= Qt.Key_9) { var target = window.nthItem(key - Qt.Key_0) @@ -883,6 +923,9 @@ PanelWindow { clip: true model: rows reuseItems: true + // Off-screen emoji lines still decode their color glyphs, which is + // most of what opening the picker costs (~100 ms); build only visible lines. + cacheBuffer: window.grid ? 0 : 320 boundsBehavior: Flickable.StopAtBounds delegate: ResultRow { @@ -892,8 +935,10 @@ PanelWindow { look: window.look appLibrary: window.appLibrary zoom: window.zoom - onHovered: function(i, item, mouse) { if (!window.actionPanelOpen && pointerGate.moved(item, mouse)) window.select(i) } - onClicked: function(i) { window.perform("open", i) } + selectedColumn: window.selectedColumn + columns: Emoji.COLUMNS + onHovered: function(i, item, mouse) { if (!window.actionPanelOpen && pointerGate.moved(item, mouse)) window.select(i, item.isGrid ? item.columnAt(mouse.x) : undefined) } + onClicked: function(i, column) { if (window.grid) window.select(i, column); window.perform("open", i) } } } @@ -972,7 +1017,8 @@ PanelWindow { id: footerStatus anchors.verticalCenter: parent.verticalCenter textFormat: Text.PlainText - text: window.toast ? window.toast.title : window.loadingVisible ? "Loading…" : window.view.title + text: window.toast ? window.toast.title : window.loadingVisible ? "Loading…" + : window.grid && window.selectedRow ? window.selectedRow.title : window.view.title color: window.look.foreground opacity: window.toast ? 0.9 : 0.6 elide: Text.ElideRight diff --git a/ui/ResultRow.qml b/ui/ResultRow.qml index ab5a1ec..0352d29 100644 --- a/ui/ResultRow.qml +++ b/ui/ResultRow.qml @@ -22,6 +22,15 @@ Item { required property string calcResult required property string calcInputCaption required property string calcResultCaption + required property string cellsJson + + // Emoji grid line (rowKind "emoji-row"): up to `columns` cells; the + // selected one is selectedColumn. + property int selectedColumn: 0 + property int columns: 8 + readonly property var cells: row.isGrid && row.cellsJson ? JSON.parse(row.cellsJson) : [] + readonly property real cellWidth: row.width / row.columns + function columnAt(x) { return Math.max(0, Math.min(row.cells.length - 1, Math.floor(x / row.cellWidth))) } property bool selected: false property var look: null @@ -29,14 +38,41 @@ Item { property real zoom: 1 signal hovered(int index, var item, var mouse) - signal clicked(int index) + signal clicked(int index, int column) readonly property bool isSection: row.rowKind === "section" readonly property bool isCalculator: row.rowKind === "calculator" + readonly property bool isGrid: row.rowKind === "emoji-row" function px(v) { return Math.round(Style.space(v) * row.zoom) } width: ListView.view ? ListView.view.width : 0 - height: row.isSection ? (row.title ? px(30) : px(6)) : (row.isCalculator ? px(118) : px(42)) + height: row.isSection ? (row.title ? px(30) : px(6)) : (row.isCalculator ? px(118) : row.isGrid ? Math.round(row.cellWidth * 0.86) : px(42)) + + // Raycast's emoji grid (ref 04 §2 grid views): a tile per emoji, the + // selected one filled and outlined. + Repeater { + model: row.isGrid ? row.cells : [] + delegate: Rectangle { + required property var modelData + required property int index + readonly property bool current: row.selected && index === row.selectedColumn + x: index * row.cellWidth + row.px(3) + y: row.px(3) + width: row.cellWidth - row.px(6) + height: row.height - row.px(6) + radius: Style.cornerRadius + color: current ? row.look.selectedBackground : Util.alpha(row.look.foreground, 0.04) + border.width: current ? Math.max(1, Style.space(2)) : 0 + border.color: row.look.accent + Text { + anchors.centerIn: parent + textFormat: Text.PlainText + text: modelData.symbol + font.pixelSize: Math.round(parent.height * 0.5) + } + } + } + // The inline calculator (ref 04 §1 Calculator): question on the left, // answer on the right, each with a caption, an arrow between them. @@ -142,7 +178,7 @@ Item { } Rectangle { - visible: !row.isSection && !row.isCalculator + visible: !row.isSection && !row.isCalculator && !row.isGrid anchors.fill: parent radius: Math.max(Style.cornerRadius, row.px(6)) color: row.selected ? row.look.selectedBackground : "transparent" @@ -278,6 +314,6 @@ Item { hoverEnabled: true cursorShape: Qt.PointingHandCursor onPositionChanged: function(mouse) { row.hovered(row.index, row, mouse) } - onClicked: row.clicked(row.index) + onClicked: function(mouse) { row.clicked(row.index, row.isGrid ? row.columnAt(mouse.x) : 0) } } }