diff --git a/.archive/README.md b/.archive/README.md new file mode 100644 index 00000000..0ebca9d9 --- /dev/null +++ b/.archive/README.md @@ -0,0 +1,3 @@ +# The archive + +0.18 changed the API and most of the plugins here have no maintainer. Please volunteer as maintainer and help getting plugins shipped. 👍 diff --git a/.archive/atom_projects/__init__.py b/.archive/atom_projects/__init__.py new file mode 100644 index 00000000..e8f50d1d --- /dev/null +++ b/.archive/atom_projects/__init__.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- + +"""List and open your Atom projects. + +Synopsis: [filter]""" + +# Copyright (c) 2022 Manuel Schneider + +import os +import re +import time +from pathlib import Path + +import cson + +from albert import * + +__title__ = "Atom Projects" +__version__ = "0.4.0" +__triggers__ = "atom " +__authors__ = "Manuel S." +__exec_deps__ = ["atom"] +__py_deps__ = ["cson"] + +projects_file = str(Path.home()) + "/.atom/projects.cson" +iconPath = iconLookup('atom') +mtime = 0 +projects = [] + + +def updateProjects(): + global mtime + try: + new_mtime = os.path.getmtime(projects_file) + except Exception as e: + warning("Could not get mtime of file: " + projects_file + str(e)) + if mtime != new_mtime: + mtime = new_mtime + with open(projects_file) as projects_cson: + global projects + projects = cson.loads(projects_cson.read()) + + +def handleQuery(query): + if not query.isTriggered: + return + + updateProjects() + + stripped = query.string.strip() + + items = [] + for project in projects: + if re.search(stripped, project['title'], re.IGNORECASE): + items.append(Item(id=__title__ + project['title'], + icon=iconPath, + text=project['title'], + subtext="Group: %s" % (project['group'] if 'group' in project else "None"), + actions=[ + ProcAction(text="Open project in Atom", + commandline=["atom"] + project['paths']) + ])) + return items diff --git a/.archive/base_converter/__init__.py b/.archive/base_converter/__init__.py new file mode 100644 index 00000000..5ed47926 --- /dev/null +++ b/.archive/base_converter/__init__.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- + +"""Convert representations of numbers. + +Synopsis: + + [padding] + + where is a literal of the form '0bXXX' (binary), '0XXX' (octal), + or '0xXXX' (hexadecimal).""" + +# Copyright (c) 2022 Manuel Schneider + +import numpy as np +from collections import defaultdict + +from albert import Item, ClipAction + +__title__ = "Base Converter" +__version__ = "0.4.1" +__triggers__ = "base " +__authors__ = ["Manuel S.", "Keating950"] +__py_deps__ = ["numpy"] + + +class keyed_defaultdict(defaultdict): + def __missing__(self, key): + return self.default_factory(key) + + +base_prefixes = keyed_defaultdict(lambda k: 8 if k[0] == "0" and len(k) > 1 else 10) +base_prefixes["0b"] = 2 +base_prefixes["0x"] = 16 + + +def buildItem(completion, dst, number, padding=0): + item = Item(id=__title__, completion=completion) + try: + src = base_prefixes[number[:2]] + dst = int(dst) + padding = int(padding) + integer = int(number, src) + item.text = np.base_repr(integer, dst) + if integer >= 0 and len(item.text) < padding: + item.text = '0'*(padding-len(item.text)) + item.text + item.subtext = "Base %s representation of %s (base %s)" % (dst, number, src) + item.addAction(ClipAction("Copy to clipboard", item.text)) + except Exception as e: + item.text = e.__class__.__name__ + item.subtext = str(e) + return item + + +def handleQuery(query): + if query.isTriggered: + fields = query.string.split() + if len(fields) == 2: + return buildItem(query.rawString, fields[0], fields[1]) + else: + item = Item(id=__title__) + item.text = __title__ + item.subtext = "Enter a query in the form of \"<dstbase> <number>\"" + return item + else: + fields = query.string.split() + if len(fields) < 2: + return + src = base_prefixes[fields[:3]] + number = fields[1] + padding = 0 if len(fields) < 3 else fields[2] + results = [] + for dst in sorted(base_prefixes.values().append(8)): + if dst == src: + continue + results.append(buildItem(query.rawString, dst, number, padding)) + return results diff --git a/.archive/binance/Binance.svg b/.archive/binance/Binance.svg new file mode 100644 index 00000000..c79e1829 --- /dev/null +++ b/.archive/binance/Binance.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/.archive/binance/__init__.py b/.archive/binance/__init__.py new file mode 100644 index 00000000..6ae6feb9 --- /dev/null +++ b/.archive/binance/__init__.py @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- + +"""Access the Binance markets. + +Synopsis: + filter + [filter]""" + +# Copyright (c) 2022 Manuel Schneider + +from albert import * +import time +import os +import urllib.request +import urllib.error +from collections import namedtuple +import json +from threading import Thread, Event + +__title__ = "Binance" +__version__ = "0.4.3" +__triggers__ = "bnc " +__authors__ = "Manuel S." + +iconPath = os.path.dirname(__file__) + "/Binance.svg" +exchangeInfoUrl = "https://api.binance.com/api/v1/exchangeInfo" +tradeUrl = "https://www.binance.com/en/trade/%s_%s?layout=pro" +markets = [] +thread = None + +Market = namedtuple("Market" , ["base", "quote"]) + +class UpdateThread(Thread): + def __init__(self): + super().__init__() + self._stopevent = Event() + + def run(self): + while True: + global thread + try: + global markets + with urllib.request.urlopen(exchangeInfoUrl) as response: + symbols = json.loads(response.read().decode())['symbols'] + markets.clear() + for symbol in symbols: + # Skip this strange 123456 market + if symbol['baseAsset'] != "123": + markets.append(Market(base=symbol['baseAsset'], + quote=symbol['quoteAsset'])) + info("Binance markets updated.") + self._stopevent.wait(3600) # Sleep 1h, wakeup on stop event + except Exception as e: + warning("Updating Binance markets failed: %s" % str(e)) + self._stopevent.wait(60) # Sleep 1 min, wakeup on stop event + + if self._stopevent.is_set(): + return + + def stop(self): + self._stopevent.set() + + +def initialize(): + global thread + thread = UpdateThread() + thread.start() + + +def finalize(): + global thread + if thread is not None: + thread.stop() + thread.join() + + +def makeItem(market): + url = tradeUrl % (market.base, market.quote) + return Item( + id="%s_%s%s" % (__title__, market.base, market.quote), + icon=iconPath, + text="%s/%s" % (market.base, market.quote), + subtext="Open the %s/%s market on binance.com" % (market.base, market.quote), + completion="%s%s%s" % (__triggers__, market.base, market.quote), + actions=[ + UrlAction("Show market in browser", url), + ClipAction('Copy URL to clipboard', url) + ] + ) + + +def handleQuery(query): + items = [] + stripped = query.string.strip().upper() + + if query.isTriggered: + if stripped: + for market in markets: + if ("%s%s" % (market.base, market.quote)).startswith(stripped): + items.append(makeItem(market)) + else: + for market in markets: + items.append(makeItem(market)) + else: + for market in markets: + if stripped and ("%s%s" % (market.base, market.quote)).startswith(stripped): + items.append(makeItem(market)) + + return items diff --git a/.archive/bitfinex/Bitfinex.svg b/.archive/bitfinex/Bitfinex.svg new file mode 100644 index 00000000..05b9efcf --- /dev/null +++ b/.archive/bitfinex/Bitfinex.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.archive/bitfinex/__init__.py b/.archive/bitfinex/__init__.py new file mode 100644 index 00000000..188785cd --- /dev/null +++ b/.archive/bitfinex/__init__.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- + +"""Access the Bitfinex markets. + +Synopsis: + filter + [filter]""" + +from albert import * +import time +import os +import urllib.request +import urllib.error +import json +from collections import namedtuple +from threading import Thread, Event + +__title__ = "BitFinex" +__version__ = "0.4.0" +__triggers__ = "bfx " +__authors__ = "Manuel S." + +iconPath = os.path.dirname(__file__) + "/Bitfinex.svg" +symbolsEndpoint = "https://api.bitfinex.com/v1/symbols" +tradeUrl = "https://www.bitfinex.com/t/%s:%s" +markets = [] +thread = None + +Market = namedtuple("Market" , ["base", "quote"]) + +class UpdateThread(Thread): + def __init__(self): + super().__init__() + self._stopevent = Event() + + def run(self): + while True: + global thread + try: + global markets + with urllib.request.urlopen(symbolsEndpoint) as response: + symbols = json.loads(response.read().decode()) + markets.clear() + for symbol in symbols: + symbol = symbol.upper() + markets.append(Market(base=symbol[0:3], quote=symbol[3:6])) + info("Bitfinex markets updated.") + self._stopevent.wait(3600) # Sleep 1h, wakeup on stop event + except Exception as e: + warning("Updating Bitfinex markets failed: %s" % str(e)) + self._stopevent.wait(60) # Sleep 1 min, wakeup on stop event + + if self._stopevent.is_set(): + return + + def stop(self): + self._stopevent.set() + + +def initialize(): + global thread + thread = UpdateThread() + thread.start() + + +def finalize(): + global thread + if thread is not None: + thread.stop() + thread.join() + +def makeItem(market): + url = tradeUrl % (market.base, market.quote) + return Item( + id="%s_%s%s" % (__title__, market.base, market.quote), + icon=iconPath, + text="%s/%s" % (market.base, market.quote), + subtext="Open the %s/%s market on bitfinex.com" % (market.base, market.quote), + completion="%s%s%s" % (__triggers__, market.base, market.quote), + actions=[ + UrlAction("Show market in browser", url), + ClipAction('Copy URL to clipboard', url) + ] + ) + + +def handleQuery(query): + items = [] + stripped = query.string.strip().upper() + + if query.isTriggered: + if stripped: + for market in markets: + if ("%s%s" % (market.base, market.quote)).startswith(stripped): + items.append(makeItem(market)) + else: + for market in markets: + items.append(makeItem(market)) + else: + for market in markets: + if stripped and ("%s%s" % (market.base, market.quote)).startswith(stripped): + items.append(makeItem(market)) + + return items diff --git a/.archive/currency_converter/__init__.py b/.archive/currency_converter/__init__.py new file mode 100644 index 00000000..9bd8d99c --- /dev/null +++ b/.archive/currency_converter/__init__.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- + +"""Convert currencies. + +Current backends: ECB, Yahoo. + +Synopsis: [to|as|in] """ + +# Copyright (c) 2022 Manuel Schneider + +import re +import time +from urllib.request import urlopen +from xml.etree import ElementTree + +from albert import * + +__title__ = "Currency converter" +__version__ = "0.4.0" +__authors__ = "Manuel S." + +iconPath = iconLookup('accessories-calculator') or ":python_module" + + +class EuropeanCentralBank: + + url = "https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml" + + def __init__(self): + self.lastUpdate = 0 + self.exchange_rates = dict() + self.name = "European Central Bank" + + def convert(self, amount, src, dst): + if self.lastUpdate < time.time()-10800: # Update every 3 hours + self.exchange_rates.clear() + with urlopen(EuropeanCentralBank.url) as response: + tree = ElementTree.fromstring(response.read().decode()) + for child in tree[2][0]: + curr = child.attrib['currency'] + rate = float(child.attrib['rate']) + self.exchange_rates[curr] = rate + self.exchange_rates["EUR"] = 1.0 # For simpler algorithmic + info("%s: Updated foreign exchange rates." % __title__) + debug(str(self.exchange_rates)) + self.lastUpdate = time.time() + + if src in self.exchange_rates and dst in self.exchange_rates: + src_rate = self.exchange_rates[src] + dst_rate = self.exchange_rates[dst] + return str(amount / src_rate * dst_rate) + +class Yahoo: + + def __init__(self): + self.name = "Yahoo" + + def convert(self, amount, src, dst): + if amount.is_integer: + amount = int(amount) + url = 'https://search.yahoo.com/search?p=%s+%s+to+%s' % (amount, src, dst) + with urlopen(url) as response: + html = response.read().decode() + m = re.search('(\d+(\.\d+)?)', html) + if m: + return m.group(1) + + +providers = [EuropeanCentralBank(), Yahoo()] +regex = re.compile(r"(\d+\.?\d*)\s+(\w{3})(?:\s+(?:to|in|as))?\s+(\w{3})") + +def handleQuery(query): + match = regex.fullmatch(query.string.strip()) + if match: + prep = (float(match.group(1)), match.group(2).upper(), match.group(3).upper()) + item = Item(id=__title__, icon=iconPath) + for provider in providers: + result = provider.convert(*prep) + if result: + item.text = result + item.subtext = "Value of %s %s in %s (Source: %s)" % (*prep, provider.name) + item.addAction(ClipAction("Copy result to clipboard", result)) + return item + else: + warning("None of the foreign exchange rate providers came up with a result for %s" % str(prep)) diff --git a/.archive/dango_emoji/__init__.py b/.archive/dango_emoji/__init__.py new file mode 100644 index 00000000..886853c8 --- /dev/null +++ b/.archive/dango_emoji/__init__.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- + +"""Find emojis using getdango.com + +Dango uses a form of artificial intelligence called deep learning to understand the nuances of \ +human emotion, and predict emoji based on what you type. If your emojis are not rendering properly \ +install an emoji font like e.g.: https://github.com/eosrei/emojione-color-font + +Synopsis: """ + + +from albert import * +import json +import os +import urllib.error +from urllib.request import urlopen, Request +from urllib.parse import urlencode + + +__title__ = "Dango Emoji" +__version__ = "0.4.1" +__triggers__ = ":" +__authors__ = "David Britt" + + +iconPath = os.path.dirname(__file__) + "/dangoemoji.png" +dangoUrl = "https://emoji.getdango.com/api/emoji" +emojipedia_url = "https://emojipedia.org/%s" + + +def handleQuery(query): + results = [] + if query.isTriggered: + + item = Item( + id=__title__, + icon=icon_path, + text=__title__ + ) + + if len(query.string) >= 2: + try: + url = "%s?%s" % (dangoUrl, urlencode({"q": query.string, "syn": 0})) + with urlopen(Request(url)) as response: + + json_data = json.loads(response.read().decode()) + + if json_data["results"][0]["score"] > 0.025: + all_emojis = [] + for emoj in json_data["results"]: + if emoj["score"] > 0.025: + all_emojis.append(emoj["text"]) + + string_emojis = ''.join(all_emojis) + + results.append(Item( + id=__title__, + icon=icon_path, + text=string_emojis, + subtext="Score > 0.025", + actions=[ + ClipAction( + "Copy translation to clipboard", string_emojis) + ] + )) + + for emoj in json_data["results"]: + results.append(Item( + id=__title__, + icon=icon_path, + text=str(emoj["text"]), + subtext=str(emoj["score"]), + actions=[ + ClipAction( + "Copy translation to clipboard", str(emoj["text"])), + UrlAction("Open in Emojipedia", + emojipedia_url % str(emoj["text"])) + ] + )) + + except urllib.error.URLError as urlerr: + print("Troubleshoot internet connection: %s" % urlerr) + item.subtext = "Connection error" + return item + else: + item.subtext = "Search emojis!" + return item + return results diff --git a/.archive/dango_emoji/dangoemoji.png b/.archive/dango_emoji/dangoemoji.png new file mode 100644 index 00000000..ea7846e7 Binary files /dev/null and b/.archive/dango_emoji/dangoemoji.png differ diff --git a/.archive/dango_kao/__init__.py b/.archive/dango_kao/__init__.py new file mode 100755 index 00000000..8701ecc3 --- /dev/null +++ b/.archive/dango_kao/__init__.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- + +"""Find kaomojis using getdango.com + +Dango uses a form of artificial intelligence called deep learning to understand the nuances of \ +human emotion, and predict emoji based on what you type. + +Synopsis: """ + +from albert import * +import os +import json +import urllib.error +from urllib.request import urlopen, Request +from urllib.parse import urlencode + +__title__ = "Dango Kaomoji" +__version__ = "0.4.0" +__triggers__ = "kao " +__authors__ = "David Britt" + +icon_path = os.path.dirname(__file__) + "/kaoicon.svg" +dangoUrl = "https://customer.getdango.com/dango/api/query/kaomoji" + + +def handleQuery(query): + results = [] + + if query.isTriggered: + + item = Item( + id=__title__, + icon=icon_path, + text=__title__, + ) + + if len(query.string) >= 2: + try: + url = "%s?%s" % (dangoUrl, urlencode({"q": query.string})) + with urlopen(Request(url)) as response: + json_data = json.loads(response.read().decode()) + for emoj in json_data["items"]: + results.append(Item( + id=__title__, + icon=icon_path, + text=emoj["text"], + actions=[ + ClipAction( + "Copy translation to clipboard", emoj["text"]) + ] + )) + except urllib.error.URLError as urlerr: + print("Troubleshoot internet connection: %s" % urlerr) + item.subtext = "Connection error" + return item + else: + item.subtext = "Search emojis!" + return item + + return results diff --git a/.archive/dango_kao/kaoicon.svg b/.archive/dango_kao/kaoicon.svg new file mode 100644 index 00000000..43c91872 --- /dev/null +++ b/.archive/dango_kao/kaoicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.archive/find/__init__.py b/.archive/find/__init__.py new file mode 100644 index 00000000..1b035bc6 --- /dev/null +++ b/.archive/find/__init__.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- + +""" +broken +""" +# Copyright (c) 2022 Manuel Schneider + +import albert +from albert import * +from time import sleep +import io +import os +import subprocess +from pathlib import Path + +__iid__ = "0.5" +__version__ = "1.0" +__id__ = "find" +__name__ = "Find" +__description__ = "Online search your file system" +__license__ = "BSD-3" +__url__ = "https://github.com/albertlauncher/python/tree/master/find" +__maintainers__ = "@manuelschneid3r" +__authors__ = ["@manuelschneid3r"] +__bin_dependencies__ = ["find"] +__default_trigger__ = "find " +__synopsis__ = "" + + +class Plugin(Plugin, QueryHandler): + def __init__(self): + albert.Plugin.__init__(self) + albert.QueryHandler.__init__(self) + + def id(self): + return __id__; + + def name(self): + return __name__; + + def takeThisAndModifyR(self, item): + item.id = "takeThisAndModifyR"; + + def takeThisAndModifyR_(self, item): + item.id = "takeThisAndModifyR_"; + + def takeThisAndModifyP(self, item): + item.id = "takeThisAndModifyP"; + + def description(self): + return __description__; + + def handleQuery(self, query): + info(query.string) + proc = subprocess.Popen(["find", Path.home(), "iname", query.string], stdout=subprocess.PIPE) + for line in io.TextIOWrapper(proc.stdout, encoding="utf-8"): + absolute = os.path.abspath(line) + item = Item( + id=absolute, + text=os.path.basename(absolute), + subtext=absolute, + completion="", + icon=[":python"], + actions=[ + Action( + id="clip", + text="setClipboardText (ClipAction)", + callable=lambda: setClipboardText(text=configLocation()) + ) + ] + ) + query.add(item) + + + def initialize(self): + info("Find::initialize") + +# def extensions(self): +# return [self.e] +## pass diff --git a/.archive/fortune/__init__.py b/.archive/fortune/__init__.py new file mode 100644 index 00000000..c1ba896f --- /dev/null +++ b/.archive/fortune/__init__.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- + +"""Display random poignant, inspirational, silly or snide phrase. + +Fortune wrapper extension. + +Synopsis: """ + +# Copyright (c) 2022 Manuel Schneider + +import subprocess as sp +from albert import * + +__title__ = "Fortune" +__version__ = "0.4.0" +__triggers__ = "fortune" +__authors__ = "Kelvin Wong" +__exec_deps__ = ["fortune"] + +iconPath = iconLookup("font") + + +def handleQuery(query): + if query.isTriggered: + newFortune = generateFortune() + if newFortune is not None: + return getFortuneItem(query, newFortune) + + +def generateFortune(): + try: + return sp.check_output(["fortune", "-s"]).decode().strip() + except sp.CalledProcessError as e: + return None + + +def getFortuneItem(query, fortune): + return Item( + id=__title__, + icon=iconPath, + text=fortune, + subtext="Copy this random, hopefully interesting, adage", + actions=[ClipAction("Copy to clipboard", fortune)] + ) diff --git a/.archive/gnome_dictionary/__init__.py b/.archive/gnome_dictionary/__init__.py new file mode 100644 index 00000000..a171a979 --- /dev/null +++ b/.archive/gnome_dictionary/__init__.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- + +"""Gnome dictionary. + +Needs 'gnome-dictionary' to be already installed. + +Sysnopsis: """ + +# Copyright (c) 2022 Manuel Schneider + +from subprocess import run + +from albert import * + +__title__ = "Gnome Dictionary" +__version__ = "0.4.0" +__triggers__ = "def " +__authors__ = "Nikhil Wanpal" +__exec_deps__ = ["gnome-dictionary"] + +iconPath = iconLookup('accessories-dictionary') + + +def handleQuery(query): + if query.isTriggered: + return Item(id=__title__, + icon=iconPath, + text=__title__, + subtext="Search for '%s' using %s" % (query.string, __title__), + actions=[ProcAction("Opens %s and searches for '%s'" % (__title__, query.string), + ["gnome-dictionary", "--look-up=%s" % query.string])]) diff --git a/Gnote.py b/.archive/gnote/__init__.py similarity index 62% rename from Gnote.py rename to .archive/gnote/__init__.py index 9bc0b783..18f7b59e 100644 --- a/Gnote.py +++ b/.archive/gnote/__init__.py @@ -1,31 +1,29 @@ -#!/usr/bin/env python +# -*- coding: utf-8 -*- -"""Access Gnotes +"""Search, open, create and delete notes. -Search, open, create and delete notes.""" +Synopsis: [filter]""" + +# Copyright (c) 2022 Manuel Schneider -from albertv0 import * -from dbus import SessionBus, Interface, DBusException -from shutil import which -from datetime import datetime import re +from datetime import datetime -__iid__ = "PythonInterface/v0.1" -__prettyname__ = "Gnote" -__version__ = "1.1" -__trigger__ = "gn " -__author__ = "Manuel Schneider" -__bin__ = __prettyname__.lower() -__dependencies__ = [__bin__, "python-dbus"] +from dbus import DBusException, Interface, SessionBus -BUS = "org.gnome.%s" % __prettyname__ -OBJ = "/org/gnome/%s/RemoteControl" % __prettyname__ -IFACE = 'org.gnome.%s.RemoteControl' % __prettyname__ +from albert import * -if which(__bin__) is None: - raise Exception("'%s' is not in $PATH." % __bin__) +__title__ = "Gnote" +__version__ = "0.4.1" +__triggers__ = "gn " +__authors__ = "Manuel S." +__exec_deps__ = ["gnote"] +__py_deps__ = ["dbus"] -iconPath = iconLookup(__bin__) +BUS = "org.gnome.%s" % __title__ +OBJ = "/org/gnome/%s/RemoteControl" % __title__ +IFACE = 'org.gnome.%s.RemoteControl' % __title__ +iconPath = iconLookup("gnote") def handleQuery(query): @@ -33,7 +31,7 @@ def handleQuery(query): if query.isTriggered: try: if not SessionBus().name_has_owner(BUS): - warning("Seems like %s is not running" % __bin__) + warning("Seems like gnote is not running") return obj = SessionBus().get_object(bus_name=BUS, object_path=OBJ) @@ -42,12 +40,11 @@ def handleQuery(query): if query.string.strip(): for note in iface.SearchNotes(query.string.lower(), False): results.append( - Item(id="%s%s" % (__prettyname__, note), + Item(id="%s%s" % (__title__, note), icon=iconPath, text=iface.GetNoteTitle(note), subtext="%s%s" % ("".join(["#%s " % re.search('.+:.+:(.+)', s).group(1) for s in iface.GetTagsForNote(note)]), datetime.fromtimestamp(iface.GetNoteChangeDate(note)).strftime("Note from %c")), - completion=query.rawString, actions=[ FuncAction("Open note", lambda note=note: iface.DisplayNote(note)), @@ -59,13 +56,12 @@ def createAndShowNote(): note = iface.CreateNote() iface.DisplayNote(note) - results.append(Item(id="%s-create" % __prettyname__, + results.append(Item(id="%s-create" % __title__, icon=iconPath, - text=__prettyname__, - subtext="%s notes" % __prettyname__, - completion=query.rawString, + text=__title__, + subtext="%s notes" % __title__, actions=[ - FuncAction("Open %s" % __prettyname__, + FuncAction("Open %s" % __title__, lambda: iface.DisplaySearch()), FuncAction("Create a new note", createAndShowNote) ])) diff --git a/GoogleTranslate.py b/.archive/google_translate/__init__.py similarity index 61% rename from GoogleTranslate.py rename to .archive/google_translate/__init__.py index 6c4b4411..30710ba8 100644 --- a/GoogleTranslate.py +++ b/.archive/google_translate/__init__.py @@ -1,36 +1,33 @@ # -*- coding: utf-8 -*- """Translate text using Google Translate. -Usage: tr -Example: tr en fr hello -Check available languages here: -https://cloud.google.com/translate/docs/languages""" +Check available languages here: https://cloud.google.com/translate/docs/languages + +Synopsis: """ + +# Copyright (c) 2022 Manuel Schneider -from albertv0 import * import json -import urllib.request import urllib.parse +import urllib.request + +from albert import * -__iid__ = "PythonInterface/v0.1" -__prettyname__ = "Google Translate" -__version__ = "1.0" -__trigger__ = "tr " -__author__ = "Manuel Schneider" -__dependencies__ = [] +__title__ = "Google Translate" +__version__ = "0.4.0" +__triggers__ = "tr " +__authors__ = "Manuel S." ua = "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.62 Safari/537.36" urltmpl = "https://translate.googleapis.com/translate_a/single?client=gtx&sl=%s&tl=%s&dt=t&q=%s" -iconPath = iconLookup('config-language') -if not iconPath: - iconPath = ":python_module" - +iconPath = iconLookup('config-language') or ":python_module" def handleQuery(query): if query.isTriggered: fields = query.string.split() - item = Item(id=__prettyname__, icon=iconPath, completion=query.rawString) + item = Item(id=__title__, icon=iconPath) if len(fields) >= 3: src = fields[0] dst = fields[1] @@ -38,13 +35,13 @@ def handleQuery(query): url = urltmpl % (src, dst, urllib.parse.quote_plus(txt)) req = urllib.request.Request(url, headers={'User-Agent': ua}) with urllib.request.urlopen(req) as response: - data = json.load(response) + data = json.loads(response.read().decode('utf-8')) result = data[0][0][0] item.text = result item.subtext = "%s-%s translation of %s" % (src.upper(), dst.upper(), txt) item.addAction(ClipAction("Copy translation to clipboard", result)) return item else: - item.text = __prettyname__ - item.subtext = "Enter a query in the form of " + item.text = __title__ + item.subtext = "Enter a query in the form of \"<srclang> <dstlang> <text>\"" return item diff --git a/.archive/googletrans/__init__.py b/.archive/googletrans/__init__.py new file mode 100644 index 00000000..3c0d240d --- /dev/null +++ b/.archive/googletrans/__init__.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- + +""" +Translator using py-googletrans +""" + +from locale import getdefaultlocale +from pathlib import Path +from time import sleep + +from albert import * +from googletrans import Translator, LANGUAGES + +md_iid = '2.0' +md_version = "1.2" +md_name = "Google Translate" +md_description = "Translate sentences using googletrans" +md_license = "BSD-3" +md_url = "https://github.com/albertlauncher/python/" +md_lib_dependencies = "googletrans==4.0.0-rc1" +md_maintainers = "@manuelschneid3r" + + +class Plugin(PluginInstance, TriggerQueryHandler): + + def __init__(self): + TriggerQueryHandler.__init__(self, + id=md_id, + name=md_name, + description=md_description, + synopsis="[[src] dest] text", + defaultTrigger='tr ') + PluginInstance.__init__(self, extensions=[self]) + self.iconUrls = [f"file:{Path(__file__).parent}/google_translate.png"] + self.translator = Translator() + self.lang = getdefaultlocale()[0][0:2] + + def handleTriggerQuery(self, query): + stripped = query.string.strip() + if stripped: + for _ in range(50): + sleep(0.01) + if not query.isValid: + return + + src = None + dest, text = self.lang, stripped + splits = text.split(maxsplit=1) + if 1 < len(splits) and splits[0] in LANGUAGES: + dest, text = splits[0], splits[1] + splits = text.split(maxsplit=1) + if 1 < len(splits) and splits[0] in LANGUAGES: + src = dest + dest, text = splits[0], splits[1] + + if src: + translation = self.translator.translate(text, src=src, dest=dest) + else: + translation = self.translator.translate(text, dest=dest) + + query.add(StandardItem( + id=md_id, + text=translation.text, + subtext=f'From {LANGUAGES[translation.src]} to {LANGUAGES[translation.dest]}', + iconUrls=self.iconUrls, + actions=[Action("copy", "Copy result to clipboard", + lambda t=translation.text: setClipboardText(t))] + )) diff --git a/.archive/googletrans/google_translate.png b/.archive/googletrans/google_translate.png new file mode 100644 index 00000000..63617cf5 Binary files /dev/null and b/.archive/googletrans/google_translate.png differ diff --git a/.archive/inhibit_sleep/__init__.py b/.archive/inhibit_sleep/__init__.py new file mode 100644 index 00000000..76829305 --- /dev/null +++ b/.archive/inhibit_sleep/__init__.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2024 Manuel Schneider + +""" +Provides an item 'Inhibit sleep' which can be used to temporarily disable system suspension. + +This is a prototype using `systemd-inhibit`. A sophisticated implementation would probably use the systemd D-Bus \ +interface documented [here](https://www.freedesktop.org/software/systemd/man/latest/org.freedesktop.login1.html). +""" + +from albert import * +from subprocess import Popen, TimeoutExpired + +md_iid = '2.3' +md_version = '1.2' +md_name = 'Inhibit sleep' +md_description = 'Inhibit system sleep mode.' +md_license = "MIT" +md_url = 'https://github.com/albertlauncher/python/tree/main/inhibit_sleep' +md_authors = "@manuelschneid3r" +md_bin_dependencies = ['systemd-inhibit', "sleep"] + + +class Plugin(PluginInstance, GlobalQueryHandler): + + def __init__(self): + PluginInstance.__init__(self) + GlobalQueryHandler.__init__( + self, self.id, self.name, self.description, + defaultTrigger='is ' + ) + self.proc = None + + def finalize(self): + if self.proc: + self.toggle() + + def toggle(self): + if self.proc: + self.proc.terminate() + try: + self.proc.wait(timeout=1) + except TimeoutExpired: + self.proc.kill() + self.proc = None + else: + self.proc = Popen(["systemd-inhibit", + "--what=idle:sleep", "--who=Albert", "--why=User", + "sleep", "infinity"]) + info(str(self.proc)) + + def configWidget(self): + return [ + { + 'type': 'label', + 'text': __doc__.strip(), + 'widget_properties': { + 'textFormat': 'Qt::MarkdownText' + } + } + ] + + def handleGlobalQuery(self, query): + stripped = query.string.strip().lower() + if stripped in "inhibit sleep": + return [ + RankItem( + StandardItem( + id=md_name, + text=md_name, + subtext=f"{'Enable' if self.proc else 'Disable'} sleep mode", + iconUrls=[f"gen:?text=💤"], + actions=[Action("inhibit", "Toggle", self.toggle)] + ), + len(stripped)/len(md_name)) + + ] + return [] diff --git a/.archive/ip/__init__.py b/.archive/ip/__init__.py new file mode 100644 index 00000000..18f9910f --- /dev/null +++ b/.archive/ip/__init__.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- + +"""Get internal and external IP address. + +Synopsis: """ + +# Copyright (c) 2022 Manuel Schneider + +import socket +from urllib import request + +from albert import * + +__title__ = "IP Addresses" +__version__ = "0.4.0" +__triggers__ = "ip " +__authors__ = ["Manuel S.", "Benedict Dudel"] + +iconPath = iconLookup("preferences-system-network") + + +def handleQuery(query): + if not query.isTriggered: + return None + + with request.urlopen("https://ipecho.net/plain") as response: + externalIP = response.read().decode() + + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(("10.255.255.255", 1)) + internalIP = s.getsockname()[0] + s.close() + + items = [] + if externalIP: + items.append(Item( + id = __title__, + icon = iconPath, + text = externalIP, + subtext = "Your external ip address from ipecho.net", + actions = [ClipAction("Copy ip address to clipboard", externalIP)] + )) + + if internalIP: + items.append(Item( + id = __title__, + icon = iconPath, + text = internalIP, + subtext = "Your internal ip address", + actions = [ClipAction("Copy ip address to clipboard", internalIP)] + )) + + return items diff --git a/.archive/lpass/__init__.py b/.archive/lpass/__init__.py new file mode 100644 index 00000000..0ea9d392 --- /dev/null +++ b/.archive/lpass/__init__.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- + +"""LastPass Vault Search + +Synopsis: """ + +# Copyright (c) 2022 Manuel Schneider + +from shutil import which +from albert import * +import subprocess +import re +import os + +__title__ = 'LastPass' +__version__ = '0.4.1' +__triggers__ = 'lp ' +__authors__ = 'David Piçarra' +__exec_deps__ = ['lpass'] + +if not which('lpass'): + raise Exception("`lpass` is not in $PATH.") +clipmgrs = ['xclip', 'xsel', 'pbcopy', 'putclip'] +hasclipmgr = False +for mgr in clipmgrs: + if which(mgr): + hasclipmgr = True + break +if not hasclipmgr: + raise Exception("`xclip`, `xsel`, `pbcopy`, or `putclip` is not in $PATH.") + +ICON_PATH = os.path.dirname(__file__)+"/lastpass.svg" + +def handleQuery(query): + if query.isTriggered: + stripped = query.string.strip() + + try: + lpass = subprocess.check_output(['lpass', 'status']) + except Exception as e: + return Item( + id=__title__, + icon=ICON_PATH, + text=f'Not logged in.', + subtext=f'Please enter your lastpass email address', + actions=[ + ProcAction("lpass login with given email", ["lpass", "login", stripped]), + ] + ) + + + if stripped: + try: + lpass = subprocess.Popen(['lpass', 'ls', '--long'], stdout=subprocess.PIPE) + try: + output = subprocess.check_output(['grep', '-i', stripped], stdin=lpass.stdout) + except subprocess.CalledProcessError as e: + return Item( + id=__title__, + icon=ICON_PATH, + text=__title__, + subtext=f'No results found for {stripped}' + ) + items = [] + for line in output.splitlines(): + match = re.match(r'\d{4}-\d{2}-\d{2} \d{2}:\d{2} (.*) \[id: (\d*)\] \[username: (.*)\]', line.decode("utf-8")) + items.append(Item( + id=__title__, + icon=ICON_PATH, + text=match.group(1), + subtext=match.group(3), + actions=[ + ProcAction("Copy password to clipboard", ["lpass", "show", "-cp", match.group(2)]), + ProcAction("Copy username to clipboard", ["lpass", "show", "-cu", match.group(2)]), + ProcAction("Copy notes to clipboard", ["lpass", "show", "-c", "--notes", match.group(2)]) + ] + )) + + return items + + except subprocess.CalledProcessError as e: + return Item( + id=__title__, + icon=ICON_PATH, + text=f'Error: {str(e.output)}', + subtext=str(e), + actions=[ClipAction('Copy CalledProcessError to clipboard', str(e))] + ) + except Exception as e: + return Item( + id=__title__, + icon=ICON_PATH, + text=f'Generic Exception: {str(e)}', + subtext=str(e), + actions=[ClipAction('Copy Exception to clipboard', str(e))] + ) + + else: + return Item( + id=__title__, + icon=ICON_PATH, + text=__title__, + subtext='Search the LastPass vault' + ) diff --git a/.archive/lpass/lastpass.svg b/.archive/lpass/lastpass.svg new file mode 100644 index 00000000..8fa4c946 --- /dev/null +++ b/.archive/lpass/lastpass.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.archive/mathematica_eval/__init__.py b/.archive/mathematica_eval/__init__.py new file mode 100644 index 00000000..31de13cc --- /dev/null +++ b/.archive/mathematica_eval/__init__.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- + +import subprocess +from tempfile import NamedTemporaryFile +from threading import Lock + +from albert import * + +md_iid = "2.0" +md_version = "1.1" +md_name = "Mathematica Eval" +md_description = "Evaluate Mathemtica code" +md_license = "GPL-3.0" +md_url = "https://github.com/albertlauncher/python/tree/master/mathematica_eval" +md_maintainers = "@tyilo" +md_bin_dependencies = ["wolframscript"] + + +class Plugin(PluginInstance, TriggerQueryHandler): + + def __init__(self): + TriggerQueryHandler.__init__(self, + id=md_id, + name=md_name, + description=md_description, + synopsis='', + defaultTrigger='mma ') + PluginInstance.__init__(self, extensions=[self]) + + def handleTriggerQuery(self, query: TriggerQuery) -> None: + stripped = query.string.strip() + if not stripped: + return + + with NamedTemporaryFile("w") as f: + f.write(stripped) + f.flush() + process = subprocess.Popen( + ["wolframscript", "-print", "-f", f.name], + encoding="utf-8", + stdout=subprocess.PIPE, + ) + + while True: + if not query.isValid: + process.kill() + return + + try: + output, _ = process.communicate(timeout=0.1) + break + except subprocess.TimeoutExpired: + pass + + result_str = output.strip() + + query.add( + StandardItem( + id=md_id, + text=result_str, + inputActionText=query.trigger + result_str, + iconUrls=["xdg:wolfram-mathematica"], + actions=[ + Action( + "copy", + "Copy result to clipboard", + lambda r=result_str: setClipboardText(r), + ), + ], + ) + ) diff --git a/.archive/multi_google_translate/__init__.py b/.archive/multi_google_translate/__init__.py new file mode 100644 index 00000000..ba8eb56e --- /dev/null +++ b/.archive/multi_google_translate/__init__.py @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- + +"""Use Google Translate to translate your sentence into multiple languages. + +Visit the following link to check available languages: \ +https://cloud.google.com/translate/docs/languages. To add or remove languages use modifier key \ +when trigger is activated or go to: '~/.config/albert/org.albert.extension.mtr/config.json' \ +Add or remove elements based on the ISO-Codes that you found on the google documentation page. + +Synopsis: [query]""" + +# Copyright (c) 2022 Manuel Schneider + +import json +import os +import urllib.error +import urllib.parse +import urllib.request +from time import sleep + +from albert import (ClipAction, Item, ProcAction, UrlAction, configLocation, + iconLookup) + +__title__ = "MultiTranslate" +__version__ = "0.4.2" +__triggers__ = "mtr " +__authors__ = "David Britt" + +iconPath = iconLookup('config-language') +if not iconPath: + iconPath = ":python_module" + +ua = "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.62 Safari/537.36" +urltmpl = "https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=%s&dt=t&q=%s" +urlbrowser = "https://translate.google.com/#auto/%s/%s" +configurationFileName = "language_config.json" +configuration_directory = os.path.join(configLocation(), __title__) +language_configuration_file = os.path.join(configuration_directory, configurationFileName) +languages = [] + +def initialize(): + if os.path.exists(language_configuration_file): + with open(language_configuration_file) as json_config: + languages.extend(json.load(json_config)["languages"]) + else: + languages.extend(["en", "zh-CN", "hi", "es", "ru", "pt", "id", "bn", "ar", "ms", "ja", "fr", "de"]) + try: + os.makedirs(configuration_directory, exist_ok=True) + try: + with open(language_configuration_file, "w") as output_file: + json.dump({"languages": languages}, output_file) + except OSError: + print("There was an error opening the file: %s" % language_configuration_file) + except OSError: + print("There was an error making the directory: %s" % configuration_directory) + + +def handleQuery(query): + results = [] + if query.isTriggered: + + # avoid rate limiting + sleep(0.2) + if not query.isValid: + return + + item = Item( + id=__title__, + icon=iconPath, + text=__title__, + actions=[ProcAction("Open the language configuration file.", + commandline=["xdg-open", language_configuration_file])] + ) + if len(query.string) >= 2: + for lang in languages: + try: + url = urltmpl % (lang, urllib.parse.quote_plus(query.string)) + req = urllib.request.Request(url, headers={'User-Agent': ua}) + with urllib.request.urlopen(req) as response: + #print(type()) + #try: + data = json.loads(response.read().decode()) + #except TypeError as typerr: + # print("Urgh this type.error. %s" % typerr) + translText = data[0][0][0] + sourceText = data[2] + if sourceText == lang: + continue + else: + results.append( + Item( + id=__title__, + icon=iconPath, + text="%s" % (translText), + subtext="%s" % lang.upper(), + actions=[ + ClipAction("Copy translation to clipboard", translText), + UrlAction("Open in your Browser", urlbrowser % (lang, query.string)) + ] + ) + ) + except urllib.error.URLError as urlerr : + print("Check your internet connection: %s" % urlerr) + item.subtext = "Check your internet connection." + return item + else: + item.subtext = "Enter a query: 'mtr <text>'. Languages {%s}" % ", ".join(languages) + return item + return results diff --git a/.archive/node_eval/__init__.py b/.archive/node_eval/__init__.py new file mode 100644 index 00000000..a9dc25f8 --- /dev/null +++ b/.archive/node_eval/__init__.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- + +"""Evaluate simple JavaScript expressions. Use it with care every keystroke triggers an evaluation.""" + +# Copyright (c) 2022 Manuel Schneider + +import os +import subprocess +from albert import * + +__title__ = 'Node Eval' +__version__ = '0.4.0' +__triggers__ = 'node ' +__authors__ = 'Hammed Oyedele' +__exec_deps__ = ['node'] + +iconPath = os.path.dirname(__file__) + '/nodejs.svg' + + +def run(exp): + return subprocess.getoutput('node --print "%s"' % exp.replace('"', '\\"')) + + +def handleQuery(query): + if query.isTriggered: + item = Item( + id=__title__, + icon=iconPath + ) + stripped = query.string.strip() + + if stripped == '': + item.text = 'Enter a JavaScript expression...' + else: + item.text = run(stripped) + item.subtext = run( + 'Object.prototype.toString.call(%s).slice(8, -1).toLowerCase()' % stripped) + item.addAction(ClipAction('Copy result to clipboard', item.text)) + + return item diff --git a/.archive/node_eval/nodejs.svg b/.archive/node_eval/nodejs.svg new file mode 100644 index 00000000..aa75111e --- /dev/null +++ b/.archive/node_eval/nodejs.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + diff --git a/Npm/__init__.py b/.archive/npm/__init__.py similarity index 67% rename from Npm/__init__.py rename to .archive/npm/__init__.py index 72ca7026..5c481197 100644 --- a/Npm/__init__.py +++ b/.archive/npm/__init__.py @@ -1,53 +1,43 @@ -"""Extension for the JavaScript package manager `npm` +# -*- coding: utf-8 -*- -The extension provides a way to install, remove and search for packages in the -npmjs.com database. To trigger the extension you just need to type `npm ` -in albert. +"""Install, remove and search packages in the npmjs.com database. -If no search query is supplied you have the option to update all globally -installed packages. -""" +If no search query is supplied you have the option to update all globally installed packages. -from albertv0 import * -from shutil import which +Synopsis: [filter]""" + +# Copyright (c) 2022 Manuel Schneider + +from albert import * import os import json import subprocess +__title__ = "npm" +__version__ = "0.4.0" +__triggers__ = "npm " +__authors__ = "Benedict Dudel" +__exec_deps__ = ["npm"] -__iid__ = "PythonInterface/v0.1" -__prettyname__ = "npm" -__version__ = "1.0" -__trigger__ = "npm " -__author__ = "Benedict Dudel" -__dependencies__ = ["npm"] - - -if which("npm") is None: - raise Exception("'npm' is not in $PATH.") - -iconPath = iconLookup("npm") -if not iconPath: - iconPath = os.path.dirname(__file__)+"/logo.svg" - +iconPath = iconLookup("npm") or os.path.dirname(__file__)+"/logo.svg" def handleQuery(query): if query.isTriggered: if not query.string.strip(): return Item( - id = __prettyname__, + id = __title__, icon = iconPath, text = "Update", subtext = "Update all globally installed packages", actions = [ - TermAction("", ["npm", "update", "--global"]) + TermAction("Update packages", ["npm", "update", "--global"]) ] ) items = getSearchResults(query.string.strip()) if not items: return Item( - id = __prettyname__, + id = __title__, icon = iconPath, text = "Search on npmjs.com", subtext = "No modules found in local database. Try to search on npmjs.com", @@ -65,10 +55,10 @@ def getSearchResults(query): proc = subprocess.run(["npm", "search", "--json", query], stdout=subprocess.PIPE) items = [] - for module in json.loads(proc.stdout.decode('utf-8')): + for module in json.loads(proc.stdout.decode()): items.append( Item( - id = __prettyname__, + id = __title__, icon = iconPath, text = "%s (%s)" % (module["name"], module["version"]), subtext = module.get("description", ""), diff --git a/Npm/logo.svg b/.archive/npm/logo.svg similarity index 94% rename from Npm/logo.svg rename to .archive/npm/logo.svg index 25e83e01..15e123ca 100644 --- a/Npm/logo.svg +++ b/.archive/npm/logo.svg @@ -1,13 +1,17 @@ - - - - - - - - - - + + + + + + + + + + + + diff --git a/.archive/packagist/__init__.py b/.archive/packagist/__init__.py new file mode 100644 index 00000000..3cbc910b --- /dev/null +++ b/.archive/packagist/__init__.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- + +"""Search for PHP packages on Packagist. + +To install packages you need to have installed composer. By default this extension will search by \ +package name. But searching for packages by type or tag is supported as well. + +Synopsis: [tag|type] """ + +# Copyright (c) 2022 Manuel Schneider + +from albert import * +import os +import json +import urllib.request + +__title__ = "Packagist" +__version__ = "0.4.0" +__triggers__ = "packagist " +__authors__ = "Benedict Dudel" +__exec_deps__ = ["composer"] + +iconPath = os.path.dirname(__file__)+"/logo.png" + + +def handleQuery(query): + if query.isTriggered: + if not query.string.strip(): + return [ + Item( + id = "packagist-search-by-tag", + icon = iconPath, + text = "by tag", + subtext = "Searching for packages by tag", + completion = "%stag " % __triggers__, + actions=[] + ), + Item( + id = "packagist-search-by-type", + icon = iconPath, + text = "by type", + subtext = "Searching for packages by type", + completion = "%stype " % __triggers__, + actions=[] + ) + ] + + if query.string.strip().startswith("tag "): + if query.string.strip()[4:]: + return getItems("https://packagist.org/search.json?tags=%s" % query.string.strip()[4:]) + + if query.string.strip().startswith("type "): + if query.string.strip()[5:]: + return getItems("https://packagist.org/search.json?type=%s" % query.string.strip()[5:]) + + return getItems("https://packagist.org/search.json?q=%s" % query.string) + +def getItems(url): + items = [] + with urllib.request.urlopen(url) as uri: + packages = json.loads(uri.read().decode()) + for package in packages['results']: + items.append( + Item( + id = "packagist-package-%s" % package["name"], + icon = iconPath, + text = package["name"], + subtext = package["description"], + completion = "%sname %s" % (__triggers__, package["name"]), + actions = [ + UrlAction( + text = "Open on packagist.org", + url = package["url"] + ), + UrlAction( + text = "Open url of repository", + url = package["repository"] + ), + TermAction( + text = "Install", + commandline = ["composer", "global", "require", package['name']] + ), + TermAction( + text = "Remove", + commandline = ["composer", "global", "remove", package['name']] + ) + ] + ) + ) + + return items diff --git a/.archive/packagist/logo.png b/.archive/packagist/logo.png new file mode 100644 index 00000000..76c0e62a Binary files /dev/null and b/.archive/packagist/logo.png differ diff --git a/.archive/php_eval/__init__.py b/.archive/php_eval/__init__.py new file mode 100644 index 00000000..513f3b38 --- /dev/null +++ b/.archive/php_eval/__init__.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- + +"""Evaluate simple PHP expressions. Use it with care every keystroke triggers an evaluation.""" + +# Copyright (c) 2022 Manuel Schneider + +import os +import subprocess +from albert import * + +__title__ = 'PHP Eval' +__version__ = '0.4.0' +__triggers__ = 'php ' +__authors__ = 'Hammed Oyedele' +__exec_deps__ = ['php'] + +iconPath = os.path.dirname(__file__) + '/php.svg' + +def run(exp): + return subprocess.getoutput('php -r "%s"' % exp.replace('"', '\\"')) + + +def handleQuery(query): + if query.isTriggered: + item = Item( + id=__title__, + icon=iconPath + ) + stripped = query.string.strip() + + if stripped == '': + item.text = 'Enter a PHP expression...' + else: + item.text = run('echo %s;' % stripped) + item.subtext = run('echo gettype(%s);' % stripped) + item.addAction(ClipAction('Copy result to clipboard', item.text)) + + return item diff --git a/.archive/php_eval/php.svg b/.archive/php_eval/php.svg new file mode 100644 index 00000000..de22880b --- /dev/null +++ b/.archive/php_eval/php.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/.archive/pidgin/__init__.py b/.archive/pidgin/__init__.py new file mode 100644 index 00000000..f3d648c9 --- /dev/null +++ b/.archive/pidgin/__init__.py @@ -0,0 +1,92 @@ +"""Open Pidgin chats. + +Matching contacts will be suggested. + +Synopsis: """ + +# Copyright (c) 2022 Manuel Schneider + +import dbus + +from albert import * + +__title__ = "Pidgin" +__version__ = "0.4.0" +__authors__ = "Greizgh" +__triggers__ = "pidgin " +__exec_deps__ = ["python"] +__py_deps__ = ["dbus"] + +iconPath = iconLookup("pidgin") +bus = dbus.SessionBus() + + +class ContactHandler: + """Handle pidgin contact list""" + + _purple = None + _contacts = [] + + def __init__(self): + self.refresh() + + def refresh(self): + """Refresh both dbus connection and pidgin contact list""" + try: + self._contacts = [] + self._purple = bus.get_object( + "im.pidgin.purple.PurpleService", "/im/pidgin/purple/PurpleObject" + ) + accounts = self._purple.PurpleAccountsGetAllActive() + for account in accounts: + buddies = self._purple.PurpleFindBuddies(account, "") + for buddy in buddies: + # if purple.PurpleBuddyIsOnline(buddy): + name = self._purple.PurpleBuddyGetAlias(buddy) + self._contacts.append((name, account)) + except dbus.DBusException: + critical("Could not connect to pidgin service") + + def isReady(self): + """Check that this handler is ready to communicate""" + return self._purple is not None + + def chatWith(self, account, name): + """Open a pidgin chat window""" + self._purple.PurpleConversationNew(1, account, name) + + def getMatch(self, query): + """Get buddies matching query""" + normalized = query.lower() + return [item for item in self._contacts if normalized in item[0].lower()] + + +handler = ContactHandler() + + +def handleQuery(query): + if not handler.isReady(): + handler.refresh() + + if query.isTriggered: + target = query.string.strip() + + if target: + items = [] + for match in handler.getMatch(target): + items.append( + Item( + id=__title__, + icon=iconPath, + text="Chat with {}".format(match[0]), + subtext="Open a pidgin chat window", + completion=match[0], + actions=[ + FuncAction( + "Open chat window", + lambda: handler.chatWith(match[1], match[0]), + ) + ], + ) + ) + return items diff --git a/.archive/rand/__init__.py b/.archive/rand/__init__.py new file mode 100644 index 00000000..da93ba10 --- /dev/null +++ b/.archive/rand/__init__.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- + +"""Draws a random integer. + +This extension provides the rand item, which can be used with : + - 1 argument a : draws an integer between 1 and a (included) + - 2 argumens a, b: draws an integer between a and b (included) + - 3 arguments a, b, nb: draws nb integers between a and b (included) + +Synopsis: + rand [min] max [numbers] +""" + +import os + +import random + +from albertv0 import * + +__iid__ = "PythonInterface/v0.1" +__prettyname__ = "Rand" +__version__ = "0.1" +__trigger__ = "rand " +__author__ = "Cyprien Ruffino" +__dependencies__ = [] + + +usage_string = "Usage: [min] max [numbers]" + + +def createBlankItem(text): + return Item( + id=__prettyname__, + icon="rand/rand.png", + text=str(text), + subtext="", + actions=[]) + + +def handleQuery(query): + if query.isTriggered: + + tokens = query.string.split(" ") + + # No arguments + if len(tokens) == 1 and tokens[0] == "": + return createBlankItem(usage_string) + + # At least one argument + try: + tokens = [int(token) for token in tokens] + except ValueError: + return createBlankItem(usage_string) + + if len(tokens) == 1: + b = tokens[0] + rand = random.randint(1, b) + return createBlankItem(str(rand)) + + elif len(tokens) == 2: + a, b = tokens + rand = random.randint(a, b) + return createBlankItem(str(rand)) + + elif len(tokens) == 3: + a, b, nb = tokens + items = [] + for i in range(nb): + rand = random.randint(a, b) + items.append(createBlankItem(rand)) + return items + + else: + return createBlankItem(usage_string) diff --git a/.archive/rand/rand.png b/.archive/rand/rand.png new file mode 100644 index 00000000..2e58996a Binary files /dev/null and b/.archive/rand/rand.png differ diff --git a/Scrot.py b/.archive/scrot/__init__.py similarity index 71% rename from Scrot.py rename to .archive/scrot/__init__.py index 37019dcd..80c2d44a 100644 --- a/Scrot.py +++ b/.archive/scrot/__init__.py @@ -1,31 +1,27 @@ -"""Extension which basically wraps the command line utility scrot to make -screenshots from albert. The extension supports taking screenshots of the whole -screen, an specific area or the current active window. +# -*- coding: utf-8 -*- -When the screenshot was made you will hear a sound which indicates that the -screenshot was taken successfully. +"""Take screenshots of screens, areas or windows. -Screenshots will be saved in XDG_PICTURES_DIR or in the temp directory.""" +This extension wraps the command line utility scrot to make screenshots from albert. When the \ +screenshot was made you will hear a sound which indicates that the screenshot was taken \ +successfully.Screenshots will be saved in XDG_PICTURES_DIR or in the temp directory. -from albertv0 import * -from shutil import which -import subprocess -import tempfile -import os +Synopsis: """ -__iid__ = "PythonInterface/v0.1" -__prettyname__ = "scrot screenshot utility" -__version__ = "1.0" -__trigger__ = "scrot " -__author__ = "Benedict Dudel" -__dependencies__ = ["scrot", "xclip"] +# Copyright (c) 2022 Manuel Schneider +import os +import subprocess +import tempfile +from shutil import which -if which("scrot") is None: - raise Exception("'scrot' is not in $PATH.") +from albert import FuncAction, Item, iconLookup -if which("xclip") is None: - raise Exception("'xclip' is not in $PATH.") +__title__ = "SCReenshOT utility" +__version__ = "0.4.0" +__triggers__ = "scrot " +__authors__ = "Benedict Dudel" +__exec_deps__ = ["scrot", "xclip"] iconPath = iconLookup("camera-photo") @@ -34,7 +30,7 @@ def handleQuery(query): if query.isTriggered: return [ Item( - id = "%s-whole-screen" % __prettyname__, + id = "%s-whole-screen" % __title__, icon = iconPath, text = "Screen", subtext = "Take a screenshot of the whole screen", @@ -50,7 +46,7 @@ def handleQuery(query): ] ), Item( - id = "%s-area-of-screen" % __prettyname__, + id = "%s-area-of-screen" % __title__, icon = iconPath, text = "Area", subtext = "Draw a rectangle with your mouse to capture an area", @@ -62,7 +58,7 @@ def handleQuery(query): ] ), Item( - id = "%s-current-window" % __prettyname__, + id = "%s-current-window" % __title__, icon = iconPath, text = "Window", subtext = "Take a screenshot of the current active window", diff --git a/.archive/texdoc/__init__.py b/.archive/texdoc/__init__.py new file mode 100644 index 00000000..de3f42b6 --- /dev/null +++ b/.archive/texdoc/__init__.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- + +"""texdoc extension + +This is an extension to search for LaTeX documentation. + +Synopsis: """ + +# Copyright (c) 2022 Manuel Schneider + +import re +import subprocess +from pathlib import Path +from albert import * + +__title__ = 'TeXdoc' +__version__ = '0.4.0' +__triggers__ = 'td' +__authors__ = 'Florian Adamsky (@cit)' +__exec_deps__ = ['texdoc'] + +iconPath = Path(__file__).parent / 'texdoc-logo.svg' +texdoc_cmd = ['texdoc', '-I', '-q', '-s', '-M'] + +def handleQuery(query): + if not query.isTriggered: + return + + query.disableSort() + + stripped_query = query.string.strip() + + if stripped_query: + process = subprocess.run(texdoc_cmd + [stripped_query], + stdout=subprocess.PIPE) + texdoc_output = process.stdout.decode('utf-8') + + results = [] + for line in texdoc_output.split("\n"): + + match = re.search('\t(/.*/)([\w\.-]+)\t\t', line, re.IGNORECASE) + if match: + directory = match.group(1).strip() + filename = match.group(2).strip() + full_path = directory.join(['/', filename]) + + results.append(Item(id = __title__, + icon = str(iconPath), + text = filename, + subtext = directory, + completion = full_path, + actions = [ + ProcAction(text = 'This action opens the documentation.', + commandline=['xdg-open', full_path]) + ])) + + return results + else: + return Item(id = __title__, + icon = str(iconPath), + text = __title__, + subtext = 'Enter a query to search with texdoc', + completion = query.rawString) diff --git a/.archive/texdoc/texdoc-logo.svg b/.archive/texdoc/texdoc-logo.svg new file mode 100644 index 00000000..0245dcd6 --- /dev/null +++ b/.archive/texdoc/texdoc-logo.svg @@ -0,0 +1,263 @@ + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.archive/timer/__init__.py b/.archive/timer/__init__.py new file mode 100644 index 00000000..5a22e160 --- /dev/null +++ b/.archive/timer/__init__.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- +# # Copyright (c) 2018-2024 Manuel Schneider +# # Copyright (c) 2020 Andreas Dominik Preikschat + +""" +Takes arguments in the form of '`[[hrs:]mins:]secs [name]`'. Empty fields resolve to `0`. \ +Fields exceeding the maximum amount of the time interval are automatically refactorized. + +Examples: +- `5:` starts a 5 minutes timer +- `1:: ` starts a 1 hour timer +- `120:` starts a 2 hours timer +""" + +import threading +from datetime import timedelta +from pathlib import Path +from time import strftime, time, localtime + +from albert import * + +md_iid = '2.3' +md_version = "1.8" +md_name = "Timer" +md_description = "Set up timers" +md_license = "MIT" +md_url = "https://github.com/albertlauncher/python/tree/main/timer" +md_authors = ["@manuelschneid3r", "@googol42"] + + +class Timer(threading.Timer): + + def __init__(self, interval, name, callback): + super().__init__(interval=interval, + function=lambda: callback(self)) + self.name = name + self.begin = int(time()) + self.end = self.begin + interval + self.start() + + +class Plugin(PluginInstance, TriggerQueryHandler): + + def __init__(self): + TriggerQueryHandler.__init__(self, + id=md_id, + name=md_name, + description=md_description, + synopsis='[[hrs:]mins:]secs [name]', + defaultTrigger='timer ') + PluginInstance.__init__(self) + self.iconUrls = [f"file:{Path(__file__).parent}/time.svg"] + self.soundPath = Path(__file__).parent / "bing.wav" + self.timers = [] + self.notification = None + + def finalize(self): + for timer in self.timers: + timer.cancel() + self.timers.clear() + + def startTimer(self, interval, name): + self.timers.append(Timer(interval, name, self.onTimerTimeout)) + + def deleteTimer(self, timer): + self.timers.remove(timer) + timer.cancel() + + def onTimerTimeout(self, timer): + self.notification = Notification( + title=f"Timer '{timer.name if timer.name else 'Timer'}'", + body=f"Timed out at {strftime('%X', localtime(timer.end))}" + ) + self.deleteTimer(timer) + + def configWidget(self): + return [ + { + 'type': 'label', + 'text': __doc__.strip(), + 'widget_properties': { 'textFormat': 'Qt::MarkdownText' } + } + ] + + def handleTriggerQuery(self, query): + if not query.isValid: + return + + if query.string.strip(): + args = query.string.strip().split(maxsplit=1) + fields = args[0].split(":") + name = args[1] if 1 < len(args) else '' + if not all(field.isdigit() or field == '' for field in fields): + return StandardItem( + id=self.name, + text="Invalid input", + subtext="Enter a query in the form of '%s[[hours:]minutes:]seconds [name]'" % self.defaultTrigger(), + iconUrls=self.iconUrls, + ) + + seconds = 0 + fields.reverse() + for i in range(len(fields)): + seconds += int(fields[i] if fields[i] else 0)*(60**i) + + query.add(StandardItem( + id=self.name, + text=str(timedelta(seconds=seconds)), + subtext='Set a timer with name "%s"' % name if name else 'Set a timer', + iconUrls=self.iconUrls, + actions=[Action("set-timer", "Set timer", lambda sec=seconds: self.startTimer(sec, name))] + )) + return + + # List timers + items = [] + for timer in self.timers: + m, s = divmod(timer.interval, 60) + h, m = divmod(m, 60) + identifier = "%d:%02d:%02d" % (h, m, s) + + timer_name_with_quotes = '"%s"' % timer.name if timer.name else '' + items.append(StandardItem( + id=self.name, + text='Delete timer %s [%s]' % (timer_name_with_quotes, identifier), + subtext="Times out %s" % strftime("%X", localtime(timer.end)), + iconUrls=self.iconUrls, + actions=[Action("delete-timer", "Delete timer", lambda t=timer: self.deleteTimer(t))] + )) + + if items: + query.add(items) diff --git a/Tomboy.py b/.archive/tomboy/__init__.py similarity index 61% rename from Tomboy.py rename to .archive/tomboy/__init__.py index 32cae72c..6e8f03ec 100644 --- a/Tomboy.py +++ b/.archive/tomboy/__init__.py @@ -1,39 +1,34 @@ -#!/usr/bin/env python +# -*- coding: utf-8 -*- -"""Access Tomboy notes +"""Search, open, create and delete Tomboy notes. -Search, open, create and delete notes.""" +Synopsis: """ -from albertv0 import * -from dbus import SessionBus, Interface, DBusException -from shutil import which -from datetime import datetime -import re - -__iid__ = "PythonInterface/v0.1" -__prettyname__ = "Tomboy" -__version__ = "1.1" -__trigger__ = "tb " -__author__ = "Manuel Schneider" -__bin__ = __prettyname__.lower() -__dependencies__ = [__bin__, "python-dbus"] +# Copyright (c) 2022 Manuel Schneider -BUS = "org.gnome.%s" % __prettyname__ -OBJ = "/org/gnome/%s/RemoteControl" % __prettyname__ -IFACE = 'org.gnome.%s.RemoteControl' % __prettyname__ - -if which(__bin__) is None: - raise Exception("'%s' is not in $PATH." % __bin__) +import re +from datetime import datetime +from dbus import DBusException, Interface, SessionBus +from albert import * -iconPath = iconLookup(__bin__) +__title__ = "Tomboy" +__version__ = "0.4.1" +__triggers__ = "tb " +__authors__ = "Manuel S." +__exec_deps__ = ["tomboy"] +__py_deps__ = ["dbus"] +BUS = "org.gnome.%s" % __title__ +OBJ = "/org/gnome/%s/RemoteControl" % __title__ +IFACE = 'org.gnome.%s.RemoteControl' % __title__ +iconPath = iconLookup("tomboy") def handleQuery(query): results = [] if query.isTriggered: try: if not SessionBus().name_has_owner(BUS): - warning("Seems like %s is not running" % __bin__) + warning("Seems like %s is not running" % __title__) return obj = SessionBus().get_object(bus_name=BUS, object_path=OBJ) @@ -42,12 +37,11 @@ def handleQuery(query): if query.string.strip(): for note in iface.SearchNotes(query.string.lower(), False): results.append( - Item(id="%s%s" % (__prettyname__, note), + Item(id="%s%s" % (__title__, note), icon=iconPath, text=iface.GetNoteTitle(note), subtext="%s%s" % ("".join(["#%s " % re.search('.+:.+:(.+)', s).group(1) for s in iface.GetTagsForNote(note)]), datetime.fromtimestamp(iface.GetNoteChangeDate(note)).strftime("Note from %c")), - completion=query.rawString, actions=[ FuncAction("Open note", lambda note=note: iface.DisplayNote(note)), @@ -59,13 +53,12 @@ def createAndShowNote(): note = iface.CreateNote() iface.DisplayNote(note) - results.append(Item(id="%s-create" % __prettyname__, + results.append(Item(id="%s-create" % __title__, icon=iconPath, - text=__prettyname__, - subtext="%s notes" % __prettyname__, - completion=query.rawString, + text=__title__, + subtext="%s notes" % __title__, actions=[ - FuncAction("Open %s" % __prettyname__, + FuncAction("Open %s" % __title__, lambda: iface.DisplaySearch()), FuncAction("Create a new note", createAndShowNote) ])) diff --git a/.archive/unicode_emoji/__init__.py b/.archive/unicode_emoji/__init__.py new file mode 100644 index 00000000..a1191072 --- /dev/null +++ b/.archive/unicode_emoji/__init__.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- + +"""Offline Unicode emoji picker. + +Synopsis: [filter]""" + +# Copyright (c) 2022 Manuel Schneider + +from albert import * +from collections import namedtuple +from threading import Thread +import datetime +import os +import subprocess +import urllib.request +import shutil + +__title__ = "Unicode Emojis" +__version__ = "0.4.3" +__triggers__ = ":" +__authors__ = ["Tim Zeitz", "Manuel S."] +__exec_deps__ = ["convert"] + +EmojiSpec = namedtuple('EmojiSpec', ['string', 'name', 'modifiers']) +emoji_data_src_url = "https://unicode.org/Public/emoji/latest/emoji-test.txt" +emoji_data_path = os.path.join(dataLocation(), "emoji.txt") +icon_path_template = os.path.join(cacheLocation(), __name__, "%s.png") +emojiSpecs = [] +thread = None + + +class WorkerThread(Thread): + def __init__(self): + super().__init__() + self.stop = False + + def run(self): + + # Create cache dir + cache_dir_path = os.path.join(cacheLocation(), __name__) + if not os.path.exists(cache_dir_path): + os.mkdir(cache_dir_path) + + # Build the index and icon cache + # global emojiSpecs + emojiSpecs.clear() + with open(emoji_data_path) as f: + for line in f: + if "; fully-qualified" in line: + emoji, desc = line.split('#', 1)[-1].split(None, 1) + desc = [d.strip().lower() for d in desc.split(':')] + emojiSpecs.append(EmojiSpec(emoji, desc[0], desc[1] if len(desc)==2 else "")) + + icon_path = icon_path_template % emoji + if not os.path.exists(icon_path): + subprocess.call(["convert", "-pointsize", "64", "-background", "transparent", "pango:%s" % emoji, icon_path]) + + if self.stop: + return + +def initialize(): + src_directory = os.path.dirname(os.path.realpath(__file__)) + + # if no emoji data exists copy offline src as fallback + if not os.path.isfile(emoji_data_path): + shutil.copyfile(os.path.join(src_directory, "emoji.txt"), emoji_data_path) + + current_version = get_emoji_data_version(emoji_data_path) + + try: + new_path = os.path.join(dataLocation(), "emoji-new.txt") + + # try to fetch the latest emoji data + with urllib.request.urlopen(emoji_data_src_url) as response, open(new_path, 'wb') as out_file: + # save it + shutil.copyfileobj(response, out_file) + + # update emoji data if the fetched data is newer + if get_emoji_data_version(new_path) > current_version: + shutil.copyfile(new_path, emoji_data_path) + + os.remove(new_path) + + except Exception as e: + warning(e) + + # Build the index and icon cache + global thread + thread = WorkerThread() + thread.start() + +def finalize(): + global thread + if thread is not None: + thread.stop = True + thread.join() + +def get_emoji_data_version(path): + with open(emoji_data_path) as f: + for line in f: + if "# Date: " in line: + return datetime.datetime.strptime(line.strip(), "# Date: %Y-%m-%d, %H:%M:%S GMT") + +def handleQuery(query): + if query.isValid and query.isTriggered: + items = [] + query_tokens = query.string.lower().split() + # filter emojiSpecs where all query words are in any of the emoji description words + for es in filter(lambda e: all(any(n in s for s in [e.name, e.modifiers]) for n in query_tokens), emojiSpecs): + items.append(Item(id = "%s%s" % (__name__, es.string), + completion = es.name if not es.modifiers else " ".join([es.name, es.modifiers]), + icon = icon_path_template % es.string, + text = es.name.capitalize(), + subtext = es.modifiers.capitalize() if es.modifiers else "(No modifiers)", + actions = [ClipAction("Copy to clipboard", es.string)])) + return items diff --git a/.archive/unicode_emoji/emoji.txt b/.archive/unicode_emoji/emoji.txt new file mode 100644 index 00000000..c7647e74 --- /dev/null +++ b/.archive/unicode_emoji/emoji.txt @@ -0,0 +1,4173 @@ +# emoji-test.txt +# Date: 2018-10-20, 13:33:46 GMT +# © 2018 Unicode®, Inc. +# Unicode and the Unicode Logo are registered trademarks of Unicode, Inc. in the U.S. and other countries. +# For terms of use, see http://www.unicode.org/terms_of_use.html +# +# Emoji Keyboard/Display Test Data for UTS #51 +# Version: 12.0 +# +# For documentation and usage, see http://www.unicode.org/reports/tr51 +# +# This file provides data for testing which emoji forms should be in keyboards and which should also be displayed/processed. +# Format: code points; status # emoji name +# Code points — list of one or more hex code points, separated by spaces +# Status +# component — an Emoji_Component, +# excluding Regional_Indicators, ASCII, and non-Emoji. +# fully-qualified — a fully-qualified emoji (see ED-18 in UTS #51), +# excluding Emoji_Component +# minimally-qualified — a minimally-qualified emoji (see ED-18a in UTS #51) +# unqualified — a unqualified emoji (See ED-19 in UTS #51) +# Notes: +# • This includes the emoji components that need emoji presentation (skin tone and hair) +# when isolated, but omits the components that need not have an emoji +# presentation when isolated. +# • The RGI set is covered by the listed fully-qualified emoji. +# • The listed minimally-qualified and unqualified cover all cases where an +# element of the RGI set is missing one or more emoji presentation selectors. +# • The file is in CLDR order, not codepoint order. This is recommended (but not required!) for keyboard palettes. +# • The groups and subgroups are illustrative. See the Emoji Order chart for more information. + + +# group: Smileys & Emotion + +# subgroup: face-smiling +1F600 ; fully-qualified # 😀 grinning face +1F603 ; fully-qualified # 😃 grinning face with big eyes +1F604 ; fully-qualified # 😄 grinning face with smiling eyes +1F601 ; fully-qualified # 😁 beaming face with smiling eyes +1F606 ; fully-qualified # 😆 grinning squinting face +1F605 ; fully-qualified # 😅 grinning face with sweat +1F923 ; fully-qualified # 🤣 rolling on the floor laughing +1F602 ; fully-qualified # 😂 face with tears of joy +1F642 ; fully-qualified # 🙂 slightly smiling face +1F643 ; fully-qualified # 🙃 upside-down face +1F609 ; fully-qualified # 😉 winking face +1F60A ; fully-qualified # 😊 smiling face with smiling eyes +1F607 ; fully-qualified # 😇 smiling face with halo + +# subgroup: face-affection +1F970 ; fully-qualified # 🥰 smiling face with 3 hearts +1F60D ; fully-qualified # 😍 smiling face with heart-eyes +1F929 ; fully-qualified # 🤩 star-struck +1F618 ; fully-qualified # 😘 face blowing a kiss +1F617 ; fully-qualified # 😗 kissing face +263A FE0F ; fully-qualified # ☺️ smiling face +263A ; unqualified # ☺ smiling face +1F61A ; fully-qualified # 😚 kissing face with closed eyes +1F619 ; fully-qualified # 😙 kissing face with smiling eyes + +# subgroup: face-tongue +1F60B ; fully-qualified # 😋 face savoring food +1F61B ; fully-qualified # 😛 face with tongue +1F61C ; fully-qualified # 😜 winking face with tongue +1F92A ; fully-qualified # 🤪 zany face +1F61D ; fully-qualified # 😝 squinting face with tongue +1F911 ; fully-qualified # 🤑 money-mouth face + +# subgroup: face-hand +1F917 ; fully-qualified # 🤗 hugging face +1F92D ; fully-qualified # 🤭 face with hand over mouth +1F92B ; fully-qualified # 🤫 shushing face +1F914 ; fully-qualified # 🤔 thinking face + +# subgroup: face-neutral-skeptical +1F910 ; fully-qualified # 🤐 zipper-mouth face +1F928 ; fully-qualified # 🤨 face with raised eyebrow +1F610 ; fully-qualified # 😐 neutral face +1F611 ; fully-qualified # 😑 expressionless face +1F636 ; fully-qualified # 😶 face without mouth +1F60F ; fully-qualified # 😏 smirking face +1F612 ; fully-qualified # 😒 unamused face +1F644 ; fully-qualified # 🙄 face with rolling eyes +1F62C ; fully-qualified # 😬 grimacing face +1F925 ; fully-qualified # 🤥 lying face + +# subgroup: face-sleepy +1F60C ; fully-qualified # 😌 relieved face +1F614 ; fully-qualified # 😔 pensive face +1F62A ; fully-qualified # 😪 sleepy face +1F924 ; fully-qualified # 🤤 drooling face +1F634 ; fully-qualified # 😴 sleeping face + +# subgroup: face-unwell +1F637 ; fully-qualified # 😷 face with medical mask +1F912 ; fully-qualified # 🤒 face with thermometer +1F915 ; fully-qualified # 🤕 face with head-bandage +1F922 ; fully-qualified # 🤢 nauseated face +1F92E ; fully-qualified # 🤮 face vomiting +1F927 ; fully-qualified # 🤧 sneezing face +1F975 ; fully-qualified # 🥵 hot face +1F976 ; fully-qualified # 🥶 cold face +1F974 ; fully-qualified # 🥴 woozy face +1F635 ; fully-qualified # 😵 dizzy face +1F92F ; fully-qualified # 🤯 exploding head + +# subgroup: face-hat +1F920 ; fully-qualified # 🤠 cowboy hat face +1F973 ; fully-qualified # 🥳 partying face + +# subgroup: face-glasses +1F60E ; fully-qualified # 😎 smiling face with sunglasses +1F913 ; fully-qualified # 🤓 nerd face +1F9D0 ; fully-qualified # 🧐 face with monocle + +# subgroup: face-concerned +1F615 ; fully-qualified # 😕 confused face +1F61F ; fully-qualified # 😟 worried face +1F641 ; fully-qualified # 🙁 slightly frowning face +2639 FE0F ; fully-qualified # ☹️ frowning face +2639 ; unqualified # ☹ frowning face +1F62E ; fully-qualified # 😮 face with open mouth +1F62F ; fully-qualified # 😯 hushed face +1F632 ; fully-qualified # 😲 astonished face +1F633 ; fully-qualified # 😳 flushed face +1F97A ; fully-qualified # 🥺 pleading face +1F626 ; fully-qualified # 😦 frowning face with open mouth +1F627 ; fully-qualified # 😧 anguished face +1F628 ; fully-qualified # 😨 fearful face +1F630 ; fully-qualified # 😰 anxious face with sweat +1F625 ; fully-qualified # 😥 sad but relieved face +1F622 ; fully-qualified # 😢 crying face +1F62D ; fully-qualified # 😭 loudly crying face +1F631 ; fully-qualified # 😱 face screaming in fear +1F616 ; fully-qualified # 😖 confounded face +1F623 ; fully-qualified # 😣 persevering face +1F61E ; fully-qualified # 😞 disappointed face +1F613 ; fully-qualified # 😓 downcast face with sweat +1F629 ; fully-qualified # 😩 weary face +1F62B ; fully-qualified # 😫 tired face +1F971 ; fully-qualified # 🥱 yawning face + +# subgroup: face-negative +1F624 ; fully-qualified # 😤 face with steam from nose +1F621 ; fully-qualified # 😡 pouting face +1F620 ; fully-qualified # 😠 angry face +1F92C ; fully-qualified # 🤬 face with symbols on mouth +1F608 ; fully-qualified # 😈 smiling face with horns +1F47F ; fully-qualified # 👿 angry face with horns +1F480 ; fully-qualified # 💀 skull +2620 FE0F ; fully-qualified # ☠️ skull and crossbones +2620 ; unqualified # ☠ skull and crossbones + +# subgroup: face-costume +1F4A9 ; fully-qualified # 💩 pile of poo +1F921 ; fully-qualified # 🤡 clown face +1F479 ; fully-qualified # 👹 ogre +1F47A ; fully-qualified # 👺 goblin +1F47B ; fully-qualified # 👻 ghost +1F47D ; fully-qualified # 👽 alien +1F47E ; fully-qualified # 👾 alien monster +1F916 ; fully-qualified # 🤖 robot face + +# subgroup: cat-face +1F63A ; fully-qualified # 😺 grinning cat face +1F638 ; fully-qualified # 😸 grinning cat face with smiling eyes +1F639 ; fully-qualified # 😹 cat face with tears of joy +1F63B ; fully-qualified # 😻 smiling cat face with heart-eyes +1F63C ; fully-qualified # 😼 cat face with wry smile +1F63D ; fully-qualified # 😽 kissing cat face +1F640 ; fully-qualified # 🙀 weary cat face +1F63F ; fully-qualified # 😿 crying cat face +1F63E ; fully-qualified # 😾 pouting cat face + +# subgroup: monkey-face +1F648 ; fully-qualified # 🙈 see-no-evil monkey +1F649 ; fully-qualified # 🙉 hear-no-evil monkey +1F64A ; fully-qualified # 🙊 speak-no-evil monkey + +# subgroup: emotion +1F48B ; fully-qualified # 💋 kiss mark +1F48C ; fully-qualified # 💌 love letter +1F498 ; fully-qualified # 💘 heart with arrow +1F49D ; fully-qualified # 💝 heart with ribbon +1F496 ; fully-qualified # 💖 sparkling heart +1F497 ; fully-qualified # 💗 growing heart +1F493 ; fully-qualified # 💓 beating heart +1F49E ; fully-qualified # 💞 revolving hearts +1F495 ; fully-qualified # 💕 two hearts +1F49F ; fully-qualified # 💟 heart decoration +2763 FE0F ; fully-qualified # ❣️ heavy heart exclamation +2763 ; unqualified # ❣ heavy heart exclamation +1F494 ; fully-qualified # 💔 broken heart +2764 FE0F ; fully-qualified # ❤️ red heart +2764 ; unqualified # ❤ red heart +1F9E1 ; fully-qualified # 🧡 orange heart +1F49B ; fully-qualified # 💛 yellow heart +1F49A ; fully-qualified # 💚 green heart +1F499 ; fully-qualified # 💙 blue heart +1F49C ; fully-qualified # 💜 purple heart +1F5A4 ; fully-qualified # 🖤 black heart +1F90D ; fully-qualified # 🤍 white heart +1F90E ; fully-qualified # 🤎 brown heart +1F4AF ; fully-qualified # 💯 hundred points +1F4A2 ; fully-qualified # 💢 anger symbol +1F4A5 ; fully-qualified # 💥 collision +1F4AB ; fully-qualified # 💫 dizzy +1F4A6 ; fully-qualified # 💦 sweat droplets +1F4A8 ; fully-qualified # 💨 dashing away +1F573 FE0F ; fully-qualified # 🕳️ hole +1F573 ; unqualified # 🕳 hole +1F4A3 ; fully-qualified # 💣 bomb +1F4AC ; fully-qualified # 💬 speech balloon +1F441 FE0F 200D 1F5E8 FE0F ; fully-qualified # 👁️‍🗨️ eye in speech bubble +1F441 200D 1F5E8 FE0F ; unqualified # 👁‍🗨️ eye in speech bubble +1F441 FE0F 200D 1F5E8 ; unqualified # 👁️‍🗨 eye in speech bubble +1F441 200D 1F5E8 ; unqualified # 👁‍🗨 eye in speech bubble +1F5E8 FE0F ; fully-qualified # 🗨️ left speech bubble +1F5E8 ; unqualified # 🗨 left speech bubble +1F5EF FE0F ; fully-qualified # 🗯️ right anger bubble +1F5EF ; unqualified # 🗯 right anger bubble +1F4AD ; fully-qualified # 💭 thought balloon +1F4A4 ; fully-qualified # 💤 zzz + +# Smileys & Emotion subtotal: 160 +# Smileys & Emotion subtotal: 160 w/o modifiers + +# group: People & Body + +# subgroup: hand-fingers-open +1F44B ; fully-qualified # 👋 waving hand +1F44B 1F3FB ; fully-qualified # 👋🏻 waving hand: light skin tone +1F44B 1F3FC ; fully-qualified # 👋🏼 waving hand: medium-light skin tone +1F44B 1F3FD ; fully-qualified # 👋🏽 waving hand: medium skin tone +1F44B 1F3FE ; fully-qualified # 👋🏾 waving hand: medium-dark skin tone +1F44B 1F3FF ; fully-qualified # 👋🏿 waving hand: dark skin tone +1F91A ; fully-qualified # 🤚 raised back of hand +1F91A 1F3FB ; fully-qualified # 🤚🏻 raised back of hand: light skin tone +1F91A 1F3FC ; fully-qualified # 🤚🏼 raised back of hand: medium-light skin tone +1F91A 1F3FD ; fully-qualified # 🤚🏽 raised back of hand: medium skin tone +1F91A 1F3FE ; fully-qualified # 🤚🏾 raised back of hand: medium-dark skin tone +1F91A 1F3FF ; fully-qualified # 🤚🏿 raised back of hand: dark skin tone +1F590 FE0F ; fully-qualified # 🖐️ hand with fingers splayed +1F590 ; unqualified # 🖐 hand with fingers splayed +1F590 1F3FB ; fully-qualified # 🖐🏻 hand with fingers splayed: light skin tone +1F590 1F3FC ; fully-qualified # 🖐🏼 hand with fingers splayed: medium-light skin tone +1F590 1F3FD ; fully-qualified # 🖐🏽 hand with fingers splayed: medium skin tone +1F590 1F3FE ; fully-qualified # 🖐🏾 hand with fingers splayed: medium-dark skin tone +1F590 1F3FF ; fully-qualified # 🖐🏿 hand with fingers splayed: dark skin tone +270B ; fully-qualified # ✋ raised hand +270B 1F3FB ; fully-qualified # ✋🏻 raised hand: light skin tone +270B 1F3FC ; fully-qualified # ✋🏼 raised hand: medium-light skin tone +270B 1F3FD ; fully-qualified # ✋🏽 raised hand: medium skin tone +270B 1F3FE ; fully-qualified # ✋🏾 raised hand: medium-dark skin tone +270B 1F3FF ; fully-qualified # ✋🏿 raised hand: dark skin tone +1F596 ; fully-qualified # 🖖 vulcan salute +1F596 1F3FB ; fully-qualified # 🖖🏻 vulcan salute: light skin tone +1F596 1F3FC ; fully-qualified # 🖖🏼 vulcan salute: medium-light skin tone +1F596 1F3FD ; fully-qualified # 🖖🏽 vulcan salute: medium skin tone +1F596 1F3FE ; fully-qualified # 🖖🏾 vulcan salute: medium-dark skin tone +1F596 1F3FF ; fully-qualified # 🖖🏿 vulcan salute: dark skin tone + +# subgroup: hand-fingers-partial +1F44C ; fully-qualified # 👌 OK hand +1F44C 1F3FB ; fully-qualified # 👌🏻 OK hand: light skin tone +1F44C 1F3FC ; fully-qualified # 👌🏼 OK hand: medium-light skin tone +1F44C 1F3FD ; fully-qualified # 👌🏽 OK hand: medium skin tone +1F44C 1F3FE ; fully-qualified # 👌🏾 OK hand: medium-dark skin tone +1F44C 1F3FF ; fully-qualified # 👌🏿 OK hand: dark skin tone +1F90F ; fully-qualified # 🤏 pinching hand +1F90F 1F3FB ; fully-qualified # 🤏🏻 pinching hand: light skin tone +1F90F 1F3FC ; fully-qualified # 🤏🏼 pinching hand: medium-light skin tone +1F90F 1F3FD ; fully-qualified # 🤏🏽 pinching hand: medium skin tone +1F90F 1F3FE ; fully-qualified # 🤏🏾 pinching hand: medium-dark skin tone +1F90F 1F3FF ; fully-qualified # 🤏🏿 pinching hand: dark skin tone +270C FE0F ; fully-qualified # ✌️ victory hand +270C ; unqualified # ✌ victory hand +270C 1F3FB ; fully-qualified # ✌🏻 victory hand: light skin tone +270C 1F3FC ; fully-qualified # ✌🏼 victory hand: medium-light skin tone +270C 1F3FD ; fully-qualified # ✌🏽 victory hand: medium skin tone +270C 1F3FE ; fully-qualified # ✌🏾 victory hand: medium-dark skin tone +270C 1F3FF ; fully-qualified # ✌🏿 victory hand: dark skin tone +1F91E ; fully-qualified # 🤞 crossed fingers +1F91E 1F3FB ; fully-qualified # 🤞🏻 crossed fingers: light skin tone +1F91E 1F3FC ; fully-qualified # 🤞🏼 crossed fingers: medium-light skin tone +1F91E 1F3FD ; fully-qualified # 🤞🏽 crossed fingers: medium skin tone +1F91E 1F3FE ; fully-qualified # 🤞🏾 crossed fingers: medium-dark skin tone +1F91E 1F3FF ; fully-qualified # 🤞🏿 crossed fingers: dark skin tone +1F91F ; fully-qualified # 🤟 love-you gesture +1F91F 1F3FB ; fully-qualified # 🤟🏻 love-you gesture: light skin tone +1F91F 1F3FC ; fully-qualified # 🤟🏼 love-you gesture: medium-light skin tone +1F91F 1F3FD ; fully-qualified # 🤟🏽 love-you gesture: medium skin tone +1F91F 1F3FE ; fully-qualified # 🤟🏾 love-you gesture: medium-dark skin tone +1F91F 1F3FF ; fully-qualified # 🤟🏿 love-you gesture: dark skin tone +1F918 ; fully-qualified # 🤘 sign of the horns +1F918 1F3FB ; fully-qualified # 🤘🏻 sign of the horns: light skin tone +1F918 1F3FC ; fully-qualified # 🤘🏼 sign of the horns: medium-light skin tone +1F918 1F3FD ; fully-qualified # 🤘🏽 sign of the horns: medium skin tone +1F918 1F3FE ; fully-qualified # 🤘🏾 sign of the horns: medium-dark skin tone +1F918 1F3FF ; fully-qualified # 🤘🏿 sign of the horns: dark skin tone +1F919 ; fully-qualified # 🤙 call me hand +1F919 1F3FB ; fully-qualified # 🤙🏻 call me hand: light skin tone +1F919 1F3FC ; fully-qualified # 🤙🏼 call me hand: medium-light skin tone +1F919 1F3FD ; fully-qualified # 🤙🏽 call me hand: medium skin tone +1F919 1F3FE ; fully-qualified # 🤙🏾 call me hand: medium-dark skin tone +1F919 1F3FF ; fully-qualified # 🤙🏿 call me hand: dark skin tone + +# subgroup: hand-single-finger +1F448 ; fully-qualified # 👈 backhand index pointing left +1F448 1F3FB ; fully-qualified # 👈🏻 backhand index pointing left: light skin tone +1F448 1F3FC ; fully-qualified # 👈🏼 backhand index pointing left: medium-light skin tone +1F448 1F3FD ; fully-qualified # 👈🏽 backhand index pointing left: medium skin tone +1F448 1F3FE ; fully-qualified # 👈🏾 backhand index pointing left: medium-dark skin tone +1F448 1F3FF ; fully-qualified # 👈🏿 backhand index pointing left: dark skin tone +1F449 ; fully-qualified # 👉 backhand index pointing right +1F449 1F3FB ; fully-qualified # 👉🏻 backhand index pointing right: light skin tone +1F449 1F3FC ; fully-qualified # 👉🏼 backhand index pointing right: medium-light skin tone +1F449 1F3FD ; fully-qualified # 👉🏽 backhand index pointing right: medium skin tone +1F449 1F3FE ; fully-qualified # 👉🏾 backhand index pointing right: medium-dark skin tone +1F449 1F3FF ; fully-qualified # 👉🏿 backhand index pointing right: dark skin tone +1F446 ; fully-qualified # 👆 backhand index pointing up +1F446 1F3FB ; fully-qualified # 👆🏻 backhand index pointing up: light skin tone +1F446 1F3FC ; fully-qualified # 👆🏼 backhand index pointing up: medium-light skin tone +1F446 1F3FD ; fully-qualified # 👆🏽 backhand index pointing up: medium skin tone +1F446 1F3FE ; fully-qualified # 👆🏾 backhand index pointing up: medium-dark skin tone +1F446 1F3FF ; fully-qualified # 👆🏿 backhand index pointing up: dark skin tone +1F595 ; fully-qualified # 🖕 middle finger +1F595 1F3FB ; fully-qualified # 🖕🏻 middle finger: light skin tone +1F595 1F3FC ; fully-qualified # 🖕🏼 middle finger: medium-light skin tone +1F595 1F3FD ; fully-qualified # 🖕🏽 middle finger: medium skin tone +1F595 1F3FE ; fully-qualified # 🖕🏾 middle finger: medium-dark skin tone +1F595 1F3FF ; fully-qualified # 🖕🏿 middle finger: dark skin tone +1F447 ; fully-qualified # 👇 backhand index pointing down +1F447 1F3FB ; fully-qualified # 👇🏻 backhand index pointing down: light skin tone +1F447 1F3FC ; fully-qualified # 👇🏼 backhand index pointing down: medium-light skin tone +1F447 1F3FD ; fully-qualified # 👇🏽 backhand index pointing down: medium skin tone +1F447 1F3FE ; fully-qualified # 👇🏾 backhand index pointing down: medium-dark skin tone +1F447 1F3FF ; fully-qualified # 👇🏿 backhand index pointing down: dark skin tone +261D FE0F ; fully-qualified # ☝️ index pointing up +261D ; unqualified # ☝ index pointing up +261D 1F3FB ; fully-qualified # ☝🏻 index pointing up: light skin tone +261D 1F3FC ; fully-qualified # ☝🏼 index pointing up: medium-light skin tone +261D 1F3FD ; fully-qualified # ☝🏽 index pointing up: medium skin tone +261D 1F3FE ; fully-qualified # ☝🏾 index pointing up: medium-dark skin tone +261D 1F3FF ; fully-qualified # ☝🏿 index pointing up: dark skin tone + +# subgroup: hand-fingers-closed +1F44D ; fully-qualified # 👍 thumbs up +1F44D 1F3FB ; fully-qualified # 👍🏻 thumbs up: light skin tone +1F44D 1F3FC ; fully-qualified # 👍🏼 thumbs up: medium-light skin tone +1F44D 1F3FD ; fully-qualified # 👍🏽 thumbs up: medium skin tone +1F44D 1F3FE ; fully-qualified # 👍🏾 thumbs up: medium-dark skin tone +1F44D 1F3FF ; fully-qualified # 👍🏿 thumbs up: dark skin tone +1F44E ; fully-qualified # 👎 thumbs down +1F44E 1F3FB ; fully-qualified # 👎🏻 thumbs down: light skin tone +1F44E 1F3FC ; fully-qualified # 👎🏼 thumbs down: medium-light skin tone +1F44E 1F3FD ; fully-qualified # 👎🏽 thumbs down: medium skin tone +1F44E 1F3FE ; fully-qualified # 👎🏾 thumbs down: medium-dark skin tone +1F44E 1F3FF ; fully-qualified # 👎🏿 thumbs down: dark skin tone +270A ; fully-qualified # ✊ raised fist +270A 1F3FB ; fully-qualified # ✊🏻 raised fist: light skin tone +270A 1F3FC ; fully-qualified # ✊🏼 raised fist: medium-light skin tone +270A 1F3FD ; fully-qualified # ✊🏽 raised fist: medium skin tone +270A 1F3FE ; fully-qualified # ✊🏾 raised fist: medium-dark skin tone +270A 1F3FF ; fully-qualified # ✊🏿 raised fist: dark skin tone +1F44A ; fully-qualified # 👊 oncoming fist +1F44A 1F3FB ; fully-qualified # 👊🏻 oncoming fist: light skin tone +1F44A 1F3FC ; fully-qualified # 👊🏼 oncoming fist: medium-light skin tone +1F44A 1F3FD ; fully-qualified # 👊🏽 oncoming fist: medium skin tone +1F44A 1F3FE ; fully-qualified # 👊🏾 oncoming fist: medium-dark skin tone +1F44A 1F3FF ; fully-qualified # 👊🏿 oncoming fist: dark skin tone +1F91B ; fully-qualified # 🤛 left-facing fist +1F91B 1F3FB ; fully-qualified # 🤛🏻 left-facing fist: light skin tone +1F91B 1F3FC ; fully-qualified # 🤛🏼 left-facing fist: medium-light skin tone +1F91B 1F3FD ; fully-qualified # 🤛🏽 left-facing fist: medium skin tone +1F91B 1F3FE ; fully-qualified # 🤛🏾 left-facing fist: medium-dark skin tone +1F91B 1F3FF ; fully-qualified # 🤛🏿 left-facing fist: dark skin tone +1F91C ; fully-qualified # 🤜 right-facing fist +1F91C 1F3FB ; fully-qualified # 🤜🏻 right-facing fist: light skin tone +1F91C 1F3FC ; fully-qualified # 🤜🏼 right-facing fist: medium-light skin tone +1F91C 1F3FD ; fully-qualified # 🤜🏽 right-facing fist: medium skin tone +1F91C 1F3FE ; fully-qualified # 🤜🏾 right-facing fist: medium-dark skin tone +1F91C 1F3FF ; fully-qualified # 🤜🏿 right-facing fist: dark skin tone + +# subgroup: hands +1F44F ; fully-qualified # 👏 clapping hands +1F44F 1F3FB ; fully-qualified # 👏🏻 clapping hands: light skin tone +1F44F 1F3FC ; fully-qualified # 👏🏼 clapping hands: medium-light skin tone +1F44F 1F3FD ; fully-qualified # 👏🏽 clapping hands: medium skin tone +1F44F 1F3FE ; fully-qualified # 👏🏾 clapping hands: medium-dark skin tone +1F44F 1F3FF ; fully-qualified # 👏🏿 clapping hands: dark skin tone +1F64C ; fully-qualified # 🙌 raising hands +1F64C 1F3FB ; fully-qualified # 🙌🏻 raising hands: light skin tone +1F64C 1F3FC ; fully-qualified # 🙌🏼 raising hands: medium-light skin tone +1F64C 1F3FD ; fully-qualified # 🙌🏽 raising hands: medium skin tone +1F64C 1F3FE ; fully-qualified # 🙌🏾 raising hands: medium-dark skin tone +1F64C 1F3FF ; fully-qualified # 🙌🏿 raising hands: dark skin tone +1F450 ; fully-qualified # 👐 open hands +1F450 1F3FB ; fully-qualified # 👐🏻 open hands: light skin tone +1F450 1F3FC ; fully-qualified # 👐🏼 open hands: medium-light skin tone +1F450 1F3FD ; fully-qualified # 👐🏽 open hands: medium skin tone +1F450 1F3FE ; fully-qualified # 👐🏾 open hands: medium-dark skin tone +1F450 1F3FF ; fully-qualified # 👐🏿 open hands: dark skin tone +1F932 ; fully-qualified # 🤲 palms up together +1F932 1F3FB ; fully-qualified # 🤲🏻 palms up together: light skin tone +1F932 1F3FC ; fully-qualified # 🤲🏼 palms up together: medium-light skin tone +1F932 1F3FD ; fully-qualified # 🤲🏽 palms up together: medium skin tone +1F932 1F3FE ; fully-qualified # 🤲🏾 palms up together: medium-dark skin tone +1F932 1F3FF ; fully-qualified # 🤲🏿 palms up together: dark skin tone +1F91D ; fully-qualified # 🤝 handshake +1F91D 1F3FB ; fully-qualified # 🤝🏻 handshake: light skin tone +1F91D 1F3FC ; fully-qualified # 🤝🏼 handshake: medium-light skin tone +1F91D 1F3FD ; fully-qualified # 🤝🏽 handshake: medium skin tone +1F91D 1F3FE ; fully-qualified # 🤝🏾 handshake: medium-dark skin tone +1F91D 1F3FF ; fully-qualified # 🤝🏿 handshake: dark skin tone +1F64F ; fully-qualified # 🙏 folded hands +1F64F 1F3FB ; fully-qualified # 🙏🏻 folded hands: light skin tone +1F64F 1F3FC ; fully-qualified # 🙏🏼 folded hands: medium-light skin tone +1F64F 1F3FD ; fully-qualified # 🙏🏽 folded hands: medium skin tone +1F64F 1F3FE ; fully-qualified # 🙏🏾 folded hands: medium-dark skin tone +1F64F 1F3FF ; fully-qualified # 🙏🏿 folded hands: dark skin tone + +# subgroup: hand-prop +270D FE0F ; fully-qualified # ✍️ writing hand +270D ; unqualified # ✍ writing hand +270D 1F3FB ; fully-qualified # ✍🏻 writing hand: light skin tone +270D 1F3FC ; fully-qualified # ✍🏼 writing hand: medium-light skin tone +270D 1F3FD ; fully-qualified # ✍🏽 writing hand: medium skin tone +270D 1F3FE ; fully-qualified # ✍🏾 writing hand: medium-dark skin tone +270D 1F3FF ; fully-qualified # ✍🏿 writing hand: dark skin tone +1F485 ; fully-qualified # 💅 nail polish +1F485 1F3FB ; fully-qualified # 💅🏻 nail polish: light skin tone +1F485 1F3FC ; fully-qualified # 💅🏼 nail polish: medium-light skin tone +1F485 1F3FD ; fully-qualified # 💅🏽 nail polish: medium skin tone +1F485 1F3FE ; fully-qualified # 💅🏾 nail polish: medium-dark skin tone +1F485 1F3FF ; fully-qualified # 💅🏿 nail polish: dark skin tone +1F933 ; fully-qualified # 🤳 selfie +1F933 1F3FB ; fully-qualified # 🤳🏻 selfie: light skin tone +1F933 1F3FC ; fully-qualified # 🤳🏼 selfie: medium-light skin tone +1F933 1F3FD ; fully-qualified # 🤳🏽 selfie: medium skin tone +1F933 1F3FE ; fully-qualified # 🤳🏾 selfie: medium-dark skin tone +1F933 1F3FF ; fully-qualified # 🤳🏿 selfie: dark skin tone + +# subgroup: body-parts +1F4AA ; fully-qualified # 💪 flexed biceps +1F4AA 1F3FB ; fully-qualified # 💪🏻 flexed biceps: light skin tone +1F4AA 1F3FC ; fully-qualified # 💪🏼 flexed biceps: medium-light skin tone +1F4AA 1F3FD ; fully-qualified # 💪🏽 flexed biceps: medium skin tone +1F4AA 1F3FE ; fully-qualified # 💪🏾 flexed biceps: medium-dark skin tone +1F4AA 1F3FF ; fully-qualified # 💪🏿 flexed biceps: dark skin tone +1F9BE ; fully-qualified # 🦾 mechanical arm +1F9BF ; fully-qualified # 🦿 mechanical leg +1F9B5 ; fully-qualified # 🦵 leg +1F9B5 1F3FB ; fully-qualified # 🦵🏻 leg: light skin tone +1F9B5 1F3FC ; fully-qualified # 🦵🏼 leg: medium-light skin tone +1F9B5 1F3FD ; fully-qualified # 🦵🏽 leg: medium skin tone +1F9B5 1F3FE ; fully-qualified # 🦵🏾 leg: medium-dark skin tone +1F9B5 1F3FF ; fully-qualified # 🦵🏿 leg: dark skin tone +1F9B6 ; fully-qualified # 🦶 foot +1F9B6 1F3FB ; fully-qualified # 🦶🏻 foot: light skin tone +1F9B6 1F3FC ; fully-qualified # 🦶🏼 foot: medium-light skin tone +1F9B6 1F3FD ; fully-qualified # 🦶🏽 foot: medium skin tone +1F9B6 1F3FE ; fully-qualified # 🦶🏾 foot: medium-dark skin tone +1F9B6 1F3FF ; fully-qualified # 🦶🏿 foot: dark skin tone +1F442 ; fully-qualified # 👂 ear +1F442 1F3FB ; fully-qualified # 👂🏻 ear: light skin tone +1F442 1F3FC ; fully-qualified # 👂🏼 ear: medium-light skin tone +1F442 1F3FD ; fully-qualified # 👂🏽 ear: medium skin tone +1F442 1F3FE ; fully-qualified # 👂🏾 ear: medium-dark skin tone +1F442 1F3FF ; fully-qualified # 👂🏿 ear: dark skin tone +1F9BB ; fully-qualified # 🦻 ear with hearing aid +1F9BB 1F3FB ; fully-qualified # 🦻🏻 ear with hearing aid: light skin tone +1F9BB 1F3FC ; fully-qualified # 🦻🏼 ear with hearing aid: medium-light skin tone +1F9BB 1F3FD ; fully-qualified # 🦻🏽 ear with hearing aid: medium skin tone +1F9BB 1F3FE ; fully-qualified # 🦻🏾 ear with hearing aid: medium-dark skin tone +1F9BB 1F3FF ; fully-qualified # 🦻🏿 ear with hearing aid: dark skin tone +1F443 ; fully-qualified # 👃 nose +1F443 1F3FB ; fully-qualified # 👃🏻 nose: light skin tone +1F443 1F3FC ; fully-qualified # 👃🏼 nose: medium-light skin tone +1F443 1F3FD ; fully-qualified # 👃🏽 nose: medium skin tone +1F443 1F3FE ; fully-qualified # 👃🏾 nose: medium-dark skin tone +1F443 1F3FF ; fully-qualified # 👃🏿 nose: dark skin tone +1F9E0 ; fully-qualified # 🧠 brain +1F9B7 ; fully-qualified # 🦷 tooth +1F9B4 ; fully-qualified # 🦴 bone +1F440 ; fully-qualified # 👀 eyes +1F441 FE0F ; fully-qualified # 👁️ eye +1F441 ; unqualified # 👁 eye +1F445 ; fully-qualified # 👅 tongue +1F444 ; fully-qualified # 👄 mouth + +# subgroup: person +1F476 ; fully-qualified # 👶 baby +1F476 1F3FB ; fully-qualified # 👶🏻 baby: light skin tone +1F476 1F3FC ; fully-qualified # 👶🏼 baby: medium-light skin tone +1F476 1F3FD ; fully-qualified # 👶🏽 baby: medium skin tone +1F476 1F3FE ; fully-qualified # 👶🏾 baby: medium-dark skin tone +1F476 1F3FF ; fully-qualified # 👶🏿 baby: dark skin tone +1F9D2 ; fully-qualified # 🧒 child +1F9D2 1F3FB ; fully-qualified # 🧒🏻 child: light skin tone +1F9D2 1F3FC ; fully-qualified # 🧒🏼 child: medium-light skin tone +1F9D2 1F3FD ; fully-qualified # 🧒🏽 child: medium skin tone +1F9D2 1F3FE ; fully-qualified # 🧒🏾 child: medium-dark skin tone +1F9D2 1F3FF ; fully-qualified # 🧒🏿 child: dark skin tone +1F466 ; fully-qualified # 👦 boy +1F466 1F3FB ; fully-qualified # 👦🏻 boy: light skin tone +1F466 1F3FC ; fully-qualified # 👦🏼 boy: medium-light skin tone +1F466 1F3FD ; fully-qualified # 👦🏽 boy: medium skin tone +1F466 1F3FE ; fully-qualified # 👦🏾 boy: medium-dark skin tone +1F466 1F3FF ; fully-qualified # 👦🏿 boy: dark skin tone +1F467 ; fully-qualified # 👧 girl +1F467 1F3FB ; fully-qualified # 👧🏻 girl: light skin tone +1F467 1F3FC ; fully-qualified # 👧🏼 girl: medium-light skin tone +1F467 1F3FD ; fully-qualified # 👧🏽 girl: medium skin tone +1F467 1F3FE ; fully-qualified # 👧🏾 girl: medium-dark skin tone +1F467 1F3FF ; fully-qualified # 👧🏿 girl: dark skin tone +1F9D1 ; fully-qualified # 🧑 person +1F9D1 1F3FB ; fully-qualified # 🧑🏻 person: light skin tone +1F9D1 1F3FC ; fully-qualified # 🧑🏼 person: medium-light skin tone +1F9D1 1F3FD ; fully-qualified # 🧑🏽 person: medium skin tone +1F9D1 1F3FE ; fully-qualified # 🧑🏾 person: medium-dark skin tone +1F9D1 1F3FF ; fully-qualified # 🧑🏿 person: dark skin tone +1F471 ; fully-qualified # 👱 person: blond hair +1F471 1F3FB ; fully-qualified # 👱🏻 person: light skin tone, blond hair +1F471 1F3FC ; fully-qualified # 👱🏼 person: medium-light skin tone, blond hair +1F471 1F3FD ; fully-qualified # 👱🏽 person: medium skin tone, blond hair +1F471 1F3FE ; fully-qualified # 👱🏾 person: medium-dark skin tone, blond hair +1F471 1F3FF ; fully-qualified # 👱🏿 person: dark skin tone, blond hair +1F468 ; fully-qualified # 👨 man +1F468 1F3FB ; fully-qualified # 👨🏻 man: light skin tone +1F468 1F3FC ; fully-qualified # 👨🏼 man: medium-light skin tone +1F468 1F3FD ; fully-qualified # 👨🏽 man: medium skin tone +1F468 1F3FE ; fully-qualified # 👨🏾 man: medium-dark skin tone +1F468 1F3FF ; fully-qualified # 👨🏿 man: dark skin tone +1F471 200D 2642 FE0F ; fully-qualified # 👱‍♂️ man: blond hair +1F471 200D 2642 ; minimally-qualified # 👱‍♂ man: blond hair +1F471 1F3FB 200D 2642 FE0F ; fully-qualified # 👱🏻‍♂️ man: light skin tone, blond hair +1F471 1F3FB 200D 2642 ; minimally-qualified # 👱🏻‍♂ man: light skin tone, blond hair +1F471 1F3FC 200D 2642 FE0F ; fully-qualified # 👱🏼‍♂️ man: medium-light skin tone, blond hair +1F471 1F3FC 200D 2642 ; minimally-qualified # 👱🏼‍♂ man: medium-light skin tone, blond hair +1F471 1F3FD 200D 2642 FE0F ; fully-qualified # 👱🏽‍♂️ man: medium skin tone, blond hair +1F471 1F3FD 200D 2642 ; minimally-qualified # 👱🏽‍♂ man: medium skin tone, blond hair +1F471 1F3FE 200D 2642 FE0F ; fully-qualified # 👱🏾‍♂️ man: medium-dark skin tone, blond hair +1F471 1F3FE 200D 2642 ; minimally-qualified # 👱🏾‍♂ man: medium-dark skin tone, blond hair +1F471 1F3FF 200D 2642 FE0F ; fully-qualified # 👱🏿‍♂️ man: dark skin tone, blond hair +1F471 1F3FF 200D 2642 ; minimally-qualified # 👱🏿‍♂ man: dark skin tone, blond hair +1F468 200D 1F9B0 ; fully-qualified # 👨‍🦰 man: red hair +1F468 1F3FB 200D 1F9B0 ; fully-qualified # 👨🏻‍🦰 man: light skin tone, red hair +1F468 1F3FC 200D 1F9B0 ; fully-qualified # 👨🏼‍🦰 man: medium-light skin tone, red hair +1F468 1F3FD 200D 1F9B0 ; fully-qualified # 👨🏽‍🦰 man: medium skin tone, red hair +1F468 1F3FE 200D 1F9B0 ; fully-qualified # 👨🏾‍🦰 man: medium-dark skin tone, red hair +1F468 1F3FF 200D 1F9B0 ; fully-qualified # 👨🏿‍🦰 man: dark skin tone, red hair +1F468 200D 1F9B1 ; fully-qualified # 👨‍🦱 man: curly hair +1F468 1F3FB 200D 1F9B1 ; fully-qualified # 👨🏻‍🦱 man: light skin tone, curly hair +1F468 1F3FC 200D 1F9B1 ; fully-qualified # 👨🏼‍🦱 man: medium-light skin tone, curly hair +1F468 1F3FD 200D 1F9B1 ; fully-qualified # 👨🏽‍🦱 man: medium skin tone, curly hair +1F468 1F3FE 200D 1F9B1 ; fully-qualified # 👨🏾‍🦱 man: medium-dark skin tone, curly hair +1F468 1F3FF 200D 1F9B1 ; fully-qualified # 👨🏿‍🦱 man: dark skin tone, curly hair +1F468 200D 1F9B3 ; fully-qualified # 👨‍🦳 man: white hair +1F468 1F3FB 200D 1F9B3 ; fully-qualified # 👨🏻‍🦳 man: light skin tone, white hair +1F468 1F3FC 200D 1F9B3 ; fully-qualified # 👨🏼‍🦳 man: medium-light skin tone, white hair +1F468 1F3FD 200D 1F9B3 ; fully-qualified # 👨🏽‍🦳 man: medium skin tone, white hair +1F468 1F3FE 200D 1F9B3 ; fully-qualified # 👨🏾‍🦳 man: medium-dark skin tone, white hair +1F468 1F3FF 200D 1F9B3 ; fully-qualified # 👨🏿‍🦳 man: dark skin tone, white hair +1F468 200D 1F9B2 ; fully-qualified # 👨‍🦲 man: bald +1F468 1F3FB 200D 1F9B2 ; fully-qualified # 👨🏻‍🦲 man: light skin tone, bald +1F468 1F3FC 200D 1F9B2 ; fully-qualified # 👨🏼‍🦲 man: medium-light skin tone, bald +1F468 1F3FD 200D 1F9B2 ; fully-qualified # 👨🏽‍🦲 man: medium skin tone, bald +1F468 1F3FE 200D 1F9B2 ; fully-qualified # 👨🏾‍🦲 man: medium-dark skin tone, bald +1F468 1F3FF 200D 1F9B2 ; fully-qualified # 👨🏿‍🦲 man: dark skin tone, bald +1F9D4 ; fully-qualified # 🧔 man: beard +1F9D4 1F3FB ; fully-qualified # 🧔🏻 man: light skin tone, beard +1F9D4 1F3FC ; fully-qualified # 🧔🏼 man: medium-light skin tone, beard +1F9D4 1F3FD ; fully-qualified # 🧔🏽 man: medium skin tone, beard +1F9D4 1F3FE ; fully-qualified # 🧔🏾 man: medium-dark skin tone, beard +1F9D4 1F3FF ; fully-qualified # 🧔🏿 man: dark skin tone, beard +1F469 ; fully-qualified # 👩 woman +1F469 1F3FB ; fully-qualified # 👩🏻 woman: light skin tone +1F469 1F3FC ; fully-qualified # 👩🏼 woman: medium-light skin tone +1F469 1F3FD ; fully-qualified # 👩🏽 woman: medium skin tone +1F469 1F3FE ; fully-qualified # 👩🏾 woman: medium-dark skin tone +1F469 1F3FF ; fully-qualified # 👩🏿 woman: dark skin tone +1F471 200D 2640 FE0F ; fully-qualified # 👱‍♀️ woman: blond hair +1F471 200D 2640 ; minimally-qualified # 👱‍♀ woman: blond hair +1F471 1F3FB 200D 2640 FE0F ; fully-qualified # 👱🏻‍♀️ woman: light skin tone, blond hair +1F471 1F3FB 200D 2640 ; minimally-qualified # 👱🏻‍♀ woman: light skin tone, blond hair +1F471 1F3FC 200D 2640 FE0F ; fully-qualified # 👱🏼‍♀️ woman: medium-light skin tone, blond hair +1F471 1F3FC 200D 2640 ; minimally-qualified # 👱🏼‍♀ woman: medium-light skin tone, blond hair +1F471 1F3FD 200D 2640 FE0F ; fully-qualified # 👱🏽‍♀️ woman: medium skin tone, blond hair +1F471 1F3FD 200D 2640 ; minimally-qualified # 👱🏽‍♀ woman: medium skin tone, blond hair +1F471 1F3FE 200D 2640 FE0F ; fully-qualified # 👱🏾‍♀️ woman: medium-dark skin tone, blond hair +1F471 1F3FE 200D 2640 ; minimally-qualified # 👱🏾‍♀ woman: medium-dark skin tone, blond hair +1F471 1F3FF 200D 2640 FE0F ; fully-qualified # 👱🏿‍♀️ woman: dark skin tone, blond hair +1F471 1F3FF 200D 2640 ; minimally-qualified # 👱🏿‍♀ woman: dark skin tone, blond hair +1F469 200D 1F9B0 ; fully-qualified # 👩‍🦰 woman: red hair +1F469 1F3FB 200D 1F9B0 ; fully-qualified # 👩🏻‍🦰 woman: light skin tone, red hair +1F469 1F3FC 200D 1F9B0 ; fully-qualified # 👩🏼‍🦰 woman: medium-light skin tone, red hair +1F469 1F3FD 200D 1F9B0 ; fully-qualified # 👩🏽‍🦰 woman: medium skin tone, red hair +1F469 1F3FE 200D 1F9B0 ; fully-qualified # 👩🏾‍🦰 woman: medium-dark skin tone, red hair +1F469 1F3FF 200D 1F9B0 ; fully-qualified # 👩🏿‍🦰 woman: dark skin tone, red hair +1F469 200D 1F9B1 ; fully-qualified # 👩‍🦱 woman: curly hair +1F469 1F3FB 200D 1F9B1 ; fully-qualified # 👩🏻‍🦱 woman: light skin tone, curly hair +1F469 1F3FC 200D 1F9B1 ; fully-qualified # 👩🏼‍🦱 woman: medium-light skin tone, curly hair +1F469 1F3FD 200D 1F9B1 ; fully-qualified # 👩🏽‍🦱 woman: medium skin tone, curly hair +1F469 1F3FE 200D 1F9B1 ; fully-qualified # 👩🏾‍🦱 woman: medium-dark skin tone, curly hair +1F469 1F3FF 200D 1F9B1 ; fully-qualified # 👩🏿‍🦱 woman: dark skin tone, curly hair +1F469 200D 1F9B3 ; fully-qualified # 👩‍🦳 woman: white hair +1F469 1F3FB 200D 1F9B3 ; fully-qualified # 👩🏻‍🦳 woman: light skin tone, white hair +1F469 1F3FC 200D 1F9B3 ; fully-qualified # 👩🏼‍🦳 woman: medium-light skin tone, white hair +1F469 1F3FD 200D 1F9B3 ; fully-qualified # 👩🏽‍🦳 woman: medium skin tone, white hair +1F469 1F3FE 200D 1F9B3 ; fully-qualified # 👩🏾‍🦳 woman: medium-dark skin tone, white hair +1F469 1F3FF 200D 1F9B3 ; fully-qualified # 👩🏿‍🦳 woman: dark skin tone, white hair +1F469 200D 1F9B2 ; fully-qualified # 👩‍🦲 woman: bald +1F469 1F3FB 200D 1F9B2 ; fully-qualified # 👩🏻‍🦲 woman: light skin tone, bald +1F469 1F3FC 200D 1F9B2 ; fully-qualified # 👩🏼‍🦲 woman: medium-light skin tone, bald +1F469 1F3FD 200D 1F9B2 ; fully-qualified # 👩🏽‍🦲 woman: medium skin tone, bald +1F469 1F3FE 200D 1F9B2 ; fully-qualified # 👩🏾‍🦲 woman: medium-dark skin tone, bald +1F469 1F3FF 200D 1F9B2 ; fully-qualified # 👩🏿‍🦲 woman: dark skin tone, bald +1F9D3 ; fully-qualified # 🧓 older person +1F9D3 1F3FB ; fully-qualified # 🧓🏻 older person: light skin tone +1F9D3 1F3FC ; fully-qualified # 🧓🏼 older person: medium-light skin tone +1F9D3 1F3FD ; fully-qualified # 🧓🏽 older person: medium skin tone +1F9D3 1F3FE ; fully-qualified # 🧓🏾 older person: medium-dark skin tone +1F9D3 1F3FF ; fully-qualified # 🧓🏿 older person: dark skin tone +1F474 ; fully-qualified # 👴 old man +1F474 1F3FB ; fully-qualified # 👴🏻 old man: light skin tone +1F474 1F3FC ; fully-qualified # 👴🏼 old man: medium-light skin tone +1F474 1F3FD ; fully-qualified # 👴🏽 old man: medium skin tone +1F474 1F3FE ; fully-qualified # 👴🏾 old man: medium-dark skin tone +1F474 1F3FF ; fully-qualified # 👴🏿 old man: dark skin tone +1F475 ; fully-qualified # 👵 old woman +1F475 1F3FB ; fully-qualified # 👵🏻 old woman: light skin tone +1F475 1F3FC ; fully-qualified # 👵🏼 old woman: medium-light skin tone +1F475 1F3FD ; fully-qualified # 👵🏽 old woman: medium skin tone +1F475 1F3FE ; fully-qualified # 👵🏾 old woman: medium-dark skin tone +1F475 1F3FF ; fully-qualified # 👵🏿 old woman: dark skin tone + +# subgroup: person-gesture +1F64D ; fully-qualified # 🙍 person frowning +1F64D 1F3FB ; fully-qualified # 🙍🏻 person frowning: light skin tone +1F64D 1F3FC ; fully-qualified # 🙍🏼 person frowning: medium-light skin tone +1F64D 1F3FD ; fully-qualified # 🙍🏽 person frowning: medium skin tone +1F64D 1F3FE ; fully-qualified # 🙍🏾 person frowning: medium-dark skin tone +1F64D 1F3FF ; fully-qualified # 🙍🏿 person frowning: dark skin tone +1F64D 200D 2642 FE0F ; fully-qualified # 🙍‍♂️ man frowning +1F64D 200D 2642 ; minimally-qualified # 🙍‍♂ man frowning +1F64D 1F3FB 200D 2642 FE0F ; fully-qualified # 🙍🏻‍♂️ man frowning: light skin tone +1F64D 1F3FB 200D 2642 ; minimally-qualified # 🙍🏻‍♂ man frowning: light skin tone +1F64D 1F3FC 200D 2642 FE0F ; fully-qualified # 🙍🏼‍♂️ man frowning: medium-light skin tone +1F64D 1F3FC 200D 2642 ; minimally-qualified # 🙍🏼‍♂ man frowning: medium-light skin tone +1F64D 1F3FD 200D 2642 FE0F ; fully-qualified # 🙍🏽‍♂️ man frowning: medium skin tone +1F64D 1F3FD 200D 2642 ; minimally-qualified # 🙍🏽‍♂ man frowning: medium skin tone +1F64D 1F3FE 200D 2642 FE0F ; fully-qualified # 🙍🏾‍♂️ man frowning: medium-dark skin tone +1F64D 1F3FE 200D 2642 ; minimally-qualified # 🙍🏾‍♂ man frowning: medium-dark skin tone +1F64D 1F3FF 200D 2642 FE0F ; fully-qualified # 🙍🏿‍♂️ man frowning: dark skin tone +1F64D 1F3FF 200D 2642 ; minimally-qualified # 🙍🏿‍♂ man frowning: dark skin tone +1F64D 200D 2640 FE0F ; fully-qualified # 🙍‍♀️ woman frowning +1F64D 200D 2640 ; minimally-qualified # 🙍‍♀ woman frowning +1F64D 1F3FB 200D 2640 FE0F ; fully-qualified # 🙍🏻‍♀️ woman frowning: light skin tone +1F64D 1F3FB 200D 2640 ; minimally-qualified # 🙍🏻‍♀ woman frowning: light skin tone +1F64D 1F3FC 200D 2640 FE0F ; fully-qualified # 🙍🏼‍♀️ woman frowning: medium-light skin tone +1F64D 1F3FC 200D 2640 ; minimally-qualified # 🙍🏼‍♀ woman frowning: medium-light skin tone +1F64D 1F3FD 200D 2640 FE0F ; fully-qualified # 🙍🏽‍♀️ woman frowning: medium skin tone +1F64D 1F3FD 200D 2640 ; minimally-qualified # 🙍🏽‍♀ woman frowning: medium skin tone +1F64D 1F3FE 200D 2640 FE0F ; fully-qualified # 🙍🏾‍♀️ woman frowning: medium-dark skin tone +1F64D 1F3FE 200D 2640 ; minimally-qualified # 🙍🏾‍♀ woman frowning: medium-dark skin tone +1F64D 1F3FF 200D 2640 FE0F ; fully-qualified # 🙍🏿‍♀️ woman frowning: dark skin tone +1F64D 1F3FF 200D 2640 ; minimally-qualified # 🙍🏿‍♀ woman frowning: dark skin tone +1F64E ; fully-qualified # 🙎 person pouting +1F64E 1F3FB ; fully-qualified # 🙎🏻 person pouting: light skin tone +1F64E 1F3FC ; fully-qualified # 🙎🏼 person pouting: medium-light skin tone +1F64E 1F3FD ; fully-qualified # 🙎🏽 person pouting: medium skin tone +1F64E 1F3FE ; fully-qualified # 🙎🏾 person pouting: medium-dark skin tone +1F64E 1F3FF ; fully-qualified # 🙎🏿 person pouting: dark skin tone +1F64E 200D 2642 FE0F ; fully-qualified # 🙎‍♂️ man pouting +1F64E 200D 2642 ; minimally-qualified # 🙎‍♂ man pouting +1F64E 1F3FB 200D 2642 FE0F ; fully-qualified # 🙎🏻‍♂️ man pouting: light skin tone +1F64E 1F3FB 200D 2642 ; minimally-qualified # 🙎🏻‍♂ man pouting: light skin tone +1F64E 1F3FC 200D 2642 FE0F ; fully-qualified # 🙎🏼‍♂️ man pouting: medium-light skin tone +1F64E 1F3FC 200D 2642 ; minimally-qualified # 🙎🏼‍♂ man pouting: medium-light skin tone +1F64E 1F3FD 200D 2642 FE0F ; fully-qualified # 🙎🏽‍♂️ man pouting: medium skin tone +1F64E 1F3FD 200D 2642 ; minimally-qualified # 🙎🏽‍♂ man pouting: medium skin tone +1F64E 1F3FE 200D 2642 FE0F ; fully-qualified # 🙎🏾‍♂️ man pouting: medium-dark skin tone +1F64E 1F3FE 200D 2642 ; minimally-qualified # 🙎🏾‍♂ man pouting: medium-dark skin tone +1F64E 1F3FF 200D 2642 FE0F ; fully-qualified # 🙎🏿‍♂️ man pouting: dark skin tone +1F64E 1F3FF 200D 2642 ; minimally-qualified # 🙎🏿‍♂ man pouting: dark skin tone +1F64E 200D 2640 FE0F ; fully-qualified # 🙎‍♀️ woman pouting +1F64E 200D 2640 ; minimally-qualified # 🙎‍♀ woman pouting +1F64E 1F3FB 200D 2640 FE0F ; fully-qualified # 🙎🏻‍♀️ woman pouting: light skin tone +1F64E 1F3FB 200D 2640 ; minimally-qualified # 🙎🏻‍♀ woman pouting: light skin tone +1F64E 1F3FC 200D 2640 FE0F ; fully-qualified # 🙎🏼‍♀️ woman pouting: medium-light skin tone +1F64E 1F3FC 200D 2640 ; minimally-qualified # 🙎🏼‍♀ woman pouting: medium-light skin tone +1F64E 1F3FD 200D 2640 FE0F ; fully-qualified # 🙎🏽‍♀️ woman pouting: medium skin tone +1F64E 1F3FD 200D 2640 ; minimally-qualified # 🙎🏽‍♀ woman pouting: medium skin tone +1F64E 1F3FE 200D 2640 FE0F ; fully-qualified # 🙎🏾‍♀️ woman pouting: medium-dark skin tone +1F64E 1F3FE 200D 2640 ; minimally-qualified # 🙎🏾‍♀ woman pouting: medium-dark skin tone +1F64E 1F3FF 200D 2640 FE0F ; fully-qualified # 🙎🏿‍♀️ woman pouting: dark skin tone +1F64E 1F3FF 200D 2640 ; minimally-qualified # 🙎🏿‍♀ woman pouting: dark skin tone +1F645 ; fully-qualified # 🙅 person gesturing NO +1F645 1F3FB ; fully-qualified # 🙅🏻 person gesturing NO: light skin tone +1F645 1F3FC ; fully-qualified # 🙅🏼 person gesturing NO: medium-light skin tone +1F645 1F3FD ; fully-qualified # 🙅🏽 person gesturing NO: medium skin tone +1F645 1F3FE ; fully-qualified # 🙅🏾 person gesturing NO: medium-dark skin tone +1F645 1F3FF ; fully-qualified # 🙅🏿 person gesturing NO: dark skin tone +1F645 200D 2642 FE0F ; fully-qualified # 🙅‍♂️ man gesturing NO +1F645 200D 2642 ; minimally-qualified # 🙅‍♂ man gesturing NO +1F645 1F3FB 200D 2642 FE0F ; fully-qualified # 🙅🏻‍♂️ man gesturing NO: light skin tone +1F645 1F3FB 200D 2642 ; minimally-qualified # 🙅🏻‍♂ man gesturing NO: light skin tone +1F645 1F3FC 200D 2642 FE0F ; fully-qualified # 🙅🏼‍♂️ man gesturing NO: medium-light skin tone +1F645 1F3FC 200D 2642 ; minimally-qualified # 🙅🏼‍♂ man gesturing NO: medium-light skin tone +1F645 1F3FD 200D 2642 FE0F ; fully-qualified # 🙅🏽‍♂️ man gesturing NO: medium skin tone +1F645 1F3FD 200D 2642 ; minimally-qualified # 🙅🏽‍♂ man gesturing NO: medium skin tone +1F645 1F3FE 200D 2642 FE0F ; fully-qualified # 🙅🏾‍♂️ man gesturing NO: medium-dark skin tone +1F645 1F3FE 200D 2642 ; minimally-qualified # 🙅🏾‍♂ man gesturing NO: medium-dark skin tone +1F645 1F3FF 200D 2642 FE0F ; fully-qualified # 🙅🏿‍♂️ man gesturing NO: dark skin tone +1F645 1F3FF 200D 2642 ; minimally-qualified # 🙅🏿‍♂ man gesturing NO: dark skin tone +1F645 200D 2640 FE0F ; fully-qualified # 🙅‍♀️ woman gesturing NO +1F645 200D 2640 ; minimally-qualified # 🙅‍♀ woman gesturing NO +1F645 1F3FB 200D 2640 FE0F ; fully-qualified # 🙅🏻‍♀️ woman gesturing NO: light skin tone +1F645 1F3FB 200D 2640 ; minimally-qualified # 🙅🏻‍♀ woman gesturing NO: light skin tone +1F645 1F3FC 200D 2640 FE0F ; fully-qualified # 🙅🏼‍♀️ woman gesturing NO: medium-light skin tone +1F645 1F3FC 200D 2640 ; minimally-qualified # 🙅🏼‍♀ woman gesturing NO: medium-light skin tone +1F645 1F3FD 200D 2640 FE0F ; fully-qualified # 🙅🏽‍♀️ woman gesturing NO: medium skin tone +1F645 1F3FD 200D 2640 ; minimally-qualified # 🙅🏽‍♀ woman gesturing NO: medium skin tone +1F645 1F3FE 200D 2640 FE0F ; fully-qualified # 🙅🏾‍♀️ woman gesturing NO: medium-dark skin tone +1F645 1F3FE 200D 2640 ; minimally-qualified # 🙅🏾‍♀ woman gesturing NO: medium-dark skin tone +1F645 1F3FF 200D 2640 FE0F ; fully-qualified # 🙅🏿‍♀️ woman gesturing NO: dark skin tone +1F645 1F3FF 200D 2640 ; minimally-qualified # 🙅🏿‍♀ woman gesturing NO: dark skin tone +1F646 ; fully-qualified # 🙆 person gesturing OK +1F646 1F3FB ; fully-qualified # 🙆🏻 person gesturing OK: light skin tone +1F646 1F3FC ; fully-qualified # 🙆🏼 person gesturing OK: medium-light skin tone +1F646 1F3FD ; fully-qualified # 🙆🏽 person gesturing OK: medium skin tone +1F646 1F3FE ; fully-qualified # 🙆🏾 person gesturing OK: medium-dark skin tone +1F646 1F3FF ; fully-qualified # 🙆🏿 person gesturing OK: dark skin tone +1F646 200D 2642 FE0F ; fully-qualified # 🙆‍♂️ man gesturing OK +1F646 200D 2642 ; minimally-qualified # 🙆‍♂ man gesturing OK +1F646 1F3FB 200D 2642 FE0F ; fully-qualified # 🙆🏻‍♂️ man gesturing OK: light skin tone +1F646 1F3FB 200D 2642 ; minimally-qualified # 🙆🏻‍♂ man gesturing OK: light skin tone +1F646 1F3FC 200D 2642 FE0F ; fully-qualified # 🙆🏼‍♂️ man gesturing OK: medium-light skin tone +1F646 1F3FC 200D 2642 ; minimally-qualified # 🙆🏼‍♂ man gesturing OK: medium-light skin tone +1F646 1F3FD 200D 2642 FE0F ; fully-qualified # 🙆🏽‍♂️ man gesturing OK: medium skin tone +1F646 1F3FD 200D 2642 ; minimally-qualified # 🙆🏽‍♂ man gesturing OK: medium skin tone +1F646 1F3FE 200D 2642 FE0F ; fully-qualified # 🙆🏾‍♂️ man gesturing OK: medium-dark skin tone +1F646 1F3FE 200D 2642 ; minimally-qualified # 🙆🏾‍♂ man gesturing OK: medium-dark skin tone +1F646 1F3FF 200D 2642 FE0F ; fully-qualified # 🙆🏿‍♂️ man gesturing OK: dark skin tone +1F646 1F3FF 200D 2642 ; minimally-qualified # 🙆🏿‍♂ man gesturing OK: dark skin tone +1F646 200D 2640 FE0F ; fully-qualified # 🙆‍♀️ woman gesturing OK +1F646 200D 2640 ; minimally-qualified # 🙆‍♀ woman gesturing OK +1F646 1F3FB 200D 2640 FE0F ; fully-qualified # 🙆🏻‍♀️ woman gesturing OK: light skin tone +1F646 1F3FB 200D 2640 ; minimally-qualified # 🙆🏻‍♀ woman gesturing OK: light skin tone +1F646 1F3FC 200D 2640 FE0F ; fully-qualified # 🙆🏼‍♀️ woman gesturing OK: medium-light skin tone +1F646 1F3FC 200D 2640 ; minimally-qualified # 🙆🏼‍♀ woman gesturing OK: medium-light skin tone +1F646 1F3FD 200D 2640 FE0F ; fully-qualified # 🙆🏽‍♀️ woman gesturing OK: medium skin tone +1F646 1F3FD 200D 2640 ; minimally-qualified # 🙆🏽‍♀ woman gesturing OK: medium skin tone +1F646 1F3FE 200D 2640 FE0F ; fully-qualified # 🙆🏾‍♀️ woman gesturing OK: medium-dark skin tone +1F646 1F3FE 200D 2640 ; minimally-qualified # 🙆🏾‍♀ woman gesturing OK: medium-dark skin tone +1F646 1F3FF 200D 2640 FE0F ; fully-qualified # 🙆🏿‍♀️ woman gesturing OK: dark skin tone +1F646 1F3FF 200D 2640 ; minimally-qualified # 🙆🏿‍♀ woman gesturing OK: dark skin tone +1F481 ; fully-qualified # 💁 person tipping hand +1F481 1F3FB ; fully-qualified # 💁🏻 person tipping hand: light skin tone +1F481 1F3FC ; fully-qualified # 💁🏼 person tipping hand: medium-light skin tone +1F481 1F3FD ; fully-qualified # 💁🏽 person tipping hand: medium skin tone +1F481 1F3FE ; fully-qualified # 💁🏾 person tipping hand: medium-dark skin tone +1F481 1F3FF ; fully-qualified # 💁🏿 person tipping hand: dark skin tone +1F481 200D 2642 FE0F ; fully-qualified # 💁‍♂️ man tipping hand +1F481 200D 2642 ; minimally-qualified # 💁‍♂ man tipping hand +1F481 1F3FB 200D 2642 FE0F ; fully-qualified # 💁🏻‍♂️ man tipping hand: light skin tone +1F481 1F3FB 200D 2642 ; minimally-qualified # 💁🏻‍♂ man tipping hand: light skin tone +1F481 1F3FC 200D 2642 FE0F ; fully-qualified # 💁🏼‍♂️ man tipping hand: medium-light skin tone +1F481 1F3FC 200D 2642 ; minimally-qualified # 💁🏼‍♂ man tipping hand: medium-light skin tone +1F481 1F3FD 200D 2642 FE0F ; fully-qualified # 💁🏽‍♂️ man tipping hand: medium skin tone +1F481 1F3FD 200D 2642 ; minimally-qualified # 💁🏽‍♂ man tipping hand: medium skin tone +1F481 1F3FE 200D 2642 FE0F ; fully-qualified # 💁🏾‍♂️ man tipping hand: medium-dark skin tone +1F481 1F3FE 200D 2642 ; minimally-qualified # 💁🏾‍♂ man tipping hand: medium-dark skin tone +1F481 1F3FF 200D 2642 FE0F ; fully-qualified # 💁🏿‍♂️ man tipping hand: dark skin tone +1F481 1F3FF 200D 2642 ; minimally-qualified # 💁🏿‍♂ man tipping hand: dark skin tone +1F481 200D 2640 FE0F ; fully-qualified # 💁‍♀️ woman tipping hand +1F481 200D 2640 ; minimally-qualified # 💁‍♀ woman tipping hand +1F481 1F3FB 200D 2640 FE0F ; fully-qualified # 💁🏻‍♀️ woman tipping hand: light skin tone +1F481 1F3FB 200D 2640 ; minimally-qualified # 💁🏻‍♀ woman tipping hand: light skin tone +1F481 1F3FC 200D 2640 FE0F ; fully-qualified # 💁🏼‍♀️ woman tipping hand: medium-light skin tone +1F481 1F3FC 200D 2640 ; minimally-qualified # 💁🏼‍♀ woman tipping hand: medium-light skin tone +1F481 1F3FD 200D 2640 FE0F ; fully-qualified # 💁🏽‍♀️ woman tipping hand: medium skin tone +1F481 1F3FD 200D 2640 ; minimally-qualified # 💁🏽‍♀ woman tipping hand: medium skin tone +1F481 1F3FE 200D 2640 FE0F ; fully-qualified # 💁🏾‍♀️ woman tipping hand: medium-dark skin tone +1F481 1F3FE 200D 2640 ; minimally-qualified # 💁🏾‍♀ woman tipping hand: medium-dark skin tone +1F481 1F3FF 200D 2640 FE0F ; fully-qualified # 💁🏿‍♀️ woman tipping hand: dark skin tone +1F481 1F3FF 200D 2640 ; minimally-qualified # 💁🏿‍♀ woman tipping hand: dark skin tone +1F64B ; fully-qualified # 🙋 person raising hand +1F64B 1F3FB ; fully-qualified # 🙋🏻 person raising hand: light skin tone +1F64B 1F3FC ; fully-qualified # 🙋🏼 person raising hand: medium-light skin tone +1F64B 1F3FD ; fully-qualified # 🙋🏽 person raising hand: medium skin tone +1F64B 1F3FE ; fully-qualified # 🙋🏾 person raising hand: medium-dark skin tone +1F64B 1F3FF ; fully-qualified # 🙋🏿 person raising hand: dark skin tone +1F64B 200D 2642 FE0F ; fully-qualified # 🙋‍♂️ man raising hand +1F64B 200D 2642 ; minimally-qualified # 🙋‍♂ man raising hand +1F64B 1F3FB 200D 2642 FE0F ; fully-qualified # 🙋🏻‍♂️ man raising hand: light skin tone +1F64B 1F3FB 200D 2642 ; minimally-qualified # 🙋🏻‍♂ man raising hand: light skin tone +1F64B 1F3FC 200D 2642 FE0F ; fully-qualified # 🙋🏼‍♂️ man raising hand: medium-light skin tone +1F64B 1F3FC 200D 2642 ; minimally-qualified # 🙋🏼‍♂ man raising hand: medium-light skin tone +1F64B 1F3FD 200D 2642 FE0F ; fully-qualified # 🙋🏽‍♂️ man raising hand: medium skin tone +1F64B 1F3FD 200D 2642 ; minimally-qualified # 🙋🏽‍♂ man raising hand: medium skin tone +1F64B 1F3FE 200D 2642 FE0F ; fully-qualified # 🙋🏾‍♂️ man raising hand: medium-dark skin tone +1F64B 1F3FE 200D 2642 ; minimally-qualified # 🙋🏾‍♂ man raising hand: medium-dark skin tone +1F64B 1F3FF 200D 2642 FE0F ; fully-qualified # 🙋🏿‍♂️ man raising hand: dark skin tone +1F64B 1F3FF 200D 2642 ; minimally-qualified # 🙋🏿‍♂ man raising hand: dark skin tone +1F64B 200D 2640 FE0F ; fully-qualified # 🙋‍♀️ woman raising hand +1F64B 200D 2640 ; minimally-qualified # 🙋‍♀ woman raising hand +1F64B 1F3FB 200D 2640 FE0F ; fully-qualified # 🙋🏻‍♀️ woman raising hand: light skin tone +1F64B 1F3FB 200D 2640 ; minimally-qualified # 🙋🏻‍♀ woman raising hand: light skin tone +1F64B 1F3FC 200D 2640 FE0F ; fully-qualified # 🙋🏼‍♀️ woman raising hand: medium-light skin tone +1F64B 1F3FC 200D 2640 ; minimally-qualified # 🙋🏼‍♀ woman raising hand: medium-light skin tone +1F64B 1F3FD 200D 2640 FE0F ; fully-qualified # 🙋🏽‍♀️ woman raising hand: medium skin tone +1F64B 1F3FD 200D 2640 ; minimally-qualified # 🙋🏽‍♀ woman raising hand: medium skin tone +1F64B 1F3FE 200D 2640 FE0F ; fully-qualified # 🙋🏾‍♀️ woman raising hand: medium-dark skin tone +1F64B 1F3FE 200D 2640 ; minimally-qualified # 🙋🏾‍♀ woman raising hand: medium-dark skin tone +1F64B 1F3FF 200D 2640 FE0F ; fully-qualified # 🙋🏿‍♀️ woman raising hand: dark skin tone +1F64B 1F3FF 200D 2640 ; minimally-qualified # 🙋🏿‍♀ woman raising hand: dark skin tone +1F9CF ; fully-qualified # 🧏 deaf person +1F9CF 1F3FB ; fully-qualified # 🧏🏻 deaf person: light skin tone +1F9CF 1F3FC ; fully-qualified # 🧏🏼 deaf person: medium-light skin tone +1F9CF 1F3FD ; fully-qualified # 🧏🏽 deaf person: medium skin tone +1F9CF 1F3FE ; fully-qualified # 🧏🏾 deaf person: medium-dark skin tone +1F9CF 1F3FF ; fully-qualified # 🧏🏿 deaf person: dark skin tone +1F9CF 200D 2642 FE0F ; fully-qualified # 🧏‍♂️ deaf man +1F9CF 200D 2642 ; minimally-qualified # 🧏‍♂ deaf man +1F9CF 1F3FB 200D 2642 FE0F ; fully-qualified # 🧏🏻‍♂️ deaf man: light skin tone +1F9CF 1F3FB 200D 2642 ; minimally-qualified # 🧏🏻‍♂ deaf man: light skin tone +1F9CF 1F3FC 200D 2642 FE0F ; fully-qualified # 🧏🏼‍♂️ deaf man: medium-light skin tone +1F9CF 1F3FC 200D 2642 ; minimally-qualified # 🧏🏼‍♂ deaf man: medium-light skin tone +1F9CF 1F3FD 200D 2642 FE0F ; fully-qualified # 🧏🏽‍♂️ deaf man: medium skin tone +1F9CF 1F3FD 200D 2642 ; minimally-qualified # 🧏🏽‍♂ deaf man: medium skin tone +1F9CF 1F3FE 200D 2642 FE0F ; fully-qualified # 🧏🏾‍♂️ deaf man: medium-dark skin tone +1F9CF 1F3FE 200D 2642 ; minimally-qualified # 🧏🏾‍♂ deaf man: medium-dark skin tone +1F9CF 1F3FF 200D 2642 FE0F ; fully-qualified # 🧏🏿‍♂️ deaf man: dark skin tone +1F9CF 1F3FF 200D 2642 ; minimally-qualified # 🧏🏿‍♂ deaf man: dark skin tone +1F9CF 200D 2640 FE0F ; fully-qualified # 🧏‍♀️ deaf woman +1F9CF 200D 2640 ; minimally-qualified # 🧏‍♀ deaf woman +1F9CF 1F3FB 200D 2640 FE0F ; fully-qualified # 🧏🏻‍♀️ deaf woman: light skin tone +1F9CF 1F3FB 200D 2640 ; minimally-qualified # 🧏🏻‍♀ deaf woman: light skin tone +1F9CF 1F3FC 200D 2640 FE0F ; fully-qualified # 🧏🏼‍♀️ deaf woman: medium-light skin tone +1F9CF 1F3FC 200D 2640 ; minimally-qualified # 🧏🏼‍♀ deaf woman: medium-light skin tone +1F9CF 1F3FD 200D 2640 FE0F ; fully-qualified # 🧏🏽‍♀️ deaf woman: medium skin tone +1F9CF 1F3FD 200D 2640 ; minimally-qualified # 🧏🏽‍♀ deaf woman: medium skin tone +1F9CF 1F3FE 200D 2640 FE0F ; fully-qualified # 🧏🏾‍♀️ deaf woman: medium-dark skin tone +1F9CF 1F3FE 200D 2640 ; minimally-qualified # 🧏🏾‍♀ deaf woman: medium-dark skin tone +1F9CF 1F3FF 200D 2640 FE0F ; fully-qualified # 🧏🏿‍♀️ deaf woman: dark skin tone +1F9CF 1F3FF 200D 2640 ; minimally-qualified # 🧏🏿‍♀ deaf woman: dark skin tone +1F647 ; fully-qualified # 🙇 person bowing +1F647 1F3FB ; fully-qualified # 🙇🏻 person bowing: light skin tone +1F647 1F3FC ; fully-qualified # 🙇🏼 person bowing: medium-light skin tone +1F647 1F3FD ; fully-qualified # 🙇🏽 person bowing: medium skin tone +1F647 1F3FE ; fully-qualified # 🙇🏾 person bowing: medium-dark skin tone +1F647 1F3FF ; fully-qualified # 🙇🏿 person bowing: dark skin tone +1F647 200D 2642 FE0F ; fully-qualified # 🙇‍♂️ man bowing +1F647 200D 2642 ; minimally-qualified # 🙇‍♂ man bowing +1F647 1F3FB 200D 2642 FE0F ; fully-qualified # 🙇🏻‍♂️ man bowing: light skin tone +1F647 1F3FB 200D 2642 ; minimally-qualified # 🙇🏻‍♂ man bowing: light skin tone +1F647 1F3FC 200D 2642 FE0F ; fully-qualified # 🙇🏼‍♂️ man bowing: medium-light skin tone +1F647 1F3FC 200D 2642 ; minimally-qualified # 🙇🏼‍♂ man bowing: medium-light skin tone +1F647 1F3FD 200D 2642 FE0F ; fully-qualified # 🙇🏽‍♂️ man bowing: medium skin tone +1F647 1F3FD 200D 2642 ; minimally-qualified # 🙇🏽‍♂ man bowing: medium skin tone +1F647 1F3FE 200D 2642 FE0F ; fully-qualified # 🙇🏾‍♂️ man bowing: medium-dark skin tone +1F647 1F3FE 200D 2642 ; minimally-qualified # 🙇🏾‍♂ man bowing: medium-dark skin tone +1F647 1F3FF 200D 2642 FE0F ; fully-qualified # 🙇🏿‍♂️ man bowing: dark skin tone +1F647 1F3FF 200D 2642 ; minimally-qualified # 🙇🏿‍♂ man bowing: dark skin tone +1F647 200D 2640 FE0F ; fully-qualified # 🙇‍♀️ woman bowing +1F647 200D 2640 ; minimally-qualified # 🙇‍♀ woman bowing +1F647 1F3FB 200D 2640 FE0F ; fully-qualified # 🙇🏻‍♀️ woman bowing: light skin tone +1F647 1F3FB 200D 2640 ; minimally-qualified # 🙇🏻‍♀ woman bowing: light skin tone +1F647 1F3FC 200D 2640 FE0F ; fully-qualified # 🙇🏼‍♀️ woman bowing: medium-light skin tone +1F647 1F3FC 200D 2640 ; minimally-qualified # 🙇🏼‍♀ woman bowing: medium-light skin tone +1F647 1F3FD 200D 2640 FE0F ; fully-qualified # 🙇🏽‍♀️ woman bowing: medium skin tone +1F647 1F3FD 200D 2640 ; minimally-qualified # 🙇🏽‍♀ woman bowing: medium skin tone +1F647 1F3FE 200D 2640 FE0F ; fully-qualified # 🙇🏾‍♀️ woman bowing: medium-dark skin tone +1F647 1F3FE 200D 2640 ; minimally-qualified # 🙇🏾‍♀ woman bowing: medium-dark skin tone +1F647 1F3FF 200D 2640 FE0F ; fully-qualified # 🙇🏿‍♀️ woman bowing: dark skin tone +1F647 1F3FF 200D 2640 ; minimally-qualified # 🙇🏿‍♀ woman bowing: dark skin tone +1F926 ; fully-qualified # 🤦 person facepalming +1F926 1F3FB ; fully-qualified # 🤦🏻 person facepalming: light skin tone +1F926 1F3FC ; fully-qualified # 🤦🏼 person facepalming: medium-light skin tone +1F926 1F3FD ; fully-qualified # 🤦🏽 person facepalming: medium skin tone +1F926 1F3FE ; fully-qualified # 🤦🏾 person facepalming: medium-dark skin tone +1F926 1F3FF ; fully-qualified # 🤦🏿 person facepalming: dark skin tone +1F926 200D 2642 FE0F ; fully-qualified # 🤦‍♂️ man facepalming +1F926 200D 2642 ; minimally-qualified # 🤦‍♂ man facepalming +1F926 1F3FB 200D 2642 FE0F ; fully-qualified # 🤦🏻‍♂️ man facepalming: light skin tone +1F926 1F3FB 200D 2642 ; minimally-qualified # 🤦🏻‍♂ man facepalming: light skin tone +1F926 1F3FC 200D 2642 FE0F ; fully-qualified # 🤦🏼‍♂️ man facepalming: medium-light skin tone +1F926 1F3FC 200D 2642 ; minimally-qualified # 🤦🏼‍♂ man facepalming: medium-light skin tone +1F926 1F3FD 200D 2642 FE0F ; fully-qualified # 🤦🏽‍♂️ man facepalming: medium skin tone +1F926 1F3FD 200D 2642 ; minimally-qualified # 🤦🏽‍♂ man facepalming: medium skin tone +1F926 1F3FE 200D 2642 FE0F ; fully-qualified # 🤦🏾‍♂️ man facepalming: medium-dark skin tone +1F926 1F3FE 200D 2642 ; minimally-qualified # 🤦🏾‍♂ man facepalming: medium-dark skin tone +1F926 1F3FF 200D 2642 FE0F ; fully-qualified # 🤦🏿‍♂️ man facepalming: dark skin tone +1F926 1F3FF 200D 2642 ; minimally-qualified # 🤦🏿‍♂ man facepalming: dark skin tone +1F926 200D 2640 FE0F ; fully-qualified # 🤦‍♀️ woman facepalming +1F926 200D 2640 ; minimally-qualified # 🤦‍♀ woman facepalming +1F926 1F3FB 200D 2640 FE0F ; fully-qualified # 🤦🏻‍♀️ woman facepalming: light skin tone +1F926 1F3FB 200D 2640 ; minimally-qualified # 🤦🏻‍♀ woman facepalming: light skin tone +1F926 1F3FC 200D 2640 FE0F ; fully-qualified # 🤦🏼‍♀️ woman facepalming: medium-light skin tone +1F926 1F3FC 200D 2640 ; minimally-qualified # 🤦🏼‍♀ woman facepalming: medium-light skin tone +1F926 1F3FD 200D 2640 FE0F ; fully-qualified # 🤦🏽‍♀️ woman facepalming: medium skin tone +1F926 1F3FD 200D 2640 ; minimally-qualified # 🤦🏽‍♀ woman facepalming: medium skin tone +1F926 1F3FE 200D 2640 FE0F ; fully-qualified # 🤦🏾‍♀️ woman facepalming: medium-dark skin tone +1F926 1F3FE 200D 2640 ; minimally-qualified # 🤦🏾‍♀ woman facepalming: medium-dark skin tone +1F926 1F3FF 200D 2640 FE0F ; fully-qualified # 🤦🏿‍♀️ woman facepalming: dark skin tone +1F926 1F3FF 200D 2640 ; minimally-qualified # 🤦🏿‍♀ woman facepalming: dark skin tone +1F937 ; fully-qualified # 🤷 person shrugging +1F937 1F3FB ; fully-qualified # 🤷🏻 person shrugging: light skin tone +1F937 1F3FC ; fully-qualified # 🤷🏼 person shrugging: medium-light skin tone +1F937 1F3FD ; fully-qualified # 🤷🏽 person shrugging: medium skin tone +1F937 1F3FE ; fully-qualified # 🤷🏾 person shrugging: medium-dark skin tone +1F937 1F3FF ; fully-qualified # 🤷🏿 person shrugging: dark skin tone +1F937 200D 2642 FE0F ; fully-qualified # 🤷‍♂️ man shrugging +1F937 200D 2642 ; minimally-qualified # 🤷‍♂ man shrugging +1F937 1F3FB 200D 2642 FE0F ; fully-qualified # 🤷🏻‍♂️ man shrugging: light skin tone +1F937 1F3FB 200D 2642 ; minimally-qualified # 🤷🏻‍♂ man shrugging: light skin tone +1F937 1F3FC 200D 2642 FE0F ; fully-qualified # 🤷🏼‍♂️ man shrugging: medium-light skin tone +1F937 1F3FC 200D 2642 ; minimally-qualified # 🤷🏼‍♂ man shrugging: medium-light skin tone +1F937 1F3FD 200D 2642 FE0F ; fully-qualified # 🤷🏽‍♂️ man shrugging: medium skin tone +1F937 1F3FD 200D 2642 ; minimally-qualified # 🤷🏽‍♂ man shrugging: medium skin tone +1F937 1F3FE 200D 2642 FE0F ; fully-qualified # 🤷🏾‍♂️ man shrugging: medium-dark skin tone +1F937 1F3FE 200D 2642 ; minimally-qualified # 🤷🏾‍♂ man shrugging: medium-dark skin tone +1F937 1F3FF 200D 2642 FE0F ; fully-qualified # 🤷🏿‍♂️ man shrugging: dark skin tone +1F937 1F3FF 200D 2642 ; minimally-qualified # 🤷🏿‍♂ man shrugging: dark skin tone +1F937 200D 2640 FE0F ; fully-qualified # 🤷‍♀️ woman shrugging +1F937 200D 2640 ; minimally-qualified # 🤷‍♀ woman shrugging +1F937 1F3FB 200D 2640 FE0F ; fully-qualified # 🤷🏻‍♀️ woman shrugging: light skin tone +1F937 1F3FB 200D 2640 ; minimally-qualified # 🤷🏻‍♀ woman shrugging: light skin tone +1F937 1F3FC 200D 2640 FE0F ; fully-qualified # 🤷🏼‍♀️ woman shrugging: medium-light skin tone +1F937 1F3FC 200D 2640 ; minimally-qualified # 🤷🏼‍♀ woman shrugging: medium-light skin tone +1F937 1F3FD 200D 2640 FE0F ; fully-qualified # 🤷🏽‍♀️ woman shrugging: medium skin tone +1F937 1F3FD 200D 2640 ; minimally-qualified # 🤷🏽‍♀ woman shrugging: medium skin tone +1F937 1F3FE 200D 2640 FE0F ; fully-qualified # 🤷🏾‍♀️ woman shrugging: medium-dark skin tone +1F937 1F3FE 200D 2640 ; minimally-qualified # 🤷🏾‍♀ woman shrugging: medium-dark skin tone +1F937 1F3FF 200D 2640 FE0F ; fully-qualified # 🤷🏿‍♀️ woman shrugging: dark skin tone +1F937 1F3FF 200D 2640 ; minimally-qualified # 🤷🏿‍♀ woman shrugging: dark skin tone + +# subgroup: person-role +1F468 200D 2695 FE0F ; fully-qualified # 👨‍⚕️ man health worker +1F468 200D 2695 ; minimally-qualified # 👨‍⚕ man health worker +1F468 1F3FB 200D 2695 FE0F ; fully-qualified # 👨🏻‍⚕️ man health worker: light skin tone +1F468 1F3FB 200D 2695 ; minimally-qualified # 👨🏻‍⚕ man health worker: light skin tone +1F468 1F3FC 200D 2695 FE0F ; fully-qualified # 👨🏼‍⚕️ man health worker: medium-light skin tone +1F468 1F3FC 200D 2695 ; minimally-qualified # 👨🏼‍⚕ man health worker: medium-light skin tone +1F468 1F3FD 200D 2695 FE0F ; fully-qualified # 👨🏽‍⚕️ man health worker: medium skin tone +1F468 1F3FD 200D 2695 ; minimally-qualified # 👨🏽‍⚕ man health worker: medium skin tone +1F468 1F3FE 200D 2695 FE0F ; fully-qualified # 👨🏾‍⚕️ man health worker: medium-dark skin tone +1F468 1F3FE 200D 2695 ; minimally-qualified # 👨🏾‍⚕ man health worker: medium-dark skin tone +1F468 1F3FF 200D 2695 FE0F ; fully-qualified # 👨🏿‍⚕️ man health worker: dark skin tone +1F468 1F3FF 200D 2695 ; minimally-qualified # 👨🏿‍⚕ man health worker: dark skin tone +1F469 200D 2695 FE0F ; fully-qualified # 👩‍⚕️ woman health worker +1F469 200D 2695 ; minimally-qualified # 👩‍⚕ woman health worker +1F469 1F3FB 200D 2695 FE0F ; fully-qualified # 👩🏻‍⚕️ woman health worker: light skin tone +1F469 1F3FB 200D 2695 ; minimally-qualified # 👩🏻‍⚕ woman health worker: light skin tone +1F469 1F3FC 200D 2695 FE0F ; fully-qualified # 👩🏼‍⚕️ woman health worker: medium-light skin tone +1F469 1F3FC 200D 2695 ; minimally-qualified # 👩🏼‍⚕ woman health worker: medium-light skin tone +1F469 1F3FD 200D 2695 FE0F ; fully-qualified # 👩🏽‍⚕️ woman health worker: medium skin tone +1F469 1F3FD 200D 2695 ; minimally-qualified # 👩🏽‍⚕ woman health worker: medium skin tone +1F469 1F3FE 200D 2695 FE0F ; fully-qualified # 👩🏾‍⚕️ woman health worker: medium-dark skin tone +1F469 1F3FE 200D 2695 ; minimally-qualified # 👩🏾‍⚕ woman health worker: medium-dark skin tone +1F469 1F3FF 200D 2695 FE0F ; fully-qualified # 👩🏿‍⚕️ woman health worker: dark skin tone +1F469 1F3FF 200D 2695 ; minimally-qualified # 👩🏿‍⚕ woman health worker: dark skin tone +1F468 200D 1F393 ; fully-qualified # 👨‍🎓 man student +1F468 1F3FB 200D 1F393 ; fully-qualified # 👨🏻‍🎓 man student: light skin tone +1F468 1F3FC 200D 1F393 ; fully-qualified # 👨🏼‍🎓 man student: medium-light skin tone +1F468 1F3FD 200D 1F393 ; fully-qualified # 👨🏽‍🎓 man student: medium skin tone +1F468 1F3FE 200D 1F393 ; fully-qualified # 👨🏾‍🎓 man student: medium-dark skin tone +1F468 1F3FF 200D 1F393 ; fully-qualified # 👨🏿‍🎓 man student: dark skin tone +1F469 200D 1F393 ; fully-qualified # 👩‍🎓 woman student +1F469 1F3FB 200D 1F393 ; fully-qualified # 👩🏻‍🎓 woman student: light skin tone +1F469 1F3FC 200D 1F393 ; fully-qualified # 👩🏼‍🎓 woman student: medium-light skin tone +1F469 1F3FD 200D 1F393 ; fully-qualified # 👩🏽‍🎓 woman student: medium skin tone +1F469 1F3FE 200D 1F393 ; fully-qualified # 👩🏾‍🎓 woman student: medium-dark skin tone +1F469 1F3FF 200D 1F393 ; fully-qualified # 👩🏿‍🎓 woman student: dark skin tone +1F468 200D 1F3EB ; fully-qualified # 👨‍🏫 man teacher +1F468 1F3FB 200D 1F3EB ; fully-qualified # 👨🏻‍🏫 man teacher: light skin tone +1F468 1F3FC 200D 1F3EB ; fully-qualified # 👨🏼‍🏫 man teacher: medium-light skin tone +1F468 1F3FD 200D 1F3EB ; fully-qualified # 👨🏽‍🏫 man teacher: medium skin tone +1F468 1F3FE 200D 1F3EB ; fully-qualified # 👨🏾‍🏫 man teacher: medium-dark skin tone +1F468 1F3FF 200D 1F3EB ; fully-qualified # 👨🏿‍🏫 man teacher: dark skin tone +1F469 200D 1F3EB ; fully-qualified # 👩‍🏫 woman teacher +1F469 1F3FB 200D 1F3EB ; fully-qualified # 👩🏻‍🏫 woman teacher: light skin tone +1F469 1F3FC 200D 1F3EB ; fully-qualified # 👩🏼‍🏫 woman teacher: medium-light skin tone +1F469 1F3FD 200D 1F3EB ; fully-qualified # 👩🏽‍🏫 woman teacher: medium skin tone +1F469 1F3FE 200D 1F3EB ; fully-qualified # 👩🏾‍🏫 woman teacher: medium-dark skin tone +1F469 1F3FF 200D 1F3EB ; fully-qualified # 👩🏿‍🏫 woman teacher: dark skin tone +1F468 200D 2696 FE0F ; fully-qualified # 👨‍⚖️ man judge +1F468 200D 2696 ; minimally-qualified # 👨‍⚖ man judge +1F468 1F3FB 200D 2696 FE0F ; fully-qualified # 👨🏻‍⚖️ man judge: light skin tone +1F468 1F3FB 200D 2696 ; minimally-qualified # 👨🏻‍⚖ man judge: light skin tone +1F468 1F3FC 200D 2696 FE0F ; fully-qualified # 👨🏼‍⚖️ man judge: medium-light skin tone +1F468 1F3FC 200D 2696 ; minimally-qualified # 👨🏼‍⚖ man judge: medium-light skin tone +1F468 1F3FD 200D 2696 FE0F ; fully-qualified # 👨🏽‍⚖️ man judge: medium skin tone +1F468 1F3FD 200D 2696 ; minimally-qualified # 👨🏽‍⚖ man judge: medium skin tone +1F468 1F3FE 200D 2696 FE0F ; fully-qualified # 👨🏾‍⚖️ man judge: medium-dark skin tone +1F468 1F3FE 200D 2696 ; minimally-qualified # 👨🏾‍⚖ man judge: medium-dark skin tone +1F468 1F3FF 200D 2696 FE0F ; fully-qualified # 👨🏿‍⚖️ man judge: dark skin tone +1F468 1F3FF 200D 2696 ; minimally-qualified # 👨🏿‍⚖ man judge: dark skin tone +1F469 200D 2696 FE0F ; fully-qualified # 👩‍⚖️ woman judge +1F469 200D 2696 ; minimally-qualified # 👩‍⚖ woman judge +1F469 1F3FB 200D 2696 FE0F ; fully-qualified # 👩🏻‍⚖️ woman judge: light skin tone +1F469 1F3FB 200D 2696 ; minimally-qualified # 👩🏻‍⚖ woman judge: light skin tone +1F469 1F3FC 200D 2696 FE0F ; fully-qualified # 👩🏼‍⚖️ woman judge: medium-light skin tone +1F469 1F3FC 200D 2696 ; minimally-qualified # 👩🏼‍⚖ woman judge: medium-light skin tone +1F469 1F3FD 200D 2696 FE0F ; fully-qualified # 👩🏽‍⚖️ woman judge: medium skin tone +1F469 1F3FD 200D 2696 ; minimally-qualified # 👩🏽‍⚖ woman judge: medium skin tone +1F469 1F3FE 200D 2696 FE0F ; fully-qualified # 👩🏾‍⚖️ woman judge: medium-dark skin tone +1F469 1F3FE 200D 2696 ; minimally-qualified # 👩🏾‍⚖ woman judge: medium-dark skin tone +1F469 1F3FF 200D 2696 FE0F ; fully-qualified # 👩🏿‍⚖️ woman judge: dark skin tone +1F469 1F3FF 200D 2696 ; minimally-qualified # 👩🏿‍⚖ woman judge: dark skin tone +1F468 200D 1F33E ; fully-qualified # 👨‍🌾 man farmer +1F468 1F3FB 200D 1F33E ; fully-qualified # 👨🏻‍🌾 man farmer: light skin tone +1F468 1F3FC 200D 1F33E ; fully-qualified # 👨🏼‍🌾 man farmer: medium-light skin tone +1F468 1F3FD 200D 1F33E ; fully-qualified # 👨🏽‍🌾 man farmer: medium skin tone +1F468 1F3FE 200D 1F33E ; fully-qualified # 👨🏾‍🌾 man farmer: medium-dark skin tone +1F468 1F3FF 200D 1F33E ; fully-qualified # 👨🏿‍🌾 man farmer: dark skin tone +1F469 200D 1F33E ; fully-qualified # 👩‍🌾 woman farmer +1F469 1F3FB 200D 1F33E ; fully-qualified # 👩🏻‍🌾 woman farmer: light skin tone +1F469 1F3FC 200D 1F33E ; fully-qualified # 👩🏼‍🌾 woman farmer: medium-light skin tone +1F469 1F3FD 200D 1F33E ; fully-qualified # 👩🏽‍🌾 woman farmer: medium skin tone +1F469 1F3FE 200D 1F33E ; fully-qualified # 👩🏾‍🌾 woman farmer: medium-dark skin tone +1F469 1F3FF 200D 1F33E ; fully-qualified # 👩🏿‍🌾 woman farmer: dark skin tone +1F468 200D 1F373 ; fully-qualified # 👨‍🍳 man cook +1F468 1F3FB 200D 1F373 ; fully-qualified # 👨🏻‍🍳 man cook: light skin tone +1F468 1F3FC 200D 1F373 ; fully-qualified # 👨🏼‍🍳 man cook: medium-light skin tone +1F468 1F3FD 200D 1F373 ; fully-qualified # 👨🏽‍🍳 man cook: medium skin tone +1F468 1F3FE 200D 1F373 ; fully-qualified # 👨🏾‍🍳 man cook: medium-dark skin tone +1F468 1F3FF 200D 1F373 ; fully-qualified # 👨🏿‍🍳 man cook: dark skin tone +1F469 200D 1F373 ; fully-qualified # 👩‍🍳 woman cook +1F469 1F3FB 200D 1F373 ; fully-qualified # 👩🏻‍🍳 woman cook: light skin tone +1F469 1F3FC 200D 1F373 ; fully-qualified # 👩🏼‍🍳 woman cook: medium-light skin tone +1F469 1F3FD 200D 1F373 ; fully-qualified # 👩🏽‍🍳 woman cook: medium skin tone +1F469 1F3FE 200D 1F373 ; fully-qualified # 👩🏾‍🍳 woman cook: medium-dark skin tone +1F469 1F3FF 200D 1F373 ; fully-qualified # 👩🏿‍🍳 woman cook: dark skin tone +1F468 200D 1F527 ; fully-qualified # 👨‍🔧 man mechanic +1F468 1F3FB 200D 1F527 ; fully-qualified # 👨🏻‍🔧 man mechanic: light skin tone +1F468 1F3FC 200D 1F527 ; fully-qualified # 👨🏼‍🔧 man mechanic: medium-light skin tone +1F468 1F3FD 200D 1F527 ; fully-qualified # 👨🏽‍🔧 man mechanic: medium skin tone +1F468 1F3FE 200D 1F527 ; fully-qualified # 👨🏾‍🔧 man mechanic: medium-dark skin tone +1F468 1F3FF 200D 1F527 ; fully-qualified # 👨🏿‍🔧 man mechanic: dark skin tone +1F469 200D 1F527 ; fully-qualified # 👩‍🔧 woman mechanic +1F469 1F3FB 200D 1F527 ; fully-qualified # 👩🏻‍🔧 woman mechanic: light skin tone +1F469 1F3FC 200D 1F527 ; fully-qualified # 👩🏼‍🔧 woman mechanic: medium-light skin tone +1F469 1F3FD 200D 1F527 ; fully-qualified # 👩🏽‍🔧 woman mechanic: medium skin tone +1F469 1F3FE 200D 1F527 ; fully-qualified # 👩🏾‍🔧 woman mechanic: medium-dark skin tone +1F469 1F3FF 200D 1F527 ; fully-qualified # 👩🏿‍🔧 woman mechanic: dark skin tone +1F468 200D 1F3ED ; fully-qualified # 👨‍🏭 man factory worker +1F468 1F3FB 200D 1F3ED ; fully-qualified # 👨🏻‍🏭 man factory worker: light skin tone +1F468 1F3FC 200D 1F3ED ; fully-qualified # 👨🏼‍🏭 man factory worker: medium-light skin tone +1F468 1F3FD 200D 1F3ED ; fully-qualified # 👨🏽‍🏭 man factory worker: medium skin tone +1F468 1F3FE 200D 1F3ED ; fully-qualified # 👨🏾‍🏭 man factory worker: medium-dark skin tone +1F468 1F3FF 200D 1F3ED ; fully-qualified # 👨🏿‍🏭 man factory worker: dark skin tone +1F469 200D 1F3ED ; fully-qualified # 👩‍🏭 woman factory worker +1F469 1F3FB 200D 1F3ED ; fully-qualified # 👩🏻‍🏭 woman factory worker: light skin tone +1F469 1F3FC 200D 1F3ED ; fully-qualified # 👩🏼‍🏭 woman factory worker: medium-light skin tone +1F469 1F3FD 200D 1F3ED ; fully-qualified # 👩🏽‍🏭 woman factory worker: medium skin tone +1F469 1F3FE 200D 1F3ED ; fully-qualified # 👩🏾‍🏭 woman factory worker: medium-dark skin tone +1F469 1F3FF 200D 1F3ED ; fully-qualified # 👩🏿‍🏭 woman factory worker: dark skin tone +1F468 200D 1F4BC ; fully-qualified # 👨‍💼 man office worker +1F468 1F3FB 200D 1F4BC ; fully-qualified # 👨🏻‍💼 man office worker: light skin tone +1F468 1F3FC 200D 1F4BC ; fully-qualified # 👨🏼‍💼 man office worker: medium-light skin tone +1F468 1F3FD 200D 1F4BC ; fully-qualified # 👨🏽‍💼 man office worker: medium skin tone +1F468 1F3FE 200D 1F4BC ; fully-qualified # 👨🏾‍💼 man office worker: medium-dark skin tone +1F468 1F3FF 200D 1F4BC ; fully-qualified # 👨🏿‍💼 man office worker: dark skin tone +1F469 200D 1F4BC ; fully-qualified # 👩‍💼 woman office worker +1F469 1F3FB 200D 1F4BC ; fully-qualified # 👩🏻‍💼 woman office worker: light skin tone +1F469 1F3FC 200D 1F4BC ; fully-qualified # 👩🏼‍💼 woman office worker: medium-light skin tone +1F469 1F3FD 200D 1F4BC ; fully-qualified # 👩🏽‍💼 woman office worker: medium skin tone +1F469 1F3FE 200D 1F4BC ; fully-qualified # 👩🏾‍💼 woman office worker: medium-dark skin tone +1F469 1F3FF 200D 1F4BC ; fully-qualified # 👩🏿‍💼 woman office worker: dark skin tone +1F468 200D 1F52C ; fully-qualified # 👨‍🔬 man scientist +1F468 1F3FB 200D 1F52C ; fully-qualified # 👨🏻‍🔬 man scientist: light skin tone +1F468 1F3FC 200D 1F52C ; fully-qualified # 👨🏼‍🔬 man scientist: medium-light skin tone +1F468 1F3FD 200D 1F52C ; fully-qualified # 👨🏽‍🔬 man scientist: medium skin tone +1F468 1F3FE 200D 1F52C ; fully-qualified # 👨🏾‍🔬 man scientist: medium-dark skin tone +1F468 1F3FF 200D 1F52C ; fully-qualified # 👨🏿‍🔬 man scientist: dark skin tone +1F469 200D 1F52C ; fully-qualified # 👩‍🔬 woman scientist +1F469 1F3FB 200D 1F52C ; fully-qualified # 👩🏻‍🔬 woman scientist: light skin tone +1F469 1F3FC 200D 1F52C ; fully-qualified # 👩🏼‍🔬 woman scientist: medium-light skin tone +1F469 1F3FD 200D 1F52C ; fully-qualified # 👩🏽‍🔬 woman scientist: medium skin tone +1F469 1F3FE 200D 1F52C ; fully-qualified # 👩🏾‍🔬 woman scientist: medium-dark skin tone +1F469 1F3FF 200D 1F52C ; fully-qualified # 👩🏿‍🔬 woman scientist: dark skin tone +1F468 200D 1F4BB ; fully-qualified # 👨‍💻 man technologist +1F468 1F3FB 200D 1F4BB ; fully-qualified # 👨🏻‍💻 man technologist: light skin tone +1F468 1F3FC 200D 1F4BB ; fully-qualified # 👨🏼‍💻 man technologist: medium-light skin tone +1F468 1F3FD 200D 1F4BB ; fully-qualified # 👨🏽‍💻 man technologist: medium skin tone +1F468 1F3FE 200D 1F4BB ; fully-qualified # 👨🏾‍💻 man technologist: medium-dark skin tone +1F468 1F3FF 200D 1F4BB ; fully-qualified # 👨🏿‍💻 man technologist: dark skin tone +1F469 200D 1F4BB ; fully-qualified # 👩‍💻 woman technologist +1F469 1F3FB 200D 1F4BB ; fully-qualified # 👩🏻‍💻 woman technologist: light skin tone +1F469 1F3FC 200D 1F4BB ; fully-qualified # 👩🏼‍💻 woman technologist: medium-light skin tone +1F469 1F3FD 200D 1F4BB ; fully-qualified # 👩🏽‍💻 woman technologist: medium skin tone +1F469 1F3FE 200D 1F4BB ; fully-qualified # 👩🏾‍💻 woman technologist: medium-dark skin tone +1F469 1F3FF 200D 1F4BB ; fully-qualified # 👩🏿‍💻 woman technologist: dark skin tone +1F468 200D 1F3A4 ; fully-qualified # 👨‍🎤 man singer +1F468 1F3FB 200D 1F3A4 ; fully-qualified # 👨🏻‍🎤 man singer: light skin tone +1F468 1F3FC 200D 1F3A4 ; fully-qualified # 👨🏼‍🎤 man singer: medium-light skin tone +1F468 1F3FD 200D 1F3A4 ; fully-qualified # 👨🏽‍🎤 man singer: medium skin tone +1F468 1F3FE 200D 1F3A4 ; fully-qualified # 👨🏾‍🎤 man singer: medium-dark skin tone +1F468 1F3FF 200D 1F3A4 ; fully-qualified # 👨🏿‍🎤 man singer: dark skin tone +1F469 200D 1F3A4 ; fully-qualified # 👩‍🎤 woman singer +1F469 1F3FB 200D 1F3A4 ; fully-qualified # 👩🏻‍🎤 woman singer: light skin tone +1F469 1F3FC 200D 1F3A4 ; fully-qualified # 👩🏼‍🎤 woman singer: medium-light skin tone +1F469 1F3FD 200D 1F3A4 ; fully-qualified # 👩🏽‍🎤 woman singer: medium skin tone +1F469 1F3FE 200D 1F3A4 ; fully-qualified # 👩🏾‍🎤 woman singer: medium-dark skin tone +1F469 1F3FF 200D 1F3A4 ; fully-qualified # 👩🏿‍🎤 woman singer: dark skin tone +1F468 200D 1F3A8 ; fully-qualified # 👨‍🎨 man artist +1F468 1F3FB 200D 1F3A8 ; fully-qualified # 👨🏻‍🎨 man artist: light skin tone +1F468 1F3FC 200D 1F3A8 ; fully-qualified # 👨🏼‍🎨 man artist: medium-light skin tone +1F468 1F3FD 200D 1F3A8 ; fully-qualified # 👨🏽‍🎨 man artist: medium skin tone +1F468 1F3FE 200D 1F3A8 ; fully-qualified # 👨🏾‍🎨 man artist: medium-dark skin tone +1F468 1F3FF 200D 1F3A8 ; fully-qualified # 👨🏿‍🎨 man artist: dark skin tone +1F469 200D 1F3A8 ; fully-qualified # 👩‍🎨 woman artist +1F469 1F3FB 200D 1F3A8 ; fully-qualified # 👩🏻‍🎨 woman artist: light skin tone +1F469 1F3FC 200D 1F3A8 ; fully-qualified # 👩🏼‍🎨 woman artist: medium-light skin tone +1F469 1F3FD 200D 1F3A8 ; fully-qualified # 👩🏽‍🎨 woman artist: medium skin tone +1F469 1F3FE 200D 1F3A8 ; fully-qualified # 👩🏾‍🎨 woman artist: medium-dark skin tone +1F469 1F3FF 200D 1F3A8 ; fully-qualified # 👩🏿‍🎨 woman artist: dark skin tone +1F468 200D 2708 FE0F ; fully-qualified # 👨‍✈️ man pilot +1F468 200D 2708 ; minimally-qualified # 👨‍✈ man pilot +1F468 1F3FB 200D 2708 FE0F ; fully-qualified # 👨🏻‍✈️ man pilot: light skin tone +1F468 1F3FB 200D 2708 ; minimally-qualified # 👨🏻‍✈ man pilot: light skin tone +1F468 1F3FC 200D 2708 FE0F ; fully-qualified # 👨🏼‍✈️ man pilot: medium-light skin tone +1F468 1F3FC 200D 2708 ; minimally-qualified # 👨🏼‍✈ man pilot: medium-light skin tone +1F468 1F3FD 200D 2708 FE0F ; fully-qualified # 👨🏽‍✈️ man pilot: medium skin tone +1F468 1F3FD 200D 2708 ; minimally-qualified # 👨🏽‍✈ man pilot: medium skin tone +1F468 1F3FE 200D 2708 FE0F ; fully-qualified # 👨🏾‍✈️ man pilot: medium-dark skin tone +1F468 1F3FE 200D 2708 ; minimally-qualified # 👨🏾‍✈ man pilot: medium-dark skin tone +1F468 1F3FF 200D 2708 FE0F ; fully-qualified # 👨🏿‍✈️ man pilot: dark skin tone +1F468 1F3FF 200D 2708 ; minimally-qualified # 👨🏿‍✈ man pilot: dark skin tone +1F469 200D 2708 FE0F ; fully-qualified # 👩‍✈️ woman pilot +1F469 200D 2708 ; minimally-qualified # 👩‍✈ woman pilot +1F469 1F3FB 200D 2708 FE0F ; fully-qualified # 👩🏻‍✈️ woman pilot: light skin tone +1F469 1F3FB 200D 2708 ; minimally-qualified # 👩🏻‍✈ woman pilot: light skin tone +1F469 1F3FC 200D 2708 FE0F ; fully-qualified # 👩🏼‍✈️ woman pilot: medium-light skin tone +1F469 1F3FC 200D 2708 ; minimally-qualified # 👩🏼‍✈ woman pilot: medium-light skin tone +1F469 1F3FD 200D 2708 FE0F ; fully-qualified # 👩🏽‍✈️ woman pilot: medium skin tone +1F469 1F3FD 200D 2708 ; minimally-qualified # 👩🏽‍✈ woman pilot: medium skin tone +1F469 1F3FE 200D 2708 FE0F ; fully-qualified # 👩🏾‍✈️ woman pilot: medium-dark skin tone +1F469 1F3FE 200D 2708 ; minimally-qualified # 👩🏾‍✈ woman pilot: medium-dark skin tone +1F469 1F3FF 200D 2708 FE0F ; fully-qualified # 👩🏿‍✈️ woman pilot: dark skin tone +1F469 1F3FF 200D 2708 ; minimally-qualified # 👩🏿‍✈ woman pilot: dark skin tone +1F468 200D 1F680 ; fully-qualified # 👨‍🚀 man astronaut +1F468 1F3FB 200D 1F680 ; fully-qualified # 👨🏻‍🚀 man astronaut: light skin tone +1F468 1F3FC 200D 1F680 ; fully-qualified # 👨🏼‍🚀 man astronaut: medium-light skin tone +1F468 1F3FD 200D 1F680 ; fully-qualified # 👨🏽‍🚀 man astronaut: medium skin tone +1F468 1F3FE 200D 1F680 ; fully-qualified # 👨🏾‍🚀 man astronaut: medium-dark skin tone +1F468 1F3FF 200D 1F680 ; fully-qualified # 👨🏿‍🚀 man astronaut: dark skin tone +1F469 200D 1F680 ; fully-qualified # 👩‍🚀 woman astronaut +1F469 1F3FB 200D 1F680 ; fully-qualified # 👩🏻‍🚀 woman astronaut: light skin tone +1F469 1F3FC 200D 1F680 ; fully-qualified # 👩🏼‍🚀 woman astronaut: medium-light skin tone +1F469 1F3FD 200D 1F680 ; fully-qualified # 👩🏽‍🚀 woman astronaut: medium skin tone +1F469 1F3FE 200D 1F680 ; fully-qualified # 👩🏾‍🚀 woman astronaut: medium-dark skin tone +1F469 1F3FF 200D 1F680 ; fully-qualified # 👩🏿‍🚀 woman astronaut: dark skin tone +1F468 200D 1F692 ; fully-qualified # 👨‍🚒 man firefighter +1F468 1F3FB 200D 1F692 ; fully-qualified # 👨🏻‍🚒 man firefighter: light skin tone +1F468 1F3FC 200D 1F692 ; fully-qualified # 👨🏼‍🚒 man firefighter: medium-light skin tone +1F468 1F3FD 200D 1F692 ; fully-qualified # 👨🏽‍🚒 man firefighter: medium skin tone +1F468 1F3FE 200D 1F692 ; fully-qualified # 👨🏾‍🚒 man firefighter: medium-dark skin tone +1F468 1F3FF 200D 1F692 ; fully-qualified # 👨🏿‍🚒 man firefighter: dark skin tone +1F469 200D 1F692 ; fully-qualified # 👩‍🚒 woman firefighter +1F469 1F3FB 200D 1F692 ; fully-qualified # 👩🏻‍🚒 woman firefighter: light skin tone +1F469 1F3FC 200D 1F692 ; fully-qualified # 👩🏼‍🚒 woman firefighter: medium-light skin tone +1F469 1F3FD 200D 1F692 ; fully-qualified # 👩🏽‍🚒 woman firefighter: medium skin tone +1F469 1F3FE 200D 1F692 ; fully-qualified # 👩🏾‍🚒 woman firefighter: medium-dark skin tone +1F469 1F3FF 200D 1F692 ; fully-qualified # 👩🏿‍🚒 woman firefighter: dark skin tone +1F46E ; fully-qualified # 👮 police officer +1F46E 1F3FB ; fully-qualified # 👮🏻 police officer: light skin tone +1F46E 1F3FC ; fully-qualified # 👮🏼 police officer: medium-light skin tone +1F46E 1F3FD ; fully-qualified # 👮🏽 police officer: medium skin tone +1F46E 1F3FE ; fully-qualified # 👮🏾 police officer: medium-dark skin tone +1F46E 1F3FF ; fully-qualified # 👮🏿 police officer: dark skin tone +1F46E 200D 2642 FE0F ; fully-qualified # 👮‍♂️ man police officer +1F46E 200D 2642 ; minimally-qualified # 👮‍♂ man police officer +1F46E 1F3FB 200D 2642 FE0F ; fully-qualified # 👮🏻‍♂️ man police officer: light skin tone +1F46E 1F3FB 200D 2642 ; minimally-qualified # 👮🏻‍♂ man police officer: light skin tone +1F46E 1F3FC 200D 2642 FE0F ; fully-qualified # 👮🏼‍♂️ man police officer: medium-light skin tone +1F46E 1F3FC 200D 2642 ; minimally-qualified # 👮🏼‍♂ man police officer: medium-light skin tone +1F46E 1F3FD 200D 2642 FE0F ; fully-qualified # 👮🏽‍♂️ man police officer: medium skin tone +1F46E 1F3FD 200D 2642 ; minimally-qualified # 👮🏽‍♂ man police officer: medium skin tone +1F46E 1F3FE 200D 2642 FE0F ; fully-qualified # 👮🏾‍♂️ man police officer: medium-dark skin tone +1F46E 1F3FE 200D 2642 ; minimally-qualified # 👮🏾‍♂ man police officer: medium-dark skin tone +1F46E 1F3FF 200D 2642 FE0F ; fully-qualified # 👮🏿‍♂️ man police officer: dark skin tone +1F46E 1F3FF 200D 2642 ; minimally-qualified # 👮🏿‍♂ man police officer: dark skin tone +1F46E 200D 2640 FE0F ; fully-qualified # 👮‍♀️ woman police officer +1F46E 200D 2640 ; minimally-qualified # 👮‍♀ woman police officer +1F46E 1F3FB 200D 2640 FE0F ; fully-qualified # 👮🏻‍♀️ woman police officer: light skin tone +1F46E 1F3FB 200D 2640 ; minimally-qualified # 👮🏻‍♀ woman police officer: light skin tone +1F46E 1F3FC 200D 2640 FE0F ; fully-qualified # 👮🏼‍♀️ woman police officer: medium-light skin tone +1F46E 1F3FC 200D 2640 ; minimally-qualified # 👮🏼‍♀ woman police officer: medium-light skin tone +1F46E 1F3FD 200D 2640 FE0F ; fully-qualified # 👮🏽‍♀️ woman police officer: medium skin tone +1F46E 1F3FD 200D 2640 ; minimally-qualified # 👮🏽‍♀ woman police officer: medium skin tone +1F46E 1F3FE 200D 2640 FE0F ; fully-qualified # 👮🏾‍♀️ woman police officer: medium-dark skin tone +1F46E 1F3FE 200D 2640 ; minimally-qualified # 👮🏾‍♀ woman police officer: medium-dark skin tone +1F46E 1F3FF 200D 2640 FE0F ; fully-qualified # 👮🏿‍♀️ woman police officer: dark skin tone +1F46E 1F3FF 200D 2640 ; minimally-qualified # 👮🏿‍♀ woman police officer: dark skin tone +1F575 FE0F ; fully-qualified # 🕵️ detective +1F575 ; unqualified # 🕵 detective +1F575 1F3FB ; fully-qualified # 🕵🏻 detective: light skin tone +1F575 1F3FC ; fully-qualified # 🕵🏼 detective: medium-light skin tone +1F575 1F3FD ; fully-qualified # 🕵🏽 detective: medium skin tone +1F575 1F3FE ; fully-qualified # 🕵🏾 detective: medium-dark skin tone +1F575 1F3FF ; fully-qualified # 🕵🏿 detective: dark skin tone +1F575 FE0F 200D 2642 FE0F ; fully-qualified # 🕵️‍♂️ man detective +1F575 200D 2642 FE0F ; unqualified # 🕵‍♂️ man detective +1F575 FE0F 200D 2642 ; unqualified # 🕵️‍♂ man detective +1F575 200D 2642 ; unqualified # 🕵‍♂ man detective +1F575 1F3FB 200D 2642 FE0F ; fully-qualified # 🕵🏻‍♂️ man detective: light skin tone +1F575 1F3FB 200D 2642 ; minimally-qualified # 🕵🏻‍♂ man detective: light skin tone +1F575 1F3FC 200D 2642 FE0F ; fully-qualified # 🕵🏼‍♂️ man detective: medium-light skin tone +1F575 1F3FC 200D 2642 ; minimally-qualified # 🕵🏼‍♂ man detective: medium-light skin tone +1F575 1F3FD 200D 2642 FE0F ; fully-qualified # 🕵🏽‍♂️ man detective: medium skin tone +1F575 1F3FD 200D 2642 ; minimally-qualified # 🕵🏽‍♂ man detective: medium skin tone +1F575 1F3FE 200D 2642 FE0F ; fully-qualified # 🕵🏾‍♂️ man detective: medium-dark skin tone +1F575 1F3FE 200D 2642 ; minimally-qualified # 🕵🏾‍♂ man detective: medium-dark skin tone +1F575 1F3FF 200D 2642 FE0F ; fully-qualified # 🕵🏿‍♂️ man detective: dark skin tone +1F575 1F3FF 200D 2642 ; minimally-qualified # 🕵🏿‍♂ man detective: dark skin tone +1F575 FE0F 200D 2640 FE0F ; fully-qualified # 🕵️‍♀️ woman detective +1F575 200D 2640 FE0F ; unqualified # 🕵‍♀️ woman detective +1F575 FE0F 200D 2640 ; unqualified # 🕵️‍♀ woman detective +1F575 200D 2640 ; unqualified # 🕵‍♀ woman detective +1F575 1F3FB 200D 2640 FE0F ; fully-qualified # 🕵🏻‍♀️ woman detective: light skin tone +1F575 1F3FB 200D 2640 ; minimally-qualified # 🕵🏻‍♀ woman detective: light skin tone +1F575 1F3FC 200D 2640 FE0F ; fully-qualified # 🕵🏼‍♀️ woman detective: medium-light skin tone +1F575 1F3FC 200D 2640 ; minimally-qualified # 🕵🏼‍♀ woman detective: medium-light skin tone +1F575 1F3FD 200D 2640 FE0F ; fully-qualified # 🕵🏽‍♀️ woman detective: medium skin tone +1F575 1F3FD 200D 2640 ; minimally-qualified # 🕵🏽‍♀ woman detective: medium skin tone +1F575 1F3FE 200D 2640 FE0F ; fully-qualified # 🕵🏾‍♀️ woman detective: medium-dark skin tone +1F575 1F3FE 200D 2640 ; minimally-qualified # 🕵🏾‍♀ woman detective: medium-dark skin tone +1F575 1F3FF 200D 2640 FE0F ; fully-qualified # 🕵🏿‍♀️ woman detective: dark skin tone +1F575 1F3FF 200D 2640 ; minimally-qualified # 🕵🏿‍♀ woman detective: dark skin tone +1F482 ; fully-qualified # 💂 guard +1F482 1F3FB ; fully-qualified # 💂🏻 guard: light skin tone +1F482 1F3FC ; fully-qualified # 💂🏼 guard: medium-light skin tone +1F482 1F3FD ; fully-qualified # 💂🏽 guard: medium skin tone +1F482 1F3FE ; fully-qualified # 💂🏾 guard: medium-dark skin tone +1F482 1F3FF ; fully-qualified # 💂🏿 guard: dark skin tone +1F482 200D 2642 FE0F ; fully-qualified # 💂‍♂️ man guard +1F482 200D 2642 ; minimally-qualified # 💂‍♂ man guard +1F482 1F3FB 200D 2642 FE0F ; fully-qualified # 💂🏻‍♂️ man guard: light skin tone +1F482 1F3FB 200D 2642 ; minimally-qualified # 💂🏻‍♂ man guard: light skin tone +1F482 1F3FC 200D 2642 FE0F ; fully-qualified # 💂🏼‍♂️ man guard: medium-light skin tone +1F482 1F3FC 200D 2642 ; minimally-qualified # 💂🏼‍♂ man guard: medium-light skin tone +1F482 1F3FD 200D 2642 FE0F ; fully-qualified # 💂🏽‍♂️ man guard: medium skin tone +1F482 1F3FD 200D 2642 ; minimally-qualified # 💂🏽‍♂ man guard: medium skin tone +1F482 1F3FE 200D 2642 FE0F ; fully-qualified # 💂🏾‍♂️ man guard: medium-dark skin tone +1F482 1F3FE 200D 2642 ; minimally-qualified # 💂🏾‍♂ man guard: medium-dark skin tone +1F482 1F3FF 200D 2642 FE0F ; fully-qualified # 💂🏿‍♂️ man guard: dark skin tone +1F482 1F3FF 200D 2642 ; minimally-qualified # 💂🏿‍♂ man guard: dark skin tone +1F482 200D 2640 FE0F ; fully-qualified # 💂‍♀️ woman guard +1F482 200D 2640 ; minimally-qualified # 💂‍♀ woman guard +1F482 1F3FB 200D 2640 FE0F ; fully-qualified # 💂🏻‍♀️ woman guard: light skin tone +1F482 1F3FB 200D 2640 ; minimally-qualified # 💂🏻‍♀ woman guard: light skin tone +1F482 1F3FC 200D 2640 FE0F ; fully-qualified # 💂🏼‍♀️ woman guard: medium-light skin tone +1F482 1F3FC 200D 2640 ; minimally-qualified # 💂🏼‍♀ woman guard: medium-light skin tone +1F482 1F3FD 200D 2640 FE0F ; fully-qualified # 💂🏽‍♀️ woman guard: medium skin tone +1F482 1F3FD 200D 2640 ; minimally-qualified # 💂🏽‍♀ woman guard: medium skin tone +1F482 1F3FE 200D 2640 FE0F ; fully-qualified # 💂🏾‍♀️ woman guard: medium-dark skin tone +1F482 1F3FE 200D 2640 ; minimally-qualified # 💂🏾‍♀ woman guard: medium-dark skin tone +1F482 1F3FF 200D 2640 FE0F ; fully-qualified # 💂🏿‍♀️ woman guard: dark skin tone +1F482 1F3FF 200D 2640 ; minimally-qualified # 💂🏿‍♀ woman guard: dark skin tone +1F477 ; fully-qualified # 👷 construction worker +1F477 1F3FB ; fully-qualified # 👷🏻 construction worker: light skin tone +1F477 1F3FC ; fully-qualified # 👷🏼 construction worker: medium-light skin tone +1F477 1F3FD ; fully-qualified # 👷🏽 construction worker: medium skin tone +1F477 1F3FE ; fully-qualified # 👷🏾 construction worker: medium-dark skin tone +1F477 1F3FF ; fully-qualified # 👷🏿 construction worker: dark skin tone +1F477 200D 2642 FE0F ; fully-qualified # 👷‍♂️ man construction worker +1F477 200D 2642 ; minimally-qualified # 👷‍♂ man construction worker +1F477 1F3FB 200D 2642 FE0F ; fully-qualified # 👷🏻‍♂️ man construction worker: light skin tone +1F477 1F3FB 200D 2642 ; minimally-qualified # 👷🏻‍♂ man construction worker: light skin tone +1F477 1F3FC 200D 2642 FE0F ; fully-qualified # 👷🏼‍♂️ man construction worker: medium-light skin tone +1F477 1F3FC 200D 2642 ; minimally-qualified # 👷🏼‍♂ man construction worker: medium-light skin tone +1F477 1F3FD 200D 2642 FE0F ; fully-qualified # 👷🏽‍♂️ man construction worker: medium skin tone +1F477 1F3FD 200D 2642 ; minimally-qualified # 👷🏽‍♂ man construction worker: medium skin tone +1F477 1F3FE 200D 2642 FE0F ; fully-qualified # 👷🏾‍♂️ man construction worker: medium-dark skin tone +1F477 1F3FE 200D 2642 ; minimally-qualified # 👷🏾‍♂ man construction worker: medium-dark skin tone +1F477 1F3FF 200D 2642 FE0F ; fully-qualified # 👷🏿‍♂️ man construction worker: dark skin tone +1F477 1F3FF 200D 2642 ; minimally-qualified # 👷🏿‍♂ man construction worker: dark skin tone +1F477 200D 2640 FE0F ; fully-qualified # 👷‍♀️ woman construction worker +1F477 200D 2640 ; minimally-qualified # 👷‍♀ woman construction worker +1F477 1F3FB 200D 2640 FE0F ; fully-qualified # 👷🏻‍♀️ woman construction worker: light skin tone +1F477 1F3FB 200D 2640 ; minimally-qualified # 👷🏻‍♀ woman construction worker: light skin tone +1F477 1F3FC 200D 2640 FE0F ; fully-qualified # 👷🏼‍♀️ woman construction worker: medium-light skin tone +1F477 1F3FC 200D 2640 ; minimally-qualified # 👷🏼‍♀ woman construction worker: medium-light skin tone +1F477 1F3FD 200D 2640 FE0F ; fully-qualified # 👷🏽‍♀️ woman construction worker: medium skin tone +1F477 1F3FD 200D 2640 ; minimally-qualified # 👷🏽‍♀ woman construction worker: medium skin tone +1F477 1F3FE 200D 2640 FE0F ; fully-qualified # 👷🏾‍♀️ woman construction worker: medium-dark skin tone +1F477 1F3FE 200D 2640 ; minimally-qualified # 👷🏾‍♀ woman construction worker: medium-dark skin tone +1F477 1F3FF 200D 2640 FE0F ; fully-qualified # 👷🏿‍♀️ woman construction worker: dark skin tone +1F477 1F3FF 200D 2640 ; minimally-qualified # 👷🏿‍♀ woman construction worker: dark skin tone +1F934 ; fully-qualified # 🤴 prince +1F934 1F3FB ; fully-qualified # 🤴🏻 prince: light skin tone +1F934 1F3FC ; fully-qualified # 🤴🏼 prince: medium-light skin tone +1F934 1F3FD ; fully-qualified # 🤴🏽 prince: medium skin tone +1F934 1F3FE ; fully-qualified # 🤴🏾 prince: medium-dark skin tone +1F934 1F3FF ; fully-qualified # 🤴🏿 prince: dark skin tone +1F478 ; fully-qualified # 👸 princess +1F478 1F3FB ; fully-qualified # 👸🏻 princess: light skin tone +1F478 1F3FC ; fully-qualified # 👸🏼 princess: medium-light skin tone +1F478 1F3FD ; fully-qualified # 👸🏽 princess: medium skin tone +1F478 1F3FE ; fully-qualified # 👸🏾 princess: medium-dark skin tone +1F478 1F3FF ; fully-qualified # 👸🏿 princess: dark skin tone +1F473 ; fully-qualified # 👳 person wearing turban +1F473 1F3FB ; fully-qualified # 👳🏻 person wearing turban: light skin tone +1F473 1F3FC ; fully-qualified # 👳🏼 person wearing turban: medium-light skin tone +1F473 1F3FD ; fully-qualified # 👳🏽 person wearing turban: medium skin tone +1F473 1F3FE ; fully-qualified # 👳🏾 person wearing turban: medium-dark skin tone +1F473 1F3FF ; fully-qualified # 👳🏿 person wearing turban: dark skin tone +1F473 200D 2642 FE0F ; fully-qualified # 👳‍♂️ man wearing turban +1F473 200D 2642 ; minimally-qualified # 👳‍♂ man wearing turban +1F473 1F3FB 200D 2642 FE0F ; fully-qualified # 👳🏻‍♂️ man wearing turban: light skin tone +1F473 1F3FB 200D 2642 ; minimally-qualified # 👳🏻‍♂ man wearing turban: light skin tone +1F473 1F3FC 200D 2642 FE0F ; fully-qualified # 👳🏼‍♂️ man wearing turban: medium-light skin tone +1F473 1F3FC 200D 2642 ; minimally-qualified # 👳🏼‍♂ man wearing turban: medium-light skin tone +1F473 1F3FD 200D 2642 FE0F ; fully-qualified # 👳🏽‍♂️ man wearing turban: medium skin tone +1F473 1F3FD 200D 2642 ; minimally-qualified # 👳🏽‍♂ man wearing turban: medium skin tone +1F473 1F3FE 200D 2642 FE0F ; fully-qualified # 👳🏾‍♂️ man wearing turban: medium-dark skin tone +1F473 1F3FE 200D 2642 ; minimally-qualified # 👳🏾‍♂ man wearing turban: medium-dark skin tone +1F473 1F3FF 200D 2642 FE0F ; fully-qualified # 👳🏿‍♂️ man wearing turban: dark skin tone +1F473 1F3FF 200D 2642 ; minimally-qualified # 👳🏿‍♂ man wearing turban: dark skin tone +1F473 200D 2640 FE0F ; fully-qualified # 👳‍♀️ woman wearing turban +1F473 200D 2640 ; minimally-qualified # 👳‍♀ woman wearing turban +1F473 1F3FB 200D 2640 FE0F ; fully-qualified # 👳🏻‍♀️ woman wearing turban: light skin tone +1F473 1F3FB 200D 2640 ; minimally-qualified # 👳🏻‍♀ woman wearing turban: light skin tone +1F473 1F3FC 200D 2640 FE0F ; fully-qualified # 👳🏼‍♀️ woman wearing turban: medium-light skin tone +1F473 1F3FC 200D 2640 ; minimally-qualified # 👳🏼‍♀ woman wearing turban: medium-light skin tone +1F473 1F3FD 200D 2640 FE0F ; fully-qualified # 👳🏽‍♀️ woman wearing turban: medium skin tone +1F473 1F3FD 200D 2640 ; minimally-qualified # 👳🏽‍♀ woman wearing turban: medium skin tone +1F473 1F3FE 200D 2640 FE0F ; fully-qualified # 👳🏾‍♀️ woman wearing turban: medium-dark skin tone +1F473 1F3FE 200D 2640 ; minimally-qualified # 👳🏾‍♀ woman wearing turban: medium-dark skin tone +1F473 1F3FF 200D 2640 FE0F ; fully-qualified # 👳🏿‍♀️ woman wearing turban: dark skin tone +1F473 1F3FF 200D 2640 ; minimally-qualified # 👳🏿‍♀ woman wearing turban: dark skin tone +1F472 ; fully-qualified # 👲 man with Chinese cap +1F472 1F3FB ; fully-qualified # 👲🏻 man with Chinese cap: light skin tone +1F472 1F3FC ; fully-qualified # 👲🏼 man with Chinese cap: medium-light skin tone +1F472 1F3FD ; fully-qualified # 👲🏽 man with Chinese cap: medium skin tone +1F472 1F3FE ; fully-qualified # 👲🏾 man with Chinese cap: medium-dark skin tone +1F472 1F3FF ; fully-qualified # 👲🏿 man with Chinese cap: dark skin tone +1F9D5 ; fully-qualified # 🧕 woman with headscarf +1F9D5 1F3FB ; fully-qualified # 🧕🏻 woman with headscarf: light skin tone +1F9D5 1F3FC ; fully-qualified # 🧕🏼 woman with headscarf: medium-light skin tone +1F9D5 1F3FD ; fully-qualified # 🧕🏽 woman with headscarf: medium skin tone +1F9D5 1F3FE ; fully-qualified # 🧕🏾 woman with headscarf: medium-dark skin tone +1F9D5 1F3FF ; fully-qualified # 🧕🏿 woman with headscarf: dark skin tone +1F935 ; fully-qualified # 🤵 man in tuxedo +1F935 1F3FB ; fully-qualified # 🤵🏻 man in tuxedo: light skin tone +1F935 1F3FC ; fully-qualified # 🤵🏼 man in tuxedo: medium-light skin tone +1F935 1F3FD ; fully-qualified # 🤵🏽 man in tuxedo: medium skin tone +1F935 1F3FE ; fully-qualified # 🤵🏾 man in tuxedo: medium-dark skin tone +1F935 1F3FF ; fully-qualified # 🤵🏿 man in tuxedo: dark skin tone +1F470 ; fully-qualified # 👰 bride with veil +1F470 1F3FB ; fully-qualified # 👰🏻 bride with veil: light skin tone +1F470 1F3FC ; fully-qualified # 👰🏼 bride with veil: medium-light skin tone +1F470 1F3FD ; fully-qualified # 👰🏽 bride with veil: medium skin tone +1F470 1F3FE ; fully-qualified # 👰🏾 bride with veil: medium-dark skin tone +1F470 1F3FF ; fully-qualified # 👰🏿 bride with veil: dark skin tone +1F930 ; fully-qualified # 🤰 pregnant woman +1F930 1F3FB ; fully-qualified # 🤰🏻 pregnant woman: light skin tone +1F930 1F3FC ; fully-qualified # 🤰🏼 pregnant woman: medium-light skin tone +1F930 1F3FD ; fully-qualified # 🤰🏽 pregnant woman: medium skin tone +1F930 1F3FE ; fully-qualified # 🤰🏾 pregnant woman: medium-dark skin tone +1F930 1F3FF ; fully-qualified # 🤰🏿 pregnant woman: dark skin tone +1F931 ; fully-qualified # 🤱 breast-feeding +1F931 1F3FB ; fully-qualified # 🤱🏻 breast-feeding: light skin tone +1F931 1F3FC ; fully-qualified # 🤱🏼 breast-feeding: medium-light skin tone +1F931 1F3FD ; fully-qualified # 🤱🏽 breast-feeding: medium skin tone +1F931 1F3FE ; fully-qualified # 🤱🏾 breast-feeding: medium-dark skin tone +1F931 1F3FF ; fully-qualified # 🤱🏿 breast-feeding: dark skin tone + +# subgroup: person-fantasy +1F47C ; fully-qualified # 👼 baby angel +1F47C 1F3FB ; fully-qualified # 👼🏻 baby angel: light skin tone +1F47C 1F3FC ; fully-qualified # 👼🏼 baby angel: medium-light skin tone +1F47C 1F3FD ; fully-qualified # 👼🏽 baby angel: medium skin tone +1F47C 1F3FE ; fully-qualified # 👼🏾 baby angel: medium-dark skin tone +1F47C 1F3FF ; fully-qualified # 👼🏿 baby angel: dark skin tone +1F385 ; fully-qualified # 🎅 Santa Claus +1F385 1F3FB ; fully-qualified # 🎅🏻 Santa Claus: light skin tone +1F385 1F3FC ; fully-qualified # 🎅🏼 Santa Claus: medium-light skin tone +1F385 1F3FD ; fully-qualified # 🎅🏽 Santa Claus: medium skin tone +1F385 1F3FE ; fully-qualified # 🎅🏾 Santa Claus: medium-dark skin tone +1F385 1F3FF ; fully-qualified # 🎅🏿 Santa Claus: dark skin tone +1F936 ; fully-qualified # 🤶 Mrs. Claus +1F936 1F3FB ; fully-qualified # 🤶🏻 Mrs. Claus: light skin tone +1F936 1F3FC ; fully-qualified # 🤶🏼 Mrs. Claus: medium-light skin tone +1F936 1F3FD ; fully-qualified # 🤶🏽 Mrs. Claus: medium skin tone +1F936 1F3FE ; fully-qualified # 🤶🏾 Mrs. Claus: medium-dark skin tone +1F936 1F3FF ; fully-qualified # 🤶🏿 Mrs. Claus: dark skin tone +1F9B8 ; fully-qualified # 🦸 superhero +1F9B8 1F3FB ; fully-qualified # 🦸🏻 superhero: light skin tone +1F9B8 1F3FC ; fully-qualified # 🦸🏼 superhero: medium-light skin tone +1F9B8 1F3FD ; fully-qualified # 🦸🏽 superhero: medium skin tone +1F9B8 1F3FE ; fully-qualified # 🦸🏾 superhero: medium-dark skin tone +1F9B8 1F3FF ; fully-qualified # 🦸🏿 superhero: dark skin tone +1F9B8 200D 2642 FE0F ; fully-qualified # 🦸‍♂️ man superhero +1F9B8 200D 2642 ; minimally-qualified # 🦸‍♂ man superhero +1F9B8 1F3FB 200D 2642 FE0F ; fully-qualified # 🦸🏻‍♂️ man superhero: light skin tone +1F9B8 1F3FB 200D 2642 ; minimally-qualified # 🦸🏻‍♂ man superhero: light skin tone +1F9B8 1F3FC 200D 2642 FE0F ; fully-qualified # 🦸🏼‍♂️ man superhero: medium-light skin tone +1F9B8 1F3FC 200D 2642 ; minimally-qualified # 🦸🏼‍♂ man superhero: medium-light skin tone +1F9B8 1F3FD 200D 2642 FE0F ; fully-qualified # 🦸🏽‍♂️ man superhero: medium skin tone +1F9B8 1F3FD 200D 2642 ; minimally-qualified # 🦸🏽‍♂ man superhero: medium skin tone +1F9B8 1F3FE 200D 2642 FE0F ; fully-qualified # 🦸🏾‍♂️ man superhero: medium-dark skin tone +1F9B8 1F3FE 200D 2642 ; minimally-qualified # 🦸🏾‍♂ man superhero: medium-dark skin tone +1F9B8 1F3FF 200D 2642 FE0F ; fully-qualified # 🦸🏿‍♂️ man superhero: dark skin tone +1F9B8 1F3FF 200D 2642 ; minimally-qualified # 🦸🏿‍♂ man superhero: dark skin tone +1F9B8 200D 2640 FE0F ; fully-qualified # 🦸‍♀️ woman superhero +1F9B8 200D 2640 ; minimally-qualified # 🦸‍♀ woman superhero +1F9B8 1F3FB 200D 2640 FE0F ; fully-qualified # 🦸🏻‍♀️ woman superhero: light skin tone +1F9B8 1F3FB 200D 2640 ; minimally-qualified # 🦸🏻‍♀ woman superhero: light skin tone +1F9B8 1F3FC 200D 2640 FE0F ; fully-qualified # 🦸🏼‍♀️ woman superhero: medium-light skin tone +1F9B8 1F3FC 200D 2640 ; minimally-qualified # 🦸🏼‍♀ woman superhero: medium-light skin tone +1F9B8 1F3FD 200D 2640 FE0F ; fully-qualified # 🦸🏽‍♀️ woman superhero: medium skin tone +1F9B8 1F3FD 200D 2640 ; minimally-qualified # 🦸🏽‍♀ woman superhero: medium skin tone +1F9B8 1F3FE 200D 2640 FE0F ; fully-qualified # 🦸🏾‍♀️ woman superhero: medium-dark skin tone +1F9B8 1F3FE 200D 2640 ; minimally-qualified # 🦸🏾‍♀ woman superhero: medium-dark skin tone +1F9B8 1F3FF 200D 2640 FE0F ; fully-qualified # 🦸🏿‍♀️ woman superhero: dark skin tone +1F9B8 1F3FF 200D 2640 ; minimally-qualified # 🦸🏿‍♀ woman superhero: dark skin tone +1F9B9 ; fully-qualified # 🦹 supervillain +1F9B9 1F3FB ; fully-qualified # 🦹🏻 supervillain: light skin tone +1F9B9 1F3FC ; fully-qualified # 🦹🏼 supervillain: medium-light skin tone +1F9B9 1F3FD ; fully-qualified # 🦹🏽 supervillain: medium skin tone +1F9B9 1F3FE ; fully-qualified # 🦹🏾 supervillain: medium-dark skin tone +1F9B9 1F3FF ; fully-qualified # 🦹🏿 supervillain: dark skin tone +1F9B9 200D 2642 FE0F ; fully-qualified # 🦹‍♂️ man supervillain +1F9B9 200D 2642 ; minimally-qualified # 🦹‍♂ man supervillain +1F9B9 1F3FB 200D 2642 FE0F ; fully-qualified # 🦹🏻‍♂️ man supervillain: light skin tone +1F9B9 1F3FB 200D 2642 ; minimally-qualified # 🦹🏻‍♂ man supervillain: light skin tone +1F9B9 1F3FC 200D 2642 FE0F ; fully-qualified # 🦹🏼‍♂️ man supervillain: medium-light skin tone +1F9B9 1F3FC 200D 2642 ; minimally-qualified # 🦹🏼‍♂ man supervillain: medium-light skin tone +1F9B9 1F3FD 200D 2642 FE0F ; fully-qualified # 🦹🏽‍♂️ man supervillain: medium skin tone +1F9B9 1F3FD 200D 2642 ; minimally-qualified # 🦹🏽‍♂ man supervillain: medium skin tone +1F9B9 1F3FE 200D 2642 FE0F ; fully-qualified # 🦹🏾‍♂️ man supervillain: medium-dark skin tone +1F9B9 1F3FE 200D 2642 ; minimally-qualified # 🦹🏾‍♂ man supervillain: medium-dark skin tone +1F9B9 1F3FF 200D 2642 FE0F ; fully-qualified # 🦹🏿‍♂️ man supervillain: dark skin tone +1F9B9 1F3FF 200D 2642 ; minimally-qualified # 🦹🏿‍♂ man supervillain: dark skin tone +1F9B9 200D 2640 FE0F ; fully-qualified # 🦹‍♀️ woman supervillain +1F9B9 200D 2640 ; minimally-qualified # 🦹‍♀ woman supervillain +1F9B9 1F3FB 200D 2640 FE0F ; fully-qualified # 🦹🏻‍♀️ woman supervillain: light skin tone +1F9B9 1F3FB 200D 2640 ; minimally-qualified # 🦹🏻‍♀ woman supervillain: light skin tone +1F9B9 1F3FC 200D 2640 FE0F ; fully-qualified # 🦹🏼‍♀️ woman supervillain: medium-light skin tone +1F9B9 1F3FC 200D 2640 ; minimally-qualified # 🦹🏼‍♀ woman supervillain: medium-light skin tone +1F9B9 1F3FD 200D 2640 FE0F ; fully-qualified # 🦹🏽‍♀️ woman supervillain: medium skin tone +1F9B9 1F3FD 200D 2640 ; minimally-qualified # 🦹🏽‍♀ woman supervillain: medium skin tone +1F9B9 1F3FE 200D 2640 FE0F ; fully-qualified # 🦹🏾‍♀️ woman supervillain: medium-dark skin tone +1F9B9 1F3FE 200D 2640 ; minimally-qualified # 🦹🏾‍♀ woman supervillain: medium-dark skin tone +1F9B9 1F3FF 200D 2640 FE0F ; fully-qualified # 🦹🏿‍♀️ woman supervillain: dark skin tone +1F9B9 1F3FF 200D 2640 ; minimally-qualified # 🦹🏿‍♀ woman supervillain: dark skin tone +1F9D9 ; fully-qualified # 🧙 mage +1F9D9 1F3FB ; fully-qualified # 🧙🏻 mage: light skin tone +1F9D9 1F3FC ; fully-qualified # 🧙🏼 mage: medium-light skin tone +1F9D9 1F3FD ; fully-qualified # 🧙🏽 mage: medium skin tone +1F9D9 1F3FE ; fully-qualified # 🧙🏾 mage: medium-dark skin tone +1F9D9 1F3FF ; fully-qualified # 🧙🏿 mage: dark skin tone +1F9D9 200D 2642 FE0F ; fully-qualified # 🧙‍♂️ man mage +1F9D9 200D 2642 ; minimally-qualified # 🧙‍♂ man mage +1F9D9 1F3FB 200D 2642 FE0F ; fully-qualified # 🧙🏻‍♂️ man mage: light skin tone +1F9D9 1F3FB 200D 2642 ; minimally-qualified # 🧙🏻‍♂ man mage: light skin tone +1F9D9 1F3FC 200D 2642 FE0F ; fully-qualified # 🧙🏼‍♂️ man mage: medium-light skin tone +1F9D9 1F3FC 200D 2642 ; minimally-qualified # 🧙🏼‍♂ man mage: medium-light skin tone +1F9D9 1F3FD 200D 2642 FE0F ; fully-qualified # 🧙🏽‍♂️ man mage: medium skin tone +1F9D9 1F3FD 200D 2642 ; minimally-qualified # 🧙🏽‍♂ man mage: medium skin tone +1F9D9 1F3FE 200D 2642 FE0F ; fully-qualified # 🧙🏾‍♂️ man mage: medium-dark skin tone +1F9D9 1F3FE 200D 2642 ; minimally-qualified # 🧙🏾‍♂ man mage: medium-dark skin tone +1F9D9 1F3FF 200D 2642 FE0F ; fully-qualified # 🧙🏿‍♂️ man mage: dark skin tone +1F9D9 1F3FF 200D 2642 ; minimally-qualified # 🧙🏿‍♂ man mage: dark skin tone +1F9D9 200D 2640 FE0F ; fully-qualified # 🧙‍♀️ woman mage +1F9D9 200D 2640 ; minimally-qualified # 🧙‍♀ woman mage +1F9D9 1F3FB 200D 2640 FE0F ; fully-qualified # 🧙🏻‍♀️ woman mage: light skin tone +1F9D9 1F3FB 200D 2640 ; minimally-qualified # 🧙🏻‍♀ woman mage: light skin tone +1F9D9 1F3FC 200D 2640 FE0F ; fully-qualified # 🧙🏼‍♀️ woman mage: medium-light skin tone +1F9D9 1F3FC 200D 2640 ; minimally-qualified # 🧙🏼‍♀ woman mage: medium-light skin tone +1F9D9 1F3FD 200D 2640 FE0F ; fully-qualified # 🧙🏽‍♀️ woman mage: medium skin tone +1F9D9 1F3FD 200D 2640 ; minimally-qualified # 🧙🏽‍♀ woman mage: medium skin tone +1F9D9 1F3FE 200D 2640 FE0F ; fully-qualified # 🧙🏾‍♀️ woman mage: medium-dark skin tone +1F9D9 1F3FE 200D 2640 ; minimally-qualified # 🧙🏾‍♀ woman mage: medium-dark skin tone +1F9D9 1F3FF 200D 2640 FE0F ; fully-qualified # 🧙🏿‍♀️ woman mage: dark skin tone +1F9D9 1F3FF 200D 2640 ; minimally-qualified # 🧙🏿‍♀ woman mage: dark skin tone +1F9DA ; fully-qualified # 🧚 fairy +1F9DA 1F3FB ; fully-qualified # 🧚🏻 fairy: light skin tone +1F9DA 1F3FC ; fully-qualified # 🧚🏼 fairy: medium-light skin tone +1F9DA 1F3FD ; fully-qualified # 🧚🏽 fairy: medium skin tone +1F9DA 1F3FE ; fully-qualified # 🧚🏾 fairy: medium-dark skin tone +1F9DA 1F3FF ; fully-qualified # 🧚🏿 fairy: dark skin tone +1F9DA 200D 2642 FE0F ; fully-qualified # 🧚‍♂️ man fairy +1F9DA 200D 2642 ; minimally-qualified # 🧚‍♂ man fairy +1F9DA 1F3FB 200D 2642 FE0F ; fully-qualified # 🧚🏻‍♂️ man fairy: light skin tone +1F9DA 1F3FB 200D 2642 ; minimally-qualified # 🧚🏻‍♂ man fairy: light skin tone +1F9DA 1F3FC 200D 2642 FE0F ; fully-qualified # 🧚🏼‍♂️ man fairy: medium-light skin tone +1F9DA 1F3FC 200D 2642 ; minimally-qualified # 🧚🏼‍♂ man fairy: medium-light skin tone +1F9DA 1F3FD 200D 2642 FE0F ; fully-qualified # 🧚🏽‍♂️ man fairy: medium skin tone +1F9DA 1F3FD 200D 2642 ; minimally-qualified # 🧚🏽‍♂ man fairy: medium skin tone +1F9DA 1F3FE 200D 2642 FE0F ; fully-qualified # 🧚🏾‍♂️ man fairy: medium-dark skin tone +1F9DA 1F3FE 200D 2642 ; minimally-qualified # 🧚🏾‍♂ man fairy: medium-dark skin tone +1F9DA 1F3FF 200D 2642 FE0F ; fully-qualified # 🧚🏿‍♂️ man fairy: dark skin tone +1F9DA 1F3FF 200D 2642 ; minimally-qualified # 🧚🏿‍♂ man fairy: dark skin tone +1F9DA 200D 2640 FE0F ; fully-qualified # 🧚‍♀️ woman fairy +1F9DA 200D 2640 ; minimally-qualified # 🧚‍♀ woman fairy +1F9DA 1F3FB 200D 2640 FE0F ; fully-qualified # 🧚🏻‍♀️ woman fairy: light skin tone +1F9DA 1F3FB 200D 2640 ; minimally-qualified # 🧚🏻‍♀ woman fairy: light skin tone +1F9DA 1F3FC 200D 2640 FE0F ; fully-qualified # 🧚🏼‍♀️ woman fairy: medium-light skin tone +1F9DA 1F3FC 200D 2640 ; minimally-qualified # 🧚🏼‍♀ woman fairy: medium-light skin tone +1F9DA 1F3FD 200D 2640 FE0F ; fully-qualified # 🧚🏽‍♀️ woman fairy: medium skin tone +1F9DA 1F3FD 200D 2640 ; minimally-qualified # 🧚🏽‍♀ woman fairy: medium skin tone +1F9DA 1F3FE 200D 2640 FE0F ; fully-qualified # 🧚🏾‍♀️ woman fairy: medium-dark skin tone +1F9DA 1F3FE 200D 2640 ; minimally-qualified # 🧚🏾‍♀ woman fairy: medium-dark skin tone +1F9DA 1F3FF 200D 2640 FE0F ; fully-qualified # 🧚🏿‍♀️ woman fairy: dark skin tone +1F9DA 1F3FF 200D 2640 ; minimally-qualified # 🧚🏿‍♀ woman fairy: dark skin tone +1F9DB ; fully-qualified # 🧛 vampire +1F9DB 1F3FB ; fully-qualified # 🧛🏻 vampire: light skin tone +1F9DB 1F3FC ; fully-qualified # 🧛🏼 vampire: medium-light skin tone +1F9DB 1F3FD ; fully-qualified # 🧛🏽 vampire: medium skin tone +1F9DB 1F3FE ; fully-qualified # 🧛🏾 vampire: medium-dark skin tone +1F9DB 1F3FF ; fully-qualified # 🧛🏿 vampire: dark skin tone +1F9DB 200D 2642 FE0F ; fully-qualified # 🧛‍♂️ man vampire +1F9DB 200D 2642 ; minimally-qualified # 🧛‍♂ man vampire +1F9DB 1F3FB 200D 2642 FE0F ; fully-qualified # 🧛🏻‍♂️ man vampire: light skin tone +1F9DB 1F3FB 200D 2642 ; minimally-qualified # 🧛🏻‍♂ man vampire: light skin tone +1F9DB 1F3FC 200D 2642 FE0F ; fully-qualified # 🧛🏼‍♂️ man vampire: medium-light skin tone +1F9DB 1F3FC 200D 2642 ; minimally-qualified # 🧛🏼‍♂ man vampire: medium-light skin tone +1F9DB 1F3FD 200D 2642 FE0F ; fully-qualified # 🧛🏽‍♂️ man vampire: medium skin tone +1F9DB 1F3FD 200D 2642 ; minimally-qualified # 🧛🏽‍♂ man vampire: medium skin tone +1F9DB 1F3FE 200D 2642 FE0F ; fully-qualified # 🧛🏾‍♂️ man vampire: medium-dark skin tone +1F9DB 1F3FE 200D 2642 ; minimally-qualified # 🧛🏾‍♂ man vampire: medium-dark skin tone +1F9DB 1F3FF 200D 2642 FE0F ; fully-qualified # 🧛🏿‍♂️ man vampire: dark skin tone +1F9DB 1F3FF 200D 2642 ; minimally-qualified # 🧛🏿‍♂ man vampire: dark skin tone +1F9DB 200D 2640 FE0F ; fully-qualified # 🧛‍♀️ woman vampire +1F9DB 200D 2640 ; minimally-qualified # 🧛‍♀ woman vampire +1F9DB 1F3FB 200D 2640 FE0F ; fully-qualified # 🧛🏻‍♀️ woman vampire: light skin tone +1F9DB 1F3FB 200D 2640 ; minimally-qualified # 🧛🏻‍♀ woman vampire: light skin tone +1F9DB 1F3FC 200D 2640 FE0F ; fully-qualified # 🧛🏼‍♀️ woman vampire: medium-light skin tone +1F9DB 1F3FC 200D 2640 ; minimally-qualified # 🧛🏼‍♀ woman vampire: medium-light skin tone +1F9DB 1F3FD 200D 2640 FE0F ; fully-qualified # 🧛🏽‍♀️ woman vampire: medium skin tone +1F9DB 1F3FD 200D 2640 ; minimally-qualified # 🧛🏽‍♀ woman vampire: medium skin tone +1F9DB 1F3FE 200D 2640 FE0F ; fully-qualified # 🧛🏾‍♀️ woman vampire: medium-dark skin tone +1F9DB 1F3FE 200D 2640 ; minimally-qualified # 🧛🏾‍♀ woman vampire: medium-dark skin tone +1F9DB 1F3FF 200D 2640 FE0F ; fully-qualified # 🧛🏿‍♀️ woman vampire: dark skin tone +1F9DB 1F3FF 200D 2640 ; minimally-qualified # 🧛🏿‍♀ woman vampire: dark skin tone +1F9DC ; fully-qualified # 🧜 merperson +1F9DC 1F3FB ; fully-qualified # 🧜🏻 merperson: light skin tone +1F9DC 1F3FC ; fully-qualified # 🧜🏼 merperson: medium-light skin tone +1F9DC 1F3FD ; fully-qualified # 🧜🏽 merperson: medium skin tone +1F9DC 1F3FE ; fully-qualified # 🧜🏾 merperson: medium-dark skin tone +1F9DC 1F3FF ; fully-qualified # 🧜🏿 merperson: dark skin tone +1F9DC 200D 2642 FE0F ; fully-qualified # 🧜‍♂️ merman +1F9DC 200D 2642 ; minimally-qualified # 🧜‍♂ merman +1F9DC 1F3FB 200D 2642 FE0F ; fully-qualified # 🧜🏻‍♂️ merman: light skin tone +1F9DC 1F3FB 200D 2642 ; minimally-qualified # 🧜🏻‍♂ merman: light skin tone +1F9DC 1F3FC 200D 2642 FE0F ; fully-qualified # 🧜🏼‍♂️ merman: medium-light skin tone +1F9DC 1F3FC 200D 2642 ; minimally-qualified # 🧜🏼‍♂ merman: medium-light skin tone +1F9DC 1F3FD 200D 2642 FE0F ; fully-qualified # 🧜🏽‍♂️ merman: medium skin tone +1F9DC 1F3FD 200D 2642 ; minimally-qualified # 🧜🏽‍♂ merman: medium skin tone +1F9DC 1F3FE 200D 2642 FE0F ; fully-qualified # 🧜🏾‍♂️ merman: medium-dark skin tone +1F9DC 1F3FE 200D 2642 ; minimally-qualified # 🧜🏾‍♂ merman: medium-dark skin tone +1F9DC 1F3FF 200D 2642 FE0F ; fully-qualified # 🧜🏿‍♂️ merman: dark skin tone +1F9DC 1F3FF 200D 2642 ; minimally-qualified # 🧜🏿‍♂ merman: dark skin tone +1F9DC 200D 2640 FE0F ; fully-qualified # 🧜‍♀️ mermaid +1F9DC 200D 2640 ; minimally-qualified # 🧜‍♀ mermaid +1F9DC 1F3FB 200D 2640 FE0F ; fully-qualified # 🧜🏻‍♀️ mermaid: light skin tone +1F9DC 1F3FB 200D 2640 ; minimally-qualified # 🧜🏻‍♀ mermaid: light skin tone +1F9DC 1F3FC 200D 2640 FE0F ; fully-qualified # 🧜🏼‍♀️ mermaid: medium-light skin tone +1F9DC 1F3FC 200D 2640 ; minimally-qualified # 🧜🏼‍♀ mermaid: medium-light skin tone +1F9DC 1F3FD 200D 2640 FE0F ; fully-qualified # 🧜🏽‍♀️ mermaid: medium skin tone +1F9DC 1F3FD 200D 2640 ; minimally-qualified # 🧜🏽‍♀ mermaid: medium skin tone +1F9DC 1F3FE 200D 2640 FE0F ; fully-qualified # 🧜🏾‍♀️ mermaid: medium-dark skin tone +1F9DC 1F3FE 200D 2640 ; minimally-qualified # 🧜🏾‍♀ mermaid: medium-dark skin tone +1F9DC 1F3FF 200D 2640 FE0F ; fully-qualified # 🧜🏿‍♀️ mermaid: dark skin tone +1F9DC 1F3FF 200D 2640 ; minimally-qualified # 🧜🏿‍♀ mermaid: dark skin tone +1F9DD ; fully-qualified # 🧝 elf +1F9DD 1F3FB ; fully-qualified # 🧝🏻 elf: light skin tone +1F9DD 1F3FC ; fully-qualified # 🧝🏼 elf: medium-light skin tone +1F9DD 1F3FD ; fully-qualified # 🧝🏽 elf: medium skin tone +1F9DD 1F3FE ; fully-qualified # 🧝🏾 elf: medium-dark skin tone +1F9DD 1F3FF ; fully-qualified # 🧝🏿 elf: dark skin tone +1F9DD 200D 2642 FE0F ; fully-qualified # 🧝‍♂️ man elf +1F9DD 200D 2642 ; minimally-qualified # 🧝‍♂ man elf +1F9DD 1F3FB 200D 2642 FE0F ; fully-qualified # 🧝🏻‍♂️ man elf: light skin tone +1F9DD 1F3FB 200D 2642 ; minimally-qualified # 🧝🏻‍♂ man elf: light skin tone +1F9DD 1F3FC 200D 2642 FE0F ; fully-qualified # 🧝🏼‍♂️ man elf: medium-light skin tone +1F9DD 1F3FC 200D 2642 ; minimally-qualified # 🧝🏼‍♂ man elf: medium-light skin tone +1F9DD 1F3FD 200D 2642 FE0F ; fully-qualified # 🧝🏽‍♂️ man elf: medium skin tone +1F9DD 1F3FD 200D 2642 ; minimally-qualified # 🧝🏽‍♂ man elf: medium skin tone +1F9DD 1F3FE 200D 2642 FE0F ; fully-qualified # 🧝🏾‍♂️ man elf: medium-dark skin tone +1F9DD 1F3FE 200D 2642 ; minimally-qualified # 🧝🏾‍♂ man elf: medium-dark skin tone +1F9DD 1F3FF 200D 2642 FE0F ; fully-qualified # 🧝🏿‍♂️ man elf: dark skin tone +1F9DD 1F3FF 200D 2642 ; minimally-qualified # 🧝🏿‍♂ man elf: dark skin tone +1F9DD 200D 2640 FE0F ; fully-qualified # 🧝‍♀️ woman elf +1F9DD 200D 2640 ; minimally-qualified # 🧝‍♀ woman elf +1F9DD 1F3FB 200D 2640 FE0F ; fully-qualified # 🧝🏻‍♀️ woman elf: light skin tone +1F9DD 1F3FB 200D 2640 ; minimally-qualified # 🧝🏻‍♀ woman elf: light skin tone +1F9DD 1F3FC 200D 2640 FE0F ; fully-qualified # 🧝🏼‍♀️ woman elf: medium-light skin tone +1F9DD 1F3FC 200D 2640 ; minimally-qualified # 🧝🏼‍♀ woman elf: medium-light skin tone +1F9DD 1F3FD 200D 2640 FE0F ; fully-qualified # 🧝🏽‍♀️ woman elf: medium skin tone +1F9DD 1F3FD 200D 2640 ; minimally-qualified # 🧝🏽‍♀ woman elf: medium skin tone +1F9DD 1F3FE 200D 2640 FE0F ; fully-qualified # 🧝🏾‍♀️ woman elf: medium-dark skin tone +1F9DD 1F3FE 200D 2640 ; minimally-qualified # 🧝🏾‍♀ woman elf: medium-dark skin tone +1F9DD 1F3FF 200D 2640 FE0F ; fully-qualified # 🧝🏿‍♀️ woman elf: dark skin tone +1F9DD 1F3FF 200D 2640 ; minimally-qualified # 🧝🏿‍♀ woman elf: dark skin tone +1F9DE ; fully-qualified # 🧞 genie +1F9DE 200D 2642 FE0F ; fully-qualified # 🧞‍♂️ man genie +1F9DE 200D 2642 ; minimally-qualified # 🧞‍♂ man genie +1F9DE 200D 2640 FE0F ; fully-qualified # 🧞‍♀️ woman genie +1F9DE 200D 2640 ; minimally-qualified # 🧞‍♀ woman genie +1F9DF ; fully-qualified # 🧟 zombie +1F9DF 200D 2642 FE0F ; fully-qualified # 🧟‍♂️ man zombie +1F9DF 200D 2642 ; minimally-qualified # 🧟‍♂ man zombie +1F9DF 200D 2640 FE0F ; fully-qualified # 🧟‍♀️ woman zombie +1F9DF 200D 2640 ; minimally-qualified # 🧟‍♀ woman zombie + +# subgroup: person-activity +1F486 ; fully-qualified # 💆 person getting massage +1F486 1F3FB ; fully-qualified # 💆🏻 person getting massage: light skin tone +1F486 1F3FC ; fully-qualified # 💆🏼 person getting massage: medium-light skin tone +1F486 1F3FD ; fully-qualified # 💆🏽 person getting massage: medium skin tone +1F486 1F3FE ; fully-qualified # 💆🏾 person getting massage: medium-dark skin tone +1F486 1F3FF ; fully-qualified # 💆🏿 person getting massage: dark skin tone +1F486 200D 2642 FE0F ; fully-qualified # 💆‍♂️ man getting massage +1F486 200D 2642 ; minimally-qualified # 💆‍♂ man getting massage +1F486 1F3FB 200D 2642 FE0F ; fully-qualified # 💆🏻‍♂️ man getting massage: light skin tone +1F486 1F3FB 200D 2642 ; minimally-qualified # 💆🏻‍♂ man getting massage: light skin tone +1F486 1F3FC 200D 2642 FE0F ; fully-qualified # 💆🏼‍♂️ man getting massage: medium-light skin tone +1F486 1F3FC 200D 2642 ; minimally-qualified # 💆🏼‍♂ man getting massage: medium-light skin tone +1F486 1F3FD 200D 2642 FE0F ; fully-qualified # 💆🏽‍♂️ man getting massage: medium skin tone +1F486 1F3FD 200D 2642 ; minimally-qualified # 💆🏽‍♂ man getting massage: medium skin tone +1F486 1F3FE 200D 2642 FE0F ; fully-qualified # 💆🏾‍♂️ man getting massage: medium-dark skin tone +1F486 1F3FE 200D 2642 ; minimally-qualified # 💆🏾‍♂ man getting massage: medium-dark skin tone +1F486 1F3FF 200D 2642 FE0F ; fully-qualified # 💆🏿‍♂️ man getting massage: dark skin tone +1F486 1F3FF 200D 2642 ; minimally-qualified # 💆🏿‍♂ man getting massage: dark skin tone +1F486 200D 2640 FE0F ; fully-qualified # 💆‍♀️ woman getting massage +1F486 200D 2640 ; minimally-qualified # 💆‍♀ woman getting massage +1F486 1F3FB 200D 2640 FE0F ; fully-qualified # 💆🏻‍♀️ woman getting massage: light skin tone +1F486 1F3FB 200D 2640 ; minimally-qualified # 💆🏻‍♀ woman getting massage: light skin tone +1F486 1F3FC 200D 2640 FE0F ; fully-qualified # 💆🏼‍♀️ woman getting massage: medium-light skin tone +1F486 1F3FC 200D 2640 ; minimally-qualified # 💆🏼‍♀ woman getting massage: medium-light skin tone +1F486 1F3FD 200D 2640 FE0F ; fully-qualified # 💆🏽‍♀️ woman getting massage: medium skin tone +1F486 1F3FD 200D 2640 ; minimally-qualified # 💆🏽‍♀ woman getting massage: medium skin tone +1F486 1F3FE 200D 2640 FE0F ; fully-qualified # 💆🏾‍♀️ woman getting massage: medium-dark skin tone +1F486 1F3FE 200D 2640 ; minimally-qualified # 💆🏾‍♀ woman getting massage: medium-dark skin tone +1F486 1F3FF 200D 2640 FE0F ; fully-qualified # 💆🏿‍♀️ woman getting massage: dark skin tone +1F486 1F3FF 200D 2640 ; minimally-qualified # 💆🏿‍♀ woman getting massage: dark skin tone +1F487 ; fully-qualified # 💇 person getting haircut +1F487 1F3FB ; fully-qualified # 💇🏻 person getting haircut: light skin tone +1F487 1F3FC ; fully-qualified # 💇🏼 person getting haircut: medium-light skin tone +1F487 1F3FD ; fully-qualified # 💇🏽 person getting haircut: medium skin tone +1F487 1F3FE ; fully-qualified # 💇🏾 person getting haircut: medium-dark skin tone +1F487 1F3FF ; fully-qualified # 💇🏿 person getting haircut: dark skin tone +1F487 200D 2642 FE0F ; fully-qualified # 💇‍♂️ man getting haircut +1F487 200D 2642 ; minimally-qualified # 💇‍♂ man getting haircut +1F487 1F3FB 200D 2642 FE0F ; fully-qualified # 💇🏻‍♂️ man getting haircut: light skin tone +1F487 1F3FB 200D 2642 ; minimally-qualified # 💇🏻‍♂ man getting haircut: light skin tone +1F487 1F3FC 200D 2642 FE0F ; fully-qualified # 💇🏼‍♂️ man getting haircut: medium-light skin tone +1F487 1F3FC 200D 2642 ; minimally-qualified # 💇🏼‍♂ man getting haircut: medium-light skin tone +1F487 1F3FD 200D 2642 FE0F ; fully-qualified # 💇🏽‍♂️ man getting haircut: medium skin tone +1F487 1F3FD 200D 2642 ; minimally-qualified # 💇🏽‍♂ man getting haircut: medium skin tone +1F487 1F3FE 200D 2642 FE0F ; fully-qualified # 💇🏾‍♂️ man getting haircut: medium-dark skin tone +1F487 1F3FE 200D 2642 ; minimally-qualified # 💇🏾‍♂ man getting haircut: medium-dark skin tone +1F487 1F3FF 200D 2642 FE0F ; fully-qualified # 💇🏿‍♂️ man getting haircut: dark skin tone +1F487 1F3FF 200D 2642 ; minimally-qualified # 💇🏿‍♂ man getting haircut: dark skin tone +1F487 200D 2640 FE0F ; fully-qualified # 💇‍♀️ woman getting haircut +1F487 200D 2640 ; minimally-qualified # 💇‍♀ woman getting haircut +1F487 1F3FB 200D 2640 FE0F ; fully-qualified # 💇🏻‍♀️ woman getting haircut: light skin tone +1F487 1F3FB 200D 2640 ; minimally-qualified # 💇🏻‍♀ woman getting haircut: light skin tone +1F487 1F3FC 200D 2640 FE0F ; fully-qualified # 💇🏼‍♀️ woman getting haircut: medium-light skin tone +1F487 1F3FC 200D 2640 ; minimally-qualified # 💇🏼‍♀ woman getting haircut: medium-light skin tone +1F487 1F3FD 200D 2640 FE0F ; fully-qualified # 💇🏽‍♀️ woman getting haircut: medium skin tone +1F487 1F3FD 200D 2640 ; minimally-qualified # 💇🏽‍♀ woman getting haircut: medium skin tone +1F487 1F3FE 200D 2640 FE0F ; fully-qualified # 💇🏾‍♀️ woman getting haircut: medium-dark skin tone +1F487 1F3FE 200D 2640 ; minimally-qualified # 💇🏾‍♀ woman getting haircut: medium-dark skin tone +1F487 1F3FF 200D 2640 FE0F ; fully-qualified # 💇🏿‍♀️ woman getting haircut: dark skin tone +1F487 1F3FF 200D 2640 ; minimally-qualified # 💇🏿‍♀ woman getting haircut: dark skin tone +1F6B6 ; fully-qualified # 🚶 person walking +1F6B6 1F3FB ; fully-qualified # 🚶🏻 person walking: light skin tone +1F6B6 1F3FC ; fully-qualified # 🚶🏼 person walking: medium-light skin tone +1F6B6 1F3FD ; fully-qualified # 🚶🏽 person walking: medium skin tone +1F6B6 1F3FE ; fully-qualified # 🚶🏾 person walking: medium-dark skin tone +1F6B6 1F3FF ; fully-qualified # 🚶🏿 person walking: dark skin tone +1F6B6 200D 2642 FE0F ; fully-qualified # 🚶‍♂️ man walking +1F6B6 200D 2642 ; minimally-qualified # 🚶‍♂ man walking +1F6B6 1F3FB 200D 2642 FE0F ; fully-qualified # 🚶🏻‍♂️ man walking: light skin tone +1F6B6 1F3FB 200D 2642 ; minimally-qualified # 🚶🏻‍♂ man walking: light skin tone +1F6B6 1F3FC 200D 2642 FE0F ; fully-qualified # 🚶🏼‍♂️ man walking: medium-light skin tone +1F6B6 1F3FC 200D 2642 ; minimally-qualified # 🚶🏼‍♂ man walking: medium-light skin tone +1F6B6 1F3FD 200D 2642 FE0F ; fully-qualified # 🚶🏽‍♂️ man walking: medium skin tone +1F6B6 1F3FD 200D 2642 ; minimally-qualified # 🚶🏽‍♂ man walking: medium skin tone +1F6B6 1F3FE 200D 2642 FE0F ; fully-qualified # 🚶🏾‍♂️ man walking: medium-dark skin tone +1F6B6 1F3FE 200D 2642 ; minimally-qualified # 🚶🏾‍♂ man walking: medium-dark skin tone +1F6B6 1F3FF 200D 2642 FE0F ; fully-qualified # 🚶🏿‍♂️ man walking: dark skin tone +1F6B6 1F3FF 200D 2642 ; minimally-qualified # 🚶🏿‍♂ man walking: dark skin tone +1F6B6 200D 2640 FE0F ; fully-qualified # 🚶‍♀️ woman walking +1F6B6 200D 2640 ; minimally-qualified # 🚶‍♀ woman walking +1F6B6 1F3FB 200D 2640 FE0F ; fully-qualified # 🚶🏻‍♀️ woman walking: light skin tone +1F6B6 1F3FB 200D 2640 ; minimally-qualified # 🚶🏻‍♀ woman walking: light skin tone +1F6B6 1F3FC 200D 2640 FE0F ; fully-qualified # 🚶🏼‍♀️ woman walking: medium-light skin tone +1F6B6 1F3FC 200D 2640 ; minimally-qualified # 🚶🏼‍♀ woman walking: medium-light skin tone +1F6B6 1F3FD 200D 2640 FE0F ; fully-qualified # 🚶🏽‍♀️ woman walking: medium skin tone +1F6B6 1F3FD 200D 2640 ; minimally-qualified # 🚶🏽‍♀ woman walking: medium skin tone +1F6B6 1F3FE 200D 2640 FE0F ; fully-qualified # 🚶🏾‍♀️ woman walking: medium-dark skin tone +1F6B6 1F3FE 200D 2640 ; minimally-qualified # 🚶🏾‍♀ woman walking: medium-dark skin tone +1F6B6 1F3FF 200D 2640 FE0F ; fully-qualified # 🚶🏿‍♀️ woman walking: dark skin tone +1F6B6 1F3FF 200D 2640 ; minimally-qualified # 🚶🏿‍♀ woman walking: dark skin tone +1F9CD ; fully-qualified # 🧍 person standing +1F9CD 1F3FB ; fully-qualified # 🧍🏻 person standing: light skin tone +1F9CD 1F3FC ; fully-qualified # 🧍🏼 person standing: medium-light skin tone +1F9CD 1F3FD ; fully-qualified # 🧍🏽 person standing: medium skin tone +1F9CD 1F3FE ; fully-qualified # 🧍🏾 person standing: medium-dark skin tone +1F9CD 1F3FF ; fully-qualified # 🧍🏿 person standing: dark skin tone +1F9CD 200D 2642 FE0F ; fully-qualified # 🧍‍♂️ man standing +1F9CD 200D 2642 ; minimally-qualified # 🧍‍♂ man standing +1F9CD 1F3FB 200D 2642 FE0F ; fully-qualified # 🧍🏻‍♂️ man standing: light skin tone +1F9CD 1F3FB 200D 2642 ; minimally-qualified # 🧍🏻‍♂ man standing: light skin tone +1F9CD 1F3FC 200D 2642 FE0F ; fully-qualified # 🧍🏼‍♂️ man standing: medium-light skin tone +1F9CD 1F3FC 200D 2642 ; minimally-qualified # 🧍🏼‍♂ man standing: medium-light skin tone +1F9CD 1F3FD 200D 2642 FE0F ; fully-qualified # 🧍🏽‍♂️ man standing: medium skin tone +1F9CD 1F3FD 200D 2642 ; minimally-qualified # 🧍🏽‍♂ man standing: medium skin tone +1F9CD 1F3FE 200D 2642 FE0F ; fully-qualified # 🧍🏾‍♂️ man standing: medium-dark skin tone +1F9CD 1F3FE 200D 2642 ; minimally-qualified # 🧍🏾‍♂ man standing: medium-dark skin tone +1F9CD 1F3FF 200D 2642 FE0F ; fully-qualified # 🧍🏿‍♂️ man standing: dark skin tone +1F9CD 1F3FF 200D 2642 ; minimally-qualified # 🧍🏿‍♂ man standing: dark skin tone +1F9CD 200D 2640 FE0F ; fully-qualified # 🧍‍♀️ woman standing +1F9CD 200D 2640 ; minimally-qualified # 🧍‍♀ woman standing +1F9CD 1F3FB 200D 2640 FE0F ; fully-qualified # 🧍🏻‍♀️ woman standing: light skin tone +1F9CD 1F3FB 200D 2640 ; minimally-qualified # 🧍🏻‍♀ woman standing: light skin tone +1F9CD 1F3FC 200D 2640 FE0F ; fully-qualified # 🧍🏼‍♀️ woman standing: medium-light skin tone +1F9CD 1F3FC 200D 2640 ; minimally-qualified # 🧍🏼‍♀ woman standing: medium-light skin tone +1F9CD 1F3FD 200D 2640 FE0F ; fully-qualified # 🧍🏽‍♀️ woman standing: medium skin tone +1F9CD 1F3FD 200D 2640 ; minimally-qualified # 🧍🏽‍♀ woman standing: medium skin tone +1F9CD 1F3FE 200D 2640 FE0F ; fully-qualified # 🧍🏾‍♀️ woman standing: medium-dark skin tone +1F9CD 1F3FE 200D 2640 ; minimally-qualified # 🧍🏾‍♀ woman standing: medium-dark skin tone +1F9CD 1F3FF 200D 2640 FE0F ; fully-qualified # 🧍🏿‍♀️ woman standing: dark skin tone +1F9CD 1F3FF 200D 2640 ; minimally-qualified # 🧍🏿‍♀ woman standing: dark skin tone +1F9CE ; fully-qualified # 🧎 person kneeling +1F9CE 1F3FB ; fully-qualified # 🧎🏻 person kneeling: light skin tone +1F9CE 1F3FC ; fully-qualified # 🧎🏼 person kneeling: medium-light skin tone +1F9CE 1F3FD ; fully-qualified # 🧎🏽 person kneeling: medium skin tone +1F9CE 1F3FE ; fully-qualified # 🧎🏾 person kneeling: medium-dark skin tone +1F9CE 1F3FF ; fully-qualified # 🧎🏿 person kneeling: dark skin tone +1F9CE 200D 2642 FE0F ; fully-qualified # 🧎‍♂️ man kneeling +1F9CE 200D 2642 ; minimally-qualified # 🧎‍♂ man kneeling +1F9CE 1F3FB 200D 2642 FE0F ; fully-qualified # 🧎🏻‍♂️ man kneeling: light skin tone +1F9CE 1F3FB 200D 2642 ; minimally-qualified # 🧎🏻‍♂ man kneeling: light skin tone +1F9CE 1F3FC 200D 2642 FE0F ; fully-qualified # 🧎🏼‍♂️ man kneeling: medium-light skin tone +1F9CE 1F3FC 200D 2642 ; minimally-qualified # 🧎🏼‍♂ man kneeling: medium-light skin tone +1F9CE 1F3FD 200D 2642 FE0F ; fully-qualified # 🧎🏽‍♂️ man kneeling: medium skin tone +1F9CE 1F3FD 200D 2642 ; minimally-qualified # 🧎🏽‍♂ man kneeling: medium skin tone +1F9CE 1F3FE 200D 2642 FE0F ; fully-qualified # 🧎🏾‍♂️ man kneeling: medium-dark skin tone +1F9CE 1F3FE 200D 2642 ; minimally-qualified # 🧎🏾‍♂ man kneeling: medium-dark skin tone +1F9CE 1F3FF 200D 2642 FE0F ; fully-qualified # 🧎🏿‍♂️ man kneeling: dark skin tone +1F9CE 1F3FF 200D 2642 ; minimally-qualified # 🧎🏿‍♂ man kneeling: dark skin tone +1F9CE 200D 2640 FE0F ; fully-qualified # 🧎‍♀️ woman kneeling +1F9CE 200D 2640 ; minimally-qualified # 🧎‍♀ woman kneeling +1F9CE 1F3FB 200D 2640 FE0F ; fully-qualified # 🧎🏻‍♀️ woman kneeling: light skin tone +1F9CE 1F3FB 200D 2640 ; minimally-qualified # 🧎🏻‍♀ woman kneeling: light skin tone +1F9CE 1F3FC 200D 2640 FE0F ; fully-qualified # 🧎🏼‍♀️ woman kneeling: medium-light skin tone +1F9CE 1F3FC 200D 2640 ; minimally-qualified # 🧎🏼‍♀ woman kneeling: medium-light skin tone +1F9CE 1F3FD 200D 2640 FE0F ; fully-qualified # 🧎🏽‍♀️ woman kneeling: medium skin tone +1F9CE 1F3FD 200D 2640 ; minimally-qualified # 🧎🏽‍♀ woman kneeling: medium skin tone +1F9CE 1F3FE 200D 2640 FE0F ; fully-qualified # 🧎🏾‍♀️ woman kneeling: medium-dark skin tone +1F9CE 1F3FE 200D 2640 ; minimally-qualified # 🧎🏾‍♀ woman kneeling: medium-dark skin tone +1F9CE 1F3FF 200D 2640 FE0F ; fully-qualified # 🧎🏿‍♀️ woman kneeling: dark skin tone +1F9CE 1F3FF 200D 2640 ; minimally-qualified # 🧎🏿‍♀ woman kneeling: dark skin tone +1F468 200D 1F9AF ; fully-qualified # 👨‍🦯 man with probing cane +1F468 1F3FB 200D 1F9AF ; fully-qualified # 👨🏻‍🦯 man with probing cane: light skin tone +1F468 1F3FC 200D 1F9AF ; fully-qualified # 👨🏼‍🦯 man with probing cane: medium-light skin tone +1F468 1F3FD 200D 1F9AF ; fully-qualified # 👨🏽‍🦯 man with probing cane: medium skin tone +1F468 1F3FE 200D 1F9AF ; fully-qualified # 👨🏾‍🦯 man with probing cane: medium-dark skin tone +1F468 1F3FF 200D 1F9AF ; fully-qualified # 👨🏿‍🦯 man with probing cane: dark skin tone +1F469 200D 1F9AF ; fully-qualified # 👩‍🦯 woman with probing cane +1F469 1F3FB 200D 1F9AF ; fully-qualified # 👩🏻‍🦯 woman with probing cane: light skin tone +1F469 1F3FC 200D 1F9AF ; fully-qualified # 👩🏼‍🦯 woman with probing cane: medium-light skin tone +1F469 1F3FD 200D 1F9AF ; fully-qualified # 👩🏽‍🦯 woman with probing cane: medium skin tone +1F469 1F3FE 200D 1F9AF ; fully-qualified # 👩🏾‍🦯 woman with probing cane: medium-dark skin tone +1F469 1F3FF 200D 1F9AF ; fully-qualified # 👩🏿‍🦯 woman with probing cane: dark skin tone +1F468 200D 1F9BC ; fully-qualified # 👨‍🦼 man in motorized wheelchair +1F468 1F3FB 200D 1F9BC ; fully-qualified # 👨🏻‍🦼 man in motorized wheelchair: light skin tone +1F468 1F3FC 200D 1F9BC ; fully-qualified # 👨🏼‍🦼 man in motorized wheelchair: medium-light skin tone +1F468 1F3FD 200D 1F9BC ; fully-qualified # 👨🏽‍🦼 man in motorized wheelchair: medium skin tone +1F468 1F3FE 200D 1F9BC ; fully-qualified # 👨🏾‍🦼 man in motorized wheelchair: medium-dark skin tone +1F468 1F3FF 200D 1F9BC ; fully-qualified # 👨🏿‍🦼 man in motorized wheelchair: dark skin tone +1F469 200D 1F9BC ; fully-qualified # 👩‍🦼 woman in motorized wheelchair +1F469 1F3FB 200D 1F9BC ; fully-qualified # 👩🏻‍🦼 woman in motorized wheelchair: light skin tone +1F469 1F3FC 200D 1F9BC ; fully-qualified # 👩🏼‍🦼 woman in motorized wheelchair: medium-light skin tone +1F469 1F3FD 200D 1F9BC ; fully-qualified # 👩🏽‍🦼 woman in motorized wheelchair: medium skin tone +1F469 1F3FE 200D 1F9BC ; fully-qualified # 👩🏾‍🦼 woman in motorized wheelchair: medium-dark skin tone +1F469 1F3FF 200D 1F9BC ; fully-qualified # 👩🏿‍🦼 woman in motorized wheelchair: dark skin tone +1F468 200D 1F9BD ; fully-qualified # 👨‍🦽 man in manual wheelchair +1F468 1F3FB 200D 1F9BD ; fully-qualified # 👨🏻‍🦽 man in manual wheelchair: light skin tone +1F468 1F3FC 200D 1F9BD ; fully-qualified # 👨🏼‍🦽 man in manual wheelchair: medium-light skin tone +1F468 1F3FD 200D 1F9BD ; fully-qualified # 👨🏽‍🦽 man in manual wheelchair: medium skin tone +1F468 1F3FE 200D 1F9BD ; fully-qualified # 👨🏾‍🦽 man in manual wheelchair: medium-dark skin tone +1F468 1F3FF 200D 1F9BD ; fully-qualified # 👨🏿‍🦽 man in manual wheelchair: dark skin tone +1F469 200D 1F9BD ; fully-qualified # 👩‍🦽 woman in manual wheelchair +1F469 1F3FB 200D 1F9BD ; fully-qualified # 👩🏻‍🦽 woman in manual wheelchair: light skin tone +1F469 1F3FC 200D 1F9BD ; fully-qualified # 👩🏼‍🦽 woman in manual wheelchair: medium-light skin tone +1F469 1F3FD 200D 1F9BD ; fully-qualified # 👩🏽‍🦽 woman in manual wheelchair: medium skin tone +1F469 1F3FE 200D 1F9BD ; fully-qualified # 👩🏾‍🦽 woman in manual wheelchair: medium-dark skin tone +1F469 1F3FF 200D 1F9BD ; fully-qualified # 👩🏿‍🦽 woman in manual wheelchair: dark skin tone +1F3C3 ; fully-qualified # 🏃 person running +1F3C3 1F3FB ; fully-qualified # 🏃🏻 person running: light skin tone +1F3C3 1F3FC ; fully-qualified # 🏃🏼 person running: medium-light skin tone +1F3C3 1F3FD ; fully-qualified # 🏃🏽 person running: medium skin tone +1F3C3 1F3FE ; fully-qualified # 🏃🏾 person running: medium-dark skin tone +1F3C3 1F3FF ; fully-qualified # 🏃🏿 person running: dark skin tone +1F3C3 200D 2642 FE0F ; fully-qualified # 🏃‍♂️ man running +1F3C3 200D 2642 ; minimally-qualified # 🏃‍♂ man running +1F3C3 1F3FB 200D 2642 FE0F ; fully-qualified # 🏃🏻‍♂️ man running: light skin tone +1F3C3 1F3FB 200D 2642 ; minimally-qualified # 🏃🏻‍♂ man running: light skin tone +1F3C3 1F3FC 200D 2642 FE0F ; fully-qualified # 🏃🏼‍♂️ man running: medium-light skin tone +1F3C3 1F3FC 200D 2642 ; minimally-qualified # 🏃🏼‍♂ man running: medium-light skin tone +1F3C3 1F3FD 200D 2642 FE0F ; fully-qualified # 🏃🏽‍♂️ man running: medium skin tone +1F3C3 1F3FD 200D 2642 ; minimally-qualified # 🏃🏽‍♂ man running: medium skin tone +1F3C3 1F3FE 200D 2642 FE0F ; fully-qualified # 🏃🏾‍♂️ man running: medium-dark skin tone +1F3C3 1F3FE 200D 2642 ; minimally-qualified # 🏃🏾‍♂ man running: medium-dark skin tone +1F3C3 1F3FF 200D 2642 FE0F ; fully-qualified # 🏃🏿‍♂️ man running: dark skin tone +1F3C3 1F3FF 200D 2642 ; minimally-qualified # 🏃🏿‍♂ man running: dark skin tone +1F3C3 200D 2640 FE0F ; fully-qualified # 🏃‍♀️ woman running +1F3C3 200D 2640 ; minimally-qualified # 🏃‍♀ woman running +1F3C3 1F3FB 200D 2640 FE0F ; fully-qualified # 🏃🏻‍♀️ woman running: light skin tone +1F3C3 1F3FB 200D 2640 ; minimally-qualified # 🏃🏻‍♀ woman running: light skin tone +1F3C3 1F3FC 200D 2640 FE0F ; fully-qualified # 🏃🏼‍♀️ woman running: medium-light skin tone +1F3C3 1F3FC 200D 2640 ; minimally-qualified # 🏃🏼‍♀ woman running: medium-light skin tone +1F3C3 1F3FD 200D 2640 FE0F ; fully-qualified # 🏃🏽‍♀️ woman running: medium skin tone +1F3C3 1F3FD 200D 2640 ; minimally-qualified # 🏃🏽‍♀ woman running: medium skin tone +1F3C3 1F3FE 200D 2640 FE0F ; fully-qualified # 🏃🏾‍♀️ woman running: medium-dark skin tone +1F3C3 1F3FE 200D 2640 ; minimally-qualified # 🏃🏾‍♀ woman running: medium-dark skin tone +1F3C3 1F3FF 200D 2640 FE0F ; fully-qualified # 🏃🏿‍♀️ woman running: dark skin tone +1F3C3 1F3FF 200D 2640 ; minimally-qualified # 🏃🏿‍♀ woman running: dark skin tone +1F483 ; fully-qualified # 💃 woman dancing +1F483 1F3FB ; fully-qualified # 💃🏻 woman dancing: light skin tone +1F483 1F3FC ; fully-qualified # 💃🏼 woman dancing: medium-light skin tone +1F483 1F3FD ; fully-qualified # 💃🏽 woman dancing: medium skin tone +1F483 1F3FE ; fully-qualified # 💃🏾 woman dancing: medium-dark skin tone +1F483 1F3FF ; fully-qualified # 💃🏿 woman dancing: dark skin tone +1F57A ; fully-qualified # 🕺 man dancing +1F57A 1F3FB ; fully-qualified # 🕺🏻 man dancing: light skin tone +1F57A 1F3FC ; fully-qualified # 🕺🏼 man dancing: medium-light skin tone +1F57A 1F3FD ; fully-qualified # 🕺🏽 man dancing: medium skin tone +1F57A 1F3FE ; fully-qualified # 🕺🏾 man dancing: medium-dark skin tone +1F57A 1F3FF ; fully-qualified # 🕺🏿 man dancing: dark skin tone +1F574 FE0F ; fully-qualified # 🕴️ man in suit levitating +1F574 ; unqualified # 🕴 man in suit levitating +1F574 1F3FB ; fully-qualified # 🕴🏻 man in suit levitating: light skin tone +1F574 1F3FC ; fully-qualified # 🕴🏼 man in suit levitating: medium-light skin tone +1F574 1F3FD ; fully-qualified # 🕴🏽 man in suit levitating: medium skin tone +1F574 1F3FE ; fully-qualified # 🕴🏾 man in suit levitating: medium-dark skin tone +1F574 1F3FF ; fully-qualified # 🕴🏿 man in suit levitating: dark skin tone +1F46F ; fully-qualified # 👯 people with bunny ears +1F46F 1F3FB ; fully-qualified # 👯🏻 people with bunny ears: light skin tone +1F46F 1F3FC ; fully-qualified # 👯🏼 people with bunny ears: medium-light skin tone +1F46F 1F3FD ; fully-qualified # 👯🏽 people with bunny ears: medium skin tone +1F46F 1F3FE ; fully-qualified # 👯🏾 people with bunny ears: medium-dark skin tone +1F46F 1F3FF ; fully-qualified # 👯🏿 people with bunny ears: dark skin tone +1F46F 200D 2642 FE0F ; fully-qualified # 👯‍♂️ men with bunny ears +1F46F 200D 2642 ; minimally-qualified # 👯‍♂ men with bunny ears +1F46F 1F3FB 200D 2642 FE0F ; fully-qualified # 👯🏻‍♂️ men with bunny ears: light skin tone +1F46F 1F3FB 200D 2642 ; minimally-qualified # 👯🏻‍♂ men with bunny ears: light skin tone +1F46F 1F3FC 200D 2642 FE0F ; fully-qualified # 👯🏼‍♂️ men with bunny ears: medium-light skin tone +1F46F 1F3FC 200D 2642 ; minimally-qualified # 👯🏼‍♂ men with bunny ears: medium-light skin tone +1F46F 1F3FD 200D 2642 FE0F ; fully-qualified # 👯🏽‍♂️ men with bunny ears: medium skin tone +1F46F 1F3FD 200D 2642 ; minimally-qualified # 👯🏽‍♂ men with bunny ears: medium skin tone +1F46F 1F3FE 200D 2642 FE0F ; fully-qualified # 👯🏾‍♂️ men with bunny ears: medium-dark skin tone +1F46F 1F3FE 200D 2642 ; minimally-qualified # 👯🏾‍♂ men with bunny ears: medium-dark skin tone +1F46F 1F3FF 200D 2642 FE0F ; fully-qualified # 👯🏿‍♂️ men with bunny ears: dark skin tone +1F46F 1F3FF 200D 2642 ; minimally-qualified # 👯🏿‍♂ men with bunny ears: dark skin tone +1F46F 200D 2640 FE0F ; fully-qualified # 👯‍♀️ women with bunny ears +1F46F 200D 2640 ; minimally-qualified # 👯‍♀ women with bunny ears +1F46F 1F3FB 200D 2640 FE0F ; fully-qualified # 👯🏻‍♀️ women with bunny ears: light skin tone +1F46F 1F3FB 200D 2640 ; minimally-qualified # 👯🏻‍♀ women with bunny ears: light skin tone +1F46F 1F3FC 200D 2640 FE0F ; fully-qualified # 👯🏼‍♀️ women with bunny ears: medium-light skin tone +1F46F 1F3FC 200D 2640 ; minimally-qualified # 👯🏼‍♀ women with bunny ears: medium-light skin tone +1F46F 1F3FD 200D 2640 FE0F ; fully-qualified # 👯🏽‍♀️ women with bunny ears: medium skin tone +1F46F 1F3FD 200D 2640 ; minimally-qualified # 👯🏽‍♀ women with bunny ears: medium skin tone +1F46F 1F3FE 200D 2640 FE0F ; fully-qualified # 👯🏾‍♀️ women with bunny ears: medium-dark skin tone +1F46F 1F3FE 200D 2640 ; minimally-qualified # 👯🏾‍♀ women with bunny ears: medium-dark skin tone +1F46F 1F3FF 200D 2640 FE0F ; fully-qualified # 👯🏿‍♀️ women with bunny ears: dark skin tone +1F46F 1F3FF 200D 2640 ; minimally-qualified # 👯🏿‍♀ women with bunny ears: dark skin tone +1F9D6 ; fully-qualified # 🧖 person in steamy room +1F9D6 1F3FB ; fully-qualified # 🧖🏻 person in steamy room: light skin tone +1F9D6 1F3FC ; fully-qualified # 🧖🏼 person in steamy room: medium-light skin tone +1F9D6 1F3FD ; fully-qualified # 🧖🏽 person in steamy room: medium skin tone +1F9D6 1F3FE ; fully-qualified # 🧖🏾 person in steamy room: medium-dark skin tone +1F9D6 1F3FF ; fully-qualified # 🧖🏿 person in steamy room: dark skin tone +1F9D6 200D 2642 FE0F ; fully-qualified # 🧖‍♂️ man in steamy room +1F9D6 200D 2642 ; minimally-qualified # 🧖‍♂ man in steamy room +1F9D6 1F3FB 200D 2642 FE0F ; fully-qualified # 🧖🏻‍♂️ man in steamy room: light skin tone +1F9D6 1F3FB 200D 2642 ; minimally-qualified # 🧖🏻‍♂ man in steamy room: light skin tone +1F9D6 1F3FC 200D 2642 FE0F ; fully-qualified # 🧖🏼‍♂️ man in steamy room: medium-light skin tone +1F9D6 1F3FC 200D 2642 ; minimally-qualified # 🧖🏼‍♂ man in steamy room: medium-light skin tone +1F9D6 1F3FD 200D 2642 FE0F ; fully-qualified # 🧖🏽‍♂️ man in steamy room: medium skin tone +1F9D6 1F3FD 200D 2642 ; minimally-qualified # 🧖🏽‍♂ man in steamy room: medium skin tone +1F9D6 1F3FE 200D 2642 FE0F ; fully-qualified # 🧖🏾‍♂️ man in steamy room: medium-dark skin tone +1F9D6 1F3FE 200D 2642 ; minimally-qualified # 🧖🏾‍♂ man in steamy room: medium-dark skin tone +1F9D6 1F3FF 200D 2642 FE0F ; fully-qualified # 🧖🏿‍♂️ man in steamy room: dark skin tone +1F9D6 1F3FF 200D 2642 ; minimally-qualified # 🧖🏿‍♂ man in steamy room: dark skin tone +1F9D6 200D 2640 FE0F ; fully-qualified # 🧖‍♀️ woman in steamy room +1F9D6 200D 2640 ; minimally-qualified # 🧖‍♀ woman in steamy room +1F9D6 1F3FB 200D 2640 FE0F ; fully-qualified # 🧖🏻‍♀️ woman in steamy room: light skin tone +1F9D6 1F3FB 200D 2640 ; minimally-qualified # 🧖🏻‍♀ woman in steamy room: light skin tone +1F9D6 1F3FC 200D 2640 FE0F ; fully-qualified # 🧖🏼‍♀️ woman in steamy room: medium-light skin tone +1F9D6 1F3FC 200D 2640 ; minimally-qualified # 🧖🏼‍♀ woman in steamy room: medium-light skin tone +1F9D6 1F3FD 200D 2640 FE0F ; fully-qualified # 🧖🏽‍♀️ woman in steamy room: medium skin tone +1F9D6 1F3FD 200D 2640 ; minimally-qualified # 🧖🏽‍♀ woman in steamy room: medium skin tone +1F9D6 1F3FE 200D 2640 FE0F ; fully-qualified # 🧖🏾‍♀️ woman in steamy room: medium-dark skin tone +1F9D6 1F3FE 200D 2640 ; minimally-qualified # 🧖🏾‍♀ woman in steamy room: medium-dark skin tone +1F9D6 1F3FF 200D 2640 FE0F ; fully-qualified # 🧖🏿‍♀️ woman in steamy room: dark skin tone +1F9D6 1F3FF 200D 2640 ; minimally-qualified # 🧖🏿‍♀ woman in steamy room: dark skin tone +1F9D7 ; fully-qualified # 🧗 person climbing +1F9D7 1F3FB ; fully-qualified # 🧗🏻 person climbing: light skin tone +1F9D7 1F3FC ; fully-qualified # 🧗🏼 person climbing: medium-light skin tone +1F9D7 1F3FD ; fully-qualified # 🧗🏽 person climbing: medium skin tone +1F9D7 1F3FE ; fully-qualified # 🧗🏾 person climbing: medium-dark skin tone +1F9D7 1F3FF ; fully-qualified # 🧗🏿 person climbing: dark skin tone +1F9D7 200D 2642 FE0F ; fully-qualified # 🧗‍♂️ man climbing +1F9D7 200D 2642 ; minimally-qualified # 🧗‍♂ man climbing +1F9D7 1F3FB 200D 2642 FE0F ; fully-qualified # 🧗🏻‍♂️ man climbing: light skin tone +1F9D7 1F3FB 200D 2642 ; minimally-qualified # 🧗🏻‍♂ man climbing: light skin tone +1F9D7 1F3FC 200D 2642 FE0F ; fully-qualified # 🧗🏼‍♂️ man climbing: medium-light skin tone +1F9D7 1F3FC 200D 2642 ; minimally-qualified # 🧗🏼‍♂ man climbing: medium-light skin tone +1F9D7 1F3FD 200D 2642 FE0F ; fully-qualified # 🧗🏽‍♂️ man climbing: medium skin tone +1F9D7 1F3FD 200D 2642 ; minimally-qualified # 🧗🏽‍♂ man climbing: medium skin tone +1F9D7 1F3FE 200D 2642 FE0F ; fully-qualified # 🧗🏾‍♂️ man climbing: medium-dark skin tone +1F9D7 1F3FE 200D 2642 ; minimally-qualified # 🧗🏾‍♂ man climbing: medium-dark skin tone +1F9D7 1F3FF 200D 2642 FE0F ; fully-qualified # 🧗🏿‍♂️ man climbing: dark skin tone +1F9D7 1F3FF 200D 2642 ; minimally-qualified # 🧗🏿‍♂ man climbing: dark skin tone +1F9D7 200D 2640 FE0F ; fully-qualified # 🧗‍♀️ woman climbing +1F9D7 200D 2640 ; minimally-qualified # 🧗‍♀ woman climbing +1F9D7 1F3FB 200D 2640 FE0F ; fully-qualified # 🧗🏻‍♀️ woman climbing: light skin tone +1F9D7 1F3FB 200D 2640 ; minimally-qualified # 🧗🏻‍♀ woman climbing: light skin tone +1F9D7 1F3FC 200D 2640 FE0F ; fully-qualified # 🧗🏼‍♀️ woman climbing: medium-light skin tone +1F9D7 1F3FC 200D 2640 ; minimally-qualified # 🧗🏼‍♀ woman climbing: medium-light skin tone +1F9D7 1F3FD 200D 2640 FE0F ; fully-qualified # 🧗🏽‍♀️ woman climbing: medium skin tone +1F9D7 1F3FD 200D 2640 ; minimally-qualified # 🧗🏽‍♀ woman climbing: medium skin tone +1F9D7 1F3FE 200D 2640 FE0F ; fully-qualified # 🧗🏾‍♀️ woman climbing: medium-dark skin tone +1F9D7 1F3FE 200D 2640 ; minimally-qualified # 🧗🏾‍♀ woman climbing: medium-dark skin tone +1F9D7 1F3FF 200D 2640 FE0F ; fully-qualified # 🧗🏿‍♀️ woman climbing: dark skin tone +1F9D7 1F3FF 200D 2640 ; minimally-qualified # 🧗🏿‍♀ woman climbing: dark skin tone + +# subgroup: person-sport +1F93A ; fully-qualified # 🤺 person fencing +1F3C7 ; fully-qualified # 🏇 horse racing +1F3C7 1F3FB ; fully-qualified # 🏇🏻 horse racing: light skin tone +1F3C7 1F3FC ; fully-qualified # 🏇🏼 horse racing: medium-light skin tone +1F3C7 1F3FD ; fully-qualified # 🏇🏽 horse racing: medium skin tone +1F3C7 1F3FE ; fully-qualified # 🏇🏾 horse racing: medium-dark skin tone +1F3C7 1F3FF ; fully-qualified # 🏇🏿 horse racing: dark skin tone +26F7 FE0F ; fully-qualified # ⛷️ skier +26F7 ; unqualified # ⛷ skier +1F3C2 ; fully-qualified # 🏂 snowboarder +1F3C2 1F3FB ; fully-qualified # 🏂🏻 snowboarder: light skin tone +1F3C2 1F3FC ; fully-qualified # 🏂🏼 snowboarder: medium-light skin tone +1F3C2 1F3FD ; fully-qualified # 🏂🏽 snowboarder: medium skin tone +1F3C2 1F3FE ; fully-qualified # 🏂🏾 snowboarder: medium-dark skin tone +1F3C2 1F3FF ; fully-qualified # 🏂🏿 snowboarder: dark skin tone +1F3CC FE0F ; fully-qualified # 🏌️ person golfing +1F3CC ; unqualified # 🏌 person golfing +1F3CC 1F3FB ; fully-qualified # 🏌🏻 person golfing: light skin tone +1F3CC 1F3FC ; fully-qualified # 🏌🏼 person golfing: medium-light skin tone +1F3CC 1F3FD ; fully-qualified # 🏌🏽 person golfing: medium skin tone +1F3CC 1F3FE ; fully-qualified # 🏌🏾 person golfing: medium-dark skin tone +1F3CC 1F3FF ; fully-qualified # 🏌🏿 person golfing: dark skin tone +1F3CC FE0F 200D 2642 FE0F ; fully-qualified # 🏌️‍♂️ man golfing +1F3CC 200D 2642 FE0F ; unqualified # 🏌‍♂️ man golfing +1F3CC FE0F 200D 2642 ; unqualified # 🏌️‍♂ man golfing +1F3CC 200D 2642 ; unqualified # 🏌‍♂ man golfing +1F3CC 1F3FB 200D 2642 FE0F ; fully-qualified # 🏌🏻‍♂️ man golfing: light skin tone +1F3CC 1F3FB 200D 2642 ; minimally-qualified # 🏌🏻‍♂ man golfing: light skin tone +1F3CC 1F3FC 200D 2642 FE0F ; fully-qualified # 🏌🏼‍♂️ man golfing: medium-light skin tone +1F3CC 1F3FC 200D 2642 ; minimally-qualified # 🏌🏼‍♂ man golfing: medium-light skin tone +1F3CC 1F3FD 200D 2642 FE0F ; fully-qualified # 🏌🏽‍♂️ man golfing: medium skin tone +1F3CC 1F3FD 200D 2642 ; minimally-qualified # 🏌🏽‍♂ man golfing: medium skin tone +1F3CC 1F3FE 200D 2642 FE0F ; fully-qualified # 🏌🏾‍♂️ man golfing: medium-dark skin tone +1F3CC 1F3FE 200D 2642 ; minimally-qualified # 🏌🏾‍♂ man golfing: medium-dark skin tone +1F3CC 1F3FF 200D 2642 FE0F ; fully-qualified # 🏌🏿‍♂️ man golfing: dark skin tone +1F3CC 1F3FF 200D 2642 ; minimally-qualified # 🏌🏿‍♂ man golfing: dark skin tone +1F3CC FE0F 200D 2640 FE0F ; fully-qualified # 🏌️‍♀️ woman golfing +1F3CC 200D 2640 FE0F ; unqualified # 🏌‍♀️ woman golfing +1F3CC FE0F 200D 2640 ; unqualified # 🏌️‍♀ woman golfing +1F3CC 200D 2640 ; unqualified # 🏌‍♀ woman golfing +1F3CC 1F3FB 200D 2640 FE0F ; fully-qualified # 🏌🏻‍♀️ woman golfing: light skin tone +1F3CC 1F3FB 200D 2640 ; minimally-qualified # 🏌🏻‍♀ woman golfing: light skin tone +1F3CC 1F3FC 200D 2640 FE0F ; fully-qualified # 🏌🏼‍♀️ woman golfing: medium-light skin tone +1F3CC 1F3FC 200D 2640 ; minimally-qualified # 🏌🏼‍♀ woman golfing: medium-light skin tone +1F3CC 1F3FD 200D 2640 FE0F ; fully-qualified # 🏌🏽‍♀️ woman golfing: medium skin tone +1F3CC 1F3FD 200D 2640 ; minimally-qualified # 🏌🏽‍♀ woman golfing: medium skin tone +1F3CC 1F3FE 200D 2640 FE0F ; fully-qualified # 🏌🏾‍♀️ woman golfing: medium-dark skin tone +1F3CC 1F3FE 200D 2640 ; minimally-qualified # 🏌🏾‍♀ woman golfing: medium-dark skin tone +1F3CC 1F3FF 200D 2640 FE0F ; fully-qualified # 🏌🏿‍♀️ woman golfing: dark skin tone +1F3CC 1F3FF 200D 2640 ; minimally-qualified # 🏌🏿‍♀ woman golfing: dark skin tone +1F3C4 ; fully-qualified # 🏄 person surfing +1F3C4 1F3FB ; fully-qualified # 🏄🏻 person surfing: light skin tone +1F3C4 1F3FC ; fully-qualified # 🏄🏼 person surfing: medium-light skin tone +1F3C4 1F3FD ; fully-qualified # 🏄🏽 person surfing: medium skin tone +1F3C4 1F3FE ; fully-qualified # 🏄🏾 person surfing: medium-dark skin tone +1F3C4 1F3FF ; fully-qualified # 🏄🏿 person surfing: dark skin tone +1F3C4 200D 2642 FE0F ; fully-qualified # 🏄‍♂️ man surfing +1F3C4 200D 2642 ; minimally-qualified # 🏄‍♂ man surfing +1F3C4 1F3FB 200D 2642 FE0F ; fully-qualified # 🏄🏻‍♂️ man surfing: light skin tone +1F3C4 1F3FB 200D 2642 ; minimally-qualified # 🏄🏻‍♂ man surfing: light skin tone +1F3C4 1F3FC 200D 2642 FE0F ; fully-qualified # 🏄🏼‍♂️ man surfing: medium-light skin tone +1F3C4 1F3FC 200D 2642 ; minimally-qualified # 🏄🏼‍♂ man surfing: medium-light skin tone +1F3C4 1F3FD 200D 2642 FE0F ; fully-qualified # 🏄🏽‍♂️ man surfing: medium skin tone +1F3C4 1F3FD 200D 2642 ; minimally-qualified # 🏄🏽‍♂ man surfing: medium skin tone +1F3C4 1F3FE 200D 2642 FE0F ; fully-qualified # 🏄🏾‍♂️ man surfing: medium-dark skin tone +1F3C4 1F3FE 200D 2642 ; minimally-qualified # 🏄🏾‍♂ man surfing: medium-dark skin tone +1F3C4 1F3FF 200D 2642 FE0F ; fully-qualified # 🏄🏿‍♂️ man surfing: dark skin tone +1F3C4 1F3FF 200D 2642 ; minimally-qualified # 🏄🏿‍♂ man surfing: dark skin tone +1F3C4 200D 2640 FE0F ; fully-qualified # 🏄‍♀️ woman surfing +1F3C4 200D 2640 ; minimally-qualified # 🏄‍♀ woman surfing +1F3C4 1F3FB 200D 2640 FE0F ; fully-qualified # 🏄🏻‍♀️ woman surfing: light skin tone +1F3C4 1F3FB 200D 2640 ; minimally-qualified # 🏄🏻‍♀ woman surfing: light skin tone +1F3C4 1F3FC 200D 2640 FE0F ; fully-qualified # 🏄🏼‍♀️ woman surfing: medium-light skin tone +1F3C4 1F3FC 200D 2640 ; minimally-qualified # 🏄🏼‍♀ woman surfing: medium-light skin tone +1F3C4 1F3FD 200D 2640 FE0F ; fully-qualified # 🏄🏽‍♀️ woman surfing: medium skin tone +1F3C4 1F3FD 200D 2640 ; minimally-qualified # 🏄🏽‍♀ woman surfing: medium skin tone +1F3C4 1F3FE 200D 2640 FE0F ; fully-qualified # 🏄🏾‍♀️ woman surfing: medium-dark skin tone +1F3C4 1F3FE 200D 2640 ; minimally-qualified # 🏄🏾‍♀ woman surfing: medium-dark skin tone +1F3C4 1F3FF 200D 2640 FE0F ; fully-qualified # 🏄🏿‍♀️ woman surfing: dark skin tone +1F3C4 1F3FF 200D 2640 ; minimally-qualified # 🏄🏿‍♀ woman surfing: dark skin tone +1F6A3 ; fully-qualified # 🚣 person rowing boat +1F6A3 1F3FB ; fully-qualified # 🚣🏻 person rowing boat: light skin tone +1F6A3 1F3FC ; fully-qualified # 🚣🏼 person rowing boat: medium-light skin tone +1F6A3 1F3FD ; fully-qualified # 🚣🏽 person rowing boat: medium skin tone +1F6A3 1F3FE ; fully-qualified # 🚣🏾 person rowing boat: medium-dark skin tone +1F6A3 1F3FF ; fully-qualified # 🚣🏿 person rowing boat: dark skin tone +1F6A3 200D 2642 FE0F ; fully-qualified # 🚣‍♂️ man rowing boat +1F6A3 200D 2642 ; minimally-qualified # 🚣‍♂ man rowing boat +1F6A3 1F3FB 200D 2642 FE0F ; fully-qualified # 🚣🏻‍♂️ man rowing boat: light skin tone +1F6A3 1F3FB 200D 2642 ; minimally-qualified # 🚣🏻‍♂ man rowing boat: light skin tone +1F6A3 1F3FC 200D 2642 FE0F ; fully-qualified # 🚣🏼‍♂️ man rowing boat: medium-light skin tone +1F6A3 1F3FC 200D 2642 ; minimally-qualified # 🚣🏼‍♂ man rowing boat: medium-light skin tone +1F6A3 1F3FD 200D 2642 FE0F ; fully-qualified # 🚣🏽‍♂️ man rowing boat: medium skin tone +1F6A3 1F3FD 200D 2642 ; minimally-qualified # 🚣🏽‍♂ man rowing boat: medium skin tone +1F6A3 1F3FE 200D 2642 FE0F ; fully-qualified # 🚣🏾‍♂️ man rowing boat: medium-dark skin tone +1F6A3 1F3FE 200D 2642 ; minimally-qualified # 🚣🏾‍♂ man rowing boat: medium-dark skin tone +1F6A3 1F3FF 200D 2642 FE0F ; fully-qualified # 🚣🏿‍♂️ man rowing boat: dark skin tone +1F6A3 1F3FF 200D 2642 ; minimally-qualified # 🚣🏿‍♂ man rowing boat: dark skin tone +1F6A3 200D 2640 FE0F ; fully-qualified # 🚣‍♀️ woman rowing boat +1F6A3 200D 2640 ; minimally-qualified # 🚣‍♀ woman rowing boat +1F6A3 1F3FB 200D 2640 FE0F ; fully-qualified # 🚣🏻‍♀️ woman rowing boat: light skin tone +1F6A3 1F3FB 200D 2640 ; minimally-qualified # 🚣🏻‍♀ woman rowing boat: light skin tone +1F6A3 1F3FC 200D 2640 FE0F ; fully-qualified # 🚣🏼‍♀️ woman rowing boat: medium-light skin tone +1F6A3 1F3FC 200D 2640 ; minimally-qualified # 🚣🏼‍♀ woman rowing boat: medium-light skin tone +1F6A3 1F3FD 200D 2640 FE0F ; fully-qualified # 🚣🏽‍♀️ woman rowing boat: medium skin tone +1F6A3 1F3FD 200D 2640 ; minimally-qualified # 🚣🏽‍♀ woman rowing boat: medium skin tone +1F6A3 1F3FE 200D 2640 FE0F ; fully-qualified # 🚣🏾‍♀️ woman rowing boat: medium-dark skin tone +1F6A3 1F3FE 200D 2640 ; minimally-qualified # 🚣🏾‍♀ woman rowing boat: medium-dark skin tone +1F6A3 1F3FF 200D 2640 FE0F ; fully-qualified # 🚣🏿‍♀️ woman rowing boat: dark skin tone +1F6A3 1F3FF 200D 2640 ; minimally-qualified # 🚣🏿‍♀ woman rowing boat: dark skin tone +1F3CA ; fully-qualified # 🏊 person swimming +1F3CA 1F3FB ; fully-qualified # 🏊🏻 person swimming: light skin tone +1F3CA 1F3FC ; fully-qualified # 🏊🏼 person swimming: medium-light skin tone +1F3CA 1F3FD ; fully-qualified # 🏊🏽 person swimming: medium skin tone +1F3CA 1F3FE ; fully-qualified # 🏊🏾 person swimming: medium-dark skin tone +1F3CA 1F3FF ; fully-qualified # 🏊🏿 person swimming: dark skin tone +1F3CA 200D 2642 FE0F ; fully-qualified # 🏊‍♂️ man swimming +1F3CA 200D 2642 ; minimally-qualified # 🏊‍♂ man swimming +1F3CA 1F3FB 200D 2642 FE0F ; fully-qualified # 🏊🏻‍♂️ man swimming: light skin tone +1F3CA 1F3FB 200D 2642 ; minimally-qualified # 🏊🏻‍♂ man swimming: light skin tone +1F3CA 1F3FC 200D 2642 FE0F ; fully-qualified # 🏊🏼‍♂️ man swimming: medium-light skin tone +1F3CA 1F3FC 200D 2642 ; minimally-qualified # 🏊🏼‍♂ man swimming: medium-light skin tone +1F3CA 1F3FD 200D 2642 FE0F ; fully-qualified # 🏊🏽‍♂️ man swimming: medium skin tone +1F3CA 1F3FD 200D 2642 ; minimally-qualified # 🏊🏽‍♂ man swimming: medium skin tone +1F3CA 1F3FE 200D 2642 FE0F ; fully-qualified # 🏊🏾‍♂️ man swimming: medium-dark skin tone +1F3CA 1F3FE 200D 2642 ; minimally-qualified # 🏊🏾‍♂ man swimming: medium-dark skin tone +1F3CA 1F3FF 200D 2642 FE0F ; fully-qualified # 🏊🏿‍♂️ man swimming: dark skin tone +1F3CA 1F3FF 200D 2642 ; minimally-qualified # 🏊🏿‍♂ man swimming: dark skin tone +1F3CA 200D 2640 FE0F ; fully-qualified # 🏊‍♀️ woman swimming +1F3CA 200D 2640 ; minimally-qualified # 🏊‍♀ woman swimming +1F3CA 1F3FB 200D 2640 FE0F ; fully-qualified # 🏊🏻‍♀️ woman swimming: light skin tone +1F3CA 1F3FB 200D 2640 ; minimally-qualified # 🏊🏻‍♀ woman swimming: light skin tone +1F3CA 1F3FC 200D 2640 FE0F ; fully-qualified # 🏊🏼‍♀️ woman swimming: medium-light skin tone +1F3CA 1F3FC 200D 2640 ; minimally-qualified # 🏊🏼‍♀ woman swimming: medium-light skin tone +1F3CA 1F3FD 200D 2640 FE0F ; fully-qualified # 🏊🏽‍♀️ woman swimming: medium skin tone +1F3CA 1F3FD 200D 2640 ; minimally-qualified # 🏊🏽‍♀ woman swimming: medium skin tone +1F3CA 1F3FE 200D 2640 FE0F ; fully-qualified # 🏊🏾‍♀️ woman swimming: medium-dark skin tone +1F3CA 1F3FE 200D 2640 ; minimally-qualified # 🏊🏾‍♀ woman swimming: medium-dark skin tone +1F3CA 1F3FF 200D 2640 FE0F ; fully-qualified # 🏊🏿‍♀️ woman swimming: dark skin tone +1F3CA 1F3FF 200D 2640 ; minimally-qualified # 🏊🏿‍♀ woman swimming: dark skin tone +26F9 FE0F ; fully-qualified # ⛹️ person bouncing ball +26F9 ; unqualified # ⛹ person bouncing ball +26F9 1F3FB ; fully-qualified # ⛹🏻 person bouncing ball: light skin tone +26F9 1F3FC ; fully-qualified # ⛹🏼 person bouncing ball: medium-light skin tone +26F9 1F3FD ; fully-qualified # ⛹🏽 person bouncing ball: medium skin tone +26F9 1F3FE ; fully-qualified # ⛹🏾 person bouncing ball: medium-dark skin tone +26F9 1F3FF ; fully-qualified # ⛹🏿 person bouncing ball: dark skin tone +26F9 FE0F 200D 2642 FE0F ; fully-qualified # ⛹️‍♂️ man bouncing ball +26F9 200D 2642 FE0F ; unqualified # ⛹‍♂️ man bouncing ball +26F9 FE0F 200D 2642 ; unqualified # ⛹️‍♂ man bouncing ball +26F9 200D 2642 ; unqualified # ⛹‍♂ man bouncing ball +26F9 1F3FB 200D 2642 FE0F ; fully-qualified # ⛹🏻‍♂️ man bouncing ball: light skin tone +26F9 1F3FB 200D 2642 ; minimally-qualified # ⛹🏻‍♂ man bouncing ball: light skin tone +26F9 1F3FC 200D 2642 FE0F ; fully-qualified # ⛹🏼‍♂️ man bouncing ball: medium-light skin tone +26F9 1F3FC 200D 2642 ; minimally-qualified # ⛹🏼‍♂ man bouncing ball: medium-light skin tone +26F9 1F3FD 200D 2642 FE0F ; fully-qualified # ⛹🏽‍♂️ man bouncing ball: medium skin tone +26F9 1F3FD 200D 2642 ; minimally-qualified # ⛹🏽‍♂ man bouncing ball: medium skin tone +26F9 1F3FE 200D 2642 FE0F ; fully-qualified # ⛹🏾‍♂️ man bouncing ball: medium-dark skin tone +26F9 1F3FE 200D 2642 ; minimally-qualified # ⛹🏾‍♂ man bouncing ball: medium-dark skin tone +26F9 1F3FF 200D 2642 FE0F ; fully-qualified # ⛹🏿‍♂️ man bouncing ball: dark skin tone +26F9 1F3FF 200D 2642 ; minimally-qualified # ⛹🏿‍♂ man bouncing ball: dark skin tone +26F9 FE0F 200D 2640 FE0F ; fully-qualified # ⛹️‍♀️ woman bouncing ball +26F9 200D 2640 FE0F ; unqualified # ⛹‍♀️ woman bouncing ball +26F9 FE0F 200D 2640 ; unqualified # ⛹️‍♀ woman bouncing ball +26F9 200D 2640 ; unqualified # ⛹‍♀ woman bouncing ball +26F9 1F3FB 200D 2640 FE0F ; fully-qualified # ⛹🏻‍♀️ woman bouncing ball: light skin tone +26F9 1F3FB 200D 2640 ; minimally-qualified # ⛹🏻‍♀ woman bouncing ball: light skin tone +26F9 1F3FC 200D 2640 FE0F ; fully-qualified # ⛹🏼‍♀️ woman bouncing ball: medium-light skin tone +26F9 1F3FC 200D 2640 ; minimally-qualified # ⛹🏼‍♀ woman bouncing ball: medium-light skin tone +26F9 1F3FD 200D 2640 FE0F ; fully-qualified # ⛹🏽‍♀️ woman bouncing ball: medium skin tone +26F9 1F3FD 200D 2640 ; minimally-qualified # ⛹🏽‍♀ woman bouncing ball: medium skin tone +26F9 1F3FE 200D 2640 FE0F ; fully-qualified # ⛹🏾‍♀️ woman bouncing ball: medium-dark skin tone +26F9 1F3FE 200D 2640 ; minimally-qualified # ⛹🏾‍♀ woman bouncing ball: medium-dark skin tone +26F9 1F3FF 200D 2640 FE0F ; fully-qualified # ⛹🏿‍♀️ woman bouncing ball: dark skin tone +26F9 1F3FF 200D 2640 ; minimally-qualified # ⛹🏿‍♀ woman bouncing ball: dark skin tone +1F3CB FE0F ; fully-qualified # 🏋️ person lifting weights +1F3CB ; unqualified # 🏋 person lifting weights +1F3CB 1F3FB ; fully-qualified # 🏋🏻 person lifting weights: light skin tone +1F3CB 1F3FC ; fully-qualified # 🏋🏼 person lifting weights: medium-light skin tone +1F3CB 1F3FD ; fully-qualified # 🏋🏽 person lifting weights: medium skin tone +1F3CB 1F3FE ; fully-qualified # 🏋🏾 person lifting weights: medium-dark skin tone +1F3CB 1F3FF ; fully-qualified # 🏋🏿 person lifting weights: dark skin tone +1F3CB FE0F 200D 2642 FE0F ; fully-qualified # 🏋️‍♂️ man lifting weights +1F3CB 200D 2642 FE0F ; unqualified # 🏋‍♂️ man lifting weights +1F3CB FE0F 200D 2642 ; unqualified # 🏋️‍♂ man lifting weights +1F3CB 200D 2642 ; unqualified # 🏋‍♂ man lifting weights +1F3CB 1F3FB 200D 2642 FE0F ; fully-qualified # 🏋🏻‍♂️ man lifting weights: light skin tone +1F3CB 1F3FB 200D 2642 ; minimally-qualified # 🏋🏻‍♂ man lifting weights: light skin tone +1F3CB 1F3FC 200D 2642 FE0F ; fully-qualified # 🏋🏼‍♂️ man lifting weights: medium-light skin tone +1F3CB 1F3FC 200D 2642 ; minimally-qualified # 🏋🏼‍♂ man lifting weights: medium-light skin tone +1F3CB 1F3FD 200D 2642 FE0F ; fully-qualified # 🏋🏽‍♂️ man lifting weights: medium skin tone +1F3CB 1F3FD 200D 2642 ; minimally-qualified # 🏋🏽‍♂ man lifting weights: medium skin tone +1F3CB 1F3FE 200D 2642 FE0F ; fully-qualified # 🏋🏾‍♂️ man lifting weights: medium-dark skin tone +1F3CB 1F3FE 200D 2642 ; minimally-qualified # 🏋🏾‍♂ man lifting weights: medium-dark skin tone +1F3CB 1F3FF 200D 2642 FE0F ; fully-qualified # 🏋🏿‍♂️ man lifting weights: dark skin tone +1F3CB 1F3FF 200D 2642 ; minimally-qualified # 🏋🏿‍♂ man lifting weights: dark skin tone +1F3CB FE0F 200D 2640 FE0F ; fully-qualified # 🏋️‍♀️ woman lifting weights +1F3CB 200D 2640 FE0F ; unqualified # 🏋‍♀️ woman lifting weights +1F3CB FE0F 200D 2640 ; unqualified # 🏋️‍♀ woman lifting weights +1F3CB 200D 2640 ; unqualified # 🏋‍♀ woman lifting weights +1F3CB 1F3FB 200D 2640 FE0F ; fully-qualified # 🏋🏻‍♀️ woman lifting weights: light skin tone +1F3CB 1F3FB 200D 2640 ; minimally-qualified # 🏋🏻‍♀ woman lifting weights: light skin tone +1F3CB 1F3FC 200D 2640 FE0F ; fully-qualified # 🏋🏼‍♀️ woman lifting weights: medium-light skin tone +1F3CB 1F3FC 200D 2640 ; minimally-qualified # 🏋🏼‍♀ woman lifting weights: medium-light skin tone +1F3CB 1F3FD 200D 2640 FE0F ; fully-qualified # 🏋🏽‍♀️ woman lifting weights: medium skin tone +1F3CB 1F3FD 200D 2640 ; minimally-qualified # 🏋🏽‍♀ woman lifting weights: medium skin tone +1F3CB 1F3FE 200D 2640 FE0F ; fully-qualified # 🏋🏾‍♀️ woman lifting weights: medium-dark skin tone +1F3CB 1F3FE 200D 2640 ; minimally-qualified # 🏋🏾‍♀ woman lifting weights: medium-dark skin tone +1F3CB 1F3FF 200D 2640 FE0F ; fully-qualified # 🏋🏿‍♀️ woman lifting weights: dark skin tone +1F3CB 1F3FF 200D 2640 ; minimally-qualified # 🏋🏿‍♀ woman lifting weights: dark skin tone +1F6B4 ; fully-qualified # 🚴 person biking +1F6B4 1F3FB ; fully-qualified # 🚴🏻 person biking: light skin tone +1F6B4 1F3FC ; fully-qualified # 🚴🏼 person biking: medium-light skin tone +1F6B4 1F3FD ; fully-qualified # 🚴🏽 person biking: medium skin tone +1F6B4 1F3FE ; fully-qualified # 🚴🏾 person biking: medium-dark skin tone +1F6B4 1F3FF ; fully-qualified # 🚴🏿 person biking: dark skin tone +1F6B4 200D 2642 FE0F ; fully-qualified # 🚴‍♂️ man biking +1F6B4 200D 2642 ; minimally-qualified # 🚴‍♂ man biking +1F6B4 1F3FB 200D 2642 FE0F ; fully-qualified # 🚴🏻‍♂️ man biking: light skin tone +1F6B4 1F3FB 200D 2642 ; minimally-qualified # 🚴🏻‍♂ man biking: light skin tone +1F6B4 1F3FC 200D 2642 FE0F ; fully-qualified # 🚴🏼‍♂️ man biking: medium-light skin tone +1F6B4 1F3FC 200D 2642 ; minimally-qualified # 🚴🏼‍♂ man biking: medium-light skin tone +1F6B4 1F3FD 200D 2642 FE0F ; fully-qualified # 🚴🏽‍♂️ man biking: medium skin tone +1F6B4 1F3FD 200D 2642 ; minimally-qualified # 🚴🏽‍♂ man biking: medium skin tone +1F6B4 1F3FE 200D 2642 FE0F ; fully-qualified # 🚴🏾‍♂️ man biking: medium-dark skin tone +1F6B4 1F3FE 200D 2642 ; minimally-qualified # 🚴🏾‍♂ man biking: medium-dark skin tone +1F6B4 1F3FF 200D 2642 FE0F ; fully-qualified # 🚴🏿‍♂️ man biking: dark skin tone +1F6B4 1F3FF 200D 2642 ; minimally-qualified # 🚴🏿‍♂ man biking: dark skin tone +1F6B4 200D 2640 FE0F ; fully-qualified # 🚴‍♀️ woman biking +1F6B4 200D 2640 ; minimally-qualified # 🚴‍♀ woman biking +1F6B4 1F3FB 200D 2640 FE0F ; fully-qualified # 🚴🏻‍♀️ woman biking: light skin tone +1F6B4 1F3FB 200D 2640 ; minimally-qualified # 🚴🏻‍♀ woman biking: light skin tone +1F6B4 1F3FC 200D 2640 FE0F ; fully-qualified # 🚴🏼‍♀️ woman biking: medium-light skin tone +1F6B4 1F3FC 200D 2640 ; minimally-qualified # 🚴🏼‍♀ woman biking: medium-light skin tone +1F6B4 1F3FD 200D 2640 FE0F ; fully-qualified # 🚴🏽‍♀️ woman biking: medium skin tone +1F6B4 1F3FD 200D 2640 ; minimally-qualified # 🚴🏽‍♀ woman biking: medium skin tone +1F6B4 1F3FE 200D 2640 FE0F ; fully-qualified # 🚴🏾‍♀️ woman biking: medium-dark skin tone +1F6B4 1F3FE 200D 2640 ; minimally-qualified # 🚴🏾‍♀ woman biking: medium-dark skin tone +1F6B4 1F3FF 200D 2640 FE0F ; fully-qualified # 🚴🏿‍♀️ woman biking: dark skin tone +1F6B4 1F3FF 200D 2640 ; minimally-qualified # 🚴🏿‍♀ woman biking: dark skin tone +1F6B5 ; fully-qualified # 🚵 person mountain biking +1F6B5 1F3FB ; fully-qualified # 🚵🏻 person mountain biking: light skin tone +1F6B5 1F3FC ; fully-qualified # 🚵🏼 person mountain biking: medium-light skin tone +1F6B5 1F3FD ; fully-qualified # 🚵🏽 person mountain biking: medium skin tone +1F6B5 1F3FE ; fully-qualified # 🚵🏾 person mountain biking: medium-dark skin tone +1F6B5 1F3FF ; fully-qualified # 🚵🏿 person mountain biking: dark skin tone +1F6B5 200D 2642 FE0F ; fully-qualified # 🚵‍♂️ man mountain biking +1F6B5 200D 2642 ; minimally-qualified # 🚵‍♂ man mountain biking +1F6B5 1F3FB 200D 2642 FE0F ; fully-qualified # 🚵🏻‍♂️ man mountain biking: light skin tone +1F6B5 1F3FB 200D 2642 ; minimally-qualified # 🚵🏻‍♂ man mountain biking: light skin tone +1F6B5 1F3FC 200D 2642 FE0F ; fully-qualified # 🚵🏼‍♂️ man mountain biking: medium-light skin tone +1F6B5 1F3FC 200D 2642 ; minimally-qualified # 🚵🏼‍♂ man mountain biking: medium-light skin tone +1F6B5 1F3FD 200D 2642 FE0F ; fully-qualified # 🚵🏽‍♂️ man mountain biking: medium skin tone +1F6B5 1F3FD 200D 2642 ; minimally-qualified # 🚵🏽‍♂ man mountain biking: medium skin tone +1F6B5 1F3FE 200D 2642 FE0F ; fully-qualified # 🚵🏾‍♂️ man mountain biking: medium-dark skin tone +1F6B5 1F3FE 200D 2642 ; minimally-qualified # 🚵🏾‍♂ man mountain biking: medium-dark skin tone +1F6B5 1F3FF 200D 2642 FE0F ; fully-qualified # 🚵🏿‍♂️ man mountain biking: dark skin tone +1F6B5 1F3FF 200D 2642 ; minimally-qualified # 🚵🏿‍♂ man mountain biking: dark skin tone +1F6B5 200D 2640 FE0F ; fully-qualified # 🚵‍♀️ woman mountain biking +1F6B5 200D 2640 ; minimally-qualified # 🚵‍♀ woman mountain biking +1F6B5 1F3FB 200D 2640 FE0F ; fully-qualified # 🚵🏻‍♀️ woman mountain biking: light skin tone +1F6B5 1F3FB 200D 2640 ; minimally-qualified # 🚵🏻‍♀ woman mountain biking: light skin tone +1F6B5 1F3FC 200D 2640 FE0F ; fully-qualified # 🚵🏼‍♀️ woman mountain biking: medium-light skin tone +1F6B5 1F3FC 200D 2640 ; minimally-qualified # 🚵🏼‍♀ woman mountain biking: medium-light skin tone +1F6B5 1F3FD 200D 2640 FE0F ; fully-qualified # 🚵🏽‍♀️ woman mountain biking: medium skin tone +1F6B5 1F3FD 200D 2640 ; minimally-qualified # 🚵🏽‍♀ woman mountain biking: medium skin tone +1F6B5 1F3FE 200D 2640 FE0F ; fully-qualified # 🚵🏾‍♀️ woman mountain biking: medium-dark skin tone +1F6B5 1F3FE 200D 2640 ; minimally-qualified # 🚵🏾‍♀ woman mountain biking: medium-dark skin tone +1F6B5 1F3FF 200D 2640 FE0F ; fully-qualified # 🚵🏿‍♀️ woman mountain biking: dark skin tone +1F6B5 1F3FF 200D 2640 ; minimally-qualified # 🚵🏿‍♀ woman mountain biking: dark skin tone +1F938 ; fully-qualified # 🤸 person cartwheeling +1F938 1F3FB ; fully-qualified # 🤸🏻 person cartwheeling: light skin tone +1F938 1F3FC ; fully-qualified # 🤸🏼 person cartwheeling: medium-light skin tone +1F938 1F3FD ; fully-qualified # 🤸🏽 person cartwheeling: medium skin tone +1F938 1F3FE ; fully-qualified # 🤸🏾 person cartwheeling: medium-dark skin tone +1F938 1F3FF ; fully-qualified # 🤸🏿 person cartwheeling: dark skin tone +1F938 200D 2642 FE0F ; fully-qualified # 🤸‍♂️ man cartwheeling +1F938 200D 2642 ; minimally-qualified # 🤸‍♂ man cartwheeling +1F938 1F3FB 200D 2642 FE0F ; fully-qualified # 🤸🏻‍♂️ man cartwheeling: light skin tone +1F938 1F3FB 200D 2642 ; minimally-qualified # 🤸🏻‍♂ man cartwheeling: light skin tone +1F938 1F3FC 200D 2642 FE0F ; fully-qualified # 🤸🏼‍♂️ man cartwheeling: medium-light skin tone +1F938 1F3FC 200D 2642 ; minimally-qualified # 🤸🏼‍♂ man cartwheeling: medium-light skin tone +1F938 1F3FD 200D 2642 FE0F ; fully-qualified # 🤸🏽‍♂️ man cartwheeling: medium skin tone +1F938 1F3FD 200D 2642 ; minimally-qualified # 🤸🏽‍♂ man cartwheeling: medium skin tone +1F938 1F3FE 200D 2642 FE0F ; fully-qualified # 🤸🏾‍♂️ man cartwheeling: medium-dark skin tone +1F938 1F3FE 200D 2642 ; minimally-qualified # 🤸🏾‍♂ man cartwheeling: medium-dark skin tone +1F938 1F3FF 200D 2642 FE0F ; fully-qualified # 🤸🏿‍♂️ man cartwheeling: dark skin tone +1F938 1F3FF 200D 2642 ; minimally-qualified # 🤸🏿‍♂ man cartwheeling: dark skin tone +1F938 200D 2640 FE0F ; fully-qualified # 🤸‍♀️ woman cartwheeling +1F938 200D 2640 ; minimally-qualified # 🤸‍♀ woman cartwheeling +1F938 1F3FB 200D 2640 FE0F ; fully-qualified # 🤸🏻‍♀️ woman cartwheeling: light skin tone +1F938 1F3FB 200D 2640 ; minimally-qualified # 🤸🏻‍♀ woman cartwheeling: light skin tone +1F938 1F3FC 200D 2640 FE0F ; fully-qualified # 🤸🏼‍♀️ woman cartwheeling: medium-light skin tone +1F938 1F3FC 200D 2640 ; minimally-qualified # 🤸🏼‍♀ woman cartwheeling: medium-light skin tone +1F938 1F3FD 200D 2640 FE0F ; fully-qualified # 🤸🏽‍♀️ woman cartwheeling: medium skin tone +1F938 1F3FD 200D 2640 ; minimally-qualified # 🤸🏽‍♀ woman cartwheeling: medium skin tone +1F938 1F3FE 200D 2640 FE0F ; fully-qualified # 🤸🏾‍♀️ woman cartwheeling: medium-dark skin tone +1F938 1F3FE 200D 2640 ; minimally-qualified # 🤸🏾‍♀ woman cartwheeling: medium-dark skin tone +1F938 1F3FF 200D 2640 FE0F ; fully-qualified # 🤸🏿‍♀️ woman cartwheeling: dark skin tone +1F938 1F3FF 200D 2640 ; minimally-qualified # 🤸🏿‍♀ woman cartwheeling: dark skin tone +1F93C ; fully-qualified # 🤼 people wrestling +1F93C 1F3FB ; fully-qualified # 🤼🏻 people wrestling: light skin tone +1F93C 1F3FC ; fully-qualified # 🤼🏼 people wrestling: medium-light skin tone +1F93C 1F3FD ; fully-qualified # 🤼🏽 people wrestling: medium skin tone +1F93C 1F3FE ; fully-qualified # 🤼🏾 people wrestling: medium-dark skin tone +1F93C 1F3FF ; fully-qualified # 🤼🏿 people wrestling: dark skin tone +1F93C 200D 2642 FE0F ; fully-qualified # 🤼‍♂️ men wrestling +1F93C 200D 2642 ; minimally-qualified # 🤼‍♂ men wrestling +1F93C 1F3FB 200D 2642 FE0F ; fully-qualified # 🤼🏻‍♂️ men wrestling: light skin tone +1F93C 1F3FB 200D 2642 ; minimally-qualified # 🤼🏻‍♂ men wrestling: light skin tone +1F93C 1F3FC 200D 2642 FE0F ; fully-qualified # 🤼🏼‍♂️ men wrestling: medium-light skin tone +1F93C 1F3FC 200D 2642 ; minimally-qualified # 🤼🏼‍♂ men wrestling: medium-light skin tone +1F93C 1F3FD 200D 2642 FE0F ; fully-qualified # 🤼🏽‍♂️ men wrestling: medium skin tone +1F93C 1F3FD 200D 2642 ; minimally-qualified # 🤼🏽‍♂ men wrestling: medium skin tone +1F93C 1F3FE 200D 2642 FE0F ; fully-qualified # 🤼🏾‍♂️ men wrestling: medium-dark skin tone +1F93C 1F3FE 200D 2642 ; minimally-qualified # 🤼🏾‍♂ men wrestling: medium-dark skin tone +1F93C 1F3FF 200D 2642 FE0F ; fully-qualified # 🤼🏿‍♂️ men wrestling: dark skin tone +1F93C 1F3FF 200D 2642 ; minimally-qualified # 🤼🏿‍♂ men wrestling: dark skin tone +1F93C 200D 2640 FE0F ; fully-qualified # 🤼‍♀️ women wrestling +1F93C 200D 2640 ; minimally-qualified # 🤼‍♀ women wrestling +1F93C 1F3FB 200D 2640 FE0F ; fully-qualified # 🤼🏻‍♀️ women wrestling: light skin tone +1F93C 1F3FB 200D 2640 ; minimally-qualified # 🤼🏻‍♀ women wrestling: light skin tone +1F93C 1F3FC 200D 2640 FE0F ; fully-qualified # 🤼🏼‍♀️ women wrestling: medium-light skin tone +1F93C 1F3FC 200D 2640 ; minimally-qualified # 🤼🏼‍♀ women wrestling: medium-light skin tone +1F93C 1F3FD 200D 2640 FE0F ; fully-qualified # 🤼🏽‍♀️ women wrestling: medium skin tone +1F93C 1F3FD 200D 2640 ; minimally-qualified # 🤼🏽‍♀ women wrestling: medium skin tone +1F93C 1F3FE 200D 2640 FE0F ; fully-qualified # 🤼🏾‍♀️ women wrestling: medium-dark skin tone +1F93C 1F3FE 200D 2640 ; minimally-qualified # 🤼🏾‍♀ women wrestling: medium-dark skin tone +1F93C 1F3FF 200D 2640 FE0F ; fully-qualified # 🤼🏿‍♀️ women wrestling: dark skin tone +1F93C 1F3FF 200D 2640 ; minimally-qualified # 🤼🏿‍♀ women wrestling: dark skin tone +1F93D ; fully-qualified # 🤽 person playing water polo +1F93D 1F3FB ; fully-qualified # 🤽🏻 person playing water polo: light skin tone +1F93D 1F3FC ; fully-qualified # 🤽🏼 person playing water polo: medium-light skin tone +1F93D 1F3FD ; fully-qualified # 🤽🏽 person playing water polo: medium skin tone +1F93D 1F3FE ; fully-qualified # 🤽🏾 person playing water polo: medium-dark skin tone +1F93D 1F3FF ; fully-qualified # 🤽🏿 person playing water polo: dark skin tone +1F93D 200D 2642 FE0F ; fully-qualified # 🤽‍♂️ man playing water polo +1F93D 200D 2642 ; minimally-qualified # 🤽‍♂ man playing water polo +1F93D 1F3FB 200D 2642 FE0F ; fully-qualified # 🤽🏻‍♂️ man playing water polo: light skin tone +1F93D 1F3FB 200D 2642 ; minimally-qualified # 🤽🏻‍♂ man playing water polo: light skin tone +1F93D 1F3FC 200D 2642 FE0F ; fully-qualified # 🤽🏼‍♂️ man playing water polo: medium-light skin tone +1F93D 1F3FC 200D 2642 ; minimally-qualified # 🤽🏼‍♂ man playing water polo: medium-light skin tone +1F93D 1F3FD 200D 2642 FE0F ; fully-qualified # 🤽🏽‍♂️ man playing water polo: medium skin tone +1F93D 1F3FD 200D 2642 ; minimally-qualified # 🤽🏽‍♂ man playing water polo: medium skin tone +1F93D 1F3FE 200D 2642 FE0F ; fully-qualified # 🤽🏾‍♂️ man playing water polo: medium-dark skin tone +1F93D 1F3FE 200D 2642 ; minimally-qualified # 🤽🏾‍♂ man playing water polo: medium-dark skin tone +1F93D 1F3FF 200D 2642 FE0F ; fully-qualified # 🤽🏿‍♂️ man playing water polo: dark skin tone +1F93D 1F3FF 200D 2642 ; minimally-qualified # 🤽🏿‍♂ man playing water polo: dark skin tone +1F93D 200D 2640 FE0F ; fully-qualified # 🤽‍♀️ woman playing water polo +1F93D 200D 2640 ; minimally-qualified # 🤽‍♀ woman playing water polo +1F93D 1F3FB 200D 2640 FE0F ; fully-qualified # 🤽🏻‍♀️ woman playing water polo: light skin tone +1F93D 1F3FB 200D 2640 ; minimally-qualified # 🤽🏻‍♀ woman playing water polo: light skin tone +1F93D 1F3FC 200D 2640 FE0F ; fully-qualified # 🤽🏼‍♀️ woman playing water polo: medium-light skin tone +1F93D 1F3FC 200D 2640 ; minimally-qualified # 🤽🏼‍♀ woman playing water polo: medium-light skin tone +1F93D 1F3FD 200D 2640 FE0F ; fully-qualified # 🤽🏽‍♀️ woman playing water polo: medium skin tone +1F93D 1F3FD 200D 2640 ; minimally-qualified # 🤽🏽‍♀ woman playing water polo: medium skin tone +1F93D 1F3FE 200D 2640 FE0F ; fully-qualified # 🤽🏾‍♀️ woman playing water polo: medium-dark skin tone +1F93D 1F3FE 200D 2640 ; minimally-qualified # 🤽🏾‍♀ woman playing water polo: medium-dark skin tone +1F93D 1F3FF 200D 2640 FE0F ; fully-qualified # 🤽🏿‍♀️ woman playing water polo: dark skin tone +1F93D 1F3FF 200D 2640 ; minimally-qualified # 🤽🏿‍♀ woman playing water polo: dark skin tone +1F93E ; fully-qualified # 🤾 person playing handball +1F93E 1F3FB ; fully-qualified # 🤾🏻 person playing handball: light skin tone +1F93E 1F3FC ; fully-qualified # 🤾🏼 person playing handball: medium-light skin tone +1F93E 1F3FD ; fully-qualified # 🤾🏽 person playing handball: medium skin tone +1F93E 1F3FE ; fully-qualified # 🤾🏾 person playing handball: medium-dark skin tone +1F93E 1F3FF ; fully-qualified # 🤾🏿 person playing handball: dark skin tone +1F93E 200D 2642 FE0F ; fully-qualified # 🤾‍♂️ man playing handball +1F93E 200D 2642 ; minimally-qualified # 🤾‍♂ man playing handball +1F93E 1F3FB 200D 2642 FE0F ; fully-qualified # 🤾🏻‍♂️ man playing handball: light skin tone +1F93E 1F3FB 200D 2642 ; minimally-qualified # 🤾🏻‍♂ man playing handball: light skin tone +1F93E 1F3FC 200D 2642 FE0F ; fully-qualified # 🤾🏼‍♂️ man playing handball: medium-light skin tone +1F93E 1F3FC 200D 2642 ; minimally-qualified # 🤾🏼‍♂ man playing handball: medium-light skin tone +1F93E 1F3FD 200D 2642 FE0F ; fully-qualified # 🤾🏽‍♂️ man playing handball: medium skin tone +1F93E 1F3FD 200D 2642 ; minimally-qualified # 🤾🏽‍♂ man playing handball: medium skin tone +1F93E 1F3FE 200D 2642 FE0F ; fully-qualified # 🤾🏾‍♂️ man playing handball: medium-dark skin tone +1F93E 1F3FE 200D 2642 ; minimally-qualified # 🤾🏾‍♂ man playing handball: medium-dark skin tone +1F93E 1F3FF 200D 2642 FE0F ; fully-qualified # 🤾🏿‍♂️ man playing handball: dark skin tone +1F93E 1F3FF 200D 2642 ; minimally-qualified # 🤾🏿‍♂ man playing handball: dark skin tone +1F93E 200D 2640 FE0F ; fully-qualified # 🤾‍♀️ woman playing handball +1F93E 200D 2640 ; minimally-qualified # 🤾‍♀ woman playing handball +1F93E 1F3FB 200D 2640 FE0F ; fully-qualified # 🤾🏻‍♀️ woman playing handball: light skin tone +1F93E 1F3FB 200D 2640 ; minimally-qualified # 🤾🏻‍♀ woman playing handball: light skin tone +1F93E 1F3FC 200D 2640 FE0F ; fully-qualified # 🤾🏼‍♀️ woman playing handball: medium-light skin tone +1F93E 1F3FC 200D 2640 ; minimally-qualified # 🤾🏼‍♀ woman playing handball: medium-light skin tone +1F93E 1F3FD 200D 2640 FE0F ; fully-qualified # 🤾🏽‍♀️ woman playing handball: medium skin tone +1F93E 1F3FD 200D 2640 ; minimally-qualified # 🤾🏽‍♀ woman playing handball: medium skin tone +1F93E 1F3FE 200D 2640 FE0F ; fully-qualified # 🤾🏾‍♀️ woman playing handball: medium-dark skin tone +1F93E 1F3FE 200D 2640 ; minimally-qualified # 🤾🏾‍♀ woman playing handball: medium-dark skin tone +1F93E 1F3FF 200D 2640 FE0F ; fully-qualified # 🤾🏿‍♀️ woman playing handball: dark skin tone +1F93E 1F3FF 200D 2640 ; minimally-qualified # 🤾🏿‍♀ woman playing handball: dark skin tone +1F939 ; fully-qualified # 🤹 person juggling +1F939 1F3FB ; fully-qualified # 🤹🏻 person juggling: light skin tone +1F939 1F3FC ; fully-qualified # 🤹🏼 person juggling: medium-light skin tone +1F939 1F3FD ; fully-qualified # 🤹🏽 person juggling: medium skin tone +1F939 1F3FE ; fully-qualified # 🤹🏾 person juggling: medium-dark skin tone +1F939 1F3FF ; fully-qualified # 🤹🏿 person juggling: dark skin tone +1F939 200D 2642 FE0F ; fully-qualified # 🤹‍♂️ man juggling +1F939 200D 2642 ; minimally-qualified # 🤹‍♂ man juggling +1F939 1F3FB 200D 2642 FE0F ; fully-qualified # 🤹🏻‍♂️ man juggling: light skin tone +1F939 1F3FB 200D 2642 ; minimally-qualified # 🤹🏻‍♂ man juggling: light skin tone +1F939 1F3FC 200D 2642 FE0F ; fully-qualified # 🤹🏼‍♂️ man juggling: medium-light skin tone +1F939 1F3FC 200D 2642 ; minimally-qualified # 🤹🏼‍♂ man juggling: medium-light skin tone +1F939 1F3FD 200D 2642 FE0F ; fully-qualified # 🤹🏽‍♂️ man juggling: medium skin tone +1F939 1F3FD 200D 2642 ; minimally-qualified # 🤹🏽‍♂ man juggling: medium skin tone +1F939 1F3FE 200D 2642 FE0F ; fully-qualified # 🤹🏾‍♂️ man juggling: medium-dark skin tone +1F939 1F3FE 200D 2642 ; minimally-qualified # 🤹🏾‍♂ man juggling: medium-dark skin tone +1F939 1F3FF 200D 2642 FE0F ; fully-qualified # 🤹🏿‍♂️ man juggling: dark skin tone +1F939 1F3FF 200D 2642 ; minimally-qualified # 🤹🏿‍♂ man juggling: dark skin tone +1F939 200D 2640 FE0F ; fully-qualified # 🤹‍♀️ woman juggling +1F939 200D 2640 ; minimally-qualified # 🤹‍♀ woman juggling +1F939 1F3FB 200D 2640 FE0F ; fully-qualified # 🤹🏻‍♀️ woman juggling: light skin tone +1F939 1F3FB 200D 2640 ; minimally-qualified # 🤹🏻‍♀ woman juggling: light skin tone +1F939 1F3FC 200D 2640 FE0F ; fully-qualified # 🤹🏼‍♀️ woman juggling: medium-light skin tone +1F939 1F3FC 200D 2640 ; minimally-qualified # 🤹🏼‍♀ woman juggling: medium-light skin tone +1F939 1F3FD 200D 2640 FE0F ; fully-qualified # 🤹🏽‍♀️ woman juggling: medium skin tone +1F939 1F3FD 200D 2640 ; minimally-qualified # 🤹🏽‍♀ woman juggling: medium skin tone +1F939 1F3FE 200D 2640 FE0F ; fully-qualified # 🤹🏾‍♀️ woman juggling: medium-dark skin tone +1F939 1F3FE 200D 2640 ; minimally-qualified # 🤹🏾‍♀ woman juggling: medium-dark skin tone +1F939 1F3FF 200D 2640 FE0F ; fully-qualified # 🤹🏿‍♀️ woman juggling: dark skin tone +1F939 1F3FF 200D 2640 ; minimally-qualified # 🤹🏿‍♀ woman juggling: dark skin tone + +# subgroup: person-resting +1F9D8 ; fully-qualified # 🧘 person in lotus position +1F9D8 1F3FB ; fully-qualified # 🧘🏻 person in lotus position: light skin tone +1F9D8 1F3FC ; fully-qualified # 🧘🏼 person in lotus position: medium-light skin tone +1F9D8 1F3FD ; fully-qualified # 🧘🏽 person in lotus position: medium skin tone +1F9D8 1F3FE ; fully-qualified # 🧘🏾 person in lotus position: medium-dark skin tone +1F9D8 1F3FF ; fully-qualified # 🧘🏿 person in lotus position: dark skin tone +1F9D8 200D 2642 FE0F ; fully-qualified # 🧘‍♂️ man in lotus position +1F9D8 200D 2642 ; minimally-qualified # 🧘‍♂ man in lotus position +1F9D8 1F3FB 200D 2642 FE0F ; fully-qualified # 🧘🏻‍♂️ man in lotus position: light skin tone +1F9D8 1F3FB 200D 2642 ; minimally-qualified # 🧘🏻‍♂ man in lotus position: light skin tone +1F9D8 1F3FC 200D 2642 FE0F ; fully-qualified # 🧘🏼‍♂️ man in lotus position: medium-light skin tone +1F9D8 1F3FC 200D 2642 ; minimally-qualified # 🧘🏼‍♂ man in lotus position: medium-light skin tone +1F9D8 1F3FD 200D 2642 FE0F ; fully-qualified # 🧘🏽‍♂️ man in lotus position: medium skin tone +1F9D8 1F3FD 200D 2642 ; minimally-qualified # 🧘🏽‍♂ man in lotus position: medium skin tone +1F9D8 1F3FE 200D 2642 FE0F ; fully-qualified # 🧘🏾‍♂️ man in lotus position: medium-dark skin tone +1F9D8 1F3FE 200D 2642 ; minimally-qualified # 🧘🏾‍♂ man in lotus position: medium-dark skin tone +1F9D8 1F3FF 200D 2642 FE0F ; fully-qualified # 🧘🏿‍♂️ man in lotus position: dark skin tone +1F9D8 1F3FF 200D 2642 ; minimally-qualified # 🧘🏿‍♂ man in lotus position: dark skin tone +1F9D8 200D 2640 FE0F ; fully-qualified # 🧘‍♀️ woman in lotus position +1F9D8 200D 2640 ; minimally-qualified # 🧘‍♀ woman in lotus position +1F9D8 1F3FB 200D 2640 FE0F ; fully-qualified # 🧘🏻‍♀️ woman in lotus position: light skin tone +1F9D8 1F3FB 200D 2640 ; minimally-qualified # 🧘🏻‍♀ woman in lotus position: light skin tone +1F9D8 1F3FC 200D 2640 FE0F ; fully-qualified # 🧘🏼‍♀️ woman in lotus position: medium-light skin tone +1F9D8 1F3FC 200D 2640 ; minimally-qualified # 🧘🏼‍♀ woman in lotus position: medium-light skin tone +1F9D8 1F3FD 200D 2640 FE0F ; fully-qualified # 🧘🏽‍♀️ woman in lotus position: medium skin tone +1F9D8 1F3FD 200D 2640 ; minimally-qualified # 🧘🏽‍♀ woman in lotus position: medium skin tone +1F9D8 1F3FE 200D 2640 FE0F ; fully-qualified # 🧘🏾‍♀️ woman in lotus position: medium-dark skin tone +1F9D8 1F3FE 200D 2640 ; minimally-qualified # 🧘🏾‍♀ woman in lotus position: medium-dark skin tone +1F9D8 1F3FF 200D 2640 FE0F ; fully-qualified # 🧘🏿‍♀️ woman in lotus position: dark skin tone +1F9D8 1F3FF 200D 2640 ; minimally-qualified # 🧘🏿‍♀ woman in lotus position: dark skin tone +1F6C0 ; fully-qualified # 🛀 person taking bath +1F6C0 1F3FB ; fully-qualified # 🛀🏻 person taking bath: light skin tone +1F6C0 1F3FC ; fully-qualified # 🛀🏼 person taking bath: medium-light skin tone +1F6C0 1F3FD ; fully-qualified # 🛀🏽 person taking bath: medium skin tone +1F6C0 1F3FE ; fully-qualified # 🛀🏾 person taking bath: medium-dark skin tone +1F6C0 1F3FF ; fully-qualified # 🛀🏿 person taking bath: dark skin tone +1F6CC ; fully-qualified # 🛌 person in bed +1F6CC 1F3FB ; fully-qualified # 🛌🏻 person in bed: light skin tone +1F6CC 1F3FC ; fully-qualified # 🛌🏼 person in bed: medium-light skin tone +1F6CC 1F3FD ; fully-qualified # 🛌🏽 person in bed: medium skin tone +1F6CC 1F3FE ; fully-qualified # 🛌🏾 person in bed: medium-dark skin tone +1F6CC 1F3FF ; fully-qualified # 🛌🏿 person in bed: dark skin tone + +# subgroup: family +1F46D ; fully-qualified # 👭 women holding hands +1F46D 1F3FB ; fully-qualified # 👭🏻 women holding hands: light skin tone +1F469 1F3FC 200D 1F91D 200D 1F469 1F3FB ; fully-qualified # 👩🏼‍🤝‍👩🏻 women holding hands: medium-light skin tone, light skin tone +1F46D 1F3FC ; fully-qualified # 👭🏼 women holding hands: medium-light skin tone +1F469 1F3FD 200D 1F91D 200D 1F469 1F3FB ; fully-qualified # 👩🏽‍🤝‍👩🏻 women holding hands: medium skin tone, light skin tone +1F469 1F3FD 200D 1F91D 200D 1F469 1F3FC ; fully-qualified # 👩🏽‍🤝‍👩🏼 women holding hands: medium skin tone, medium-light skin tone +1F46D 1F3FD ; fully-qualified # 👭🏽 women holding hands: medium skin tone +1F469 1F3FE 200D 1F91D 200D 1F469 1F3FB ; fully-qualified # 👩🏾‍🤝‍👩🏻 women holding hands: medium-dark skin tone, light skin tone +1F469 1F3FE 200D 1F91D 200D 1F469 1F3FC ; fully-qualified # 👩🏾‍🤝‍👩🏼 women holding hands: medium-dark skin tone, medium-light skin tone +1F469 1F3FE 200D 1F91D 200D 1F469 1F3FD ; fully-qualified # 👩🏾‍🤝‍👩🏽 women holding hands: medium-dark skin tone, medium skin tone +1F46D 1F3FE ; fully-qualified # 👭🏾 women holding hands: medium-dark skin tone +1F469 1F3FF 200D 1F91D 200D 1F469 1F3FB ; fully-qualified # 👩🏿‍🤝‍👩🏻 women holding hands: dark skin tone, light skin tone +1F469 1F3FF 200D 1F91D 200D 1F469 1F3FC ; fully-qualified # 👩🏿‍🤝‍👩🏼 women holding hands: dark skin tone, medium-light skin tone +1F469 1F3FF 200D 1F91D 200D 1F469 1F3FD ; fully-qualified # 👩🏿‍🤝‍👩🏽 women holding hands: dark skin tone, medium skin tone +1F469 1F3FF 200D 1F91D 200D 1F469 1F3FE ; fully-qualified # 👩🏿‍🤝‍👩🏾 women holding hands: dark skin tone, medium-dark skin tone +1F46D 1F3FF ; fully-qualified # 👭🏿 women holding hands: dark skin tone +1F46B ; fully-qualified # 👫 woman and man holding hands +1F46B 1F3FB ; fully-qualified # 👫🏻 woman and man holding hands: light skin tone +1F469 1F3FB 200D 1F91D 200D 1F468 1F3FC ; fully-qualified # 👩🏻‍🤝‍👨🏼 woman and man holding hands: light skin tone, medium-light skin tone +1F469 1F3FB 200D 1F91D 200D 1F468 1F3FD ; fully-qualified # 👩🏻‍🤝‍👨🏽 woman and man holding hands: light skin tone, medium skin tone +1F469 1F3FB 200D 1F91D 200D 1F468 1F3FE ; fully-qualified # 👩🏻‍🤝‍👨🏾 woman and man holding hands: light skin tone, medium-dark skin tone +1F469 1F3FB 200D 1F91D 200D 1F468 1F3FF ; fully-qualified # 👩🏻‍🤝‍👨🏿 woman and man holding hands: light skin tone, dark skin tone +1F469 1F3FC 200D 1F91D 200D 1F468 1F3FB ; fully-qualified # 👩🏼‍🤝‍👨🏻 woman and man holding hands: medium-light skin tone, light skin tone +1F46B 1F3FC ; fully-qualified # 👫🏼 woman and man holding hands: medium-light skin tone +1F469 1F3FC 200D 1F91D 200D 1F468 1F3FD ; fully-qualified # 👩🏼‍🤝‍👨🏽 woman and man holding hands: medium-light skin tone, medium skin tone +1F469 1F3FC 200D 1F91D 200D 1F468 1F3FE ; fully-qualified # 👩🏼‍🤝‍👨🏾 woman and man holding hands: medium-light skin tone, medium-dark skin tone +1F469 1F3FC 200D 1F91D 200D 1F468 1F3FF ; fully-qualified # 👩🏼‍🤝‍👨🏿 woman and man holding hands: medium-light skin tone, dark skin tone +1F469 1F3FD 200D 1F91D 200D 1F468 1F3FB ; fully-qualified # 👩🏽‍🤝‍👨🏻 woman and man holding hands: medium skin tone, light skin tone +1F469 1F3FD 200D 1F91D 200D 1F468 1F3FC ; fully-qualified # 👩🏽‍🤝‍👨🏼 woman and man holding hands: medium skin tone, medium-light skin tone +1F46B 1F3FD ; fully-qualified # 👫🏽 woman and man holding hands: medium skin tone +1F469 1F3FD 200D 1F91D 200D 1F468 1F3FE ; fully-qualified # 👩🏽‍🤝‍👨🏾 woman and man holding hands: medium skin tone, medium-dark skin tone +1F469 1F3FD 200D 1F91D 200D 1F468 1F3FF ; fully-qualified # 👩🏽‍🤝‍👨🏿 woman and man holding hands: medium skin tone, dark skin tone +1F469 1F3FE 200D 1F91D 200D 1F468 1F3FB ; fully-qualified # 👩🏾‍🤝‍👨🏻 woman and man holding hands: medium-dark skin tone, light skin tone +1F469 1F3FE 200D 1F91D 200D 1F468 1F3FC ; fully-qualified # 👩🏾‍🤝‍👨🏼 woman and man holding hands: medium-dark skin tone, medium-light skin tone +1F469 1F3FE 200D 1F91D 200D 1F468 1F3FD ; fully-qualified # 👩🏾‍🤝‍👨🏽 woman and man holding hands: medium-dark skin tone, medium skin tone +1F46B 1F3FE ; fully-qualified # 👫🏾 woman and man holding hands: medium-dark skin tone +1F469 1F3FE 200D 1F91D 200D 1F468 1F3FF ; fully-qualified # 👩🏾‍🤝‍👨🏿 woman and man holding hands: medium-dark skin tone, dark skin tone +1F469 1F3FF 200D 1F91D 200D 1F468 1F3FB ; fully-qualified # 👩🏿‍🤝‍👨🏻 woman and man holding hands: dark skin tone, light skin tone +1F469 1F3FF 200D 1F91D 200D 1F468 1F3FC ; fully-qualified # 👩🏿‍🤝‍👨🏼 woman and man holding hands: dark skin tone, medium-light skin tone +1F469 1F3FF 200D 1F91D 200D 1F468 1F3FD ; fully-qualified # 👩🏿‍🤝‍👨🏽 woman and man holding hands: dark skin tone, medium skin tone +1F469 1F3FF 200D 1F91D 200D 1F468 1F3FE ; fully-qualified # 👩🏿‍🤝‍👨🏾 woman and man holding hands: dark skin tone, medium-dark skin tone +1F46B 1F3FF ; fully-qualified # 👫🏿 woman and man holding hands: dark skin tone +1F46C ; fully-qualified # 👬 men holding hands +1F46C 1F3FB ; fully-qualified # 👬🏻 men holding hands: light skin tone +1F468 1F3FC 200D 1F91D 200D 1F468 1F3FB ; fully-qualified # 👨🏼‍🤝‍👨🏻 men holding hands: medium-light skin tone, light skin tone +1F46C 1F3FC ; fully-qualified # 👬🏼 men holding hands: medium-light skin tone +1F468 1F3FD 200D 1F91D 200D 1F468 1F3FB ; fully-qualified # 👨🏽‍🤝‍👨🏻 men holding hands: medium skin tone, light skin tone +1F468 1F3FD 200D 1F91D 200D 1F468 1F3FC ; fully-qualified # 👨🏽‍🤝‍👨🏼 men holding hands: medium skin tone, medium-light skin tone +1F46C 1F3FD ; fully-qualified # 👬🏽 men holding hands: medium skin tone +1F468 1F3FE 200D 1F91D 200D 1F468 1F3FB ; fully-qualified # 👨🏾‍🤝‍👨🏻 men holding hands: medium-dark skin tone, light skin tone +1F468 1F3FE 200D 1F91D 200D 1F468 1F3FC ; fully-qualified # 👨🏾‍🤝‍👨🏼 men holding hands: medium-dark skin tone, medium-light skin tone +1F468 1F3FE 200D 1F91D 200D 1F468 1F3FD ; fully-qualified # 👨🏾‍🤝‍👨🏽 men holding hands: medium-dark skin tone, medium skin tone +1F46C 1F3FE ; fully-qualified # 👬🏾 men holding hands: medium-dark skin tone +1F468 1F3FF 200D 1F91D 200D 1F468 1F3FB ; fully-qualified # 👨🏿‍🤝‍👨🏻 men holding hands: dark skin tone, light skin tone +1F468 1F3FF 200D 1F91D 200D 1F468 1F3FC ; fully-qualified # 👨🏿‍🤝‍👨🏼 men holding hands: dark skin tone, medium-light skin tone +1F468 1F3FF 200D 1F91D 200D 1F468 1F3FD ; fully-qualified # 👨🏿‍🤝‍👨🏽 men holding hands: dark skin tone, medium skin tone +1F468 1F3FF 200D 1F91D 200D 1F468 1F3FE ; fully-qualified # 👨🏿‍🤝‍👨🏾 men holding hands: dark skin tone, medium-dark skin tone +1F46C 1F3FF ; fully-qualified # 👬🏿 men holding hands: dark skin tone +1F48F ; fully-qualified # 💏 kiss +1F48F 1F3FB ; fully-qualified # 💏🏻 kiss: light skin tone +1F48F 1F3FC ; fully-qualified # 💏🏼 kiss: medium-light skin tone +1F48F 1F3FD ; fully-qualified # 💏🏽 kiss: medium skin tone +1F48F 1F3FE ; fully-qualified # 💏🏾 kiss: medium-dark skin tone +1F48F 1F3FF ; fully-qualified # 💏🏿 kiss: dark skin tone +1F469 200D 2764 FE0F 200D 1F48B 200D 1F468 ; fully-qualified # 👩‍❤️‍💋‍👨 kiss: woman, man +1F469 200D 2764 200D 1F48B 200D 1F468 ; minimally-qualified # 👩‍❤‍💋‍👨 kiss: woman, man +1F468 200D 2764 FE0F 200D 1F48B 200D 1F468 ; fully-qualified # 👨‍❤️‍💋‍👨 kiss: man, man +1F468 200D 2764 200D 1F48B 200D 1F468 ; minimally-qualified # 👨‍❤‍💋‍👨 kiss: man, man +1F469 200D 2764 FE0F 200D 1F48B 200D 1F469 ; fully-qualified # 👩‍❤️‍💋‍👩 kiss: woman, woman +1F469 200D 2764 200D 1F48B 200D 1F469 ; minimally-qualified # 👩‍❤‍💋‍👩 kiss: woman, woman +1F491 ; fully-qualified # 💑 couple with heart +1F491 1F3FB ; fully-qualified # 💑🏻 couple with heart: light skin tone +1F491 1F3FC ; fully-qualified # 💑🏼 couple with heart: medium-light skin tone +1F491 1F3FD ; fully-qualified # 💑🏽 couple with heart: medium skin tone +1F491 1F3FE ; fully-qualified # 💑🏾 couple with heart: medium-dark skin tone +1F491 1F3FF ; fully-qualified # 💑🏿 couple with heart: dark skin tone +1F469 200D 2764 FE0F 200D 1F468 ; fully-qualified # 👩‍❤️‍👨 couple with heart: woman, man +1F469 200D 2764 200D 1F468 ; minimally-qualified # 👩‍❤‍👨 couple with heart: woman, man +1F468 200D 2764 FE0F 200D 1F468 ; fully-qualified # 👨‍❤️‍👨 couple with heart: man, man +1F468 200D 2764 200D 1F468 ; minimally-qualified # 👨‍❤‍👨 couple with heart: man, man +1F469 200D 2764 FE0F 200D 1F469 ; fully-qualified # 👩‍❤️‍👩 couple with heart: woman, woman +1F469 200D 2764 200D 1F469 ; minimally-qualified # 👩‍❤‍👩 couple with heart: woman, woman +1F46A ; fully-qualified # 👪 family +1F46A 1F3FB ; fully-qualified # 👪🏻 family: light skin tone +1F46A 1F3FC ; fully-qualified # 👪🏼 family: medium-light skin tone +1F46A 1F3FD ; fully-qualified # 👪🏽 family: medium skin tone +1F46A 1F3FE ; fully-qualified # 👪🏾 family: medium-dark skin tone +1F46A 1F3FF ; fully-qualified # 👪🏿 family: dark skin tone +1F468 200D 1F469 200D 1F466 ; fully-qualified # 👨‍👩‍👦 family: man, woman, boy +1F468 200D 1F469 200D 1F467 ; fully-qualified # 👨‍👩‍👧 family: man, woman, girl +1F468 200D 1F469 200D 1F467 200D 1F466 ; fully-qualified # 👨‍👩‍👧‍👦 family: man, woman, girl, boy +1F468 200D 1F469 200D 1F466 200D 1F466 ; fully-qualified # 👨‍👩‍👦‍👦 family: man, woman, boy, boy +1F468 200D 1F469 200D 1F467 200D 1F467 ; fully-qualified # 👨‍👩‍👧‍👧 family: man, woman, girl, girl +1F468 200D 1F468 200D 1F466 ; fully-qualified # 👨‍👨‍👦 family: man, man, boy +1F468 200D 1F468 200D 1F467 ; fully-qualified # 👨‍👨‍👧 family: man, man, girl +1F468 200D 1F468 200D 1F467 200D 1F466 ; fully-qualified # 👨‍👨‍👧‍👦 family: man, man, girl, boy +1F468 200D 1F468 200D 1F466 200D 1F466 ; fully-qualified # 👨‍👨‍👦‍👦 family: man, man, boy, boy +1F468 200D 1F468 200D 1F467 200D 1F467 ; fully-qualified # 👨‍👨‍👧‍👧 family: man, man, girl, girl +1F469 200D 1F469 200D 1F466 ; fully-qualified # 👩‍👩‍👦 family: woman, woman, boy +1F469 200D 1F469 200D 1F467 ; fully-qualified # 👩‍👩‍👧 family: woman, woman, girl +1F469 200D 1F469 200D 1F467 200D 1F466 ; fully-qualified # 👩‍👩‍👧‍👦 family: woman, woman, girl, boy +1F469 200D 1F469 200D 1F466 200D 1F466 ; fully-qualified # 👩‍👩‍👦‍👦 family: woman, woman, boy, boy +1F469 200D 1F469 200D 1F467 200D 1F467 ; fully-qualified # 👩‍👩‍👧‍👧 family: woman, woman, girl, girl +1F468 200D 1F466 ; fully-qualified # 👨‍👦 family: man, boy +1F468 200D 1F466 200D 1F466 ; fully-qualified # 👨‍👦‍👦 family: man, boy, boy +1F468 200D 1F467 ; fully-qualified # 👨‍👧 family: man, girl +1F468 200D 1F467 200D 1F466 ; fully-qualified # 👨‍👧‍👦 family: man, girl, boy +1F468 200D 1F467 200D 1F467 ; fully-qualified # 👨‍👧‍👧 family: man, girl, girl +1F469 200D 1F466 ; fully-qualified # 👩‍👦 family: woman, boy +1F469 200D 1F466 200D 1F466 ; fully-qualified # 👩‍👦‍👦 family: woman, boy, boy +1F469 200D 1F467 ; fully-qualified # 👩‍👧 family: woman, girl +1F469 200D 1F467 200D 1F466 ; fully-qualified # 👩‍👧‍👦 family: woman, girl, boy +1F469 200D 1F467 200D 1F467 ; fully-qualified # 👩‍👧‍👧 family: woman, girl, girl + +# subgroup: person-symbol +1F5E3 FE0F ; fully-qualified # 🗣️ speaking head +1F5E3 ; unqualified # 🗣 speaking head +1F464 ; fully-qualified # 👤 bust in silhouette +1F465 ; fully-qualified # 👥 busts in silhouette +1F463 ; fully-qualified # 👣 footprints + +# People & Body subtotal: 2266 +# People & Body subtotal: 446 w/o modifiers + +# group: Component + +# subgroup: skin-tone +1F3FB ; component # 🏻 light skin tone +1F3FC ; component # 🏼 medium-light skin tone +1F3FD ; component # 🏽 medium skin tone +1F3FE ; component # 🏾 medium-dark skin tone +1F3FF ; component # 🏿 dark skin tone + +# subgroup: hair-style +1F9B0 ; component # 🦰 red hair +1F9B1 ; component # 🦱 curly hair +1F9B3 ; component # 🦳 white hair +1F9B2 ; component # 🦲 bald + +# Component subtotal: 9 +# Component subtotal: 4 w/o modifiers + +# group: Animals & Nature + +# subgroup: animal-mammal +1F435 ; fully-qualified # 🐵 monkey face +1F412 ; fully-qualified # 🐒 monkey +1F98D ; fully-qualified # 🦍 gorilla +1F436 ; fully-qualified # 🐶 dog face +1F415 ; fully-qualified # 🐕 dog +1F9AE ; fully-qualified # 🦮 guide dog +1F415 200D 1F9BA ; fully-qualified # 🐕‍🦺 service dog +1F429 ; fully-qualified # 🐩 poodle +1F43A ; fully-qualified # 🐺 wolf face +1F98A ; fully-qualified # 🦊 fox face +1F99D ; fully-qualified # 🦝 raccoon +1F431 ; fully-qualified # 🐱 cat face +1F408 ; fully-qualified # 🐈 cat +1F981 ; fully-qualified # 🦁 lion face +1F42F ; fully-qualified # 🐯 tiger face +1F405 ; fully-qualified # 🐅 tiger +1F406 ; fully-qualified # 🐆 leopard +1F434 ; fully-qualified # 🐴 horse face +1F40E ; fully-qualified # 🐎 horse +1F984 ; fully-qualified # 🦄 unicorn face +1F993 ; fully-qualified # 🦓 zebra +1F98C ; fully-qualified # 🦌 deer +1F42E ; fully-qualified # 🐮 cow face +1F402 ; fully-qualified # 🐂 ox +1F403 ; fully-qualified # 🐃 water buffalo +1F404 ; fully-qualified # 🐄 cow +1F437 ; fully-qualified # 🐷 pig face +1F416 ; fully-qualified # 🐖 pig +1F417 ; fully-qualified # 🐗 boar +1F43D ; fully-qualified # 🐽 pig nose +1F40F ; fully-qualified # 🐏 ram +1F411 ; fully-qualified # 🐑 ewe +1F410 ; fully-qualified # 🐐 goat +1F42A ; fully-qualified # 🐪 camel +1F42B ; fully-qualified # 🐫 two-hump camel +1F999 ; fully-qualified # 🦙 llama +1F992 ; fully-qualified # 🦒 giraffe +1F418 ; fully-qualified # 🐘 elephant +1F98F ; fully-qualified # 🦏 rhinoceros +1F99B ; fully-qualified # 🦛 hippopotamus +1F42D ; fully-qualified # 🐭 mouse face +1F401 ; fully-qualified # 🐁 mouse +1F400 ; fully-qualified # 🐀 rat +1F439 ; fully-qualified # 🐹 hamster face +1F430 ; fully-qualified # 🐰 rabbit face +1F407 ; fully-qualified # 🐇 rabbit +1F43F FE0F ; fully-qualified # 🐿️ chipmunk +1F43F ; unqualified # 🐿 chipmunk +1F994 ; fully-qualified # 🦔 hedgehog +1F987 ; fully-qualified # 🦇 bat +1F43B ; fully-qualified # 🐻 bear face +1F428 ; fully-qualified # 🐨 koala +1F43C ; fully-qualified # 🐼 panda face +1F9A5 ; fully-qualified # 🦥 sloth +1F9A6 ; fully-qualified # 🦦 otter +1F9A7 ; fully-qualified # 🦧 orangutan +1F9A8 ; fully-qualified # 🦨 skunk +1F998 ; fully-qualified # 🦘 kangaroo +1F9A1 ; fully-qualified # 🦡 badger +1F43E ; fully-qualified # 🐾 paw prints + +# subgroup: animal-bird +1F983 ; fully-qualified # 🦃 turkey +1F414 ; fully-qualified # 🐔 chicken +1F413 ; fully-qualified # 🐓 rooster +1F423 ; fully-qualified # 🐣 hatching chick +1F424 ; fully-qualified # 🐤 baby chick +1F425 ; fully-qualified # 🐥 front-facing baby chick +1F426 ; fully-qualified # 🐦 bird +1F427 ; fully-qualified # 🐧 penguin +1F54A FE0F ; fully-qualified # 🕊️ dove +1F54A ; unqualified # 🕊 dove +1F985 ; fully-qualified # 🦅 eagle +1F986 ; fully-qualified # 🦆 duck +1F9A2 ; fully-qualified # 🦢 swan +1F989 ; fully-qualified # 🦉 owl +1F9A9 ; fully-qualified # 🦩 flamingo +1F99A ; fully-qualified # 🦚 peacock +1F99C ; fully-qualified # 🦜 parrot + +# subgroup: animal-amphibian +1F438 ; fully-qualified # 🐸 frog face + +# subgroup: animal-reptile +1F40A ; fully-qualified # 🐊 crocodile +1F422 ; fully-qualified # 🐢 turtle +1F98E ; fully-qualified # 🦎 lizard +1F40D ; fully-qualified # 🐍 snake +1F432 ; fully-qualified # 🐲 dragon face +1F409 ; fully-qualified # 🐉 dragon +1F995 ; fully-qualified # 🦕 sauropod +1F996 ; fully-qualified # 🦖 T-Rex + +# subgroup: animal-marine +1F433 ; fully-qualified # 🐳 spouting whale +1F40B ; fully-qualified # 🐋 whale +1F42C ; fully-qualified # 🐬 dolphin +1F41F ; fully-qualified # 🐟 fish +1F420 ; fully-qualified # 🐠 tropical fish +1F421 ; fully-qualified # 🐡 blowfish +1F988 ; fully-qualified # 🦈 shark +1F419 ; fully-qualified # 🐙 octopus +1F41A ; fully-qualified # 🐚 spiral shell + +# subgroup: animal-bug +1F40C ; fully-qualified # 🐌 snail +1F98B ; fully-qualified # 🦋 butterfly +1F41B ; fully-qualified # 🐛 bug +1F41C ; fully-qualified # 🐜 ant +1F41D ; fully-qualified # 🐝 honeybee +1F41E ; fully-qualified # 🐞 lady beetle +1F997 ; fully-qualified # 🦗 cricket +1F577 FE0F ; fully-qualified # 🕷️ spider +1F577 ; unqualified # 🕷 spider +1F578 FE0F ; fully-qualified # 🕸️ spider web +1F578 ; unqualified # 🕸 spider web +1F982 ; fully-qualified # 🦂 scorpion +1F99F ; fully-qualified # 🦟 mosquito +1F9A0 ; fully-qualified # 🦠 microbe + +# subgroup: plant-flower +1F490 ; fully-qualified # 💐 bouquet +1F338 ; fully-qualified # 🌸 cherry blossom +1F4AE ; fully-qualified # 💮 white flower +1F3F5 FE0F ; fully-qualified # 🏵️ rosette +1F3F5 ; unqualified # 🏵 rosette +1F339 ; fully-qualified # 🌹 rose +1F940 ; fully-qualified # 🥀 wilted flower +1F33A ; fully-qualified # 🌺 hibiscus +1F33B ; fully-qualified # 🌻 sunflower +1F33C ; fully-qualified # 🌼 blossom +1F337 ; fully-qualified # 🌷 tulip + +# subgroup: plant-other +1F331 ; fully-qualified # 🌱 seedling +1F332 ; fully-qualified # 🌲 evergreen tree +1F333 ; fully-qualified # 🌳 deciduous tree +1F334 ; fully-qualified # 🌴 palm tree +1F335 ; fully-qualified # 🌵 cactus +1F33E ; fully-qualified # 🌾 sheaf of rice +1F33F ; fully-qualified # 🌿 herb +2618 FE0F ; fully-qualified # ☘️ shamrock +2618 ; unqualified # ☘ shamrock +1F340 ; fully-qualified # 🍀 four leaf clover +1F341 ; fully-qualified # 🍁 maple leaf +1F342 ; fully-qualified # 🍂 fallen leaf +1F343 ; fully-qualified # 🍃 leaf fluttering in wind + +# Animals & Nature subtotal: 133 +# Animals & Nature subtotal: 133 w/o modifiers + +# group: Food & Drink + +# subgroup: food-fruit +1F347 ; fully-qualified # 🍇 grapes +1F348 ; fully-qualified # 🍈 melon +1F349 ; fully-qualified # 🍉 watermelon +1F34A ; fully-qualified # 🍊 tangerine +1F34B ; fully-qualified # 🍋 lemon +1F34C ; fully-qualified # 🍌 banana +1F34D ; fully-qualified # 🍍 pineapple +1F96D ; fully-qualified # 🥭 mango +1F34E ; fully-qualified # 🍎 red apple +1F34F ; fully-qualified # 🍏 green apple +1F350 ; fully-qualified # 🍐 pear +1F351 ; fully-qualified # 🍑 peach +1F352 ; fully-qualified # 🍒 cherries +1F353 ; fully-qualified # 🍓 strawberry +1F95D ; fully-qualified # 🥝 kiwi fruit +1F345 ; fully-qualified # 🍅 tomato +1F965 ; fully-qualified # 🥥 coconut + +# subgroup: food-vegetable +1F951 ; fully-qualified # 🥑 avocado +1F346 ; fully-qualified # 🍆 eggplant +1F954 ; fully-qualified # 🥔 potato +1F955 ; fully-qualified # 🥕 carrot +1F33D ; fully-qualified # 🌽 ear of corn +1F336 FE0F ; fully-qualified # 🌶️ hot pepper +1F336 ; unqualified # 🌶 hot pepper +1F952 ; fully-qualified # 🥒 cucumber +1F96C ; fully-qualified # 🥬 leafy green +1F966 ; fully-qualified # 🥦 broccoli +1F9C4 ; fully-qualified # 🧄 garlic +1F9C5 ; fully-qualified # 🧅 onion +1F344 ; fully-qualified # 🍄 mushroom +1F95C ; fully-qualified # 🥜 peanuts +1F330 ; fully-qualified # 🌰 chestnut + +# subgroup: food-prepared +1F35E ; fully-qualified # 🍞 bread +1F950 ; fully-qualified # 🥐 croissant +1F956 ; fully-qualified # 🥖 baguette bread +1F968 ; fully-qualified # 🥨 pretzel +1F96F ; fully-qualified # 🥯 bagel +1F95E ; fully-qualified # 🥞 pancakes +1F9C7 ; fully-qualified # 🧇 waffle +1F9C0 ; fully-qualified # 🧀 cheese wedge +1F356 ; fully-qualified # 🍖 meat on bone +1F357 ; fully-qualified # 🍗 poultry leg +1F969 ; fully-qualified # 🥩 cut of meat +1F953 ; fully-qualified # 🥓 bacon +1F354 ; fully-qualified # 🍔 hamburger +1F35F ; fully-qualified # 🍟 french fries +1F355 ; fully-qualified # 🍕 pizza +1F32D ; fully-qualified # 🌭 hot dog +1F96A ; fully-qualified # 🥪 sandwich +1F32E ; fully-qualified # 🌮 taco +1F32F ; fully-qualified # 🌯 burrito +1F959 ; fully-qualified # 🥙 stuffed flatbread +1F9C6 ; fully-qualified # 🧆 falafel +1F95A ; fully-qualified # 🥚 egg +1F373 ; fully-qualified # 🍳 cooking +1F958 ; fully-qualified # 🥘 shallow pan of food +1F372 ; fully-qualified # 🍲 pot of food +1F963 ; fully-qualified # 🥣 bowl with spoon +1F957 ; fully-qualified # 🥗 green salad +1F37F ; fully-qualified # 🍿 popcorn +1F9C8 ; fully-qualified # 🧈 butter +1F9C2 ; fully-qualified # 🧂 salt +1F96B ; fully-qualified # 🥫 canned food + +# subgroup: food-asian +1F371 ; fully-qualified # 🍱 bento box +1F358 ; fully-qualified # 🍘 rice cracker +1F359 ; fully-qualified # 🍙 rice ball +1F35A ; fully-qualified # 🍚 cooked rice +1F35B ; fully-qualified # 🍛 curry rice +1F35C ; fully-qualified # 🍜 steaming bowl +1F35D ; fully-qualified # 🍝 spaghetti +1F360 ; fully-qualified # 🍠 roasted sweet potato +1F362 ; fully-qualified # 🍢 oden +1F363 ; fully-qualified # 🍣 sushi +1F364 ; fully-qualified # 🍤 fried shrimp +1F365 ; fully-qualified # 🍥 fish cake with swirl +1F96E ; fully-qualified # 🥮 moon cake +1F361 ; fully-qualified # 🍡 dango +1F95F ; fully-qualified # 🥟 dumpling +1F960 ; fully-qualified # 🥠 fortune cookie +1F961 ; fully-qualified # 🥡 takeout box + +# subgroup: food-marine +1F980 ; fully-qualified # 🦀 crab +1F99E ; fully-qualified # 🦞 lobster +1F990 ; fully-qualified # 🦐 shrimp +1F991 ; fully-qualified # 🦑 squid +1F9AA ; fully-qualified # 🦪 oyster + +# subgroup: food-sweet +1F366 ; fully-qualified # 🍦 soft ice cream +1F367 ; fully-qualified # 🍧 shaved ice +1F368 ; fully-qualified # 🍨 ice cream +1F369 ; fully-qualified # 🍩 doughnut +1F36A ; fully-qualified # 🍪 cookie +1F382 ; fully-qualified # 🎂 birthday cake +1F370 ; fully-qualified # 🍰 shortcake +1F9C1 ; fully-qualified # 🧁 cupcake +1F967 ; fully-qualified # 🥧 pie +1F36B ; fully-qualified # 🍫 chocolate bar +1F36C ; fully-qualified # 🍬 candy +1F36D ; fully-qualified # 🍭 lollipop +1F36E ; fully-qualified # 🍮 custard +1F36F ; fully-qualified # 🍯 honey pot + +# subgroup: drink +1F37C ; fully-qualified # 🍼 baby bottle +1F95B ; fully-qualified # 🥛 glass of milk +2615 ; fully-qualified # ☕ hot beverage +1F375 ; fully-qualified # 🍵 teacup without handle +1F376 ; fully-qualified # 🍶 sake +1F37E ; fully-qualified # 🍾 bottle with popping cork +1F377 ; fully-qualified # 🍷 wine glass +1F378 ; fully-qualified # 🍸 cocktail glass +1F379 ; fully-qualified # 🍹 tropical drink +1F37A ; fully-qualified # 🍺 beer mug +1F37B ; fully-qualified # 🍻 clinking beer mugs +1F942 ; fully-qualified # 🥂 clinking glasses +1F943 ; fully-qualified # 🥃 tumbler glass +1F964 ; fully-qualified # 🥤 cup with straw +1F9C3 ; fully-qualified # 🧃 beverage box +1F9C9 ; fully-qualified # 🧉 mate +1F9CA ; fully-qualified # 🧊 ice cube + +# subgroup: dishware +1F962 ; fully-qualified # 🥢 chopsticks +1F37D FE0F ; fully-qualified # 🍽️ fork and knife with plate +1F37D ; unqualified # 🍽 fork and knife with plate +1F374 ; fully-qualified # 🍴 fork and knife +1F944 ; fully-qualified # 🥄 spoon +1F52A ; fully-qualified # 🔪 kitchen knife +1F3FA ; fully-qualified # 🏺 amphora + +# Food & Drink subtotal: 123 +# Food & Drink subtotal: 123 w/o modifiers + +# group: Travel & Places + +# subgroup: place-map +1F30D ; fully-qualified # 🌍 globe showing Europe-Africa +1F30E ; fully-qualified # 🌎 globe showing Americas +1F30F ; fully-qualified # 🌏 globe showing Asia-Australia +1F310 ; fully-qualified # 🌐 globe with meridians +1F5FA FE0F ; fully-qualified # 🗺️ world map +1F5FA ; unqualified # 🗺 world map +1F5FE ; fully-qualified # 🗾 map of Japan +1F9ED ; fully-qualified # 🧭 compass + +# subgroup: place-geographic +1F3D4 FE0F ; fully-qualified # 🏔️ snow-capped mountain +1F3D4 ; unqualified # 🏔 snow-capped mountain +26F0 FE0F ; fully-qualified # ⛰️ mountain +26F0 ; unqualified # ⛰ mountain +1F30B ; fully-qualified # 🌋 volcano +1F5FB ; fully-qualified # 🗻 mount fuji +1F3D5 FE0F ; fully-qualified # 🏕️ camping +1F3D5 ; unqualified # 🏕 camping +1F3D6 FE0F ; fully-qualified # 🏖️ beach with umbrella +1F3D6 ; unqualified # 🏖 beach with umbrella +1F3DC FE0F ; fully-qualified # 🏜️ desert +1F3DC ; unqualified # 🏜 desert +1F3DD FE0F ; fully-qualified # 🏝️ desert island +1F3DD ; unqualified # 🏝 desert island +1F3DE FE0F ; fully-qualified # 🏞️ national park +1F3DE ; unqualified # 🏞 national park + +# subgroup: place-building +1F3DF FE0F ; fully-qualified # 🏟️ stadium +1F3DF ; unqualified # 🏟 stadium +1F3DB FE0F ; fully-qualified # 🏛️ classical building +1F3DB ; unqualified # 🏛 classical building +1F3D7 FE0F ; fully-qualified # 🏗️ building construction +1F3D7 ; unqualified # 🏗 building construction +1F9F1 ; fully-qualified # 🧱 brick +1F3D8 FE0F ; fully-qualified # 🏘️ houses +1F3D8 ; unqualified # 🏘 houses +1F3DA FE0F ; fully-qualified # 🏚️ derelict house +1F3DA ; unqualified # 🏚 derelict house +1F3E0 ; fully-qualified # 🏠 house +1F3E1 ; fully-qualified # 🏡 house with garden +1F3E2 ; fully-qualified # 🏢 office building +1F3E3 ; fully-qualified # 🏣 Japanese post office +1F3E4 ; fully-qualified # 🏤 post office +1F3E5 ; fully-qualified # 🏥 hospital +1F3E6 ; fully-qualified # 🏦 bank +1F3E8 ; fully-qualified # 🏨 hotel +1F3E9 ; fully-qualified # 🏩 love hotel +1F3EA ; fully-qualified # 🏪 convenience store +1F3EB ; fully-qualified # 🏫 school +1F3EC ; fully-qualified # 🏬 department store +1F3ED ; fully-qualified # 🏭 factory +1F3EF ; fully-qualified # 🏯 Japanese castle +1F3F0 ; fully-qualified # 🏰 castle +1F492 ; fully-qualified # 💒 wedding +1F5FC ; fully-qualified # 🗼 Tokyo tower +1F5FD ; fully-qualified # 🗽 Statue of Liberty + +# subgroup: place-religious +26EA ; fully-qualified # ⛪ church +1F54C ; fully-qualified # 🕌 mosque +1F6D5 ; fully-qualified # 🛕 hindu temple +1F54D ; fully-qualified # 🕍 synagogue +26E9 FE0F ; fully-qualified # ⛩️ shinto shrine +26E9 ; unqualified # ⛩ shinto shrine +1F54B ; fully-qualified # 🕋 kaaba + +# subgroup: place-other +26F2 ; fully-qualified # ⛲ fountain +26FA ; fully-qualified # ⛺ tent +1F301 ; fully-qualified # 🌁 foggy +1F303 ; fully-qualified # 🌃 night with stars +1F3D9 FE0F ; fully-qualified # 🏙️ cityscape +1F3D9 ; unqualified # 🏙 cityscape +1F304 ; fully-qualified # 🌄 sunrise over mountains +1F305 ; fully-qualified # 🌅 sunrise +1F306 ; fully-qualified # 🌆 cityscape at dusk +1F307 ; fully-qualified # 🌇 sunset +1F309 ; fully-qualified # 🌉 bridge at night +2668 FE0F ; fully-qualified # ♨️ hot springs +2668 ; unqualified # ♨ hot springs +1F30C ; fully-qualified # 🌌 milky way +1F3A0 ; fully-qualified # 🎠 carousel horse +1F3A1 ; fully-qualified # 🎡 ferris wheel +1F3A2 ; fully-qualified # 🎢 roller coaster +1F488 ; fully-qualified # 💈 barber pole +1F3AA ; fully-qualified # 🎪 circus tent + +# subgroup: transport-ground +1F682 ; fully-qualified # 🚂 locomotive +1F683 ; fully-qualified # 🚃 railway car +1F684 ; fully-qualified # 🚄 high-speed train +1F685 ; fully-qualified # 🚅 bullet train +1F686 ; fully-qualified # 🚆 train +1F687 ; fully-qualified # 🚇 metro +1F688 ; fully-qualified # 🚈 light rail +1F689 ; fully-qualified # 🚉 station +1F68A ; fully-qualified # 🚊 tram +1F69D ; fully-qualified # 🚝 monorail +1F69E ; fully-qualified # 🚞 mountain railway +1F68B ; fully-qualified # 🚋 tram car +1F68C ; fully-qualified # 🚌 bus +1F68D ; fully-qualified # 🚍 oncoming bus +1F68E ; fully-qualified # 🚎 trolleybus +1F690 ; fully-qualified # 🚐 minibus +1F691 ; fully-qualified # 🚑 ambulance +1F692 ; fully-qualified # 🚒 fire engine +1F693 ; fully-qualified # 🚓 police car +1F694 ; fully-qualified # 🚔 oncoming police car +1F695 ; fully-qualified # 🚕 taxi +1F696 ; fully-qualified # 🚖 oncoming taxi +1F697 ; fully-qualified # 🚗 automobile +1F698 ; fully-qualified # 🚘 oncoming automobile +1F699 ; fully-qualified # 🚙 sport utility vehicle +1F69A ; fully-qualified # 🚚 delivery truck +1F69B ; fully-qualified # 🚛 articulated lorry +1F69C ; fully-qualified # 🚜 tractor +1F3CE FE0F ; fully-qualified # 🏎️ racing car +1F3CE ; unqualified # 🏎 racing car +1F3CD FE0F ; fully-qualified # 🏍️ motorcycle +1F3CD ; unqualified # 🏍 motorcycle +1F6F5 ; fully-qualified # 🛵 motor scooter +1F9BD ; fully-qualified # 🦽 manual wheelchair +1F9BC ; fully-qualified # 🦼 motorized wheelchair +1F6FA ; fully-qualified # 🛺 auto rickshaw +1F6B2 ; fully-qualified # 🚲 bicycle +1F6F4 ; fully-qualified # 🛴 kick scooter +1F6F9 ; fully-qualified # 🛹 skateboard +1F68F ; fully-qualified # 🚏 bus stop +1F6E3 FE0F ; fully-qualified # 🛣️ motorway +1F6E3 ; unqualified # 🛣 motorway +1F6E4 FE0F ; fully-qualified # 🛤️ railway track +1F6E4 ; unqualified # 🛤 railway track +1F6E2 FE0F ; fully-qualified # 🛢️ oil drum +1F6E2 ; unqualified # 🛢 oil drum +26FD ; fully-qualified # ⛽ fuel pump +1F6A8 ; fully-qualified # 🚨 police car light +1F6A5 ; fully-qualified # 🚥 horizontal traffic light +1F6A6 ; fully-qualified # 🚦 vertical traffic light +1F6D1 ; fully-qualified # 🛑 stop sign +1F6A7 ; fully-qualified # 🚧 construction + +# subgroup: transport-water +2693 ; fully-qualified # ⚓ anchor +26F5 ; fully-qualified # ⛵ sailboat +1F6F6 ; fully-qualified # 🛶 canoe +1F6A4 ; fully-qualified # 🚤 speedboat +1F6F3 FE0F ; fully-qualified # 🛳️ passenger ship +1F6F3 ; unqualified # 🛳 passenger ship +26F4 FE0F ; fully-qualified # ⛴️ ferry +26F4 ; unqualified # ⛴ ferry +1F6E5 FE0F ; fully-qualified # 🛥️ motor boat +1F6E5 ; unqualified # 🛥 motor boat +1F6A2 ; fully-qualified # 🚢 ship + +# subgroup: transport-air +2708 FE0F ; fully-qualified # ✈️ airplane +2708 ; unqualified # ✈ airplane +1F6E9 FE0F ; fully-qualified # 🛩️ small airplane +1F6E9 ; unqualified # 🛩 small airplane +1F6EB ; fully-qualified # 🛫 airplane departure +1F6EC ; fully-qualified # 🛬 airplane arrival +1FA82 ; fully-qualified # 🪂 parachute +1F4BA ; fully-qualified # 💺 seat +1F681 ; fully-qualified # 🚁 helicopter +1F69F ; fully-qualified # 🚟 suspension railway +1F6A0 ; fully-qualified # 🚠 mountain cableway +1F6A1 ; fully-qualified # 🚡 aerial tramway +1F6F0 FE0F ; fully-qualified # 🛰️ satellite +1F6F0 ; unqualified # 🛰 satellite +1F680 ; fully-qualified # 🚀 rocket +1F6F8 ; fully-qualified # 🛸 flying saucer + +# subgroup: hotel +1F6CE FE0F ; fully-qualified # 🛎️ bellhop bell +1F6CE ; unqualified # 🛎 bellhop bell +1F9F3 ; fully-qualified # 🧳 luggage + +# subgroup: time +231B ; fully-qualified # ⌛ hourglass done +23F3 ; fully-qualified # ⏳ hourglass not done +231A ; fully-qualified # ⌚ watch +23F0 ; fully-qualified # ⏰ alarm clock +23F1 FE0F ; fully-qualified # ⏱️ stopwatch +23F1 ; unqualified # ⏱ stopwatch +23F2 FE0F ; fully-qualified # ⏲️ timer clock +23F2 ; unqualified # ⏲ timer clock +1F570 FE0F ; fully-qualified # 🕰️ mantelpiece clock +1F570 ; unqualified # 🕰 mantelpiece clock +1F55B ; fully-qualified # 🕛 twelve o’clock +1F567 ; fully-qualified # 🕧 twelve-thirty +1F550 ; fully-qualified # 🕐 one o’clock +1F55C ; fully-qualified # 🕜 one-thirty +1F551 ; fully-qualified # 🕑 two o’clock +1F55D ; fully-qualified # 🕝 two-thirty +1F552 ; fully-qualified # 🕒 three o’clock +1F55E ; fully-qualified # 🕞 three-thirty +1F553 ; fully-qualified # 🕓 four o’clock +1F55F ; fully-qualified # 🕟 four-thirty +1F554 ; fully-qualified # 🕔 five o’clock +1F560 ; fully-qualified # 🕠 five-thirty +1F555 ; fully-qualified # 🕕 six o’clock +1F561 ; fully-qualified # 🕡 six-thirty +1F556 ; fully-qualified # 🕖 seven o’clock +1F562 ; fully-qualified # 🕢 seven-thirty +1F557 ; fully-qualified # 🕗 eight o’clock +1F563 ; fully-qualified # 🕣 eight-thirty +1F558 ; fully-qualified # 🕘 nine o’clock +1F564 ; fully-qualified # 🕤 nine-thirty +1F559 ; fully-qualified # 🕙 ten o’clock +1F565 ; fully-qualified # 🕥 ten-thirty +1F55A ; fully-qualified # 🕚 eleven o’clock +1F566 ; fully-qualified # 🕦 eleven-thirty + +# subgroup: sky & weather +1F311 ; fully-qualified # 🌑 new moon +1F312 ; fully-qualified # 🌒 waxing crescent moon +1F313 ; fully-qualified # 🌓 first quarter moon +1F314 ; fully-qualified # 🌔 waxing gibbous moon +1F315 ; fully-qualified # 🌕 full moon +1F316 ; fully-qualified # 🌖 waning gibbous moon +1F317 ; fully-qualified # 🌗 last quarter moon +1F318 ; fully-qualified # 🌘 waning crescent moon +1F319 ; fully-qualified # 🌙 crescent moon +1F31A ; fully-qualified # 🌚 new moon face +1F31B ; fully-qualified # 🌛 first quarter moon face +1F31C ; fully-qualified # 🌜 last quarter moon face +1F321 FE0F ; fully-qualified # 🌡️ thermometer +1F321 ; unqualified # 🌡 thermometer +2600 FE0F ; fully-qualified # ☀️ sun +2600 ; unqualified # ☀ sun +1F31D ; fully-qualified # 🌝 full moon face +1F31E ; fully-qualified # 🌞 sun with face +1FA90 ; fully-qualified # 🪐 ringed planet +2B50 ; fully-qualified # ⭐ star +1F31F ; fully-qualified # 🌟 glowing star +1F320 ; fully-qualified # 🌠 shooting star +2601 FE0F ; fully-qualified # ☁️ cloud +2601 ; unqualified # ☁ cloud +26C5 ; fully-qualified # ⛅ sun behind cloud +26C8 FE0F ; fully-qualified # ⛈️ cloud with lightning and rain +26C8 ; unqualified # ⛈ cloud with lightning and rain +1F324 FE0F ; fully-qualified # 🌤️ sun behind small cloud +1F324 ; unqualified # 🌤 sun behind small cloud +1F325 FE0F ; fully-qualified # 🌥️ sun behind large cloud +1F325 ; unqualified # 🌥 sun behind large cloud +1F326 FE0F ; fully-qualified # 🌦️ sun behind rain cloud +1F326 ; unqualified # 🌦 sun behind rain cloud +1F327 FE0F ; fully-qualified # 🌧️ cloud with rain +1F327 ; unqualified # 🌧 cloud with rain +1F328 FE0F ; fully-qualified # 🌨️ cloud with snow +1F328 ; unqualified # 🌨 cloud with snow +1F329 FE0F ; fully-qualified # 🌩️ cloud with lightning +1F329 ; unqualified # 🌩 cloud with lightning +1F32A FE0F ; fully-qualified # 🌪️ tornado +1F32A ; unqualified # 🌪 tornado +1F32B FE0F ; fully-qualified # 🌫️ fog +1F32B ; unqualified # 🌫 fog +1F32C FE0F ; fully-qualified # 🌬️ wind face +1F32C ; unqualified # 🌬 wind face +1F300 ; fully-qualified # 🌀 cyclone +1F308 ; fully-qualified # 🌈 rainbow +1F302 ; fully-qualified # 🌂 closed umbrella +2602 FE0F ; fully-qualified # ☂️ umbrella +2602 ; unqualified # ☂ umbrella +2614 ; fully-qualified # ☔ umbrella with rain drops +26F1 FE0F ; fully-qualified # ⛱️ umbrella on ground +26F1 ; unqualified # ⛱ umbrella on ground +26A1 ; fully-qualified # ⚡ high voltage +2744 FE0F ; fully-qualified # ❄️ snowflake +2744 ; unqualified # ❄ snowflake +2603 FE0F ; fully-qualified # ☃️ snowman +2603 ; unqualified # ☃ snowman +26C4 ; fully-qualified # ⛄ snowman without snow +2604 FE0F ; fully-qualified # ☄️ comet +2604 ; unqualified # ☄ comet +1F525 ; fully-qualified # 🔥 fire +1F4A7 ; fully-qualified # 💧 droplet +1F30A ; fully-qualified # 🌊 water wave + +# Travel & Places subtotal: 259 +# Travel & Places subtotal: 259 w/o modifiers + +# group: Activities + +# subgroup: event +1F383 ; fully-qualified # 🎃 jack-o-lantern +1F384 ; fully-qualified # 🎄 Christmas tree +1F386 ; fully-qualified # 🎆 fireworks +1F387 ; fully-qualified # 🎇 sparkler +1F9E8 ; fully-qualified # 🧨 firecracker +2728 ; fully-qualified # ✨ sparkles +1F388 ; fully-qualified # 🎈 balloon +1F389 ; fully-qualified # 🎉 party popper +1F38A ; fully-qualified # 🎊 confetti ball +1F38B ; fully-qualified # 🎋 tanabata tree +1F38D ; fully-qualified # 🎍 pine decoration +1F38E ; fully-qualified # 🎎 Japanese dolls +1F38F ; fully-qualified # 🎏 carp streamer +1F390 ; fully-qualified # 🎐 wind chime +1F391 ; fully-qualified # 🎑 moon viewing ceremony +1F9E7 ; fully-qualified # 🧧 red envelope +1F380 ; fully-qualified # 🎀 ribbon +1F381 ; fully-qualified # 🎁 wrapped gift +1F397 FE0F ; fully-qualified # 🎗️ reminder ribbon +1F397 ; unqualified # 🎗 reminder ribbon +1F39F FE0F ; fully-qualified # 🎟️ admission tickets +1F39F ; unqualified # 🎟 admission tickets +1F3AB ; fully-qualified # 🎫 ticket + +# subgroup: award-medal +1F396 FE0F ; fully-qualified # 🎖️ military medal +1F396 ; unqualified # 🎖 military medal +1F3C6 ; fully-qualified # 🏆 trophy +1F3C5 ; fully-qualified # 🏅 sports medal +1F947 ; fully-qualified # 🥇 1st place medal +1F948 ; fully-qualified # 🥈 2nd place medal +1F949 ; fully-qualified # 🥉 3rd place medal + +# subgroup: sport +26BD ; fully-qualified # ⚽ soccer ball +26BE ; fully-qualified # ⚾ baseball +1F94E ; fully-qualified # 🥎 softball +1F3C0 ; fully-qualified # 🏀 basketball +1F3D0 ; fully-qualified # 🏐 volleyball +1F3C8 ; fully-qualified # 🏈 american football +1F3C9 ; fully-qualified # 🏉 rugby football +1F3BE ; fully-qualified # 🎾 tennis +1F94F ; fully-qualified # 🥏 flying disc +1F3B3 ; fully-qualified # 🎳 bowling +1F3CF ; fully-qualified # 🏏 cricket game +1F3D1 ; fully-qualified # 🏑 field hockey +1F3D2 ; fully-qualified # 🏒 ice hockey +1F94D ; fully-qualified # 🥍 lacrosse +1F3D3 ; fully-qualified # 🏓 ping pong +1F3F8 ; fully-qualified # 🏸 badminton +1F94A ; fully-qualified # 🥊 boxing glove +1F94B ; fully-qualified # 🥋 martial arts uniform +1F945 ; fully-qualified # 🥅 goal net +26F3 ; fully-qualified # ⛳ flag in hole +26F8 FE0F ; fully-qualified # ⛸️ ice skate +26F8 ; unqualified # ⛸ ice skate +1F3A3 ; fully-qualified # 🎣 fishing pole +1F93F ; fully-qualified # 🤿 diving mask +1F3BD ; fully-qualified # 🎽 running shirt +1F3BF ; fully-qualified # 🎿 skis +1F6F7 ; fully-qualified # 🛷 sled +1F94C ; fully-qualified # 🥌 curling stone + +# subgroup: game +1F3AF ; fully-qualified # 🎯 direct hit +1FA80 ; fully-qualified # 🪀 yo-yo +1FA81 ; fully-qualified # 🪁 kite +1F3B1 ; fully-qualified # 🎱 pool 8 ball +1F52E ; fully-qualified # 🔮 crystal ball +1F9FF ; fully-qualified # 🧿 nazar amulet +1F3AE ; fully-qualified # 🎮 video game +1F579 FE0F ; fully-qualified # 🕹️ joystick +1F579 ; unqualified # 🕹 joystick +1F3B0 ; fully-qualified # 🎰 slot machine +1F3B2 ; fully-qualified # 🎲 game die +1F9E9 ; fully-qualified # 🧩 jigsaw +1F9F8 ; fully-qualified # 🧸 teddy bear +2660 FE0F ; fully-qualified # ♠️ spade suit +2660 ; unqualified # ♠ spade suit +2665 FE0F ; fully-qualified # ♥️ heart suit +2665 ; unqualified # ♥ heart suit +2666 FE0F ; fully-qualified # ♦️ diamond suit +2666 ; unqualified # ♦ diamond suit +2663 FE0F ; fully-qualified # ♣️ club suit +2663 ; unqualified # ♣ club suit +265F FE0F ; fully-qualified # ♟️ chess pawn +265F ; unqualified # ♟ chess pawn +1F0CF ; fully-qualified # 🃏 joker +1F004 ; fully-qualified # 🀄 mahjong red dragon +1F3B4 ; fully-qualified # 🎴 flower playing cards + +# subgroup: arts & crafts +1F3AD ; fully-qualified # 🎭 performing arts +1F5BC FE0F ; fully-qualified # 🖼️ framed picture +1F5BC ; unqualified # 🖼 framed picture +1F3A8 ; fully-qualified # 🎨 artist palette +1F9F5 ; fully-qualified # 🧵 thread +1F9F6 ; fully-qualified # 🧶 yarn + +# Activities subtotal: 90 +# Activities subtotal: 90 w/o modifiers + +# group: Objects + +# subgroup: clothing +1F453 ; fully-qualified # 👓 glasses +1F576 FE0F ; fully-qualified # 🕶️ sunglasses +1F576 ; unqualified # 🕶 sunglasses +1F97D ; fully-qualified # 🥽 goggles +1F97C ; fully-qualified # 🥼 lab coat +1F9BA ; fully-qualified # 🦺 safety vest +1F454 ; fully-qualified # 👔 necktie +1F455 ; fully-qualified # 👕 t-shirt +1F456 ; fully-qualified # 👖 jeans +1F9E3 ; fully-qualified # 🧣 scarf +1F9E4 ; fully-qualified # 🧤 gloves +1F9E5 ; fully-qualified # 🧥 coat +1F9E6 ; fully-qualified # 🧦 socks +1F457 ; fully-qualified # 👗 dress +1F458 ; fully-qualified # 👘 kimono +1F97B ; fully-qualified # 🥻 sari +1FA71 ; fully-qualified # 🩱 one-piece +1FA72 ; fully-qualified # 🩲 briefs +1FA73 ; fully-qualified # 🩳 shorts +1F459 ; fully-qualified # 👙 bikini +1F45A ; fully-qualified # 👚 woman’s clothes +1F45B ; fully-qualified # 👛 purse +1F45C ; fully-qualified # 👜 handbag +1F45D ; fully-qualified # 👝 clutch bag +1F6CD FE0F ; fully-qualified # 🛍️ shopping bags +1F6CD ; unqualified # 🛍 shopping bags +1F392 ; fully-qualified # 🎒 backpack +1F45E ; fully-qualified # 👞 man’s shoe +1F45F ; fully-qualified # 👟 running shoe +1F97E ; fully-qualified # 🥾 hiking boot +1F97F ; fully-qualified # 🥿 flat shoe +1F460 ; fully-qualified # 👠 high-heeled shoe +1F461 ; fully-qualified # 👡 woman’s sandal +1FA70 ; fully-qualified # 🩰 ballet shoes +1F462 ; fully-qualified # 👢 woman’s boot +1F451 ; fully-qualified # 👑 crown +1F452 ; fully-qualified # 👒 woman’s hat +1F3A9 ; fully-qualified # 🎩 top hat +1F393 ; fully-qualified # 🎓 graduation cap +1F9E2 ; fully-qualified # 🧢 billed cap +26D1 FE0F ; fully-qualified # ⛑️ rescue worker’s helmet +26D1 ; unqualified # ⛑ rescue worker’s helmet +1F4FF ; fully-qualified # 📿 prayer beads +1F484 ; fully-qualified # 💄 lipstick +1F48D ; fully-qualified # 💍 ring +1F48E ; fully-qualified # 💎 gem stone + +# subgroup: sound +1F507 ; fully-qualified # 🔇 muted speaker +1F508 ; fully-qualified # 🔈 speaker low volume +1F509 ; fully-qualified # 🔉 speaker medium volume +1F50A ; fully-qualified # 🔊 speaker high volume +1F4E2 ; fully-qualified # 📢 loudspeaker +1F4E3 ; fully-qualified # 📣 megaphone +1F4EF ; fully-qualified # 📯 postal horn +1F514 ; fully-qualified # 🔔 bell +1F515 ; fully-qualified # 🔕 bell with slash + +# subgroup: music +1F3BC ; fully-qualified # 🎼 musical score +1F3B5 ; fully-qualified # 🎵 musical note +1F3B6 ; fully-qualified # 🎶 musical notes +1F399 FE0F ; fully-qualified # 🎙️ studio microphone +1F399 ; unqualified # 🎙 studio microphone +1F39A FE0F ; fully-qualified # 🎚️ level slider +1F39A ; unqualified # 🎚 level slider +1F39B FE0F ; fully-qualified # 🎛️ control knobs +1F39B ; unqualified # 🎛 control knobs +1F3A4 ; fully-qualified # 🎤 microphone +1F3A7 ; fully-qualified # 🎧 headphone +1F4FB ; fully-qualified # 📻 radio + +# subgroup: musical-instrument +1F3B7 ; fully-qualified # 🎷 saxophone +1F3B8 ; fully-qualified # 🎸 guitar +1F3B9 ; fully-qualified # 🎹 musical keyboard +1F3BA ; fully-qualified # 🎺 trumpet +1F3BB ; fully-qualified # 🎻 violin +1FA95 ; fully-qualified # 🪕 banjo +1F941 ; fully-qualified # 🥁 drum + +# subgroup: phone +1F4F1 ; fully-qualified # 📱 mobile phone +1F4F2 ; fully-qualified # 📲 mobile phone with arrow +260E FE0F ; fully-qualified # ☎️ telephone +260E ; unqualified # ☎ telephone +1F4DE ; fully-qualified # 📞 telephone receiver +1F4DF ; fully-qualified # 📟 pager +1F4E0 ; fully-qualified # 📠 fax machine + +# subgroup: computer +1F50B ; fully-qualified # 🔋 battery +1F50C ; fully-qualified # 🔌 electric plug +1F4BB ; fully-qualified # 💻 laptop computer +1F5A5 FE0F ; fully-qualified # 🖥️ desktop computer +1F5A5 ; unqualified # 🖥 desktop computer +1F5A8 FE0F ; fully-qualified # 🖨️ printer +1F5A8 ; unqualified # 🖨 printer +2328 FE0F ; fully-qualified # ⌨️ keyboard +2328 ; unqualified # ⌨ keyboard +1F5B1 FE0F ; fully-qualified # 🖱️ computer mouse +1F5B1 ; unqualified # 🖱 computer mouse +1F5B2 FE0F ; fully-qualified # 🖲️ trackball +1F5B2 ; unqualified # 🖲 trackball +1F4BD ; fully-qualified # 💽 computer disk +1F4BE ; fully-qualified # 💾 floppy disk +1F4BF ; fully-qualified # 💿 optical disk +1F4C0 ; fully-qualified # 📀 dvd +1F9EE ; fully-qualified # 🧮 abacus + +# subgroup: light & video +1F3A5 ; fully-qualified # 🎥 movie camera +1F39E FE0F ; fully-qualified # 🎞️ film frames +1F39E ; unqualified # 🎞 film frames +1F4FD FE0F ; fully-qualified # 📽️ film projector +1F4FD ; unqualified # 📽 film projector +1F3AC ; fully-qualified # 🎬 clapper board +1F4FA ; fully-qualified # 📺 television +1F4F7 ; fully-qualified # 📷 camera +1F4F8 ; fully-qualified # 📸 camera with flash +1F4F9 ; fully-qualified # 📹 video camera +1F4FC ; fully-qualified # 📼 videocassette +1F50D ; fully-qualified # 🔍 magnifying glass tilted left +1F50E ; fully-qualified # 🔎 magnifying glass tilted right +1F56F FE0F ; fully-qualified # 🕯️ candle +1F56F ; unqualified # 🕯 candle +1F4A1 ; fully-qualified # 💡 light bulb +1F526 ; fully-qualified # 🔦 flashlight +1F3EE ; fully-qualified # 🏮 red paper lantern +1FA94 ; fully-qualified # 🪔 diya lamp + +# subgroup: book-paper +1F4D4 ; fully-qualified # 📔 notebook with decorative cover +1F4D5 ; fully-qualified # 📕 closed book +1F4D6 ; fully-qualified # 📖 open book +1F4D7 ; fully-qualified # 📗 green book +1F4D8 ; fully-qualified # 📘 blue book +1F4D9 ; fully-qualified # 📙 orange book +1F4DA ; fully-qualified # 📚 books +1F4D3 ; fully-qualified # 📓 notebook +1F4D2 ; fully-qualified # 📒 ledger +1F4C3 ; fully-qualified # 📃 page with curl +1F4DC ; fully-qualified # 📜 scroll +1F4C4 ; fully-qualified # 📄 page facing up +1F4F0 ; fully-qualified # 📰 newspaper +1F5DE FE0F ; fully-qualified # 🗞️ rolled-up newspaper +1F5DE ; unqualified # 🗞 rolled-up newspaper +1F4D1 ; fully-qualified # 📑 bookmark tabs +1F516 ; fully-qualified # 🔖 bookmark +1F3F7 FE0F ; fully-qualified # 🏷️ label +1F3F7 ; unqualified # 🏷 label + +# subgroup: money +1F4B0 ; fully-qualified # 💰 money bag +1F4B4 ; fully-qualified # 💴 yen banknote +1F4B5 ; fully-qualified # 💵 dollar banknote +1F4B6 ; fully-qualified # 💶 euro banknote +1F4B7 ; fully-qualified # 💷 pound banknote +1F4B8 ; fully-qualified # 💸 money with wings +1F4B3 ; fully-qualified # 💳 credit card +1F9FE ; fully-qualified # 🧾 receipt +1F4B9 ; fully-qualified # 💹 chart increasing with yen +1F4B1 ; fully-qualified # 💱 currency exchange +1F4B2 ; fully-qualified # 💲 heavy dollar sign + +# subgroup: mail +2709 FE0F ; fully-qualified # ✉️ envelope +2709 ; unqualified # ✉ envelope +1F4E7 ; fully-qualified # 📧 e-mail +1F4E8 ; fully-qualified # 📨 incoming envelope +1F4E9 ; fully-qualified # 📩 envelope with arrow +1F4E4 ; fully-qualified # 📤 outbox tray +1F4E5 ; fully-qualified # 📥 inbox tray +1F4E6 ; fully-qualified # 📦 package +1F4EB ; fully-qualified # 📫 closed mailbox with raised flag +1F4EA ; fully-qualified # 📪 closed mailbox with lowered flag +1F4EC ; fully-qualified # 📬 open mailbox with raised flag +1F4ED ; fully-qualified # 📭 open mailbox with lowered flag +1F4EE ; fully-qualified # 📮 postbox +1F5F3 FE0F ; fully-qualified # 🗳️ ballot box with ballot +1F5F3 ; unqualified # 🗳 ballot box with ballot + +# subgroup: writing +270F FE0F ; fully-qualified # ✏️ pencil +270F ; unqualified # ✏ pencil +2712 FE0F ; fully-qualified # ✒️ black nib +2712 ; unqualified # ✒ black nib +1F58B FE0F ; fully-qualified # 🖋️ fountain pen +1F58B ; unqualified # 🖋 fountain pen +1F58A FE0F ; fully-qualified # 🖊️ pen +1F58A ; unqualified # 🖊 pen +1F58C FE0F ; fully-qualified # 🖌️ paintbrush +1F58C ; unqualified # 🖌 paintbrush +1F58D FE0F ; fully-qualified # 🖍️ crayon +1F58D ; unqualified # 🖍 crayon +1F4DD ; fully-qualified # 📝 memo + +# subgroup: office +1F4BC ; fully-qualified # 💼 briefcase +1F4C1 ; fully-qualified # 📁 file folder +1F4C2 ; fully-qualified # 📂 open file folder +1F5C2 FE0F ; fully-qualified # 🗂️ card index dividers +1F5C2 ; unqualified # 🗂 card index dividers +1F4C5 ; fully-qualified # 📅 calendar +1F4C6 ; fully-qualified # 📆 tear-off calendar +1F5D2 FE0F ; fully-qualified # 🗒️ spiral notepad +1F5D2 ; unqualified # 🗒 spiral notepad +1F5D3 FE0F ; fully-qualified # 🗓️ spiral calendar +1F5D3 ; unqualified # 🗓 spiral calendar +1F4C7 ; fully-qualified # 📇 card index +1F4C8 ; fully-qualified # 📈 chart increasing +1F4C9 ; fully-qualified # 📉 chart decreasing +1F4CA ; fully-qualified # 📊 bar chart +1F4CB ; fully-qualified # 📋 clipboard +1F4CC ; fully-qualified # 📌 pushpin +1F4CD ; fully-qualified # 📍 round pushpin +1F4CE ; fully-qualified # 📎 paperclip +1F587 FE0F ; fully-qualified # 🖇️ linked paperclips +1F587 ; unqualified # 🖇 linked paperclips +1F4CF ; fully-qualified # 📏 straight ruler +1F4D0 ; fully-qualified # 📐 triangular ruler +2702 FE0F ; fully-qualified # ✂️ scissors +2702 ; unqualified # ✂ scissors +1F5C3 FE0F ; fully-qualified # 🗃️ card file box +1F5C3 ; unqualified # 🗃 card file box +1F5C4 FE0F ; fully-qualified # 🗄️ file cabinet +1F5C4 ; unqualified # 🗄 file cabinet +1F5D1 FE0F ; fully-qualified # 🗑️ wastebasket +1F5D1 ; unqualified # 🗑 wastebasket + +# subgroup: lock +1F512 ; fully-qualified # 🔒 locked +1F513 ; fully-qualified # 🔓 unlocked +1F50F ; fully-qualified # 🔏 locked with pen +1F510 ; fully-qualified # 🔐 locked with key +1F511 ; fully-qualified # 🔑 key +1F5DD FE0F ; fully-qualified # 🗝️ old key +1F5DD ; unqualified # 🗝 old key + +# subgroup: tool +1F528 ; fully-qualified # 🔨 hammer +1FA93 ; fully-qualified # 🪓 axe +26CF FE0F ; fully-qualified # ⛏️ pick +26CF ; unqualified # ⛏ pick +2692 FE0F ; fully-qualified # ⚒️ hammer and pick +2692 ; unqualified # ⚒ hammer and pick +1F6E0 FE0F ; fully-qualified # 🛠️ hammer and wrench +1F6E0 ; unqualified # 🛠 hammer and wrench +1F5E1 FE0F ; fully-qualified # 🗡️ dagger +1F5E1 ; unqualified # 🗡 dagger +2694 FE0F ; fully-qualified # ⚔️ crossed swords +2694 ; unqualified # ⚔ crossed swords +1F52B ; fully-qualified # 🔫 pistol +1F3F9 ; fully-qualified # 🏹 bow and arrow +1F6E1 FE0F ; fully-qualified # 🛡️ shield +1F6E1 ; unqualified # 🛡 shield +1F527 ; fully-qualified # 🔧 wrench +1F529 ; fully-qualified # 🔩 nut and bolt +2699 FE0F ; fully-qualified # ⚙️ gear +2699 ; unqualified # ⚙ gear +1F5DC FE0F ; fully-qualified # 🗜️ clamp +1F5DC ; unqualified # 🗜 clamp +2696 FE0F ; fully-qualified # ⚖️ balance scale +2696 ; unqualified # ⚖ balance scale +1F9AF ; fully-qualified # 🦯 probing cane +1F517 ; fully-qualified # 🔗 link +26D3 FE0F ; fully-qualified # ⛓️ chains +26D3 ; unqualified # ⛓ chains +1F9F0 ; fully-qualified # 🧰 toolbox +1F9F2 ; fully-qualified # 🧲 magnet + +# subgroup: science +2697 FE0F ; fully-qualified # ⚗️ alembic +2697 ; unqualified # ⚗ alembic +1F9EA ; fully-qualified # 🧪 test tube +1F9EB ; fully-qualified # 🧫 petri dish +1F9EC ; fully-qualified # 🧬 dna +1F52C ; fully-qualified # 🔬 microscope +1F52D ; fully-qualified # 🔭 telescope +1F4E1 ; fully-qualified # 📡 satellite antenna + +# subgroup: medical +1F489 ; fully-qualified # 💉 syringe +1FA78 ; fully-qualified # 🩸 drop of blood +1F48A ; fully-qualified # 💊 pill +1FA79 ; fully-qualified # 🩹 adhesive bandage +1FA7A ; fully-qualified # 🩺 stethoscope + +# subgroup: household +1F6AA ; fully-qualified # 🚪 door +1F6CF FE0F ; fully-qualified # 🛏️ bed +1F6CF ; unqualified # 🛏 bed +1F6CB FE0F ; fully-qualified # 🛋️ couch and lamp +1F6CB ; unqualified # 🛋 couch and lamp +1FA91 ; fully-qualified # 🪑 chair +1F6BD ; fully-qualified # 🚽 toilet +1F6BF ; fully-qualified # 🚿 shower +1F6C1 ; fully-qualified # 🛁 bathtub +1FA92 ; fully-qualified # 🪒 razor +1F9F4 ; fully-qualified # 🧴 lotion bottle +1F9F7 ; fully-qualified # 🧷 safety pin +1F9F9 ; fully-qualified # 🧹 broom +1F9FA ; fully-qualified # 🧺 basket +1F9FB ; fully-qualified # 🧻 roll of paper +1F9FC ; fully-qualified # 🧼 soap +1F9FD ; fully-qualified # 🧽 sponge +1F9EF ; fully-qualified # 🧯 fire extinguisher +1F6D2 ; fully-qualified # 🛒 shopping cart + +# subgroup: other-object +1F6AC ; fully-qualified # 🚬 cigarette +26B0 FE0F ; fully-qualified # ⚰️ coffin +26B0 ; unqualified # ⚰ coffin +26B1 FE0F ; fully-qualified # ⚱️ funeral urn +26B1 ; unqualified # ⚱ funeral urn +1F5FF ; fully-qualified # 🗿 moai + +# Objects subtotal: 282 +# Objects subtotal: 282 w/o modifiers + +# group: Symbols + +# subgroup: transport-sign +1F3E7 ; fully-qualified # 🏧 ATM sign +1F6AE ; fully-qualified # 🚮 litter in bin sign +1F6B0 ; fully-qualified # 🚰 potable water +267F ; fully-qualified # ♿ wheelchair symbol +1F6B9 ; fully-qualified # 🚹 men’s room +1F6BA ; fully-qualified # 🚺 women’s room +1F6BB ; fully-qualified # 🚻 restroom +1F6BC ; fully-qualified # 🚼 baby symbol +1F6BE ; fully-qualified # 🚾 water closet +1F6C2 ; fully-qualified # 🛂 passport control +1F6C3 ; fully-qualified # 🛃 customs +1F6C4 ; fully-qualified # 🛄 baggage claim +1F6C5 ; fully-qualified # 🛅 left luggage + +# subgroup: warning +26A0 FE0F ; fully-qualified # ⚠️ warning +26A0 ; unqualified # ⚠ warning +1F6B8 ; fully-qualified # 🚸 children crossing +26D4 ; fully-qualified # ⛔ no entry +1F6AB ; fully-qualified # 🚫 prohibited +1F6B3 ; fully-qualified # 🚳 no bicycles +1F6AD ; fully-qualified # 🚭 no smoking +1F6AF ; fully-qualified # 🚯 no littering +1F6B1 ; fully-qualified # 🚱 non-potable water +1F6B7 ; fully-qualified # 🚷 no pedestrians +1F4F5 ; fully-qualified # 📵 no mobile phones +1F51E ; fully-qualified # 🔞 no one under eighteen +2622 FE0F ; fully-qualified # ☢️ radioactive +2622 ; unqualified # ☢ radioactive +2623 FE0F ; fully-qualified # ☣️ biohazard +2623 ; unqualified # ☣ biohazard + +# subgroup: arrow +2B06 FE0F ; fully-qualified # ⬆️ up arrow +2B06 ; unqualified # ⬆ up arrow +2197 FE0F ; fully-qualified # ↗️ up-right arrow +2197 ; unqualified # ↗ up-right arrow +27A1 FE0F ; fully-qualified # ➡️ right arrow +27A1 ; unqualified # ➡ right arrow +2198 FE0F ; fully-qualified # ↘️ down-right arrow +2198 ; unqualified # ↘ down-right arrow +2B07 FE0F ; fully-qualified # ⬇️ down arrow +2B07 ; unqualified # ⬇ down arrow +2199 FE0F ; fully-qualified # ↙️ down-left arrow +2199 ; unqualified # ↙ down-left arrow +2B05 FE0F ; fully-qualified # ⬅️ left arrow +2B05 ; unqualified # ⬅ left arrow +2196 FE0F ; fully-qualified # ↖️ up-left arrow +2196 ; unqualified # ↖ up-left arrow +2195 FE0F ; fully-qualified # ↕️ up-down arrow +2195 ; unqualified # ↕ up-down arrow +2194 FE0F ; fully-qualified # ↔️ left-right arrow +2194 ; unqualified # ↔ left-right arrow +21A9 FE0F ; fully-qualified # ↩️ right arrow curving left +21A9 ; unqualified # ↩ right arrow curving left +21AA FE0F ; fully-qualified # ↪️ left arrow curving right +21AA ; unqualified # ↪ left arrow curving right +2934 FE0F ; fully-qualified # ⤴️ right arrow curving up +2934 ; unqualified # ⤴ right arrow curving up +2935 FE0F ; fully-qualified # ⤵️ right arrow curving down +2935 ; unqualified # ⤵ right arrow curving down +1F503 ; fully-qualified # 🔃 clockwise vertical arrows +1F504 ; fully-qualified # 🔄 counterclockwise arrows button +1F519 ; fully-qualified # 🔙 BACK arrow +1F51A ; fully-qualified # 🔚 END arrow +1F51B ; fully-qualified # 🔛 ON! arrow +1F51C ; fully-qualified # 🔜 SOON arrow +1F51D ; fully-qualified # 🔝 TOP arrow + +# subgroup: religion +1F6D0 ; fully-qualified # 🛐 place of worship +269B FE0F ; fully-qualified # ⚛️ atom symbol +269B ; unqualified # ⚛ atom symbol +1F549 FE0F ; fully-qualified # 🕉️ om +1F549 ; unqualified # 🕉 om +2721 FE0F ; fully-qualified # ✡️ star of David +2721 ; unqualified # ✡ star of David +2638 FE0F ; fully-qualified # ☸️ wheel of dharma +2638 ; unqualified # ☸ wheel of dharma +262F FE0F ; fully-qualified # ☯️ yin yang +262F ; unqualified # ☯ yin yang +271D FE0F ; fully-qualified # ✝️ latin cross +271D ; unqualified # ✝ latin cross +2626 FE0F ; fully-qualified # ☦️ orthodox cross +2626 ; unqualified # ☦ orthodox cross +262A FE0F ; fully-qualified # ☪️ star and crescent +262A ; unqualified # ☪ star and crescent +262E FE0F ; fully-qualified # ☮️ peace symbol +262E ; unqualified # ☮ peace symbol +1F54E ; fully-qualified # 🕎 menorah +1F52F ; fully-qualified # 🔯 dotted six-pointed star + +# subgroup: zodiac +2648 ; fully-qualified # ♈ Aries +2649 ; fully-qualified # ♉ Taurus +264A ; fully-qualified # ♊ Gemini +264B ; fully-qualified # ♋ Cancer +264C ; fully-qualified # ♌ Leo +264D ; fully-qualified # ♍ Virgo +264E ; fully-qualified # ♎ Libra +264F ; fully-qualified # ♏ Scorpio +2650 ; fully-qualified # ♐ Sagittarius +2651 ; fully-qualified # ♑ Capricorn +2652 ; fully-qualified # ♒ Aquarius +2653 ; fully-qualified # ♓ Pisces +26CE ; fully-qualified # ⛎ Ophiuchus + +# subgroup: av-symbol +1F500 ; fully-qualified # 🔀 shuffle tracks button +1F501 ; fully-qualified # 🔁 repeat button +1F502 ; fully-qualified # 🔂 repeat single button +25B6 FE0F ; fully-qualified # ▶️ play button +25B6 ; unqualified # ▶ play button +23E9 ; fully-qualified # ⏩ fast-forward button +23ED FE0F ; fully-qualified # ⏭️ next track button +23ED ; unqualified # ⏭ next track button +23EF FE0F ; fully-qualified # ⏯️ play or pause button +23EF ; unqualified # ⏯ play or pause button +25C0 FE0F ; fully-qualified # ◀️ reverse button +25C0 ; unqualified # ◀ reverse button +23EA ; fully-qualified # ⏪ fast reverse button +23EE FE0F ; fully-qualified # ⏮️ last track button +23EE ; unqualified # ⏮ last track button +1F53C ; fully-qualified # 🔼 upwards button +23EB ; fully-qualified # ⏫ fast up button +1F53D ; fully-qualified # 🔽 downwards button +23EC ; fully-qualified # ⏬ fast down button +23F8 FE0F ; fully-qualified # ⏸️ pause button +23F8 ; unqualified # ⏸ pause button +23F9 FE0F ; fully-qualified # ⏹️ stop button +23F9 ; unqualified # ⏹ stop button +23FA FE0F ; fully-qualified # ⏺️ record button +23FA ; unqualified # ⏺ record button +23CF FE0F ; fully-qualified # ⏏️ eject button +23CF ; unqualified # ⏏ eject button +1F3A6 ; fully-qualified # 🎦 cinema +1F505 ; fully-qualified # 🔅 dim button +1F506 ; fully-qualified # 🔆 bright button +1F4F6 ; fully-qualified # 📶 antenna bars +1F4F3 ; fully-qualified # 📳 vibration mode +1F4F4 ; fully-qualified # 📴 mobile phone off + +# subgroup: gender +2640 FE0F ; fully-qualified # ♀️ female sign +2640 ; unqualified # ♀ female sign +2642 FE0F ; fully-qualified # ♂️ male sign +2642 ; unqualified # ♂ male sign + +# subgroup: other-symbol +2695 FE0F ; fully-qualified # ⚕️ medical symbol +2695 ; unqualified # ⚕ medical symbol +267E FE0F ; fully-qualified # ♾️ infinity +267E ; unqualified # ♾ infinity +267B FE0F ; fully-qualified # ♻️ recycling symbol +267B ; unqualified # ♻ recycling symbol +269C FE0F ; fully-qualified # ⚜️ fleur-de-lis +269C ; unqualified # ⚜ fleur-de-lis +1F531 ; fully-qualified # 🔱 trident emblem +1F4DB ; fully-qualified # 📛 name badge +1F530 ; fully-qualified # 🔰 Japanese symbol for beginner +2B55 ; fully-qualified # ⭕ heavy large circle +2705 ; fully-qualified # ✅ white heavy check mark +2611 FE0F ; fully-qualified # ☑️ ballot box with check +2611 ; unqualified # ☑ ballot box with check +2714 FE0F ; fully-qualified # ✔️ heavy check mark +2714 ; unqualified # ✔ heavy check mark +2716 FE0F ; fully-qualified # ✖️ heavy multiplication x +2716 ; unqualified # ✖ heavy multiplication x +274C ; fully-qualified # ❌ cross mark +274E ; fully-qualified # ❎ cross mark button +2795 ; fully-qualified # ➕ heavy plus sign +2796 ; fully-qualified # ➖ heavy minus sign +2797 ; fully-qualified # ➗ heavy division sign +27B0 ; fully-qualified # ➰ curly loop +27BF ; fully-qualified # ➿ double curly loop +303D FE0F ; fully-qualified # 〽️ part alternation mark +303D ; unqualified # 〽 part alternation mark +2733 FE0F ; fully-qualified # ✳️ eight-spoked asterisk +2733 ; unqualified # ✳ eight-spoked asterisk +2734 FE0F ; fully-qualified # ✴️ eight-pointed star +2734 ; unqualified # ✴ eight-pointed star +2747 FE0F ; fully-qualified # ❇️ sparkle +2747 ; unqualified # ❇ sparkle +203C FE0F ; fully-qualified # ‼️ double exclamation mark +203C ; unqualified # ‼ double exclamation mark +2049 FE0F ; fully-qualified # ⁉️ exclamation question mark +2049 ; unqualified # ⁉ exclamation question mark +2753 ; fully-qualified # ❓ question mark +2754 ; fully-qualified # ❔ white question mark +2755 ; fully-qualified # ❕ white exclamation mark +2757 ; fully-qualified # ❗ exclamation mark +3030 FE0F ; fully-qualified # 〰️ wavy dash +3030 ; unqualified # 〰 wavy dash +00A9 FE0F ; fully-qualified # ©️ copyright +00A9 ; unqualified # © copyright +00AE FE0F ; fully-qualified # ®️ registered +00AE ; unqualified # ® registered +2122 FE0F ; fully-qualified # ™️ trade mark +2122 ; unqualified # ™ trade mark + +# subgroup: keycap +0023 FE0F 20E3 ; fully-qualified # #️⃣ keycap: # +0023 20E3 ; unqualified # #⃣ keycap: # +002A FE0F 20E3 ; fully-qualified # *️⃣ keycap: * +002A 20E3 ; unqualified # *⃣ keycap: * +0030 FE0F 20E3 ; fully-qualified # 0️⃣ keycap: 0 +0030 20E3 ; unqualified # 0⃣ keycap: 0 +0031 FE0F 20E3 ; fully-qualified # 1️⃣ keycap: 1 +0031 20E3 ; unqualified # 1⃣ keycap: 1 +0032 FE0F 20E3 ; fully-qualified # 2️⃣ keycap: 2 +0032 20E3 ; unqualified # 2⃣ keycap: 2 +0033 FE0F 20E3 ; fully-qualified # 3️⃣ keycap: 3 +0033 20E3 ; unqualified # 3⃣ keycap: 3 +0034 FE0F 20E3 ; fully-qualified # 4️⃣ keycap: 4 +0034 20E3 ; unqualified # 4⃣ keycap: 4 +0035 FE0F 20E3 ; fully-qualified # 5️⃣ keycap: 5 +0035 20E3 ; unqualified # 5⃣ keycap: 5 +0036 FE0F 20E3 ; fully-qualified # 6️⃣ keycap: 6 +0036 20E3 ; unqualified # 6⃣ keycap: 6 +0037 FE0F 20E3 ; fully-qualified # 7️⃣ keycap: 7 +0037 20E3 ; unqualified # 7⃣ keycap: 7 +0038 FE0F 20E3 ; fully-qualified # 8️⃣ keycap: 8 +0038 20E3 ; unqualified # 8⃣ keycap: 8 +0039 FE0F 20E3 ; fully-qualified # 9️⃣ keycap: 9 +0039 20E3 ; unqualified # 9⃣ keycap: 9 +1F51F ; fully-qualified # 🔟 keycap: 10 + +# subgroup: alphanum +1F520 ; fully-qualified # 🔠 input latin uppercase +1F521 ; fully-qualified # 🔡 input latin lowercase +1F522 ; fully-qualified # 🔢 input numbers +1F523 ; fully-qualified # 🔣 input symbols +1F524 ; fully-qualified # 🔤 input latin letters +1F170 FE0F ; fully-qualified # 🅰️ A button (blood type) +1F170 ; unqualified # 🅰 A button (blood type) +1F18E ; fully-qualified # 🆎 AB button (blood type) +1F171 FE0F ; fully-qualified # 🅱️ B button (blood type) +1F171 ; unqualified # 🅱 B button (blood type) +1F191 ; fully-qualified # 🆑 CL button +1F192 ; fully-qualified # 🆒 COOL button +1F193 ; fully-qualified # 🆓 FREE button +2139 FE0F ; fully-qualified # ℹ️ information +2139 ; unqualified # ℹ information +1F194 ; fully-qualified # 🆔 ID button +24C2 FE0F ; fully-qualified # Ⓜ️ circled M +24C2 ; unqualified # Ⓜ circled M +1F195 ; fully-qualified # 🆕 NEW button +1F196 ; fully-qualified # 🆖 NG button +1F17E FE0F ; fully-qualified # 🅾️ O button (blood type) +1F17E ; unqualified # 🅾 O button (blood type) +1F197 ; fully-qualified # 🆗 OK button +1F17F FE0F ; fully-qualified # 🅿️ P button +1F17F ; unqualified # 🅿 P button +1F198 ; fully-qualified # 🆘 SOS button +1F199 ; fully-qualified # 🆙 UP! button +1F19A ; fully-qualified # 🆚 VS button +1F201 ; fully-qualified # 🈁 Japanese “here” button +1F202 FE0F ; fully-qualified # 🈂️ Japanese “service charge” button +1F202 ; unqualified # 🈂 Japanese “service charge” button +1F237 FE0F ; fully-qualified # 🈷️ Japanese “monthly amount” button +1F237 ; unqualified # 🈷 Japanese “monthly amount” button +1F236 ; fully-qualified # 🈶 Japanese “not free of charge” button +1F22F ; fully-qualified # 🈯 Japanese “reserved” button +1F250 ; fully-qualified # 🉐 Japanese “bargain” button +1F239 ; fully-qualified # 🈹 Japanese “discount” button +1F21A ; fully-qualified # 🈚 Japanese “free of charge” button +1F232 ; fully-qualified # 🈲 Japanese “prohibited” button +1F251 ; fully-qualified # 🉑 Japanese “acceptable” button +1F238 ; fully-qualified # 🈸 Japanese “application” button +1F234 ; fully-qualified # 🈴 Japanese “passing grade” button +1F233 ; fully-qualified # 🈳 Japanese “vacancy” button +3297 FE0F ; fully-qualified # ㊗️ Japanese “congratulations” button +3297 ; unqualified # ㊗ Japanese “congratulations” button +3299 FE0F ; fully-qualified # ㊙️ Japanese “secret” button +3299 ; unqualified # ㊙ Japanese “secret” button +1F23A ; fully-qualified # 🈺 Japanese “open for business” button +1F235 ; fully-qualified # 🈵 Japanese “no vacancy” button + +# subgroup: geometric +1F534 ; fully-qualified # 🔴 red circle +1F7E0 ; fully-qualified # 🟠 orange circle +1F7E1 ; fully-qualified # 🟡 yellow circle +1F7E2 ; fully-qualified # 🟢 green circle +1F535 ; fully-qualified # 🔵 blue circle +1F7E3 ; fully-qualified # 🟣 purple circle +26AA ; fully-qualified # ⚪ white circle +1F7E4 ; fully-qualified # 🟤 brown circle +1F7E5 ; fully-qualified # 🟥 red square +1F7E7 ; fully-qualified # 🟧 orange square +1F7E8 ; fully-qualified # 🟨 yellow square +1F7E9 ; fully-qualified # 🟩 green square +1F7E6 ; fully-qualified # 🟦 blue square +1F7EA ; fully-qualified # 🟪 purple square +26AB ; fully-qualified # ⚫ black circle +2B1C ; fully-qualified # ⬜ white large square +1F7EB ; fully-qualified # 🟫 brown square +2B1B ; fully-qualified # ⬛ black large square +25FC FE0F ; fully-qualified # ◼️ black medium square +25FC ; unqualified # ◼ black medium square +25FB FE0F ; fully-qualified # ◻️ white medium square +25FB ; unqualified # ◻ white medium square +25FD ; fully-qualified # ◽ white medium-small square +25FE ; fully-qualified # ◾ black medium-small square +25AB FE0F ; fully-qualified # ▫️ white small square +25AB ; unqualified # ▫ white small square +25AA FE0F ; fully-qualified # ▪️ black small square +25AA ; unqualified # ▪ black small square +1F536 ; fully-qualified # 🔶 large orange diamond +1F537 ; fully-qualified # 🔷 large blue diamond +1F538 ; fully-qualified # 🔸 small orange diamond +1F539 ; fully-qualified # 🔹 small blue diamond +1F53A ; fully-qualified # 🔺 red triangle pointed up +1F53B ; fully-qualified # 🔻 red triangle pointed down +1F4A0 ; fully-qualified # 💠 diamond with a dot +1F518 ; fully-qualified # 🔘 radio button +1F532 ; fully-qualified # 🔲 black square button +1F533 ; fully-qualified # 🔳 white square button + +# Symbols subtotal: 297 +# Symbols subtotal: 297 w/o modifiers + +# group: Flags + +# subgroup: flag +1F3C1 ; fully-qualified # 🏁 chequered flag +1F6A9 ; fully-qualified # 🚩 triangular flag +1F38C ; fully-qualified # 🎌 crossed flags +1F3F4 ; fully-qualified # 🏴 black flag +1F3F3 FE0F ; fully-qualified # 🏳️ white flag +1F3F3 ; unqualified # 🏳 white flag +1F3F3 FE0F 200D 1F308 ; fully-qualified # 🏳️‍🌈 rainbow flag +1F3F3 200D 1F308 ; unqualified # 🏳‍🌈 rainbow flag +1F3F4 200D 2620 FE0F ; fully-qualified # 🏴‍☠️ pirate flag +1F3F4 200D 2620 ; minimally-qualified # 🏴‍☠ pirate flag + +# subgroup: country-flag +1F1E6 1F1E8 ; fully-qualified # 🇦🇨 flag: Ascension Island +1F1E6 1F1E9 ; fully-qualified # 🇦🇩 flag: Andorra +1F1E6 1F1EA ; fully-qualified # 🇦🇪 flag: United Arab Emirates +1F1E6 1F1EB ; fully-qualified # 🇦🇫 flag: Afghanistan +1F1E6 1F1EC ; fully-qualified # 🇦🇬 flag: Antigua & Barbuda +1F1E6 1F1EE ; fully-qualified # 🇦🇮 flag: Anguilla +1F1E6 1F1F1 ; fully-qualified # 🇦🇱 flag: Albania +1F1E6 1F1F2 ; fully-qualified # 🇦🇲 flag: Armenia +1F1E6 1F1F4 ; fully-qualified # 🇦🇴 flag: Angola +1F1E6 1F1F6 ; fully-qualified # 🇦🇶 flag: Antarctica +1F1E6 1F1F7 ; fully-qualified # 🇦🇷 flag: Argentina +1F1E6 1F1F8 ; fully-qualified # 🇦🇸 flag: American Samoa +1F1E6 1F1F9 ; fully-qualified # 🇦🇹 flag: Austria +1F1E6 1F1FA ; fully-qualified # 🇦🇺 flag: Australia +1F1E6 1F1FC ; fully-qualified # 🇦🇼 flag: Aruba +1F1E6 1F1FD ; fully-qualified # 🇦🇽 flag: Åland Islands +1F1E6 1F1FF ; fully-qualified # 🇦🇿 flag: Azerbaijan +1F1E7 1F1E6 ; fully-qualified # 🇧🇦 flag: Bosnia & Herzegovina +1F1E7 1F1E7 ; fully-qualified # 🇧🇧 flag: Barbados +1F1E7 1F1E9 ; fully-qualified # 🇧🇩 flag: Bangladesh +1F1E7 1F1EA ; fully-qualified # 🇧🇪 flag: Belgium +1F1E7 1F1EB ; fully-qualified # 🇧🇫 flag: Burkina Faso +1F1E7 1F1EC ; fully-qualified # 🇧🇬 flag: Bulgaria +1F1E7 1F1ED ; fully-qualified # 🇧🇭 flag: Bahrain +1F1E7 1F1EE ; fully-qualified # 🇧🇮 flag: Burundi +1F1E7 1F1EF ; fully-qualified # 🇧🇯 flag: Benin +1F1E7 1F1F1 ; fully-qualified # 🇧🇱 flag: St. Barthélemy +1F1E7 1F1F2 ; fully-qualified # 🇧🇲 flag: Bermuda +1F1E7 1F1F3 ; fully-qualified # 🇧🇳 flag: Brunei +1F1E7 1F1F4 ; fully-qualified # 🇧🇴 flag: Bolivia +1F1E7 1F1F6 ; fully-qualified # 🇧🇶 flag: Caribbean Netherlands +1F1E7 1F1F7 ; fully-qualified # 🇧🇷 flag: Brazil +1F1E7 1F1F8 ; fully-qualified # 🇧🇸 flag: Bahamas +1F1E7 1F1F9 ; fully-qualified # 🇧🇹 flag: Bhutan +1F1E7 1F1FB ; fully-qualified # 🇧🇻 flag: Bouvet Island +1F1E7 1F1FC ; fully-qualified # 🇧🇼 flag: Botswana +1F1E7 1F1FE ; fully-qualified # 🇧🇾 flag: Belarus +1F1E7 1F1FF ; fully-qualified # 🇧🇿 flag: Belize +1F1E8 1F1E6 ; fully-qualified # 🇨🇦 flag: Canada +1F1E8 1F1E8 ; fully-qualified # 🇨🇨 flag: Cocos (Keeling) Islands +1F1E8 1F1E9 ; fully-qualified # 🇨🇩 flag: Congo - Kinshasa +1F1E8 1F1EB ; fully-qualified # 🇨🇫 flag: Central African Republic +1F1E8 1F1EC ; fully-qualified # 🇨🇬 flag: Congo - Brazzaville +1F1E8 1F1ED ; fully-qualified # 🇨🇭 flag: Switzerland +1F1E8 1F1EE ; fully-qualified # 🇨🇮 flag: Côte d’Ivoire +1F1E8 1F1F0 ; fully-qualified # 🇨🇰 flag: Cook Islands +1F1E8 1F1F1 ; fully-qualified # 🇨🇱 flag: Chile +1F1E8 1F1F2 ; fully-qualified # 🇨🇲 flag: Cameroon +1F1E8 1F1F3 ; fully-qualified # 🇨🇳 flag: China +1F1E8 1F1F4 ; fully-qualified # 🇨🇴 flag: Colombia +1F1E8 1F1F5 ; fully-qualified # 🇨🇵 flag: Clipperton Island +1F1E8 1F1F7 ; fully-qualified # 🇨🇷 flag: Costa Rica +1F1E8 1F1FA ; fully-qualified # 🇨🇺 flag: Cuba +1F1E8 1F1FB ; fully-qualified # 🇨🇻 flag: Cape Verde +1F1E8 1F1FC ; fully-qualified # 🇨🇼 flag: Curaçao +1F1E8 1F1FD ; fully-qualified # 🇨🇽 flag: Christmas Island +1F1E8 1F1FE ; fully-qualified # 🇨🇾 flag: Cyprus +1F1E8 1F1FF ; fully-qualified # 🇨🇿 flag: Czechia +1F1E9 1F1EA ; fully-qualified # 🇩🇪 flag: Germany +1F1E9 1F1EC ; fully-qualified # 🇩🇬 flag: Diego Garcia +1F1E9 1F1EF ; fully-qualified # 🇩🇯 flag: Djibouti +1F1E9 1F1F0 ; fully-qualified # 🇩🇰 flag: Denmark +1F1E9 1F1F2 ; fully-qualified # 🇩🇲 flag: Dominica +1F1E9 1F1F4 ; fully-qualified # 🇩🇴 flag: Dominican Republic +1F1E9 1F1FF ; fully-qualified # 🇩🇿 flag: Algeria +1F1EA 1F1E6 ; fully-qualified # 🇪🇦 flag: Ceuta & Melilla +1F1EA 1F1E8 ; fully-qualified # 🇪🇨 flag: Ecuador +1F1EA 1F1EA ; fully-qualified # 🇪🇪 flag: Estonia +1F1EA 1F1EC ; fully-qualified # 🇪🇬 flag: Egypt +1F1EA 1F1ED ; fully-qualified # 🇪🇭 flag: Western Sahara +1F1EA 1F1F7 ; fully-qualified # 🇪🇷 flag: Eritrea +1F1EA 1F1F8 ; fully-qualified # 🇪🇸 flag: Spain +1F1EA 1F1F9 ; fully-qualified # 🇪🇹 flag: Ethiopia +1F1EA 1F1FA ; fully-qualified # 🇪🇺 flag: European Union +1F1EB 1F1EE ; fully-qualified # 🇫🇮 flag: Finland +1F1EB 1F1EF ; fully-qualified # 🇫🇯 flag: Fiji +1F1EB 1F1F0 ; fully-qualified # 🇫🇰 flag: Falkland Islands +1F1EB 1F1F2 ; fully-qualified # 🇫🇲 flag: Micronesia +1F1EB 1F1F4 ; fully-qualified # 🇫🇴 flag: Faroe Islands +1F1EB 1F1F7 ; fully-qualified # 🇫🇷 flag: France +1F1EC 1F1E6 ; fully-qualified # 🇬🇦 flag: Gabon +1F1EC 1F1E7 ; fully-qualified # 🇬🇧 flag: United Kingdom +1F1EC 1F1E9 ; fully-qualified # 🇬🇩 flag: Grenada +1F1EC 1F1EA ; fully-qualified # 🇬🇪 flag: Georgia +1F1EC 1F1EB ; fully-qualified # 🇬🇫 flag: French Guiana +1F1EC 1F1EC ; fully-qualified # 🇬🇬 flag: Guernsey +1F1EC 1F1ED ; fully-qualified # 🇬🇭 flag: Ghana +1F1EC 1F1EE ; fully-qualified # 🇬🇮 flag: Gibraltar +1F1EC 1F1F1 ; fully-qualified # 🇬🇱 flag: Greenland +1F1EC 1F1F2 ; fully-qualified # 🇬🇲 flag: Gambia +1F1EC 1F1F3 ; fully-qualified # 🇬🇳 flag: Guinea +1F1EC 1F1F5 ; fully-qualified # 🇬🇵 flag: Guadeloupe +1F1EC 1F1F6 ; fully-qualified # 🇬🇶 flag: Equatorial Guinea +1F1EC 1F1F7 ; fully-qualified # 🇬🇷 flag: Greece +1F1EC 1F1F8 ; fully-qualified # 🇬🇸 flag: South Georgia & South Sandwich Islands +1F1EC 1F1F9 ; fully-qualified # 🇬🇹 flag: Guatemala +1F1EC 1F1FA ; fully-qualified # 🇬🇺 flag: Guam +1F1EC 1F1FC ; fully-qualified # 🇬🇼 flag: Guinea-Bissau +1F1EC 1F1FE ; fully-qualified # 🇬🇾 flag: Guyana +1F1ED 1F1F0 ; fully-qualified # 🇭🇰 flag: Hong Kong SAR China +1F1ED 1F1F2 ; fully-qualified # 🇭🇲 flag: Heard & McDonald Islands +1F1ED 1F1F3 ; fully-qualified # 🇭🇳 flag: Honduras +1F1ED 1F1F7 ; fully-qualified # 🇭🇷 flag: Croatia +1F1ED 1F1F9 ; fully-qualified # 🇭🇹 flag: Haiti +1F1ED 1F1FA ; fully-qualified # 🇭🇺 flag: Hungary +1F1EE 1F1E8 ; fully-qualified # 🇮🇨 flag: Canary Islands +1F1EE 1F1E9 ; fully-qualified # 🇮🇩 flag: Indonesia +1F1EE 1F1EA ; fully-qualified # 🇮🇪 flag: Ireland +1F1EE 1F1F1 ; fully-qualified # 🇮🇱 flag: Israel +1F1EE 1F1F2 ; fully-qualified # 🇮🇲 flag: Isle of Man +1F1EE 1F1F3 ; fully-qualified # 🇮🇳 flag: India +1F1EE 1F1F4 ; fully-qualified # 🇮🇴 flag: British Indian Ocean Territory +1F1EE 1F1F6 ; fully-qualified # 🇮🇶 flag: Iraq +1F1EE 1F1F7 ; fully-qualified # 🇮🇷 flag: Iran +1F1EE 1F1F8 ; fully-qualified # 🇮🇸 flag: Iceland +1F1EE 1F1F9 ; fully-qualified # 🇮🇹 flag: Italy +1F1EF 1F1EA ; fully-qualified # 🇯🇪 flag: Jersey +1F1EF 1F1F2 ; fully-qualified # 🇯🇲 flag: Jamaica +1F1EF 1F1F4 ; fully-qualified # 🇯🇴 flag: Jordan +1F1EF 1F1F5 ; fully-qualified # 🇯🇵 flag: Japan +1F1F0 1F1EA ; fully-qualified # 🇰🇪 flag: Kenya +1F1F0 1F1EC ; fully-qualified # 🇰🇬 flag: Kyrgyzstan +1F1F0 1F1ED ; fully-qualified # 🇰🇭 flag: Cambodia +1F1F0 1F1EE ; fully-qualified # 🇰🇮 flag: Kiribati +1F1F0 1F1F2 ; fully-qualified # 🇰🇲 flag: Comoros +1F1F0 1F1F3 ; fully-qualified # 🇰🇳 flag: St. Kitts & Nevis +1F1F0 1F1F5 ; fully-qualified # 🇰🇵 flag: North Korea +1F1F0 1F1F7 ; fully-qualified # 🇰🇷 flag: South Korea +1F1F0 1F1FC ; fully-qualified # 🇰🇼 flag: Kuwait +1F1F0 1F1FE ; fully-qualified # 🇰🇾 flag: Cayman Islands +1F1F0 1F1FF ; fully-qualified # 🇰🇿 flag: Kazakhstan +1F1F1 1F1E6 ; fully-qualified # 🇱🇦 flag: Laos +1F1F1 1F1E7 ; fully-qualified # 🇱🇧 flag: Lebanon +1F1F1 1F1E8 ; fully-qualified # 🇱🇨 flag: St. Lucia +1F1F1 1F1EE ; fully-qualified # 🇱🇮 flag: Liechtenstein +1F1F1 1F1F0 ; fully-qualified # 🇱🇰 flag: Sri Lanka +1F1F1 1F1F7 ; fully-qualified # 🇱🇷 flag: Liberia +1F1F1 1F1F8 ; fully-qualified # 🇱🇸 flag: Lesotho +1F1F1 1F1F9 ; fully-qualified # 🇱🇹 flag: Lithuania +1F1F1 1F1FA ; fully-qualified # 🇱🇺 flag: Luxembourg +1F1F1 1F1FB ; fully-qualified # 🇱🇻 flag: Latvia +1F1F1 1F1FE ; fully-qualified # 🇱🇾 flag: Libya +1F1F2 1F1E6 ; fully-qualified # 🇲🇦 flag: Morocco +1F1F2 1F1E8 ; fully-qualified # 🇲🇨 flag: Monaco +1F1F2 1F1E9 ; fully-qualified # 🇲🇩 flag: Moldova +1F1F2 1F1EA ; fully-qualified # 🇲🇪 flag: Montenegro +1F1F2 1F1EB ; fully-qualified # 🇲🇫 flag: St. Martin +1F1F2 1F1EC ; fully-qualified # 🇲🇬 flag: Madagascar +1F1F2 1F1ED ; fully-qualified # 🇲🇭 flag: Marshall Islands +1F1F2 1F1F0 ; fully-qualified # 🇲🇰 flag: Macedonia +1F1F2 1F1F1 ; fully-qualified # 🇲🇱 flag: Mali +1F1F2 1F1F2 ; fully-qualified # 🇲🇲 flag: Myanmar (Burma) +1F1F2 1F1F3 ; fully-qualified # 🇲🇳 flag: Mongolia +1F1F2 1F1F4 ; fully-qualified # 🇲🇴 flag: Macau SAR China +1F1F2 1F1F5 ; fully-qualified # 🇲🇵 flag: Northern Mariana Islands +1F1F2 1F1F6 ; fully-qualified # 🇲🇶 flag: Martinique +1F1F2 1F1F7 ; fully-qualified # 🇲🇷 flag: Mauritania +1F1F2 1F1F8 ; fully-qualified # 🇲🇸 flag: Montserrat +1F1F2 1F1F9 ; fully-qualified # 🇲🇹 flag: Malta +1F1F2 1F1FA ; fully-qualified # 🇲🇺 flag: Mauritius +1F1F2 1F1FB ; fully-qualified # 🇲🇻 flag: Maldives +1F1F2 1F1FC ; fully-qualified # 🇲🇼 flag: Malawi +1F1F2 1F1FD ; fully-qualified # 🇲🇽 flag: Mexico +1F1F2 1F1FE ; fully-qualified # 🇲🇾 flag: Malaysia +1F1F2 1F1FF ; fully-qualified # 🇲🇿 flag: Mozambique +1F1F3 1F1E6 ; fully-qualified # 🇳🇦 flag: Namibia +1F1F3 1F1E8 ; fully-qualified # 🇳🇨 flag: New Caledonia +1F1F3 1F1EA ; fully-qualified # 🇳🇪 flag: Niger +1F1F3 1F1EB ; fully-qualified # 🇳🇫 flag: Norfolk Island +1F1F3 1F1EC ; fully-qualified # 🇳🇬 flag: Nigeria +1F1F3 1F1EE ; fully-qualified # 🇳🇮 flag: Nicaragua +1F1F3 1F1F1 ; fully-qualified # 🇳🇱 flag: Netherlands +1F1F3 1F1F4 ; fully-qualified # 🇳🇴 flag: Norway +1F1F3 1F1F5 ; fully-qualified # 🇳🇵 flag: Nepal +1F1F3 1F1F7 ; fully-qualified # 🇳🇷 flag: Nauru +1F1F3 1F1FA ; fully-qualified # 🇳🇺 flag: Niue +1F1F3 1F1FF ; fully-qualified # 🇳🇿 flag: New Zealand +1F1F4 1F1F2 ; fully-qualified # 🇴🇲 flag: Oman +1F1F5 1F1E6 ; fully-qualified # 🇵🇦 flag: Panama +1F1F5 1F1EA ; fully-qualified # 🇵🇪 flag: Peru +1F1F5 1F1EB ; fully-qualified # 🇵🇫 flag: French Polynesia +1F1F5 1F1EC ; fully-qualified # 🇵🇬 flag: Papua New Guinea +1F1F5 1F1ED ; fully-qualified # 🇵🇭 flag: Philippines +1F1F5 1F1F0 ; fully-qualified # 🇵🇰 flag: Pakistan +1F1F5 1F1F1 ; fully-qualified # 🇵🇱 flag: Poland +1F1F5 1F1F2 ; fully-qualified # 🇵🇲 flag: St. Pierre & Miquelon +1F1F5 1F1F3 ; fully-qualified # 🇵🇳 flag: Pitcairn Islands +1F1F5 1F1F7 ; fully-qualified # 🇵🇷 flag: Puerto Rico +1F1F5 1F1F8 ; fully-qualified # 🇵🇸 flag: Palestinian Territories +1F1F5 1F1F9 ; fully-qualified # 🇵🇹 flag: Portugal +1F1F5 1F1FC ; fully-qualified # 🇵🇼 flag: Palau +1F1F5 1F1FE ; fully-qualified # 🇵🇾 flag: Paraguay +1F1F6 1F1E6 ; fully-qualified # 🇶🇦 flag: Qatar +1F1F7 1F1EA ; fully-qualified # 🇷🇪 flag: Réunion +1F1F7 1F1F4 ; fully-qualified # 🇷🇴 flag: Romania +1F1F7 1F1F8 ; fully-qualified # 🇷🇸 flag: Serbia +1F1F7 1F1FA ; fully-qualified # 🇷🇺 flag: Russia +1F1F7 1F1FC ; fully-qualified # 🇷🇼 flag: Rwanda +1F1F8 1F1E6 ; fully-qualified # 🇸🇦 flag: Saudi Arabia +1F1F8 1F1E7 ; fully-qualified # 🇸🇧 flag: Solomon Islands +1F1F8 1F1E8 ; fully-qualified # 🇸🇨 flag: Seychelles +1F1F8 1F1E9 ; fully-qualified # 🇸🇩 flag: Sudan +1F1F8 1F1EA ; fully-qualified # 🇸🇪 flag: Sweden +1F1F8 1F1EC ; fully-qualified # 🇸🇬 flag: Singapore +1F1F8 1F1ED ; fully-qualified # 🇸🇭 flag: St. Helena +1F1F8 1F1EE ; fully-qualified # 🇸🇮 flag: Slovenia +1F1F8 1F1EF ; fully-qualified # 🇸🇯 flag: Svalbard & Jan Mayen +1F1F8 1F1F0 ; fully-qualified # 🇸🇰 flag: Slovakia +1F1F8 1F1F1 ; fully-qualified # 🇸🇱 flag: Sierra Leone +1F1F8 1F1F2 ; fully-qualified # 🇸🇲 flag: San Marino +1F1F8 1F1F3 ; fully-qualified # 🇸🇳 flag: Senegal +1F1F8 1F1F4 ; fully-qualified # 🇸🇴 flag: Somalia +1F1F8 1F1F7 ; fully-qualified # 🇸🇷 flag: Suriname +1F1F8 1F1F8 ; fully-qualified # 🇸🇸 flag: South Sudan +1F1F8 1F1F9 ; fully-qualified # 🇸🇹 flag: São Tomé & Príncipe +1F1F8 1F1FB ; fully-qualified # 🇸🇻 flag: El Salvador +1F1F8 1F1FD ; fully-qualified # 🇸🇽 flag: Sint Maarten +1F1F8 1F1FE ; fully-qualified # 🇸🇾 flag: Syria +1F1F8 1F1FF ; fully-qualified # 🇸🇿 flag: Swaziland +1F1F9 1F1E6 ; fully-qualified # 🇹🇦 flag: Tristan da Cunha +1F1F9 1F1E8 ; fully-qualified # 🇹🇨 flag: Turks & Caicos Islands +1F1F9 1F1E9 ; fully-qualified # 🇹🇩 flag: Chad +1F1F9 1F1EB ; fully-qualified # 🇹🇫 flag: French Southern Territories +1F1F9 1F1EC ; fully-qualified # 🇹🇬 flag: Togo +1F1F9 1F1ED ; fully-qualified # 🇹🇭 flag: Thailand +1F1F9 1F1EF ; fully-qualified # 🇹🇯 flag: Tajikistan +1F1F9 1F1F0 ; fully-qualified # 🇹🇰 flag: Tokelau +1F1F9 1F1F1 ; fully-qualified # 🇹🇱 flag: Timor-Leste +1F1F9 1F1F2 ; fully-qualified # 🇹🇲 flag: Turkmenistan +1F1F9 1F1F3 ; fully-qualified # 🇹🇳 flag: Tunisia +1F1F9 1F1F4 ; fully-qualified # 🇹🇴 flag: Tonga +1F1F9 1F1F7 ; fully-qualified # 🇹🇷 flag: Turkey +1F1F9 1F1F9 ; fully-qualified # 🇹🇹 flag: Trinidad & Tobago +1F1F9 1F1FB ; fully-qualified # 🇹🇻 flag: Tuvalu +1F1F9 1F1FC ; fully-qualified # 🇹🇼 flag: Taiwan +1F1F9 1F1FF ; fully-qualified # 🇹🇿 flag: Tanzania +1F1FA 1F1E6 ; fully-qualified # 🇺🇦 flag: Ukraine +1F1FA 1F1EC ; fully-qualified # 🇺🇬 flag: Uganda +1F1FA 1F1F2 ; fully-qualified # 🇺🇲 flag: U.S. Outlying Islands +1F1FA 1F1F3 ; fully-qualified # 🇺🇳 flag: United Nations +1F1FA 1F1F8 ; fully-qualified # 🇺🇸 flag: United States +1F1FA 1F1FE ; fully-qualified # 🇺🇾 flag: Uruguay +1F1FA 1F1FF ; fully-qualified # 🇺🇿 flag: Uzbekistan +1F1FB 1F1E6 ; fully-qualified # 🇻🇦 flag: Vatican City +1F1FB 1F1E8 ; fully-qualified # 🇻🇨 flag: St. Vincent & Grenadines +1F1FB 1F1EA ; fully-qualified # 🇻🇪 flag: Venezuela +1F1FB 1F1EC ; fully-qualified # 🇻🇬 flag: British Virgin Islands +1F1FB 1F1EE ; fully-qualified # 🇻🇮 flag: U.S. Virgin Islands +1F1FB 1F1F3 ; fully-qualified # 🇻🇳 flag: Vietnam +1F1FB 1F1FA ; fully-qualified # 🇻🇺 flag: Vanuatu +1F1FC 1F1EB ; fully-qualified # 🇼🇫 flag: Wallis & Futuna +1F1FC 1F1F8 ; fully-qualified # 🇼🇸 flag: Samoa +1F1FD 1F1F0 ; fully-qualified # 🇽🇰 flag: Kosovo +1F1FE 1F1EA ; fully-qualified # 🇾🇪 flag: Yemen +1F1FE 1F1F9 ; fully-qualified # 🇾🇹 flag: Mayotte +1F1FF 1F1E6 ; fully-qualified # 🇿🇦 flag: South Africa +1F1FF 1F1F2 ; fully-qualified # 🇿🇲 flag: Zambia +1F1FF 1F1FC ; fully-qualified # 🇿🇼 flag: Zimbabwe + +# subgroup: subdivision-flag +1F3F4 E0067 E0062 E0065 E006E E0067 E007F ; fully-qualified # 🏴󠁧󠁢󠁥󠁮󠁧󠁿 flag: England +1F3F4 E0067 E0062 E0073 E0063 E0074 E007F ; fully-qualified # 🏴󠁧󠁢󠁳󠁣󠁴󠁿 flag: Scotland +1F3F4 E0067 E0062 E0077 E006C E0073 E007F ; fully-qualified # 🏴󠁧󠁢󠁷󠁬󠁳󠁿 flag: Wales + +# Flags subtotal: 271 +# Flags subtotal: 271 w/o modifiers + +# Status Counts +# fully-qualified : 3044 +# minimally-qualified : 591 +# unqualified : 246 +# component : 9 + +#EOF diff --git a/.archive/units/__init__.py b/.archive/units/__init__.py new file mode 100644 index 00000000..aef326b8 --- /dev/null +++ b/.archive/units/__init__.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- + +"""Convert units. + +This extension is a wrapper for the (extremely) powerful GNU units tool. Note that spaces are \ +interpreted as separators, i.e. dont use spaces between numbers and units. + +Synopsis: + [dst] + to """ + +# Copyright (c) 2022 Manuel Schneider + +import re +import subprocess as sp +from albert import * + +__title__ = "GNU Units" +__version__ = "0.4.2" +__triggers__ = "units " +__authors__ = ["Manuel S.", "iyzana"] +__exec_deps__ = ["units"] + +icon = iconLookup('calc') or ":python_module" + +regex = re.compile(r"(\S+)(?:\s+to)\s+(\S+)") +unitListOutput = re.compile(r"(\d+(e[+-]\d{2,})?;)+[\d.]+(e[+-]\d{2,})?") + + +def getUnitsResult(args): + command = ['units', '--terse', '--'] + list(args) + query = "units -t -- %s" % ' '.join(args) + try: + output = sp.check_output(command, stderr=sp.STDOUT).decode().strip() + + # usually we want terse output, but when we get a unit-list output + # it looks like this 1;124;18;11;14.025322 which is not friendly + # so we're falling back to not quite terse output + if unitListOutput.fullmatch(output): + command = ['units', '--strict', '--one-line', + '--quiet', '--'] + list(args) + query = "units -s1q -- %s" % ' '.join(args) + output = sp.check_output( + command, stderr=sp.STDOUT).decode().strip() + + return (output, query, True) + except sp.CalledProcessError as e: + return (e.stdout.decode().strip().splitlines()[0], query, False) + + +def handleQuery(query): + if query.isTriggered: + args = query.string.split() + item = Item(id='python.gnu_units', icon=icon) + if args: + result, command, success = getUnitsResult(args) + item.text = result + item.subtext = "Result of '%s'" % command + item.addAction(ClipAction("Copy to clipboard", item.text)) + else: + item.text = "Empty input" + item.subtext = "Enter a query of the form []" + return item + else: + match = regex.fullmatch(query.string.strip()) + if match: + args = match.group(1, 2) + result, command, success = getUnitsResult(args) + if not success: + return + item = Item(id='python.gnu_units', icon=icon) + item.text = result + item.subtext = "Result of '%s'" % command + item.addAction(ClipAction("Copy to clipboard", item.text)) + return item diff --git a/.archive/vpn/__init__.py b/.archive/vpn/__init__.py new file mode 100644 index 00000000..f529bc93 --- /dev/null +++ b/.archive/vpn/__init__.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020 janeklb +# Copyright (c) 2023 Bierchermuesli +# Copyright (c) 2020-2024 Manuel Schneider + +import subprocess +from collections import namedtuple + +from albert import * + +md_iid = "3.0" +md_version = "2.0" +md_name = "VPN" +md_description = "Manage NetworkManager VPN connections" +md_license = "MIT" +md_url = "https://github.com/albertlauncher/python/tree/main/vpn" +md_authors = ["@janeklb", "@Bierchermuesli", "@manuelschneid3r"] +md_bin_dependencies = ["nmcli"] + + +class Plugin(PluginInstance, TriggerQueryHandler): + + VPNConnection = namedtuple('VPNConnection', ['name', 'connected']) + + def __init__(self): + PluginInstance.__init__(self) + TriggerQueryHandler.__init__(self) + + def defaultTrigger(self): + return "vpn " + + def getVPNConnections(self): + consStr = subprocess.check_output( + 'nmcli -t connection show', + shell=True, + encoding='UTF-8' + ) + for conStr in consStr.splitlines(): + con = conStr.split(':') + if con[2] in ['vpn', 'wireguard']: + yield self.VPNConnection(name=con[0], connected=con[3] != '') + + @staticmethod + def buildItem(con): + name = con.name + command = 'down' if con.connected else 'up' + text = f'Connect to {name}' if command == 'up' else f'Disconnect from {name}' + commandline = ['nmcli', 'connection', command, 'id', name] + return StandardItem( + id=f'vpn-{command}-{name}', + text=name, + subtext=text, + iconUrls=['xdg:network-wired'], + inputActionText=name, + actions=[Action("run", text=text, callable=lambda: runDetachedProcess(commandline))] + ) + + def handleTriggerQuery(self, query): + if query.isValid: + connections = self.getVPNConnections() + if query.string: + connections = [con for con in connections if query.string.lower() in con.name.lower()] + query.add([self.buildItem(con) for con in connections]) diff --git a/.archive/window_switcher/__init__.py b/.archive/window_switcher/__init__.py new file mode 100644 index 00000000..ea65e157 --- /dev/null +++ b/.archive/window_switcher/__init__.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- + +"""List and manage X11 windows. + +Synopsis: """ + +# Copyright (c) 2022 Manuel Schneider + +import subprocess +from collections import namedtuple +from albert import Item, ProcAction, iconLookup + +__title__ = "Window Switcher" +__version__ = "0.4.5" +__authors__ = ["Ed Perez", "Manuel S.", "dshoreman"] +__exec_deps__ = ["wmctrl"] + +Window = namedtuple("Window", ["wid", "desktop", "wm_class", "host", "wm_name"]) + +def handleQuery(query): + stripped = query.string.strip().lower() + if stripped: + results = [] + for line in subprocess.check_output(['wmctrl', '-l', '-x']).splitlines(): + win = Window(*parseWindow(line)) + + if win.desktop == "-1": + continue + + win_instance, win_class = win.wm_class.replace(' ', '-').split('.') + matches = [ + win_instance.lower(), + win_class.lower(), + win.wm_name.lower() + ] + + if any(stripped in match for match in matches): + iconPath = iconLookup(win_instance) or iconLookup(win_class.lower()) + results.append(Item(id="%s%s" % (__title__, win.wm_class), + icon=iconPath, + text="%s - Desktop %s" % (win_class.replace('-',' '), win.desktop), + subtext=win.wm_name, + actions=[ProcAction("Switch Window", + ["wmctrl", '-i', '-a', win.wid] ), + ProcAction("Move window to this desktop", + ["wmctrl", '-i', '-R', win.wid] ), + ProcAction("Close the window gracefully.", + ["wmctrl", '-c', win.wid])])) + return results + +def parseWindow(line): + win_id, desktop, rest = line.decode().split(None, 2) + win_class, rest = rest.split(' ', 1) + host, title = rest.strip().split(None, 1) + + return [win_id, desktop, win_class, host, title] diff --git a/.archive/xkcd/.gitignore b/.archive/xkcd/.gitignore new file mode 100644 index 00000000..8f793ad3 --- /dev/null +++ b/.archive/xkcd/.gitignore @@ -0,0 +1,72 @@ +# Compiled source # +###################### +*.com +*.class +*.dll +*.exe +*.o +*.so +*.pyc + +# Packages # +###################### +# it's better to unpack these files and commit the raw source +# git has its own built in compression methods +*.7z +*.dmg +*.gz +*.iso +*.jar +*.rar +*.tar +*.zip + +# Logs and databases # +###################### +*.log +*.sql +*.sqlite + +# OS generated files # +###################### +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +tags + +_build + +# CMake +build* +documentation +*.user* +log +*.dir* +*.a +*.make +doc/html* +doc/latex* + +# Python +.mypy_cache +# Python - Coverage +.coverage +coverage.xml +htmlcov/ + +# Vim +Session.vim +.netrwhist +*~ +tags +.projections.json +compile_commands.json + + +# backup files - created during sed operations +*.bak diff --git a/.archive/xkcd/LICENSE b/.archive/xkcd/LICENSE new file mode 100644 index 00000000..de8114f9 --- /dev/null +++ b/.archive/xkcd/LICENSE @@ -0,0 +1,18 @@ +Copyright 2019 Nikos Koukis + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/.archive/xkcd/README.md b/.archive/xkcd/README.md new file mode 100644 index 00000000..e271f7d1 --- /dev/null +++ b/.archive/xkcd/README.md @@ -0,0 +1,54 @@ +# Albert xkcd Plugin + + + + + + +## Description + +The xkcd Albert plugin lets you launch an xkcd comic in your browser from the +albert prompt. Here are its main features: + +* On toggle (default trigger: `xkcd`) it shows you the comics in newest-first + order. +* If you want to find a comic with a specific title then you can use fuzzy search to do so): + * `xkcd some words from the title` + +## Demo + +![demo_gif](https://github.com/bergercookie/xkcd-albert-plugin/blob/master/misc/demo.gif) + +## Motivation + +I love reading xkcd. I also wanted to get into developing plugins for Albert +thus this was the perfect opportunity to do so. + +## Manual installation instructions + +Requirements: + +- Albert - [Installation instructions](https://albertlauncher.github.io/docs/installing/) + - Albert Python Interface: v0.2 +- Python version >= 3.5 + + +Download and run the ``install-plugin.sh`` script or run the following to do +that automatically: + +``````sh +curl https://raw.githubusercontent.com/bergercookie/xkcd-albert-plugin/master/install-plugin.sh | bash +`````` + +## Self Promotion + +If you find this tool useful, please [star it on +Github](https://github.com/bergercookie/xkcd-albert-plugin) + +## TODO List + +See [ISSUES list](https://github.com/bergercookie/xkcd-albert-plugin/issues) for +the things that I'm currently either working on or interested in implementing in +the near future. In case there's something you are interesting in working on, +don't hesitate to either ask for clarifications or just do it and directly make +a PR. diff --git a/.archive/xkcd/__init__.py b/.archive/xkcd/__init__.py new file mode 100644 index 00000000..0089739b --- /dev/null +++ b/.archive/xkcd/__init__.py @@ -0,0 +1,126 @@ +"""Fetch xkcd comics like a boss.""" + +# Copyright (c) 2022 Manuel Schneider + +from datetime import datetime, timedelta +from pathlib import Path +import json +import os +import subprocess +import sys + +import albertv0 as v0 +from fuzzywuzzy import process +from shutil import which + +__iid__ = "PythonInterface/v0.2" +__prettyname__ = "xkcd" +__version__ = "0.1" +__trigger__ = "xkcd" +__author__ = "Nikos Koukis" +__dependencies__ = [] +__homepage__ = "https://github.com/bergercookie/xkcd-albert-plugin" + + +# TODO pyproject toml file +# TODO xkcd-dl executable? +# TODO Upload to github - change support url on error +# TODO Send to albert plugins + +if not which("xkcd-dl"): + raise RuntimeError("xkcd-dl not in $PATH - Please install it via pip3 first.") + +iconPath = v0.iconLookup("xkcd") +if not iconPath: + iconPath = os.path.join(os.path.dirname(__file__), "image.png") +SETTINGS_PATH = Path(v0.cacheLocation()) / "xkcd" +LAST_UPDATE_PATH = SETTINGS_PATH / "last_update" +XKCD_DICT = Path.home() / ".xkcd_dict.json" + + +def initialize(): + # Called when the extension is loaded (ticked in the settings) - blocking + + # create cache location + SETTINGS_PATH.mkdir(parents=False, exist_ok=True) + if not LAST_UPDATE_PATH.is_file(): + update_date_file() + update_xkcd_db() + + +def finalize(): + pass + + +def handleQuery(query): + results = [] + + # check whether I have downlaoded the latest metadata + with open(LAST_UPDATE_PATH, "r") as f: + date_str = float(f.readline().strip()) + + last_date = datetime.fromtimestamp(date_str) + if datetime.now() - last_date > timedelta(days=1): # run an update daily + update_date_file() + update_xkcd_db() + + if query.isTriggered: + try: + with open(XKCD_DICT, "r", encoding="utf-8") as f: + d = json.load(f) + + if len(query.string) in [0, 1]: # Display all items + for k, v in d.items(): + results.append(get_as_item(k, v)) + else: # fuzzy search + desc_to_item = {item[1]["description"]: item for item in d.items()} + matched = process.extract( + query.string.strip(), list(desc_to_item.keys()), limit=20 + ) + for m in [elem[0] for elem in matched]: + # bypass a unicode issue - use .get + item = desc_to_item.get(m) + if item: + results.append(get_as_item(*item)) + + except Exception as e: # user to report error + results.insert( + 0, + v0.Item( + id=__prettyname__, + icon=iconPath, + text="Something went wrong! Press [ENTER] to copy error and report it", + actions=[ + v0.ClipAction( + f"Copy error - report it to {__homepage__[8:]}", + f"{sys.exc_info()}", + ) + ], + ), + ) + + return results + + +def get_as_item(k: str, v: dict): + return v0.Item( + id=__prettyname__, + icon=iconPath, + text=v["description"], + subtext=v["date-published"], + completion="", + actions=[ + v0.UrlAction("Open in xkcd.com", f"https://www.xkcd.com/{k}"), + v0.ClipAction("Copy URL", f"https://www.xkcd.com/{k}"), + ], + ) + + +def update_date_file(): + now = (datetime.now() - datetime(1970, 1, 1)).total_seconds() + with open(LAST_UPDATE_PATH, "w") as f: + f.write(str(now)) + + +def update_xkcd_db(): + return subprocess.call(["xkcd-dl", "-u"]) diff --git a/.archive/xkcd/image.png b/.archive/xkcd/image.png new file mode 100644 index 00000000..690194bc Binary files /dev/null and b/.archive/xkcd/image.png differ diff --git a/.archive/xkcd/install-plugin.sh b/.archive/xkcd/install-plugin.sh new file mode 100755 index 00000000..2e98fac9 --- /dev/null +++ b/.archive/xkcd/install-plugin.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +set -Eeuo pipefail +## do this if there's any error with the installation and you want to report a bug +# set -x + +# supplementary funs ----------------------------------------------------------- +function announce +{ + echo + echo "**********************************************************************" + echo -e "$@" + echo "**********************************************************************" + echo +} + +function announce_err +{ + announce "[ERROR] $*" +} + +function install_pkg +{ + announce "Installing \"$*\"" + pip3 install --user --upgrade "$*" + announce "Installed $*" +} + +function is_installed +{ + if [ "$(which "$*" 2>&1 1>/dev/null)" = "1" ] + then + return 1 + else + return 0 + fi +} + +# Check prereqs ---------------------------------------------------------------- +ret=$(is_installed albert) +if [ "$ret" = "1" ] +then + announce_err "Please install albert first. Exiting" + return 1 +fi +ret=$(is_installed git) +if [ "$ret" = "1" ] +then + announce_err "Please install git first. Exiting" + return 1 +fi + +DST="$HOME/.local/share/albert/org.albert.extension.python/modules" +if [[ ! -d "$DST" ]] +then + announce_err "Local extensions directory doesn't exist. Please check your albert installation. Exiting" + return 1 +fi + +# Install ---------------------------------------------------------------------- +install_pkg git+https://github.com/tasdikrahman/xkcd-dl +install_pkg fuzzywuzzy + +# Seesm like the xkcd-dl beautifulsoup4 version is outdated +install_pkg beautifulsoup4 + +PLUGIN_DIR="$DST/xkcd" +if [ -d "$PLUGIN_DIR" ] +then + rm -rf "$PLUGIN_DIR" +fi +announce "Cloning and installing xkcd-albert-plugin -> $PLUGIN_DIR" +git clone https://github.com/bergercookie/xkcd-albert-plugin "$PLUGIN_DIR" +announce "Installed xkcd-albert-plugin -> $PLUGIN_DIR" + +announce "Plugin ready - Enable it from the Albert settings" diff --git a/.archive/xkcd/misc/demo.gif b/.archive/xkcd/misc/demo.gif new file mode 100644 index 00000000..4205356f Binary files /dev/null and b/.archive/xkcd/misc/demo.gif differ diff --git a/.archive/youtube/__init__.py b/.archive/youtube/__init__.py new file mode 100644 index 00000000..f1b58ec0 --- /dev/null +++ b/.archive/youtube/__init__.py @@ -0,0 +1,189 @@ +import json +import re +import tempfile +import time +from concurrent.futures import ThreadPoolExecutor +from pathlib import Path +from typing import Any +from urllib.parse import urlencode +from urllib.request import Request, urlopen + +from albert import Action, StandardItem, TriggerQuery, PluginInstance, TriggerQueryHandler, openUrl # pylint: disable=import-error + + +md_iid = '2.0' +md_version = '1.5' +md_name = 'YouTube' +md_description = 'Query and open YouTube videos and channels' +md_url = 'https://github.com/albertlauncher/python/' +md_maintainers = '@stevenxxiu' + +DATA_REGEX = re.compile(r'\b(var\s|window\[")ytInitialData("\])?\s*=\s*(.*)\s*;', re.MULTILINE) + +HEADERS = { + 'User-Agent': ( + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36' + ) +} + + +def log_html(html: bytes) -> None: + log_time = time.strftime('%Y%m%d-%H%M%S') + log_name = 'albert.plugins.youtube_dump' + log_path = Path(f'/tmp/{log_name}-{log_time}.html') + + with log_path.open('wb') as sr: + sr.write(html) + + critical(f'The HTML output has been dumped to {log_path}') + critical('If the page looks ok in a browser, please include the dump in a new issue:') + critical(' https://www.github.com/albertlauncher/albert/issues/new') + + +def urlopen_with_headers(url: str) -> Any: + req = Request(headers=HEADERS, url=url) + return urlopen(req) + + +def text_from(val: dict[str, Any]) -> str: + text = val['simpleText'] if 'runs' not in val else ''.join(str(v['text']) for v in val['runs']) + + return text.strip() + + +def download_item_icon(item: StandardItem, temp_dir: Path) -> None: + url = item.icon[0] + video_id = url.split('/')[-2] + path = temp_dir / f'{video_id}.png' + with urlopen_with_headers(url) as response, path.open('wb') as sr: + sr.write(response.read()) + item.icon = ["file:" + str(path)] + + +def entry_to_item(type_, data) -> StandardItem | None: + icon = Plugin.iconUrls[0] + match type_: + case 'videoRenderer': + subtext = ['Video'] + action = 'Watch on Youtube' + url_path = f'watch?v={data["videoId"]}' + if 'lengthText' in data: + subtext.append(text_from(data['lengthText'])) + if 'shortViewCountText' in data: + subtext.append(text_from(data['shortViewCountText'])) + if 'publishedTimeText' in data: + subtext.append(text_from(data['publishedTimeText'])) + if data['thumbnail']['thumbnails']: + icon = data['thumbnail']['thumbnails'][0]['url'].split('?', 1)[0] + case 'channelRenderer': + subtext = ['Channel'] + action = 'Show on Youtube' + url_path = f'channel/{data["channelId"]}' + if 'videoCountText' in data: + subtext.append(text_from(data['videoCountText'])) + if 'subscriberCountText' in data: + subtext.append(text_from(data['subscriberCountText'])) + case _: + return None + + return StandardItem( + id=f'{md_name}/{url_path}', + text=text_from(data['title']), + subtext=' | '.join(subtext), + iconUrls=[icon], + actions=[Action(f'{md_name}/{url_path}', action, lambda: openUrl(f'https://www.youtube.com/{url_path}'))], + ) + + +def results_to_items(results: dict) -> list[StandardItem]: + items: list[Item] = [] + for result in results: + for type_, data in result.items(): + try: + item = entry_to_item(type_, data) + if item is None: + continue + items.append(item) + except KeyError as e: + critical(e) + critical(json.dumps(result, indent=4)) + return items + + +class Plugin(PluginInstance, TriggerQueryHandler): + temp_dir = None + iconUrls = [f"file:{Path(__file__).parent}/youtube.svg"] + + def __init__(self): + TriggerQueryHandler.__init__(self, + id=md_id, + name=md_name, + description=md_description, + synopsis='query', + defaultTrigger='yt ') + PluginInstance.__init__(self, extensions=[self]) + self.temp_dir = Path(tempfile.mkdtemp(prefix='albert_yt_')) + + def finalize(self) -> None: + for child in self.temp_dir.iterdir(): + child.unlink() + self.temp_dir.rmdir() + + def handleTriggerQuery(self, query: TriggerQuery) -> None: + query_str = query.string.strip() + if not query_str: + return + + # Avoid rate limiting + for _ in range(50): + time.sleep(0.01) + if not query.isValid: + return + + info(f'Searching YouTube for \'{query_str}\'') + url = f'https://www.youtube.com/results?{urlencode({"search_query": query_str})}' + + with urlopen_with_headers(url) as response: + response_bytes: bytes = response.read() + match = re.search(DATA_REGEX, response_bytes.decode()) + if match is None: + critical( + 'Failed to receive expected data from YouTube. This likely means API changes, but could just be a ' + 'failed request.' + ) + log_html(response_bytes) + return + + results = json.loads(match.group(3)) + primary_contents = results['contents']['twoColumnSearchResultsRenderer']['primaryContents'] + results = primary_contents['sectionListRenderer']['contents'][0]['itemSectionRenderer']['contents'] + items = results_to_items(results) + + # Purge previous icons + for child in self.temp_dir.iterdir(): + child.unlink() + + # Download icons + with ThreadPoolExecutor(max_workers=10) as e: + for item in items: + e.submit(download_item_icon, item, self.temp_dir) + if not query.isValid: + return + + for item in items: + query.add(item) + + # Add a link to the *YouTube* page, in case there's more results, including results we didn't include + item = StandardItem( + id=f'{md_name}/show_more', + text='Show more in browser', + iconUrls=self.iconUrls, + actions=[ + Action( + f'{md_name}/show_more', + 'Show more in browser', + lambda: openUrl(f'https://www.youtube.com/results?search_query={query_str}'), + ) + ], + ) + query.add(item) diff --git a/.archive/youtube/youtube.svg b/.archive/youtube/youtube.svg new file mode 100644 index 00000000..8d07b9db --- /dev/null +++ b/.archive/youtube/youtube.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.github/workflows/telegram_notify_comments.yml b/.github/workflows/telegram_notify_comments.yml new file mode 100644 index 00000000..a7658abd --- /dev/null +++ b/.github/workflows/telegram_notify_comments.yml @@ -0,0 +1,25 @@ +name: Telegram Notifications + +on: + + issue_comment: + types: [created] + +jobs: + notify: + + runs-on: ubuntu-latest + + steps: + - name: Send notifications to Telegram + run: > + curl -s + -X POST https://api.telegram.org/bot${{ secrets.TELEGRAM_NOTIFIER_BOT_TOKEN }}/sendMessage + -d chat_id=${{ secrets.TELEGRAM_ALBERT_CHAT_ID }} + -d text="${MESSAGE}" + -d parse_mode=HTML + -d disable_web_page_preview=true + >> /dev/null + env: + MESSAGE: "${{ github.event.comment.user.login }} on ${{ github.event.repository.name }}#${{ github.event.issue.number }}: ${{ github.event.issue.title }}%0A${{ github.event.comment.body }}" + diff --git a/.github/workflows/telegram_notify_issues.yml b/.github/workflows/telegram_notify_issues.yml new file mode 100644 index 00000000..dfcc0ab4 --- /dev/null +++ b/.github/workflows/telegram_notify_issues.yml @@ -0,0 +1,24 @@ +name: Telegram Notifications + +on: + issues: + types: [opened, reopened] + +jobs: + notify: + + runs-on: ubuntu-latest + + steps: + - name: Send notifications to Telegram + run: > + curl -s + -X POST https://api.telegram.org/bot${{ secrets.TELEGRAM_NOTIFIER_BOT_TOKEN }}/sendMessage + -d chat_id=${{ secrets.TELEGRAM_ALBERT_CHAT_ID }} + -d text="${MESSAGE}" + -d parse_mode=HTML + -d disable_web_page_preview=true + >> /dev/null + env: + MESSAGE: "New issue:%0A${{ github.event.repository.name }}#${{ github.event.issue.number }}: ${{ github.event.issue.title }}" + diff --git a/.gitignore b/.gitignore index c20c2ab7..bef49833 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ __pycache__ - +/.idea +/.vscode +/.venv +albert.pyi \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..e69de29b diff --git a/AUR/__init__.py b/AUR/__init__.py deleted file mode 100644 index c13e34ae..00000000 --- a/AUR/__init__.py +++ /dev/null @@ -1,100 +0,0 @@ -# -*- coding: utf-8 -*- - -"""Arch User Repository extension - -This extension adapts the AUR web interface. You can search for packages and open their URLs. - -This extension is also intended to be used to quickly install the packages. Currently yaourt and -pacaur can be used. If you are missing your favorite AUR helper tool send a PR.""" - -from albertv0 import * -from shutil import which -from datetime import datetime -from shlex import split -from urllib import request, parse -import json -import os -import re - -__iid__ = "PythonInterface/v0.1" -__prettyname__ = "Archlinux User Repository" -__version__ = "1.0" -__trigger__ = "aur " -__author__ = "Manuel Schneider" -__dependencies__ = [] - -iconPath = os.path.dirname(__file__)+"/arch.svg" -baseurl = 'https://aur.archlinux.org/rpc/' -install_cmdline = None - -if which("yaourt"): - install_cmdline = "yaourt -S aur/%s" -elif which("pacaur"): - install_cmdline = "pacaur -S aur/%s" - - -def handleQuery(query): - if not query.isTriggered: - return - - stripped = query.string.strip() - - if stripped: - params = { - 'v': '5', - 'type': 'search', - 'by': 'name', - 'arg': stripped - } - url = "%s?%s" % (baseurl, parse.urlencode(params)) - req = request.Request(url) - - with request.urlopen(req) as response: - data = json.load(response) - if data['type'] == "error": - return Item( - id=__prettyname__, - icon=iconPath, - text="Error", - subtext=data['error'], - completion=query.rawString - ) - else: - results = [] - pattern = re.compile(query.string, re.IGNORECASE) - for entry in data['results']: - name = entry['Name'] - - item = Item( - id=__prettyname__, - icon=iconPath, - text="%s %s (%s)" % (pattern.sub(lambda m: "%s" % m.group(0), name), entry['Version'], entry['NumVotes']), - completion=query.rawString - ) - subtext = entry['Description'] if entry['Description'] else "" - if entry['OutOfDate']: - subtext = '[Out of date: %s] %s' % (datetime.fromtimestamp(entry['OutOfDate']).strftime("%F"), subtext) - if entry['Maintainer'] is None: - subtext = '[Orphan] %s' % subtext - item.subtext = subtext - - if install_cmdline: - item.addAction(TermAction("Install with yaourt", split(install_cmdline % name))) - - if install_cmdline: - item.addAction(TermAction("Install with yaourt (noconfirm)", split(install_cmdline % name) + ["--noconfirm"])) - - item.addAction(UrlAction("Open AUR website", "https://aur.archlinux.org/packages/%s/" % name)) - - if entry['URL']: - item.addAction(UrlAction("Open project website", entry['URL'])) - - results.append(item) - return results - else: - return Item(id=__prettyname__, - icon=iconPath, - text=__prettyname__, - subtext="Enter a query to search the AUR", - completion=query.rawString, - actions=[UrlAction("Open AUR packages website", "https://aur.archlinux.org/packages/")]) diff --git a/AUR/arch.svg b/AUR/arch.svg deleted file mode 100644 index f57758c5..00000000 --- a/AUR/arch.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/ApiTest/__init__.py b/ApiTest/__init__.py deleted file mode 100644 index cbb9cdad..00000000 --- a/ApiTest/__init__.py +++ /dev/null @@ -1,98 +0,0 @@ -"""This is a simple python template extension that should show the API in a comprehensible way. -Use the module docstring to provide a detailed description of the extension""" - -from albertv0 import * -import os -from time import sleep - - -__iid__ = "PythonInterface/v0.1" -__prettyname__ = "Api Test" -__version__ = "1.0" -__trigger__ = "test " -__author__ = "Manuel Schneider" -__dependencies__ = ["whatever"] - -iconPath = iconLookup("albert") - - -# Can be omitted -def initialize(): - pass - - -# Can be omitted -def finalize(): - pass - - -def handleQuery(query): - - # Note that when storing a reference to query, e.g. in a closure, you must not use - # query.isValid. Apart from the query beeing invalid anyway it will crash the appplication. - # The Python type holds a pointer to the C++ type used for isValid(). The C++ type will be - # deleted when the query is finished. Therfore getting isValid will result in a SEGFAULT. - - if query.string.startswith("delay"): - sleep(2) - return Item(id=__prettyname__, - icon=os.path.dirname(__file__)+"/plugin.svg", - text="Delayed test item", - subtext="Query string: %s" % query.string) - - if query.string.startswith("throw"): - raise ValueError('EXPLICITLY REQUESTED TEST EXCEPTION!') - - info(query.string) - info(query.rawString) - info(query.trigger) - info(str(query.isTriggered)) - info(str(query.isValid)) - - critical(query.string) - warning(query.string) - debug(query.string) - debug(query.string) - - results = [] - - item = Item() - - item.icon = iconPath - item.text = 'Python item containing %s' % query.string - item.subtext = 'Python description' - item.completion = __trigger__ + 'Completion Harharhar' - item.urgency = ItemBase.Notification # Alert, Normal - info(item.icon) - info(item.text) - info(item.subtext) - info(item.completion) - info(str(item.urgency)) - def function(): info(query.string) - item.addAction(FuncAction("Print info", function)) - item.addAction(FuncAction("Print warning", lambda: warning(query.string))) - results.append(item) - - item = Item(id=__prettyname__, - icon=os.path.dirname(__file__)+"/plugin.svg", - text="This is the primary text", - subtext="This is the subtext, some kind of description", - completion=__trigger__ + 'Hellooohooo!', - urgency=ItemBase.Alert, - actions=[ - FuncAction(text="FuncAction", - callable=lambda: critical(query.string)), - ClipAction(text="ClipAction", - clipboardText="blabla"), - UrlAction(text="UrlAction", - url="https://www.google.de"), - ProcAction(text="ProcAction", - commandline=["espeak", "hello"], - cwd="~"), # optional - TermAction(text="TermAction", - commandline=["sleep", "5"], - cwd="~/git") # optional - ]) - results.append(item) - - return results diff --git a/ApiTest/plugin.svg b/ApiTest/plugin.svg deleted file mode 100644 index 39adbca3..00000000 --- a/ApiTest/plugin.svg +++ /dev/null @@ -1,339 +0,0 @@ - - - - - - - image/svg+xml - - Plug-in - - - Lapo Calamandrei - - - - - - - - plugin - plug-in - extension - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/BaseConverter.py b/BaseConverter.py deleted file mode 100644 index 8b21e36e..00000000 --- a/BaseConverter.py +++ /dev/null @@ -1,37 +0,0 @@ -# -*- coding: utf-8 -*- - -"""Convert representations of numbers. -Usage: base -Example: base 10 16 1234567890""" - -from albertv0 import * -import numpy as np - -__iid__ = "PythonInterface/v0.1" -__prettyname__ = "Base Converter" -__version__ = "1.0" -__trigger__ = "base " -__author__ = "Manuel Schneider" -__dependencies__ = ["numpy"] - - -def handleQuery(query): - if query.isTriggered: - fields = query.string.split() - item = Item(id=__prettyname__, completion=query.rawString) - if len(fields) == 3: - try: - src = int(fields[0]) - dst = int(fields[1]) - number = fields[2] - item.text = np.base_repr(int(number, src), dst) - item.subtext = "Base %s representation of %s (base %s)" % (dst, number, src) - item.addAction(ClipAction("Copy to clipboard", item.text)) - except Exception as e: - item.text = e.__class__.__name__ - item.subtext = str(e) - return item - else: - item.text = __prettyname__ - item.subtext = "Enter a query in the form of " - return item diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..4742ff5a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,16 @@ +## How to contribute to this repository + +### Do you have an issue? + +* **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/albertlauncher/albert/issues). +* Create a new issue using the templates provided. +* Ping the authors of the related plugin. + +### Do you want to contribute code? + +* Add a copyright notice, otherwise the code is in public domain. +* You agree to publish your contribution under the MIT license. +* Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable. +* Changes that do not add anything substantial to the stability, functionality, or testability will generally not be accepted. + +Thanks! :heart: diff --git a/CopyQ.py b/CopyQ.py deleted file mode 100644 index 542f1b36..00000000 --- a/CopyQ.py +++ /dev/null @@ -1,83 +0,0 @@ -"""CopyQ Clipboard Management""" - -import html -import subprocess -from albertv0 import * -from shutil import which -import json -import re - -__iid__ = "PythonInterface/v0.1" -__prettyname__ = "CopyQ" -__version__ = "1.0" -__trigger__ = "cq " -__author__ = "Manuel Schneider" -__dependencies__ = ["copyq"] - - -if which("copyq") is None: - raise Exception("'copyq' is not in $PATH.") - -iconPath = iconLookup('copyq') - -copyq_script_getAll = r""" -var result=[]; -for ( var i = 0; i < size(); ++i ) { - var obj = {}; - obj.row = i; - obj.mimetypes = str(read("?", i)).split("\n"); - obj.mimetypes.pop(); - obj.text = str(read(i)); - result.push(obj); -} -JSON.stringify(result); -""" - -copyq_script_getMatches = r""" -var result=[]; -var match = "%s"; -for ( var i = 0; i < size(); ++i ) { - if (str(read(i)).search(new RegExp(match, "i")) !== -1) { - var obj = {}; - obj.row = i; - obj.mimetypes = str(read("?", i)).split("\n"); - obj.mimetypes.pop(); - obj.text = str(read(i)); - result.push(obj); - } -} -JSON.stringify(result); -""" - - -def handleQuery(query): - if query.isTriggered: - - script = copyq_script_getMatches % query.string if query.string else copyq_script_getAll - - proc = subprocess.run(['copyq', '-'], input=script.encode(), stdout=subprocess.PIPE) - json_arr = json.loads(proc.stdout.decode()) - - items = [] - pattern = re.compile(query.string, re.IGNORECASE) - for json_obj in json_arr: - row = json_obj['row'] - text = json_obj['text'] - if not text: - text = "No text" - else: - text = pattern.sub(lambda m: "%s" % m.group(0), html.escape(" ".join(filter(None, text.replace("\n", " ").split(" "))))) - items.append( - Item( - id=__prettyname__, - icon=iconPath, - text=text, - subtext="%s: %s" % (row, ", ".join(json_obj['mimetypes'])), - actions=[ - ProcAction("Paste", ["copyq", "select(%s); sleep(60); paste();" % row]), - ProcAction("Copy", ["copyq", "select(%s);" % row]), - ProcAction("Remove", ["copyq", "remove(%s);" % row]), - ] - ) - ) - return items diff --git a/Currency.py b/Currency.py deleted file mode 100644 index 15af48b1..00000000 --- a/Currency.py +++ /dev/null @@ -1,45 +0,0 @@ -# -*- coding: utf-8 -*- - -"""Convert currencies using Google Finance. -Usage: exch -Example: exch 5 usd eur""" - -from albertv0 import * -import urllib -import re - -__iid__ = "PythonInterface/v0.1" -__prettyname__ = "Currency converter" -__version__ = "1.0" -__trigger__ = "exch " -__author__ = "Manuel Schneider" -__dependencies__ = [] - -iconPath = iconLookup('accessories-calculator') -if not iconPath: - iconPath = ":python_module" - - -def handleQuery(query): - if query.isTriggered: - fields = query.string.split() - item = Item(id=__prettyname__, icon=iconPath, completion=query.rawString) - if len(fields) == 3: - url = 'https://finance.google.com/finance/converter?a=%s&from=%s&to=%s' % tuple(fields) - with urllib.request.urlopen(url) as response: - html = response.read().decode("latin-1") - m = re.search('
.*(\d+\.\d+).*', html) - if m: - result = m.group(1) - item.text = result - item.subtext = "Value of %s %s in %s" % tuple([x.upper() for x in fields]) - item.addAction(ClipAction("Copy result to clipboard", result)) - return item - else: - item.text = "Error: HTTP reply does not contain a result" - item.subtext = "Maybe Google Finance changed their website" - return item - else: - item.text = __prettyname__ - item.subtext = "Enter a query in the form of " - return item diff --git a/GoldenDict.py b/GoldenDict.py deleted file mode 100644 index a46bcf51..00000000 --- a/GoldenDict.py +++ /dev/null @@ -1,31 +0,0 @@ -# -*- coding: utf-8 -*- - -"""Fire up an external search in GoldenDict. -Just type gd """ - -from albertv0 import * -from subprocess import run -from shutil import which - -__iid__ = "PythonInterface/v0.1" -__prettyname__ = "GoldenDict" -__version__ = "1.0" -__trigger__ = "gd " -__author__ = "Manuel Schneider" -__dependencies__ = ["goldendict"] - -if which("goldendict") is None: - raise Exception("'goldendict' is not in $PATH.") - -iconPath = iconLookup('goldendict') - - -def handleQuery(query): - if query.isTriggered: - return Item(id=__prettyname__, - icon=iconPath, - text=__prettyname__, - subtext="Look up '%s' using %s" % (query.string, __prettyname__), - completion=query.rawString, - actions=[ProcAction("Start query in %s" % __prettyname__, - ["goldendict", query.string])]) diff --git a/Kill.py b/Kill.py deleted file mode 100644 index 240593dd..00000000 --- a/Kill.py +++ /dev/null @@ -1,42 +0,0 @@ -""" Kill Process Extension """ - -import os -from signal import SIGKILL, SIGTERM -from albertv0 import * - -__iid__ = "PythonInterface/v0.1" -__prettyname__ = "Kill Process" -__version__ = "1.2" -__trigger__ = "kill " -__author__ = "Benedict Dudel, Manuel Schneider" -__dependencies__ = [] - -iconPath = iconLookup('process-stop') - - -def handleQuery(query): - if query.isTriggered: - results = [] - uid = os.getuid() - for dir_entry in os.scandir('/proc'): - if dir_entry.name.isdigit() and dir_entry.stat().st_uid == uid: - try: - proc_command = open(os.path.join(dir_entry.path, 'comm'), 'r').read().strip() - if query.string in proc_command: - proc_cmdline = open(os.path.join(dir_entry.path, 'cmdline'), 'r').read().strip().replace("\0", " ") - results.append( - Item( - id="kill_%s" % dir_entry.name, - icon=iconPath, - text=proc_command.replace(query.string, "%s" % query.string), - subtext=proc_cmdline, - completion=query.rawString, - actions=[ - FuncAction("Terminate", lambda pid=int(dir_entry.name): os.kill(pid, SIGTERM)), - FuncAction("Kill", lambda pid=int(dir_entry.name): os.kill(pid, SIGKILL)) - ] - ) - ) - except IOError: # TOCTOU dirs may disappear - continue - return results diff --git a/Locate.py b/Locate.py deleted file mode 100644 index 4078952b..00000000 --- a/Locate.py +++ /dev/null @@ -1,54 +0,0 @@ -"""locate adapter extension - -Note that it is up to you to ensure that the database is up to date""" - -import os -import subprocess -from shutil import which -from albertv0 import * -import re - -__iid__ = "PythonInterface/v0.1" -__prettyname__ = "Locate" -__version__ = "1.0" -__trigger__ = "'" -__author__ = "Manuel Schneider" -__dependencies__ = ['locate'] - -if which("locate") is None: - raise Exception("'locate' is not in $PATH.") - -for iconName in ["system-search", "search", "text-x-generic"]: - iconPath = iconLookup(iconName) - if iconPath: - break - - -def handleQuery(query): - results = [] - if query.isTriggered: - if len(query.string) > 2: - pattern = re.compile(query.string, re.IGNORECASE) - proc = subprocess.Popen(['locate', '-bi', query.string], stdout=subprocess.PIPE) - for line in proc.stdout: - path = line.decode().strip() - basename = os.path.basename(path) - results.append( - Item( - id=path, - icon=iconPath, - text=pattern.sub(lambda m: "%s" % m.group(0), basename), - subtext=path, - completion="%s%s" % (__trigger__, basename), - actions=[UrlAction("Open", "file://%s" % path)])) - else: - results.append( - Item( - id=__prettyname__, - icon=iconPath, - text="Update locate database", - subtext="Type at least three chars for a seach", - completion=query.rawString, - actions=[TermAction("Update database", ["sudo", "updatedb"])])) - - return results diff --git a/Pacman.py b/Pacman.py deleted file mode 100644 index 471ccc0c..00000000 --- a/Pacman.py +++ /dev/null @@ -1,84 +0,0 @@ -"""Extension for the package manager `pacman` - -The extension provides a way to install, remove and search for packages in the -archlinux.org database. To trigger the extension you just need to type `pacman ` -in albert. - -If no search query is supplied you have the option to do a system update. -Otherwise albert will try to search for packages with the search query within -the package name. - -For more information about `pacman` please have a look at: - - https://wiki.archlinux.org/index.php/pacman""" - -from albertv0 import * -from shutil import which -import subprocess -import re - -__iid__ = "PythonInterface/v0.1" -__prettyname__ = "PacMan" -__version__ = "1.1" -__trigger__ = "pacman " -__author__ = "Manuel Schneider, Benedict Dudel" -__dependencies__ = ["pacman", "expac"] - - -if which("pacman") is None: - raise Exception("'pacman' is not in $PATH.") - -iconPath = iconLookup("system-software-install") - - -def handleQuery(query): - if query.isTriggered: - if not query.string.strip(): - return Item( - id="%s-update" % __prettyname__, - icon=iconPath, - text="Update all packages on the system", - subtext="Synchronizes the repository databases and updates the system's packages", - completion=__trigger__, - actions=[TermAction("Update the system", ["sudo", "pacman", "-Syu"])] - ) - - items = [] - pattern = re.compile(query.string, re.IGNORECASE) - proc = subprocess.Popen(["expac", "-Ss", "%n\n%v\n%r\n%d\n%u", query.string], - stdout=subprocess.PIPE) - for line in proc.stdout: - name = line.decode().rstrip() - vers = proc.stdout.readline().decode().rstrip() - repo = proc.stdout.readline().decode().rstrip() - desc = proc.stdout.readline().decode().rstrip() - purl = proc.stdout.readline().decode().rstrip() - - items.append(Item( - id="%s%s%s" % (__prettyname__, repo, name), - icon=iconPath, - text="%s %s [%s]" % (pattern.sub(lambda m: "%s" % m.group(0), name), vers, repo), - subtext=pattern.sub(lambda m: "%s" % m.group(0), desc), - completion="%s%s" % (query.trigger, name), - actions=[ - TermAction("Install", ["sudo", "pacman", "-S", name]), - TermAction("Remove", ["sudo", "pacman", "-Rs", name]), - UrlAction("Show on packages.archlinux.org", - "https://www.archlinux.org/packages/%s/x86_64/%s/" % (repo, name)), - UrlAction("Show project website", purl) - ] - )) - - if not items: - return Item( - id="%s-empty" % __prettyname__, - icon=iconPath, - text="Search on archlinux.org", - subtext="No results found in the local database", - completion=__trigger__, - actions=[ - UrlAction("Search on archlinux.org", - "https://www.archlinux.org/packages/?q=%s" % query.string.strip()) - ] - ) - - return items diff --git a/Pass.py b/Pass.py deleted file mode 100644 index 8f341ced..00000000 --- a/Pass.py +++ /dev/null @@ -1,90 +0,0 @@ -""" Passwordstore Extension """ - -import os -import fnmatch -from albertv0 import * -from shutil import which - -__iid__ = "PythonInterface/v0.1" -__prettyname__ = "Pass" -__version__ = "1.0" -__trigger__ = "pass " -__author__ = "Benedict Dudel" -__dependencies__ = ["pass"] - - -if which("pass") is None: - raise Exception("'pass' is not in $PATH.") - -HOME_DIR = os.environ["HOME"] -PASS_DIR = os.environ.get("PASSWORD_STORE_DIR", os.path.join(HOME_DIR, ".password-store/")) -ICON_PATH = iconLookup("dialog-password") - - -def handleQuery(query): - if query.isTriggered: - if query.string.strip().startswith("generate"): - return generatePassword(query) - - return showPasswords(query) - -def generatePassword(query): - location = query.string.strip()[9:] - - return [Item( - id=__prettyname__, - icon=ICON_PATH, - text="Generate a new password", - subtext="The new password will be located at %s" % location, - completion="pass %s" % query.string, - actions=[ - ProcAction("Generate", ["pass", "generate", "--clip", location, "20"]) - ] - )] - -def showPasswords(query): - passwords = [] - if query.string.strip(): - passwords = getPasswordsFromSearch(query) - else: - passwords = getPasswords() - - results = [] - for password in passwords: - name = password.split("/")[-1] - results.append( - Item( - id=password, - icon=ICON_PATH, - text=name, - subtext=password, - completion="pass %s" % password, - actions=[ - ProcAction("Copy", ["pass", "--clip", password]), - ProcAction("Edit", ["pass", "edit", password]), - ProcAction("Remove", ["pass", "rm", "--force", password]), - ] - ), - ) - - return results - -def getPasswords(): - passwords = [] - for root, dirnames, filenames in os.walk(PASS_DIR): - for filename in fnmatch.filter(filenames, "*.gpg"): - passwords.append( - os.path.join(root, filename.replace(".gpg", "")).replace(PASS_DIR, "") - ) - - return sorted(passwords, key=lambda s: s.lower()) - -def getPasswordsFromSearch(query): - passwords = [] - for password in getPasswords(): - if query.string.strip() not in password: - continue - - passwords.append(password) - - return passwords diff --git a/Python/__init__.py b/Python/__init__.py deleted file mode 100644 index a8bd3c37..00000000 --- a/Python/__init__.py +++ /dev/null @@ -1,44 +0,0 @@ -# -*- coding: utf-8 -*- - -"""Evaluate simple python expressions. -Use it with care every keystroke triggers an evaluation.""" - -from albertv0 import * -from math import * -from builtins import pow -try: - import numpy as np -except ImportError: - pass -import os - -__iid__ = "PythonInterface/v0.1" -__prettyname__ = "Python Eval" -__version__ = "1.0" -__trigger__ = "py " -__author__ = "Manuel Schneider" -__dependencies__ = [] - - -iconPath = os.path.dirname(__file__)+"/python.svg" - - -def handleQuery(query): - if query.isTriggered: - item = Item(id=__prettyname__, icon=iconPath, completion=query.rawString) - stripped = query.string.strip() - - if stripped == '': - item.text = "Enter a python expression" - item.subtext = "Math is in the namespace and, if installed, also Numpy as 'np'" - return item - else: - try: - result = eval(stripped) - except Exception as ex: - result = ex - item.text = str(result) - item.subtext = type(result).__name__ - item.addAction(ClipAction("Copy result to clipboard", str(result))) - item.addAction(FuncAction("Execute", lambda: exec(str(result)))) - return item diff --git a/README.md b/README.md index 17b6d7f2..95b590ae 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,3 @@ -### This is the official repository for python extensions. +# ⚠️ ARCHIVED ⚠️ -This repository is shipped with albert. If you want to have bleeding edge extensions or share your extension clone the repository. Check the docs on Python extensions. - -To install the extensions in user space type the following in your terminal: - -``` -git clone https://github.com/albertlauncher/python.git "~/.local/share/albert/org.albert.extension.python/modules" -``` - -If you send a PR I'll invite you to the reviewers team (if I don't forget it), I'd appreciate if you could review others contributions. +The plugins in this repository have been moved to [dedicated repositories](https://github.com/orgs/albertlauncher/repositories) using `history_to_submodules.sh`. diff --git a/Units.py b/Units.py deleted file mode 100644 index ab777171..00000000 --- a/Units.py +++ /dev/null @@ -1,44 +0,0 @@ -# -*- coding: utf-8 -*- - -"""This extension is an adaptor for the powerful GNU units. -Synopsis: 'units [to]' -Note that spaces are separators.""" - -from albertv0 import * -import subprocess as sp -from shutil import which - -__iid__ = "PythonInterface/v0.1" -__prettyname__ = "GNU Units" -__version__ = "1.0" -__trigger__ = "units " -__author__ = "Manuel Schneider" -__dependencies__ = ["units"] - -if which("units") is None: - raise Exception("'units' is not in $PATH.") - -icon = iconLookup('calc') -if not icon: - icon = ":python_module" - - -def handleQuery(query): - if query.isTriggered: - args = query.string.split() - item = Item(id='python.gnu_units', icon=icon, completion=query.rawString) - if len(args) < 1: - item.text = 'Enter something to convert' - item.subtext = 'Units takes one or two arguments.' - elif len(args) > 2: - item.text = 'Too many arguments' - item.subtext = 'Units takes one or two arguments.' - else: - try: - item.text = sp.check_output(['units', '-t'] + args, - stderr=sp.STDOUT).decode('utf-8').strip() - except sp.CalledProcessError as e: - item.text = e.stdout.decode('utf-8').strip().partition('\n')[0] - item.subtext = "Result of 'units -t %s'" % query.string - item.addAction(ClipAction("Copy to clipboard", item.text)) - return item diff --git a/Wikipedia/__init__.py b/Wikipedia/__init__.py deleted file mode 100644 index 25b82af6..00000000 --- a/Wikipedia/__init__.py +++ /dev/null @@ -1,85 +0,0 @@ -# -*- coding: utf-8 -*- - -"""Search Wikipedia articles.""" - -from albertv0 import * -from locale import getlocale -from urllib import request, parse -import json -import os - -__iid__ = "PythonInterface/v0.1" -__prettyname__ = "Wikipedia" -__version__ = "1.1" -__trigger__ = "wiki " -__author__ = "Manuel Schneider" -__dependencies__ = [] - -iconPath = iconLookup('wikipedia') -if not iconPath: - iconPath = os.path.dirname(__file__)+"/wikipedia.svg" -baseurl = 'https://en.wikipedia.org/w/api.php' -user_agent = "org.arlbert.extension.python.wikipedia" -limit = 20 - - -def initialize(): - global baseurl - params = { - 'action': 'query', - 'meta': 'siteinfo', - 'utf8': 1, - 'siprop': 'languages', - 'format': 'json' - } - - get_url = "%s?%s" % (baseurl, parse.urlencode(params)) - req = request.Request(get_url, headers={'User-Agent': user_agent}) - with request.urlopen(req) as response: - data = json.load(response) - languages = [lang['code'] for lang in data['query']['languages']] - local_lang_code = getlocale()[0][0:2] - if local_lang_code in languages: - baseurl = baseurl.replace("en", local_lang_code) - - -def handleQuery(query): - if query.isTriggered: - - stripped = query.string.strip() - - if stripped: - results = [] - - params = { - 'action': 'opensearch', - 'search': stripped, - 'limit': limit, - 'utf8': 1, - 'format': 'json' - } - get_url = "%s?%s" % (baseurl, parse.urlencode(params)) - req = request.Request(get_url, headers={'User-Agent': user_agent}) - - with request.urlopen(req) as response: - data = json.load(response) - - for i in range(0, min(limit, len(data[1]))): - title = data[1][i] - summary = data[2][i] - url = data[3][i] - - results.append(Item(id=__prettyname__, - icon=iconPath, - text=title, - subtext=summary if summary else url, - completion=title, - actions=[UrlAction("Open Wikipedia", url)])) - - return results - else: - return Item(id=__prettyname__, - icon=iconPath, - text=__prettyname__, - subtext="Enter a query to search on Wikipedia", - completion=query.rawString) diff --git a/Wikipedia/wikipedia.svg b/Wikipedia/wikipedia.svg deleted file mode 100644 index 290f4359..00000000 --- a/Wikipedia/wikipedia.svg +++ /dev/null @@ -1,641 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Zeal.py b/Zeal.py deleted file mode 100644 index 170978d2..00000000 --- a/Zeal.py +++ /dev/null @@ -1,33 +0,0 @@ -# -*- coding: utf-8 -*- - -"""Fire up an external search in Zeal. -Just type zl """ - -from albertv0 import * -from subprocess import run -from shutil import which - -__iid__ = "PythonInterface/v0.1" -__prettyname__ = "Zeal" -__version__ = "1.0" -__trigger__ = "zl " -__author__ = "Manuel Schneider" -__dependencies__ = ["zeal"] - -if which("zeal") is None: - raise Exception("'zeal' is not in $PATH.") - -iconPath = iconLookup('zeal') - - -def handleQuery(query): - if query.isTriggered: - return Item( - id=__prettyname__, - icon=iconPath, - text=__prettyname__, - subtext="Look up %s" % __prettyname__, - completion=query.rawString, - actions=[ProcAction("Start query in %s" % __prettyname__, - ["zeal", query.string])] - ) diff --git a/arch_wiki/__init__.py b/arch_wiki/__init__.py new file mode 100644 index 00000000..c24c2476 --- /dev/null +++ b/arch_wiki/__init__.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2024 Manuel Schneider + +import json +from pathlib import Path +from time import sleep +from urllib import request, parse + +from albert import * + +md_iid = "3.0" +md_version = '2.0' +md_name = "Arch Linux Wiki" +md_description = "Search Arch Linux Wiki articles" +md_license = "MIT" +md_url = "https://github.com/albertlauncher/python/tree/main/arch_wiki" +md_authors = "@manuelschneid3r" + + +class Plugin(PluginInstance, TriggerQueryHandler): + + baseurl = 'https://wiki.archlinux.org/api.php' + search_url = "https://wiki.archlinux.org/index.php?search=%s" + user_agent = "org.albert.extension.python.archwiki" + iconUrls = [f"file:{Path(__file__).parent}/arch.svg"] + + def __init__(self): + PluginInstance.__init__(self) + TriggerQueryHandler.__init__(self) + + def defaultTrigger(self): + return 'awiki ' + + def handleTriggerQuery(self, query): + stripped = query.string.strip() + if stripped: + + # avoid rate limiting + for _ in range(50): + sleep(0.01) + if not query.isValid: + return + + results = [] + + params = { + 'action': 'opensearch', + 'search': stripped, + 'limit': "max", + 'redirects': 'resolve', + 'utf8': 1, + 'format': 'json' + } + get_url = "%s?%s" % (self.baseurl, parse.urlencode(params)) + req = request.Request(get_url, headers={'User-Agent': self.user_agent}) + + with request.urlopen(req) as response: + data = json.loads(response.read().decode()) + for i in range(0, len(data[1])): + title = data[1][i] + summary = data[2][i] + url = data[3][i] + + results.append(StandardItem(id=self.id(), + text=title, + subtext=summary if summary else url, + iconUrls=self.iconUrls, + actions=[ + Action("open", "Open article", lambda u=url: openUrl(u)), + Action("copy", "Copy URL", lambda u=url: setClipboardText(u)) + ])) + if results: + query.add(results) + else: + query.add(StandardItem(id=self.id(), + text="Search '%s'" % query.string, + subtext="No results. Start online search on Arch Wiki", + iconUrls=self.iconUrls, + actions=[Action("search", "Open search", + lambda s=query.string: openUrl(self.search_url % s))])) + + else: + query.add(StandardItem(id=self.id(), + text=md_name, + iconUrls=self.iconUrls, + subtext="Enter a query to search on the Arch Wiki")) diff --git a/arch_wiki/arch.svg b/arch_wiki/arch.svg new file mode 100644 index 00000000..b95bef86 --- /dev/null +++ b/arch_wiki/arch.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/aur/__init__.py b/aur/__init__.py new file mode 100644 index 00000000..0ca8917a --- /dev/null +++ b/aur/__init__.py @@ -0,0 +1,145 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2024 Manuel Schneider + +""" +Search for packages and open their URLs. This extension is also intended to be used to \ +quickly install the packages. If you are missing your favorite AUR helper tool send a PR. +""" + +import json +from datetime import datetime +from pathlib import Path +from shutil import which +from time import sleep +from urllib import request, parse + +from albert import * + +md_iid = "3.0" +md_version = "2.0" +md_name = "AUR" +md_description = "Query and install AUR packages" +md_license = "MIT" +md_url = "https://github.com/albertlauncher/python/tree/main/aur" +md_authors = "@manuelschneid3r" + + +class Plugin(PluginInstance, TriggerQueryHandler): + + aur_url = "https://aur.archlinux.org/packages/" + baseurl = 'https://aur.archlinux.org/rpc/' + iconUrls = [f"file:{Path(__file__).parent}/arch.svg"] + + def __init__(self): + PluginInstance.__init__(self) + TriggerQueryHandler.__init__(self) + + if which("yaourt"): + self.install_cmdline = "yaourt -S aur/%s" + elif which("pacaur"): + self.install_cmdline = "pacaur -S aur/%s" + elif which("yay"): + self.install_cmdline = "yay -S aur/%s" + elif which("paru"): + self.install_cmdline = "paru -S aur/%s" + else: + info("No supported AUR helper found.") + self.install_cmdline = None + + def defaultTrigger(self): + return 'aur ' + + def configWidget(self): + return [ + { + 'type': 'label', + 'text': __doc__.strip() + } + ] + + def handleTriggerQuery(self, query): + for _ in range(50): + sleep(0.01) + if not query.isValid: + return + + stripped = query.string.strip() + if stripped: + params = { + 'v': '5', + 'type': 'search', + 'by': 'name', + 'arg': stripped + } + url = "%s?%s" % (self.baseurl, parse.urlencode(params)) + req = request.Request(url) + + with request.urlopen(req) as response: + data = json.loads(response.read().decode()) + if data['type'] == "error": + query.add(StandardItem( + id=self.id(), + text="Error", + subtext=data['error'], + iconUrls=self.iconUrls + )) + else: + results = [] + results_json = data['results'] + results_json.sort(key=lambda i: i['Name']) + results_json.sort(key=lambda i: len(i['Name'])) + + for entry in results_json: + name = entry['Name'] + item = StandardItem( + id=self.id(), + iconUrls=self.iconUrls, + text=f"{entry['Name']} {entry['Version']}" + ) + + subtext = f"⭐{entry['NumVotes']}" + if entry['Maintainer'] is None: + subtext += ', Unmaintained!' + if entry['OutOfDate']: + subtext += ', Out of date: %s' % datetime.fromtimestamp(entry['OutOfDate']).strftime("%F") + if entry['Description']: + subtext += ', %s' % entry['Description'] + item.subtext = subtext + + actions = [] + if self.install_cmdline: + pacman = self.install_cmdline.split(" ", 1)[0] + actions.append(Action( + id="inst", + text="Install using %s" % pacman, + callable=lambda n=name: runTerminal( + script=self.install_cmdline % n + " ; exec $SHELL" + ) + )) + actions.append(Action( + id="instnc", + text="Install using %s (noconfirm)" % pacman, + callable=lambda n=name: runTerminal( + script=self.install_cmdline % n + " --noconfirm ; exec $SHELL" + ) + )) + + actions.append(Action("open-aursite", "Open AUR website", + lambda n=name: openUrl(f"{self.aur_url}{n}/"))) + + if entry['URL']: + actions.append(Action("open-website", "Open project website", + lambda u=entry['URL']: openUrl(u))) + + item.actions = actions + results.append(item) + + query.add(results) + else: + query.add(StandardItem( + id=self.id(), + text=md_name, + subtext="Enter a query to search the AUR", + iconUrls=self.iconUrls, + actions=[Action("open-aur", "Open AUR packages website", lambda: openUrl(self.aur_url))] + )) diff --git a/aur/arch.svg b/aur/arch.svg new file mode 100644 index 00000000..b95bef86 --- /dev/null +++ b/aur/arch.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/bitwarden/__init__.py b/bitwarden/__init__.py new file mode 100644 index 00000000..87afe63a --- /dev/null +++ b/bitwarden/__init__.py @@ -0,0 +1,219 @@ +# -*- coding: utf-8 -*- + +import time +from dataclasses import dataclass +from enum import Enum +from pathlib import Path +from subprocess import CalledProcessError, run + +from albert import * + +md_iid = "3.0" +md_version = "3.1" +md_name = "Bitwarden" +md_description = "'rbw' wrapper extension" +md_license = "MIT" +md_url = "https://github.com/albertlauncher/python/tree/main/bitwarden" +md_authors = ["@ovitor", "@daviddeadly", "@manuelschneid3r"] +md_bin_dependencies = ["rbw"] + +MAX_MINUTES_CACHE_TIMEOUT = 60 +DEFAULT_MINUTE_CACHE_TIMEOUT = 5 + + +@dataclass(frozen=True) +class ConfigKeys: + CACHE_TIMEOUT = "cache_timeout" + + +class Plugin(PluginInstance, TriggerQueryHandler): + _cached_items = None + _last_fetch_time = 0 + + iconUrls = [f"file:{Path(__file__).parent}/bw.svg"] + + def __init__(self): + PluginInstance.__init__(self) + TriggerQueryHandler.__init__(self) + + self.cache_timeout = ( + self.readConfig(ConfigKeys.CACHE_TIMEOUT, int) + or DEFAULT_MINUTE_CACHE_TIMEOUT + ) + + def defaultTrigger(self): + return "bw " + + @property + def cache_timeout(self): + return int(self._cache_timeout / 60) + + @cache_timeout.setter + def cache_timeout(self, value): + self._cache_timeout = int(value * 60) + self.writeConfig(ConfigKeys.CACHE_TIMEOUT, value) + + def configWidget(self): + return [ + { + "type": "label", + "text": "Cache (result of `rbw list`) duration", + }, + { + "type": "spinbox", + "property": ConfigKeys.CACHE_TIMEOUT, + "label": f"Minutes: (max: {MAX_MINUTES_CACHE_TIMEOUT}, disable: 0)", + "widget_properties": {"maximum": MAX_MINUTES_CACHE_TIMEOUT}, + }, + ] + + def handleTriggerQuery(self, query): + results = [] + if query.string.strip().lower() == "sync": + results.append( + StandardItem( + id="sync", + text="Sync Bitwarden Vault", + iconUrls=self.iconUrls, + actions=[ + Action( + id="sync", + text="Syncing Bitwarden Vault", + callable=lambda: self._sync_vault(), + ) + ], + ) + ) + + for p in self._filter_items(query): + results.append( + StandardItem( + id=p["id"], + text=p["path"], + subtext=p["user"], + iconUrls=self.iconUrls, + actions=[ + Action( + id="copy", + text="Copy password to clipboard", + callable=lambda item=p: self._password_to_clipboard(item), + ), + Action( + id="copy-auth", + text="Copy auth code to clipboard", + callable=lambda item=p: self._code_to_clipboard(item), + ), + Action( + id="copy-username", + text="Copy username to clipboard", + callable=lambda username=p["user"]: setClipboardText( + text=username + ), + ), + Action( + id="edit", + text="Edit entry in terminal", + callable=lambda item=p: self._edit_entry(item), + ), + ], + ) + ) + + query.add(results) + + def _get_items(self): + not_first_time = self._cached_items is not None + + time_passed = time.time() - self._last_fetch_time + is_chache_fresh = time_passed < self._cache_timeout + + if not_first_time and is_chache_fresh: + return self._cached_items + + field_names = ["id", "name", "user", "folder"] + raw_items = run( + ["rbw", "list", "--fields", ",".join(field_names)], + capture_output=True, + encoding="utf-8", + check=True, + ) + + items = [] + + for line in raw_items.stdout.splitlines(): + fields = line.split("\t") + item = dict(zip(field_names, fields)) + + if item["folder"]: + item["path"] = item["folder"] + "/" + item["name"] + else: + item["path"] = item["name"] + + items.append(item) + + self._cached_items = items + self._last_fetch_time = time.time() + + return items + + def _filter_items(self, query): + passwords = self._get_items() or [] + search_fields = ["path", "user"] + # Use a set for faster membership tests + words = set(query.string.strip().lower().split()) + + filtered_passwords = [] + + for p in passwords: + match_all_words_with_any_field = all( + any(word in p[field].lower() for field in search_fields) + for word in words + ) + + if match_all_words_with_any_field: + filtered_passwords.append(p) + + return filtered_passwords + + def _sync_vault(self): + run(["rbw", "sync"], check=True) + + self._cached_items = None + self._last_fetch_time = 0 + + @staticmethod + def _password_to_clipboard(item): + rbw_id = item["id"] + + password = run( + ["rbw", "get", rbw_id], capture_output=True, encoding="utf-8", check=True + ).stdout.strip() + + setClipboardText(text=password) + + @staticmethod + def _code_to_clipboard(item): + rbw_id = item["id"] + + try: + code = run( + ["rbw", "code", rbw_id], + capture_output=True, + encoding="utf-8", + check=True, + ).stdout.strip() + except CalledProcessError as err: + code = run( + ["echo", err.__str__()], + capture_output=True, + encoding="utf-8", + check=True, + ).stdout.strip() + + setClipboardText(text=code) + + @staticmethod + def _edit_entry(item): + rbw_id = item["id"] + + runTerminal(script=f"rbw edit {rbw_id}") diff --git a/bitwarden/bw.svg b/bitwarden/bw.svg new file mode 100644 index 00000000..b487d569 --- /dev/null +++ b/bitwarden/bw.svg @@ -0,0 +1,4 @@ + + + + diff --git a/coingecko/__init__.py b/coingecko/__init__.py new file mode 100644 index 00000000..df23f20d --- /dev/null +++ b/coingecko/__init__.py @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2024 Manuel Schneider + +from albert import * +from time import time +from urllib import request +from json import load, loads, dumps +from pathlib import Path +from threading import Thread, Event + +md_iid = "3.0" +md_version = "2.1" +md_name = "CoinGecko" +md_description = "Access CoinGecko" +md_license = "MIT" +md_url = "https://github.com/albertlauncher/python/tree/main/coingecko" +md_authors = "@manuelschneid3r" + + +class CoinFetcherThread(Thread): + def __init__(self, callback, path: Path): + super().__init__() + self._stop_event = Event() + self.callback = callback + self.path = path + + def _fetchCoins(self): + url = "https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=250" + debug(f"Fetching data from {url}") + try: + response = request.urlopen(url, timeout=5) + if response.getcode() == 200: + json_data = loads(response.read().decode('utf-8')) + with open(self.path, 'w') as f: + f.write(dumps(json_data)) + else: + warning(f"Request failed with status code: {response.getcode()}") + except Exception as e: + warning(f"Request failed: {str(e)}") + + def run(self): + while True: + # update if older than 1h + if not self.path.is_file() or (time() - self.path.lstat().st_mtime) > 3600: + self._fetchCoins() + self.callback() + self._stop_event.wait(300) # Check every 5 mins, wakeup on stop event + if self._stop_event.is_set(): + return + + def stop(self): + self._stop_event.set() + + +class NameItem(StandardItem): + def __init__(self, + identifier: str, + name: str, + symbol: str, + rank: int, + price: float, + cap: float, + vol: float, + change24h: float): + StandardItem.__init__( + self, + id=identifier, + text=f"{name} {price} {symbol}/$", + subtext=f"#{rank}, 24h: {change24h}%, Cap: {cap:n} $, Vol: {vol:n} $", + inputActionText=str(price), + iconUrls=Plugin.iconUrls, + actions=[ + Action("show", f"Show {name} on CoinGecko", + lambda coin_id=identifier: openUrl(Plugin.coinsUrl + coin_id)), + Action("url", "Copy URL to clipboard", + lambda coin_id=identifier: setClipboardText(Plugin.coinsUrl + coin_id)) + ] + ) + self.name = name + self.symbol = symbol + + +class Plugin(PluginInstance, IndexQueryHandler): + + coinsUrl = "https://www.coingecko.com/en/coins/" + iconUrls = [f"file:{Path(__file__).parent}/coingecko.png"] + + def __init__(self): + PluginInstance.__init__(self) + IndexQueryHandler.__init__(self) + + self.items = [] + self.mtime = 0 + cache_location = self.cacheLocation() + cache_location.mkdir(parents=True, exist_ok=True) + self.coinCacheFilePath = cache_location / "coins.json" + self.thread = CoinFetcherThread(self.updateIndexItems, self.coinCacheFilePath) + self.thread.start() + + def __del__(self): + self.thread.stop() + self.thread.join() + + def defaultTrigger(self): + return 'cg ' + + def synopsis(self, query): + return "< symbol | name >" + + def updateIndexItems(self): + if self.coinCacheFilePath.is_file() and (mtime := self.coinCacheFilePath.lstat().st_mtime) > self.mtime: + self.mtime = mtime + with open(self.coinCacheFilePath) as f: + self.items.clear() + for json_object in load(f): + self.items.append(NameItem( + identifier=json_object['id'], + name=json_object['name'], + symbol=json_object['symbol'].upper(), + rank=json_object['market_cap_rank'], + price=json_object['current_price'], + cap=json_object['market_cap'], + vol=json_object['total_volume'], + change24h=json_object['price_change_percentage_24h'] + )) + + index_items = [] + for item in self.items: + index_items.append(IndexItem(item=item, string=item.name)) + index_items.append(IndexItem(item=item, string=item.symbol)) + self.setIndexItems(index_items) + + # override default trigger handling to sort by rank + def handleTriggerQuery(self, query): + m = Matcher(query.string) + query.add([item for item in self.items if m.match(item.symbol, item.name)]) diff --git a/coingecko/coingecko.png b/coingecko/coingecko.png new file mode 100644 index 00000000..ad08ef14 Binary files /dev/null and b/coingecko/coingecko.png differ diff --git a/color/__init__.py b/color/__init__.py new file mode 100644 index 00000000..482f709f --- /dev/null +++ b/color/__init__.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2024 Manuel Schneider + +""" +Displays a color parsed from a code, which may be in one of these formats: + +* #RGB (each of R, G, and B is a single hex digit) +* #RRGGBB +* #AARRGGBB +* #RRRGGGBBB +* #RRRRGGGGBBBB + +Note: This extension started as a prototype to test the internal color pixmap generator. However it may serve as a \ +starting point for people having a real need for color workflows. PR's welcome. +""" + +from albert import * +from string import hexdigits + +md_iid = "3.0" +md_version = "2.0" +md_name = "Color" +md_description = "Display color for color codes" +md_license = "MIT" +md_url = "https://github.com/albertlauncher/python/tree/main/color" +md_authors = "@manuelschneid3r" + + +class Plugin(PluginInstance, GlobalQueryHandler): + + def __init__(self): + PluginInstance.__init__(self) + GlobalQueryHandler.__init__(self) + + def defaultTrigger(self): + return '#' + + def handleGlobalQuery(self, query): + rank_items = [] + s = query.string.strip() + if s: + if s.startswith('#'): # remove hash + s = s[1:] + + # check length and hex + if any([len(s) == l for l in [3, 6, 8, 9, 12]]) and all(c in hexdigits for c in s): + rank_items.append( + RankItem( + StandardItem( + id=self.id(), + text=s, + subtext="The color for this code.", + iconUrls=[f"gen:?background=%23{s}"], + ), + 1 + ) + ) + + return rank_items + + def configWidget(self): + return [{ 'type': 'label', 'text': __doc__.strip() }] + \ No newline at end of file diff --git a/copyq/__init__.py b/copyq/__init__.py new file mode 100644 index 00000000..cd21adb7 --- /dev/null +++ b/copyq/__init__.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017-2024 Manuel Schneider +# Copyright (c) 2023 Oskar Haarklou Veileborg (@BarrensZeppelin) + +import json +import subprocess + +from albert import * + +md_iid = "3.0" +md_version = "2.0" +md_name = "CopyQ" +md_description = "Access CopyQ clipboard" +md_license = "BSD-2-Clause" +md_url = "https://github.com/albertlauncher/python/tree/main/copyq" +md_authors = ["@ManuelSchneid3r", "@BarrensZeppelin"] +md_bin_dependencies = ["copyq"] + + +copyq_script_getAll = r""" +var result=[]; +for ( var i = 0; i < size(); ++i ) { + var obj = {}; + obj.row = i; + obj.mimetypes = str(read("?", i)).split("\n"); + obj.mimetypes.pop(); + obj.text = str(read(i)); + result.push(obj); +} +JSON.stringify(result); +""" + +copyq_script_getMatches = r""" +var result=[]; +var match = "%s"; +for ( var i = 0; i < size(); ++i ) { + if (str(read(i)).search(new RegExp(match, "i")) !== -1) { + var obj = {}; + obj.row = i; + obj.mimetypes = str(read("?", i)).split("\n"); + obj.mimetypes.pop(); + obj.text = str(read(i)); + result.push(obj); + } +} +JSON.stringify(result); +""" + + +class Plugin(PluginInstance, TriggerQueryHandler): + + def __init__(self): + PluginInstance.__init__(self) + TriggerQueryHandler.__init__(self) + + def defaultTrigger(self): + return "cp " + + def handleTriggerQuery(self, query): + items = [] + script = copyq_script_getMatches % query.string if query.string else copyq_script_getAll + proc = subprocess.run(["copyq", "-"], input=script.encode(), stdout=subprocess.PIPE) + json_arr = json.loads(proc.stdout.decode()) + + for json_obj in json_arr: + row = json_obj["row"] + text = json_obj["text"] + if not text: + text = "No text" + else: + text = " ".join(filter(None, text.replace("\n", " ").split(" "))) + + act = lambda s=script, r=row: ( + lambda: runDetachedProcess(["copyq", s % r]) + ) + items.append( + StandardItem( + id=self.id(), + iconUrls=["xdg:copyq"], + text=text, + subtext="%s: %s" % (row, ", ".join(json_obj["mimetypes"])), + actions=[ + Action("paste", "Paste", act("select(%s); sleep(60); paste();")), + Action("copy", "Copy", act("select(%s);")), + Action("remove", "Remove", act("remove(%s);")), + ], + ) + ) + + query.add(items) diff --git a/dice_roll/__init__.py b/dice_roll/__init__.py new file mode 100644 index 00000000..deb9bc2c --- /dev/null +++ b/dice_roll/__init__.py @@ -0,0 +1,158 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2024 Jonah Lawrence + +from __future__ import annotations + +import random +import re +from pathlib import Path + +import albert + +__doc__ = f""" +Roll any number of dice using the format `_d_`. + +Example: "roll 2d6 3d8 1d20" +""" + +md_iid = "3.0" +md_version = "2.0" +md_name = "Dice Roll" +md_description = "Roll any number of dice" +md_license = "MIT" +md_url = "https://github.com/albertlauncher/python/tree/main/dice_roll" +md_authors = "@DenverCoder1" + + +def get_icon_path(num_sides: int | None) -> str: + """Get the path to the icon for a die with num_sides sides. + + Args: + num_sides (Optional[int]): Number of sides on the die or None for the overall total. + + Returns: + str: The path to the icon. + """ + icons_path = Path(__file__).parent / "icons" + # get the icon for the number of sides or d20 if there is no icon for that number + icon = f"d{num_sides}" if Path(icons_path / f"d{num_sides}.svg").exists() else "d20" + # use the overall total icon if the number of sides is None + if num_sides is None: + icon = "dice" + # return the path to the icon + return str(f"file:{icons_path / f'{icon}.svg'}") + + +def roll_dice(num_dice: int, num_sides: int) -> tuple[int, list[int]]: + """Roll multiple dice with num_sides sides. + + Args: + num_dice (int): Number of dice to roll. + num_sides (int): Number of sides on each die. + + Returns: + Tuple[int, List[int]]: The total and a list of the rolls. + """ + rolls = [random.randint(1, num_sides) for _ in range(num_dice)] + return sum(rolls), rolls + + +def get_item_from_rolls( + rolls: list[int], + sum_rolls: int, + num_sides: int | None = None) -> albert.Item: + """Creates an Albert Item from a list of rolls, the total, and the number of sides. + If num_sides is not provided, an "Overall Total" summary item is created. + + Args: + rolls (List[int]): List of rolls. + sum_rolls (int): Total of all rolls. + num_sides (Optional[int]): Number of sides on each die. + + Returns: + albert.Item: The item to be added to the list of results. + """ + return albert.StandardItem( + id=get_icon_path(num_sides), + iconUrls=[get_icon_path(num_sides)], + text=( + f"Rolled {len(rolls)}d{num_sides} - Total: {sum_rolls}" + if num_sides + else f"Overall Total: {sum_rolls}" + ), + subtext=f"Rolls: {', '.join(map(str, rolls))}", + actions=[ + albert.Action( + id="copytotal", + text="Copy result to clipboard", + callable=lambda: albert.setClipboardText(text=str(sum_rolls)), + ), + albert.Action( + id="copyrolls", + text="Copy result to clipboard", + callable=lambda: albert.setClipboardText(text=", ".join(map(str, rolls))), + ), + ], + ) + + +def get_items(query_string: str) -> list[albert.Item]: + """Convert a query string of dice rolls into a list of Albert Items. + + Args: + query_string (str): The query string to be parsed. + + Returns: + List[albert.Item]: The list of items to display. + """ + results = [] + sum_all_rolls = 0 + all_rolls = [] + # get (num_dice, num_sides) pairs from query string + dice_regex = re.compile(r"(\d+)d(\d+)", re.I) + matches = dice_regex.findall(query_string) + # roll each pair + for match in matches: + num_dice, num_sides = int(match[0]), int(match[1]) + # get random numbers from 1 to num_sides for each die + sum_rolls, rolls = roll_dice(num_dice, num_sides) + # add rolls and total to aggregators + sum_all_rolls += sum_rolls + all_rolls.extend(rolls) + # create item for the dice rolls + results.append(get_item_from_rolls(rolls, sum_rolls, num_sides)) + # if there are multiple dice types, add a summary item + if len(matches) > 1: + # prepend the summary item to the list of results + results.insert(0, get_item_from_rolls(all_rolls, sum_all_rolls)) + return results + + +class Plugin(albert.PluginInstance, albert.TriggerQueryHandler): + """A plugin to roll dice""" + + def __init__(self): + albert.PluginInstance.__init__(self) + albert.TriggerQueryHandler.__init__(self) + + def synopsis(self, query): + return "d [d ...]" + + def defaultTrigger(self): + return "roll " + + def configWidget(self): + return [{ 'type': 'label', 'text': __doc__.strip() }] + + def handleTriggerQuery(self, query: albert.Query) -> None: + query_string = query.string.strip() + try: + items = get_items(query_string) + query.add(items) + except Exception: + query.add([albert.StandardItem( + id="error", + iconUrls=[get_icon_path(None)], + text="Something went wrong.", + subtext="Make sure you're using the correct format.", + )]) diff --git a/dice_roll/icons/d10.svg b/dice_roll/icons/d10.svg new file mode 100644 index 00000000..1f155355 --- /dev/null +++ b/dice_roll/icons/d10.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/dice_roll/icons/d100.svg b/dice_roll/icons/d100.svg new file mode 100644 index 00000000..4c133a01 --- /dev/null +++ b/dice_roll/icons/d100.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/dice_roll/icons/d12.svg b/dice_roll/icons/d12.svg new file mode 100644 index 00000000..d88b4cad --- /dev/null +++ b/dice_roll/icons/d12.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/dice_roll/icons/d2.svg b/dice_roll/icons/d2.svg new file mode 100644 index 00000000..fe35afa0 --- /dev/null +++ b/dice_roll/icons/d2.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/dice_roll/icons/d20.svg b/dice_roll/icons/d20.svg new file mode 100644 index 00000000..13562d7c --- /dev/null +++ b/dice_roll/icons/d20.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/dice_roll/icons/d4.svg b/dice_roll/icons/d4.svg new file mode 100644 index 00000000..c9952782 --- /dev/null +++ b/dice_roll/icons/d4.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/dice_roll/icons/d6.svg b/dice_roll/icons/d6.svg new file mode 100644 index 00000000..b0f892b2 --- /dev/null +++ b/dice_roll/icons/d6.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/dice_roll/icons/d8.svg b/dice_roll/icons/d8.svg new file mode 100644 index 00000000..381b5f80 --- /dev/null +++ b/dice_roll/icons/d8.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/dice_roll/icons/dice.svg b/dice_roll/icons/dice.svg new file mode 100644 index 00000000..880f7b9d --- /dev/null +++ b/dice_roll/icons/dice.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/docker/__init__.py b/docker/__init__.py new file mode 100644 index 00000000..f6639c03 --- /dev/null +++ b/docker/__init__.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2024 Manuel Schneider + +from pathlib import Path + +import docker +from albert import * + +md_iid = "3.0" +md_version = "4.0" +md_name = "Docker" +md_description = "Manage docker images and containers" +md_license = "MIT" +md_url = "https://github.com/albertlauncher/python/tree/main/docker" +md_authors = "@manuelschneid3r" +md_bin_dependencies = "docker" +md_lib_dependencies = "docker" + + +class Plugin(PluginInstance, TriggerQueryHandler): + # Global query handler not applicable, queries take seconds sometimes + + def __init__(self): + PluginInstance.__init__(self) + TriggerQueryHandler.__init__(self) + self.icon_urls_running = [f"file:{Path(__file__).parent}/running.png"] + self.icon_urls_stopped = [f"file:{Path(__file__).parent}/stopped.png"] + self.client = None + + def synopsis(self, query): + return "" + + def defaultTrigger(self): + return "d " + + def handleTriggerQuery(self, query): + items = [] + + if not self.client: + try: + self.client = docker.from_env() + except Exception as e: + items.append(StandardItem( + id='except', + text="Failed starting docker client", + subtext=str(e), + iconUrls=self.icon_urls_running, + )) + return items + + try: + for container in self.client.containers.list(all=True): + if query.string in container.name: + # Create dynamic actions + if container.status == 'running': + actions = [Action("stop", "Stop container", lambda c=container: c.stop()), + Action("restart", "Restart container", lambda c=container: c.restart())] + else: + actions = [Action("start", "Start container", lambda c=container: c.start())] + actions.extend([ + Action("logs", "Logs", + lambda c=container.id: runTerminal("docker logs -f %s ; exec $SHELL" % c)), + Action("remove", "Remove (forced, with volumes)", + lambda c=container: c.remove(v=True, force=True)), + Action("copy-id", "Copy id to clipboard", + lambda cid=container.id: setClipboardText(cid)) + ]) + + items.append(StandardItem( + id=container.id, + text="%s (%s)" % (container.name, ", ".join(container.image.tags)), + subtext="Container: %s" % container.id, + iconUrls=self.icon_urls_running if container.status == 'running' else self.icon_urls_stopped, + actions=actions + )) + + for image in reversed(self.client.images.list()): + for tag in sorted(image.tags, key=len): # order by resulting score + if query.string in tag: + items.append(StandardItem( + id=image.short_id, + text=", ".join(image.tags), + subtext="Image: %s" % image.id, + iconUrls=self.icon_urls_stopped, + actions=[ + # Action("run", "Run with command: %s" % query.string, + # lambda i=image, s=query.string: client.containers.run(i, s)), + Action("rmi", "Remove image", lambda i=image: i.remove()) + ] + )) + except Exception as e: + warning(str(e)) + self.client = None + + query.add(items) diff --git a/docker/running.png b/docker/running.png new file mode 100644 index 00000000..126af0bd Binary files /dev/null and b/docker/running.png differ diff --git a/docker/stopped.png b/docker/stopped.png new file mode 100644 index 00000000..4fe48bf1 Binary files /dev/null and b/docker/stopped.png differ diff --git a/duckduckgo/__init__.py b/duckduckgo/__init__.py new file mode 100644 index 00000000..3666115e --- /dev/null +++ b/duckduckgo/__init__.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2024 Manuel Schneider + +""" +Inline DuckDuckGo web search using the 'duckduckgo-search' library. +""" + +from albert import * +from pathlib import Path +from duckduckgo_search import DDGS +from itertools import islice +from time import sleep + +md_iid = "3.0" +md_version = "2.0" +md_name = 'DuckDuckGo' +md_description = 'Inline DuckDuckGo web search' +md_license = "MIT" +md_url = 'https://github.com/albertlauncher/python/tree/main/duckduckgo' +md_lib_dependencies = "duckduckgo-search" +md_authors = "@manuelschneid3r" + + +class Plugin(PluginInstance, TriggerQueryHandler): + + def __init__(self): + PluginInstance.__init__(self) + TriggerQueryHandler.__init__(self) + self.ddg = DDGS() + self.iconUrls = [f"file:{Path(__file__).parent}/duckduckgo.svg"] + + def defaultTrigger(self): + return "ddg " + + def handleTriggerQuery(self, query): + + stripped = query.string.strip() + if stripped: + + # dont flood + for _ in range(25): + sleep(0.01) + if not query.isValid: + return + + for r in islice(self.ddg.text(stripped, safesearch='off'), 10): + query.add( + StandardItem( + id=self.id(), + text=r['title'], + subtext=r['body'], + iconUrls=self.iconUrls, + actions=[Action("open", "Open link", lambda u=r['href']: openUrl(u))] + ) + ) diff --git a/duckduckgo/duckduckgo.svg b/duckduckgo/duckduckgo.svg new file mode 100644 index 00000000..87f27951 --- /dev/null +++ b/duckduckgo/duckduckgo.svg @@ -0,0 +1,118 @@ + + diff --git a/emoji/__init__.py b/emoji/__init__.py new file mode 100644 index 00000000..49bfa0a3 --- /dev/null +++ b/emoji/__init__.py @@ -0,0 +1,222 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2024 Manuel Schneider + +import json +import re +import threading +import urllib.request +import builtins +from locale import getdefaultlocale +from pathlib import Path + +from albert import * + +md_iid = "3.0" +md_version = "3.1" +md_name = "Emoji" +md_description = "Find and copy emojis by name" +md_license = "MIT" +md_url = "https://github.com/albertlauncher/python/tree/main/emoji" +md_authors = "@manuelschneid3r" + + +class Plugin(PluginInstance, IndexQueryHandler): + + def __init__(self): + PluginInstance.__init__(self) + IndexQueryHandler.__init__(self) + self.thread = None + + self._use_derived = self.readConfig('use_derived', bool) + if self._use_derived is None: + self._use_derived = False + + def __del__(self): + if self.thread and self.thread.is_alive(): + self.thread.join() + + def defaultTrigger(self): + return ':' + + @property + def use_derived(self): + return self._use_derived + + @use_derived.setter + def use_derived(self, value): + self._use_derived = value + self.writeConfig('use_derived', value) + self.updateIndexItems() + + def configWidget(self): + return [ + { + 'type': 'checkbox', + 'property': 'use_derived', + 'label': 'Use derived emojis' + } + ] + + def updateIndexItems(self): + if self.thread and self.thread.is_alive(): + self.thread.join() + self.thread = threading.Thread(target=self.update_index_items_task) + self.thread.start() + + def update_index_items_task(self): + + def download_file(url: str, path: Path): + debug(f"Downloading {url}.") + headers = {'User-Agent': 'Mozilla/5.0'} # otherwise github returns html + request = urllib.request.Request(url, headers=headers) + with urllib.request.urlopen(request, timeout=3) as response: + if response.getcode() == 200: + debug(f"Success. Storing to {path}.") + with builtins.open(path, 'wb') as file: + file.write(response.read()) + else: + raise RuntimeError(f"Failed to download {url}. Status code: {response.getcode()}") + + def get_fully_qualified_emojis(cache_path: Path) -> list: + """Returns fully qualified emoji strings""" + + def convert_to_unicode_char(hex_code: str): + return chr(int(hex_code, 16)) + + def convert_to_unicode_str(hex_codes: str): + hex_list = hex_codes.split() + return ''.join([convert_to_unicode_char(hex_code) for hex_code in hex_list]) + + path = cache_path / 'emoji_list.txt' + if not path.is_file(): + info("Fetching emoji list.") + url = 'https://unicode.org/Public/emoji/latest/emoji-test.txt' + download_file(url, path) + + # components = set() + fully_qualified = [] + + with path.open("r") as f: + + emoji_list_re_str = r""" + ^ + (?P .*\S) + \s*;\s* + (?P \S+) + \s*\#\s* + (?P \S+) + \s* + (?P E\d+.\d+) + \s* + (?P [^:]+) + (?: : \s* (?P .+))? + \n + $ + """ + + line_re = re.compile(emoji_list_re_str, re.VERBOSE) + for line in f: + if match := line_re.match(line): + if match.group("status") == "fully-qualified": + fully_qualified.append(convert_to_unicode_str(match.group("codepoints"))) + + return fully_qualified + + def get_annotations(cache_path: Path, use_derived: bool) -> dict: + + # determine locale + + if lang := getdefaultlocale()[0]: + lang = lang[0:2] + else: + warning("Failed getting locale. There will be no localized emoji aliases.") + lang = 'en' + + # fetch localized cldr annotations 'full' + + path_full = cache_path / f'emoji_annotations_full_{lang}.json' + if not path_full.is_file(): + url = 'https://raw.githubusercontent.com/unicode-org/cldr-json/main/cldr-json/' \ + 'cldr-annotations-full/annotations/%s/annotations.json' % lang + download_file(url, path_full) + + with path_full.open("r", encoding='utf-8') as file_full: + json_full = json.load(file_full)['annotations']['annotations'] + + if not use_derived: + return json_full + + # fetch localized cldr annotations 'derived' + + path_derived = cache_path / f'emoji_annotations_derived_{lang}.json' + if not path_derived.is_file(): + url = 'https://raw.githubusercontent.com/unicode-org/cldr-json/main/cldr-json/' \ + 'cldr-annotations-derived-full/annotationsDerived/%s/annotations.json' % lang + download_file(url, path_derived) + + # open, read, parse, merge, return + + with path_derived.open("r", encoding='utf-8') as file_derived: + json_derived = json.load(file_derived)['annotationsDerived']['annotations'] + return json_full | json_derived + + cache_location = self.cacheLocation() + cache_location.mkdir(parents=True, exist_ok=True) + emojis = get_fully_qualified_emojis(cache_location) + annotations = get_annotations(cache_location, self.use_derived) + + def remove_redundancy(sentences): + sets_of_words = [set(sentence.lower().split()) for sentence in sentences] + unique = [] + for sow, sentence in zip(sets_of_words, sentences): + for other_sow in sets_of_words: + if sow != other_sow: + if all([any([oword.startswith(word) for oword in other_sow]) for word in sow]): + break + else: + unique.append(sentence) + return unique + + index_items = [] + for emoji in emojis: + try: + ann = annotations[emoji] + except KeyError: + try: + non_rgi_emoji = emoji.replace('\uFE0F', '') + ann = annotations[non_rgi_emoji] + except KeyError as e: + debug(f"Found no translation for {e}. Emoji will not be available.") + continue + + title = ann['tts'][0] + aliases = remove_redundancy([title.replace(':', '').replace(',', ''), *ann['default']]) + + actions = [] + if havePasteSupport(): + actions.append( + Action( + "paste", "Copy and paste to front-most window", + lambda emj=emoji: setClipboardTextAndPaste(emj) + ) + ) + + actions.append( + Action( + "copy", "Copy to clipboard", + lambda emj=emoji: setClipboardText(emj) + ) + ) + + item = StandardItem( + id=emoji, + text=title.capitalize(), + subtext=", ".join([a.capitalize() for a in aliases]), + iconUrls=[f"gen:?text={emoji}"], + actions=actions + ) + + for alias in aliases: + index_items.append(IndexItem(item=item, string=alias)) + + self.setIndexItems(index_items) diff --git a/goldendict/__init__.py b/goldendict/__init__.py new file mode 100644 index 00000000..c25c72bb --- /dev/null +++ b/goldendict/__init__.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017-2024 Manuel Schneider + +import os +import shutil + +from albert import * + +md_iid = "3.0" +md_version = "2.0" +md_name = "GoldenDict" +md_description = "Quick access to GoldenDict" +md_license = "MIT" +md_url = "https://github.com/albertlauncher/python/tree/main/goldendict" +md_authors = "@manuelschneid3r" + + +class Plugin(PluginInstance, TriggerQueryHandler): + + def __init__(self): + PluginInstance.__init__(self) + TriggerQueryHandler.__init__(self) + + commands = [ + '/var/lib/flatpak/exports/bin/org.goldendict.GoldenDict', # flatpak + '/var/lib/flatpak/exports/bin/io.github.xiaoyifang.goldendict_ng', # flatpak ng + 'goldendict', # native + 'goldendict-ng', # native ng + ] + + executables = [e for e in [shutil.which(c) for c in commands] if e] + + if not executables: + raise RuntimeError(f'None of the GoldenDict distributions found.') + + self.executable = executables[0] + self.iconUrls = [f'xdg:{os.path.basename(self.executable)}'] + + if len(executables) > 1: + warning(f"Multiple GoldenDict commands found: {', '.join(executables)}") + warning(f"Using {self.executable}") + + def defaultTrigger(self): + return "gd " + + def handleTriggerQuery(self, query): + q = query.string.strip() + query.add( + StandardItem( + id=md_name, + text=md_name, + subtext=f"Look up '{q}' in GoldenDict", + iconUrls=self.iconUrls, + actions=[Action(md_name, md_name, lambda e=self.executable: runDetachedProcess([e, q]))], + ) + ) diff --git a/history_to_submodules.sh b/history_to_submodules.sh new file mode 100755 index 00000000..054cc79c --- /dev/null +++ b/history_to_submodules.sh @@ -0,0 +1,106 @@ +#! /usr/bin/env bash + +#set -x +set -e + +# Find history dirs +# git log --pretty=format: --name-only | grep -E '^(.archive/)?[^/]+' -o | sort -u | uniq + +git clone --bare git@github.com:albertlauncher/python.git .bare || true + +function filter ( + + if [[ $# -lt 2 ]]; then + echo "Error: At least two arguments required." + return 1 + fi + + local repo_name="$(echo $1 | tr '_' '-')" + local filter_path="$2" + shift 2 + + declare -a additional_filter_paths + if [[ $# -gt 0 ]]; then + additional_filter_paths=$(printf -- "--path %s " "$@") + fi + + echo "ℹ️" $repo_name $filter_path $additional_filter_paths + + git clone .bare $repo_name + cd $repo_name + git filter-repo --subdirectory-filter ${filter_path} ${additional_filter_paths[@]} + + gh repo create "albertlauncher/albert-plugin-python-$repo_name" --public --disable-wiki + git remote add origin "git@github.com:albertlauncher/albert-plugin-python-$repo_name.git" + git push -f --set-upstream origin main + open "https://github.com/albertlauncher/albert-plugin-python-$repo_name" + + cd .. +) + + +filter arch_wiki arch_wiki .archive/arch_wiki ArchWiki +filter atom_projects atom_projects atom_projects.py .archive/atom_projects AtomProjects.py +filter aur aur .archive/aur AUR +filter base_converter base_converter base_converter.py .archive/base_converter BaseConverter.py +filter binance binance .archive/binance Binance +filter bitfinex bitfinex .archive/bitfinex Bitfinex +filter bitwarden bitwarden bitwarden.py .archive/bitwarden +filter coingecko coingecko +filter coinmarketcap coinmarketcap .archive/coinmarketcap CoinMarketCap +filter color color +filter copyq copyq copyq.py .archive/copyq CopyQ.py +filter currency_converter currency_converter currency_converter.py .archive/currency_converter Currency.py +filter dango_emoji dango_emoji .archive/dango_emoji dangoemoji +filter dango_kao dango_kao .archive/dango_kao dangokao +filter datetime datetime datetime.py .archive/datetime DateTime.py epoch Epoch.py +filter dice_roll dice_roll .archive/dice_roll +filter docker docker .archive/docker +filter duckduckgo duckduckgo +filter emoji emoji EmojiPicker unicode_emoji .archive/unicode_emoji +filter find find find .archive/find +filter fortune fortune fortune.py .archive/fortune Fortune.py +filter gnome_dictionary gnome_dictionary gnome_dictionary.py .archive/gnome_dictionary GnomeDictionary.py +filter gnote gnote gnote.py .archive/gnote Gnote.py +filter goldendict goldendict goldendict.py .archive/goldendict GoldenDict.py +filter google_translate google_translate google_translate.py .archive/google_translate GoogleTranslate.py +filter googletrans googletrans .archive/googletrans +filter inhibit_sleep inhibit_sleep .archive/inhibit_sleep +filter ip ip ip.py .archive/ip Ip.py +filter jetbrains_projects jetbrains_projects jetbrains-projects.py .archive/jetbrains_projects JetbrainsProjects +filter kill kill kill.py .archive/kill Kill.py +filter locate locate locate.py .archive/locate Locate.py +filter lpass lpass .archive/lpass +filter mathematica_eval mathematica_eval mathematica_eval.py .archive/mathematica_eval +filter multi_google_translate multi_google_translate multi_google_translate.py .archive/multi_google_translate MultiGoogleTranslate.py +filter node_eval node_eval .archive/node_eval +filter npm npm .archive/npm Npm +filter packagist packagist .archive/packagist Packagist +filter pacman pacman pacman.py .archive/pacman Pacman.py +filter pass pass pass.py .archive/pass Pass.py +filter php_eval php_eval .archive/php_eval +filter pidgin pidgin pidgin.py .archive/pidgin Pidgin.py +filter pomodoro pomodoro .archive/pomodoro Pomodoro +filter python_eval python_eval python Python +filter rand rand .archive/rand +filter scrot scrot scrot.py Scrot.py .archive/scrot +filter syncthing syncthing +filter tex_to_unicode tex_to_unicode tex_to_unicode.py .archive/tex_to_unicode +filter texdoc texdoc .archive/texdoc +filter timer timer Timer .archive/timer +filter tomboy tomboy tomboy.py Tomboy.py .archive/tomboy +filter translators translators +filter trash trash trash.py Trash.py .archive/trash +filter unit_converter unit_converter .archive/unit_converter +filter units units units.py Units.py .archive/units +filter virtualbox virtualbox virtualbox.py VirtualBox.py .archive/virtualbox +filter vpn vpn vpn.py .archive/vpn +filter vscode_projects vscode_projects +filter wikipedia wikipedia Wikipedia Wikipedia.py +filter x_window_switcher x_window_switcher window_switcher window_switcher.py WindowSwitcher.py SwitchApp.py .archive/window_switcher +filter xkcd xkcd .archive/xkcd +filter youtube youtube youtube.py Youtube.py .archive/youtube +filter zeal zeal zeal.py Zeal.py .archive/zeal + + + diff --git a/jetbrains_projects/__init__.py b/jetbrains_projects/__init__.py new file mode 100644 index 00000000..06229396 --- /dev/null +++ b/jetbrains_projects/__init__.py @@ -0,0 +1,291 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2018-2023 Thomas Queste +# Copyright (c) 2023 Valentin Maerten + +""" +This plugin allows you to quickly open projects of the Jetbrains IDEs + +- Android Studio +- Aqua +- CLion +- DataGrip +- DataSpell +- GoLand +- IntelliJ IDEA +- PhpStorm +- PyCharm +- Rider +- RubyMine +- RustRover +- WebStorm +- Writerside. + +Note that for this plugin to find the IDEs, a commandline launcher in $PATH is required. +Open the IDE and click Tools -> Create Command-line Launcher to add one. +""" + +from dataclasses import dataclass +from pathlib import Path +from typing import Union, List +from shutil import which +from sys import platform +from xml.etree import ElementTree +from albert import * + +md_iid = "3.0" +md_version = "4.1" +md_name = "Jetbrains projects" +md_description = "Open your JetBrains projects" +md_license = "MIT" +md_url = "https://github.com/albertlauncher/python/tree/main/jetbrains_projects" +md_authors = ["@tomsquest", "@vmaerten", "@manuelschneid3r", "@d3v2a"] + + +@dataclass +class Project: + name: str + path: str + last_opened: int + + +@dataclass +class Editor: + name: str + icon: Path + config_dir_prefix: str + binary: str + + # Rider calls recentProjects.xml -> recentSolutions.xml and in it RecentProjectsManager -> RiderRecentProjectsManager + is_rider: bool + + def __init__( + self, + name: str, + icon: Path, + config_dir_prefix: str, + binaries: list[str], + is_rider = False): + self.name = name + self.icon = icon + self.config_dir_prefix = config_dir_prefix + self.binary = self._find_binary(binaries) + self.is_rider = is_rider + + @staticmethod + def _find_binary(binaries: list[str]) -> Union[str, None]: + for binary in binaries: + if which(binary): + return binary + return None + + def list_projects(self) -> List[Project]: + config_dir = Path.home() / ".config" + if platform == "darwin": + config_dir = Path.home() / "Library" / "Application Support" + + dirs = list(config_dir.glob(f"{self.config_dir_prefix}*/")) + if not dirs: + return [] + latest = sorted(dirs)[-1] + if not self.is_rider: + recent_projects_xml = "recentProjects.xml" + else: + recent_projects_xml = "recentSolutions.xml" + return self._parse_recent_projects(Path(latest) / "options" / recent_projects_xml) + + def _parse_recent_projects(self, recent_projects_file: Path) -> list[Project]: + try: + root = ElementTree.parse(recent_projects_file).getroot() + if not self.is_rider: + entries = root.findall(".//component[@name='RecentProjectsManager']//entry[@key]") + else: + entries = root.findall(".//component[@name='RiderRecentProjectsManager']//entry[@key]") + + projects = [] + for entry in entries: + project_path = entry.attrib["key"] + project_path = project_path.replace("$USER_HOME$", str(Path.home())) + project_name = Path(project_path).name + files = Path(project_path + "/.idea").glob("*.iml") + tag_opened = entry.find(".//option[@name='projectOpenTimestamp']") + last_opened = tag_opened.attrib["value"] if tag_opened is not None and "value" in tag_opened.attrib else None + + if project_path and last_opened: + projects.append( + Project(name=project_name, path=project_path, last_opened=int(last_opened)) + ) + for file in files: + name = file.name.replace(".iml", "") + if name != project_name: + projects.append(Project(name=name, path=project_path, last_opened=int(last_opened))) + + return projects + except (ElementTree.ParseError, FileNotFoundError): + return [] + + +class Plugin(PluginInstance, TriggerQueryHandler): + + executables = [] + + def __init__(self): + PluginInstance.__init__(self) + TriggerQueryHandler.__init__(self) + + self.fuzzy = False + + self._match_path = self.readConfig('match_path', bool) + if self._match_path is None: + self._match_path = False + + plugin_dir = Path(__file__).parent + editors = [ + Editor( + name="Android Studio", + icon=plugin_dir / "icons" / "androidstudio.svg", + config_dir_prefix="Google/AndroidStudio", + binaries=["studio", "androidstudio", "android-studio", "android-studio-canary", "jdk-android-studio", + "android-studio-system-jdk"]), + Editor( + name="Aqua", + icon=plugin_dir / "icons" / "aqua.svg", + config_dir_prefix="JetBrains/Aqua", + binaries=["aqua", "aqua-eap"]), + Editor( + name="CLion", + icon=plugin_dir / "icons" / "clion.svg", + config_dir_prefix="JetBrains/CLion", + binaries=["clion", "clion-eap"]), + Editor( + name="DataGrip", + icon=plugin_dir / "icons" / "datagrip.svg", + config_dir_prefix="JetBrains/DataGrip", + binaries=["datagrip", "datagrip-eap"]), + Editor( + name="DataSpell", + icon=plugin_dir / "icons" / "dataspell.svg", + config_dir_prefix="JetBrains/DataSpell", + binaries=["dataspell", "dataspell-eap"]), + Editor( + name="GoLand", + icon=plugin_dir / "icons" / "goland.svg", + config_dir_prefix="JetBrains/GoLand", + binaries=["goland", "goland-eap"]), + Editor( + name="IntelliJ IDEA", + icon=plugin_dir / "icons" / "idea.svg", + config_dir_prefix="JetBrains/IntelliJIdea", + binaries=["idea", "idea.sh", "idea-ultimate", "idea-ce-eap", "idea-ue-eap", "intellij-idea-ce", + "intellij-idea-ce-eap", "intellij-idea-ue-bundled-jre", "intellij-idea-ultimate-edition", + "intellij-idea-community-edition-jre", "intellij-idea-community-edition-no-jre"]), + Editor( + name="PhpStorm", + icon=plugin_dir / "icons" / "phpstorm.svg", + config_dir_prefix="JetBrains/PhpStorm", + binaries=["phpstorm", "phpstorm-eap"]), + Editor( + name="PyCharm", + icon=plugin_dir / "icons" / "pycharm.svg", + config_dir_prefix="JetBrains/PyCharm", + binaries=["charm", "pycharm", "pycharm-eap", "pycharm-professional"]), + Editor( + name="Rider", + icon=plugin_dir / "icons" / "rider.svg", + config_dir_prefix="JetBrains/Rider", + binaries=["rider", "rider-eap"], + is_rider=True), + Editor( + name="RubyMine", + icon=plugin_dir / "icons" / "rubymine.svg", + config_dir_prefix="JetBrains/RubyMine", + binaries=["rubymine", "rubymine-eap", "jetbrains-rubymine", "jetbrains-rubymine-eap"]), + Editor( + name="RustRover", + icon=plugin_dir / "icons" / "rustrover.svg", + config_dir_prefix="JetBrains/RustRover", + binaries=["rustrover", "rustrover-eap"]), + Editor( + name="WebStorm", + icon=plugin_dir / "icons" / "webstorm.svg", + config_dir_prefix="JetBrains/WebStorm", + binaries=["webstorm", "webstorm-eap"]), + Editor( + name="Writerside", + icon=plugin_dir / "icons" / "writerside.svg", + config_dir_prefix="JetBrains/Writerside", + binaries=["writerside", "writerside-eap"]), + ] + self.editors = [e for e in editors if e.binary is not None] + + @property + def match_path(self): + return self._match_path + + @match_path.setter + def match_path(self, value): + self._match_path = value + self.writeConfig('match_path', value) + + def supportsFuzzyMatching(self): + return True + + def setFuzzyMatching(self, enabled): + self.fuzzy = enabled + + def defaultTrigger(self): + return "jb " + + def handleTriggerQuery(self, query: Query): + editor_project_pairs = [] + + m = Matcher(query.string, MatchConfig(fuzzy=self.fuzzy)) + + for editor in self.editors: + for project in editor.list_projects(): + if Path(project.path).exists(): + if self._match_path: + if m.match(project.name, project.path): + editor_project_pairs.append((editor, project)) + else: + if m.match(project.name): + editor_project_pairs.append((editor, project)) + + # sort by last opened + editor_project_pairs.sort(key=lambda pair: pair[1].last_opened, reverse=True) + + query.add([self._make_item(editor, project) for editor, project in editor_project_pairs]) + + @staticmethod + def _make_item(editor: Editor, project: Project) -> Item: + return StandardItem( + id="%s-%s-%s" % (editor.binary, project.path, project.last_opened), + text=project.name, + subtext=project.path, + inputActionText=project.name, + iconUrls=["file:" + str(editor.icon)], + actions=[ + Action( + "Open", + "Open in %s" % editor.name, + lambda selected_project=project.path: runDetachedProcess( + [editor.binary, selected_project] + ), + ) + ], + ) + + def configWidget(self): + return [ + { + 'type': 'checkbox', + 'property': 'match_path', + 'label': 'Match path' + }, + { + 'type': 'label', + 'text': __doc__.strip(), + 'widget_properties': { + 'textFormat': 'Qt::MarkdownText' + } + } + ] diff --git a/jetbrains_projects/icons/androidstudio.svg b/jetbrains_projects/icons/androidstudio.svg new file mode 100644 index 00000000..98fc3c6e --- /dev/null +++ b/jetbrains_projects/icons/androidstudio.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jetbrains_projects/icons/aqua.svg b/jetbrains_projects/icons/aqua.svg new file mode 100644 index 00000000..a2d7e161 --- /dev/null +++ b/jetbrains_projects/icons/aqua.svg @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jetbrains_projects/icons/clion.svg b/jetbrains_projects/icons/clion.svg new file mode 100644 index 00000000..001a2da6 --- /dev/null +++ b/jetbrains_projects/icons/clion.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jetbrains_projects/icons/datagrip.svg b/jetbrains_projects/icons/datagrip.svg new file mode 100644 index 00000000..5931f912 --- /dev/null +++ b/jetbrains_projects/icons/datagrip.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jetbrains_projects/icons/dataspell.svg b/jetbrains_projects/icons/dataspell.svg new file mode 100644 index 00000000..44cf2e17 --- /dev/null +++ b/jetbrains_projects/icons/dataspell.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jetbrains_projects/icons/goland.svg b/jetbrains_projects/icons/goland.svg new file mode 100644 index 00000000..3640b1d8 --- /dev/null +++ b/jetbrains_projects/icons/goland.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jetbrains_projects/icons/idea.svg b/jetbrains_projects/icons/idea.svg new file mode 100644 index 00000000..04d5d615 --- /dev/null +++ b/jetbrains_projects/icons/idea.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jetbrains_projects/icons/phpstorm.svg b/jetbrains_projects/icons/phpstorm.svg new file mode 100644 index 00000000..7e3600d2 --- /dev/null +++ b/jetbrains_projects/icons/phpstorm.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jetbrains_projects/icons/pycharm.svg b/jetbrains_projects/icons/pycharm.svg new file mode 100644 index 00000000..82469f36 --- /dev/null +++ b/jetbrains_projects/icons/pycharm.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jetbrains_projects/icons/rider.svg b/jetbrains_projects/icons/rider.svg new file mode 100644 index 00000000..d1ab1530 --- /dev/null +++ b/jetbrains_projects/icons/rider.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jetbrains_projects/icons/rubymine.svg b/jetbrains_projects/icons/rubymine.svg new file mode 100644 index 00000000..7a68d96f --- /dev/null +++ b/jetbrains_projects/icons/rubymine.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jetbrains_projects/icons/rustrover.svg b/jetbrains_projects/icons/rustrover.svg new file mode 100644 index 00000000..bd5621c5 --- /dev/null +++ b/jetbrains_projects/icons/rustrover.svg @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jetbrains_projects/icons/webstorm.svg b/jetbrains_projects/icons/webstorm.svg new file mode 100644 index 00000000..c55a10e4 --- /dev/null +++ b/jetbrains_projects/icons/webstorm.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/jetbrains_projects/icons/writerside.svg b/jetbrains_projects/icons/writerside.svg new file mode 100644 index 00000000..087eed26 --- /dev/null +++ b/jetbrains_projects/icons/writerside.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/kill/__init__.py b/kill/__init__.py new file mode 100644 index 00000000..c9c69231 --- /dev/null +++ b/kill/__init__.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2022 Manuel Schneider +# Copyright (c) 2022 Benedict Dudel +# Copyright (c) 2022 Pete Hamlin + + +import os +from signal import SIGKILL, SIGTERM + +from albert import * + +md_iid = "3.0" +md_version = "2.0" +md_name = "Kill Process" +md_description = "Kill processes" +md_license = "MIT" +md_url = "https://github.com/albertlauncher/python/tree/main/kill" +md_authors = ["@Pete-Hamlin", "@BenedictDwudel", "@ManuelSchneid3r"] + + +class Plugin(PluginInstance, TriggerQueryHandler): + def __init__(self): + PluginInstance.__init__(self) + TriggerQueryHandler.__init__(self) + + def defaultTrigger(self): + return "kill " + + def handleTriggerQuery(self, query): + if not query.isValid: + return + results = [] + uid = os.getuid() + for dir_entry in os.scandir("/proc"): + try: + if dir_entry.name.isdigit() and dir_entry.stat().st_uid == uid: + proc_command = ( + open(os.path.join(dir_entry.path, "comm"), "r").read().strip() + ) + if query.string in proc_command: + debug(proc_command) + proc_cmdline = ( + open(os.path.join(dir_entry.path, "cmdline"), "r") + .read() + .strip() + .replace("\0", " ") + ) + results.append( + StandardItem( + id="kill", + iconUrls=["xdg:process-stop"], + text=proc_command, + subtext=proc_cmdline, + actions=[ + Action( + "terminate", + "Terminate process", + lambda pid=int(dir_entry.name): os.kill( + pid, SIGTERM + ), + ), + Action( + "kill", + "Kill process", + lambda pid=int(dir_entry.name): os.kill( + pid, SIGKILL + ), + ), + ], + ) + ) + except FileNotFoundError: # TOCTOU dirs may disappear + continue + except IOError: # TOCTOU dirs may disappear + continue + query.add(results) diff --git a/locate/__init__.py b/locate/__init__.py new file mode 100644 index 00000000..55015ba8 --- /dev/null +++ b/locate/__init__.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2022-2024 Manuel Schneider + +""" +`locate` wrapper. Note that it is up to you to ensure that the locate database is \ +up to date. Pass params as necessary. The input is split using a shell lexer. +""" + + +import shlex +import subprocess +from pathlib import Path + +from albert import * + +md_iid = "3.0" +md_version = "2.0" +md_name = "Locate" +md_description = "Find and open files using locate" +md_license = "MIT" +md_url = "https://github.com/albertlauncher/python/tree/main/locate" +md_bin_dependencies = "locate" +md_authors = "@manuelschneid3r" + + +class Plugin(PluginInstance, TriggerQueryHandler): + + def __init__(self): + PluginInstance.__init__(self) + TriggerQueryHandler.__init__(self) + + self.iconUrls = [ + "xdg:preferences-system-search", + "xdg:system-search", + "xdg:search", + "xdg:text-x-generic", + f"file:{Path(__file__).parent}/locate.svg" + ] + + def synopsis(self, query): + return "" + + def defaultTrigger(self): + return "'" + + def handleTriggerQuery(self, query): + if len(query.string) > 2: + + try: + args = shlex.split(query.string) + except ValueError: + return + + result = subprocess.run(['locate', *args], stdout=subprocess.PIPE, text=True) + if not query.isValid: + return + lines = sorted(result.stdout.splitlines(), reverse=True) + if not query.isValid: + return + + for path in lines: + query.add( + StandardItem( + id=path, + text=Path(path).name, + subtext=path, + iconUrls=self.iconUrls, + actions=[ + Action("open", "Open", lambda p=path: openUrl("file://%s" % p)) + ] + ) + ) + else: + query.add( + StandardItem( + id="updatedb", + text="Update locate database", + subtext="Type at least three chars for a search", + iconUrls=self.iconUrls, + actions=[ + Action("update", "Update", lambda: runTerminal("sudo updatedb")) + ] + ) + ) diff --git a/locate/locate.svg b/locate/locate.svg new file mode 100644 index 00000000..18df7995 --- /dev/null +++ b/locate/locate.svg @@ -0,0 +1,248 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pacman/__init__.py b/pacman/__init__.py new file mode 100644 index 00000000..eb0e3e14 --- /dev/null +++ b/pacman/__init__.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2024 Manuel Schneider + +import subprocess +from time import sleep +import pathlib + +from albert import Action, StandardItem, PluginInstance, TriggerQueryHandler, runTerminal, openUrl + +md_iid = "3.0" +md_version = "2.0" +md_name = "PacMan" +md_description = "Search, install and remove packages" +md_license = "MIT" +md_url = "https://github.com/albertlauncher/python/tree/main/pacman" +md_authors = "@ManuelSchneid3r" +md_bin_dependencies = ["pacman", "expac"] + + +class Plugin(PluginInstance, TriggerQueryHandler): + + pkgs_url = "https://www.archlinux.org/packages/" + + def __init__(self): + PluginInstance.__init__(self) + TriggerQueryHandler.__init__(self) + self.iconUrls = [ + "xdg:archlinux-logo", + "xdg:system-software-install", + f"file:{pathlib.Path(__file__).parent}/arch.svg" + ] + + def synopsis(self, query): + return "" + + def defaultTrigger(self): + return "pac " + + def handleTriggerQuery(self, query): + stripped = query.string.strip() + + # Update item on empty queries + if not stripped: + query.add(StandardItem( + id="%s-update" % self.id, + text="Pacman package manager", + subtext="Enter the package you are looking for or hit enter to update.", + iconUrls=self.iconUrls, + actions=[ + Action("up-nc", "Update packages (no confirm)", + lambda: runTerminal("sudo pacman -Syu --noconfirm")), + Action("up", "Update packages", lambda: runTerminal("sudo pacman -Syu")), + Action("up-cache", "Update pacman cache", lambda: runTerminal("sudo pacman -Sy")) + ] + )) + return + + # avoid rate limiting + for _ in range(50): + sleep(0.01) + if not query.isValid: + return + + # Get data. Results are sorted, so we can merge in O(n) + proc_s = subprocess.Popen(["expac", "-Ss", "%n\t%v\t%r\t%d\t%u\t%E", stripped], + stdout=subprocess.PIPE, universal_newlines=True) + proc_q = subprocess.Popen(["expac", "-Qs", "%n", stripped], stdout=subprocess.PIPE, universal_newlines=True) + proc_q.wait() + + items = [] + local_pkgs = set(proc_q.stdout.read().split('\n')) + remote_pkgs = [tuple(line.split('\t')) for line in proc_s.stdout.read().split('\n')[:-1]] # newline at end + + for pkg_name, pkg_vers, pkg_repo, pkg_desc, pkg_purl, pkg_deps in remote_pkgs: + if stripped not in pkg_name: + continue + + pkg_installed = True if pkg_name in local_pkgs else False + + actions = [] + if pkg_installed: + actions.extend([ + Action("rem", "Remove", lambda n=pkg_name: runTerminal("sudo pacman -Rs %s" % n)), + Action("reinst", "Reinstall", lambda n=pkg_name: runTerminal("sudo pacman -S %s" % n)) + ]) + else: + actions.append(Action("inst", "Install", lambda n=pkg_name: runTerminal("sudo pacman -S %s" % n))) + + actions.append(Action("pkg_url", "Show on packages.archlinux.org", + lambda r=pkg_repo, n=pkg_name: openUrl(f"{self.pkgs_url}{r}/x86_64/{n}/"))) + if pkg_purl: + actions.append(Action("proj_url", "Show project website", lambda u=pkg_purl: openUrl(u))) + + item = StandardItem( + id="%s_%s_%s" % (self.id, pkg_repo, pkg_name), + iconUrls=self.iconUrls, + text="%s %s [%s]" % (pkg_name, pkg_vers, pkg_repo), + subtext=f"{pkg_desc} [Installed]" if pkg_installed else f"{pkg_desc}", + inputActionText="%s%s" % (query.trigger, pkg_name), + actions=actions + ) + items.append(item) + + if items: + query.add(items) + else: + query.add(StandardItem( + id="%s-empty" % self.id, + text="Search on archlinux.org", + subtext="No results found in the local database", + iconUrls=self.iconUrls, + actions=[Action("search", "Search on archlinux.org", lambda: openUrl(f"{self.pkgs_url}?q={stripped}"))] + )) diff --git a/pacman/arch.svg b/pacman/arch.svg new file mode 100644 index 00000000..b95bef86 --- /dev/null +++ b/pacman/arch.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pass/__init__.py b/pass/__init__.py new file mode 100644 index 00000000..2a0ce20a --- /dev/null +++ b/pass/__init__.py @@ -0,0 +1,171 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017 Benedict Dudel +# Copyright (c) 2023 Max +# Copyright (c) 2023 Pete-Hamlin + +import fnmatch +import os +from albert import * + +md_iid = "3.0" +md_version = "2.0" +md_name = "Pass" +md_description = "Manage passwords in pass" +md_license = "BSD-3" +md_url = "https://github.com/albertlauncher/python/tree/main/pass" +md_authors = ["@benedictdudel", "@maxmil", "@Pete-Hamlin"] +md_bin_dependencies = ["pass"] + +HOME_DIR = os.environ["HOME"] +PASS_DIR = os.environ.get("PASSWORD_STORE_DIR", os.path.join(HOME_DIR, ".password-store/")) + + +class Plugin(PluginInstance, TriggerQueryHandler): + def __init__(self): + PluginInstance.__init__(self) + TriggerQueryHandler.__init__(self) + self.iconUrls = ["xdg:dialog-password"] + self._use_otp = self.readConfig("use_otp", bool) or False + self._otp_glob = self.readConfig("otp_glob", str) or "*-otp.gpg" + + @property + def use_otp(self): + return self._use_otp + + @use_otp.setter + def use_otp(self, value): + print(f"Setting _use_otp to {value}") + self._use_otp = value + self.writeConfig("use_otp", value) + + @property + def otp_glob(self): + return self._otp_glob + + @otp_glob.setter + def otp_glob(self, value): + print(f"Setting _otp_glob to {value}") + self._otp_glob = value + self.writeConfig("otp_glob", value) + + def defaultTrigger(self): + return "pass " + + def synopsis(self, query): + return "" + + def configWidget(self): + return [ + {"type": "checkbox", "property": "use_otp", "label": "Enable pass OTP extension"}, + { + "type": "lineedit", + "property": "otp_glob", + "label": "Glob pattern for OTP passwords", + "widget_properties": {"placeholderText": "*-otp.gpg"}, + }, + ] + + def handleTriggerQuery(self, query): + if query.string.strip().startswith("generate"): + self.generatePassword(query) + elif query.string.strip().startswith("otp") and self._use_otp: + self.showOtp(query) + else: + self.showPasswords(query) + + def generatePassword(self, query): + location = query.string.strip()[9:] + + query.add( + StandardItem( + id="generate_password", + iconUrls=self.iconUrls, + text="Generate a new password", + subtext="The new password will be located at %s" % location, + inputActionText="pass %s" % query.string, + actions=[ + Action( + "generate", + "Generate", + lambda: runDetachedProcess(["pass", "generate", "--clip", location, "20"]), + ) + ], + ) + ) + + def showOtp(self, query): + otp_query = query.string.strip()[4:] + passwords = [] + if otp_query: + passwords = self.getPasswordsFromSearch(otp_query, otp=True) + else: + passwords = self.getPasswords(otp=True) + + results = [] + for password in passwords: + results.append( + StandardItem( + id=password, + iconUrls=self.iconUrls, + text=password.split("/")[-1], + subtext=password, + actions=[ + Action( + "copy", + "Copy", + lambda pwd=password: runDetachedProcess(["pass", "otp", "--clip", pwd]), + ), + ], + ), + ) + query.add(results) + + def showPasswords(self, query): + if query.string.strip(): + passwords = self.getPasswordsFromSearch(query.string) + else: + passwords = self.getPasswords() + + results = [] + for password in passwords: + name = password.split("/")[-1] + results.append( + StandardItem( + id=password, + text=name, + subtext=password, + iconUrls=self.iconUrls, + inputActionText="pass %s" % password, + actions=[ + Action( + "copy", + "Copy", + lambda pwd=password: runDetachedProcess(["pass", "--clip", pwd]), + ), + Action( + "edit", + "Edit", + lambda pwd=password: runDetachedProcess(["pass", "edit", pwd]), + ), + Action( + "remove", + "Remove", + lambda pwd=password: runDetachedProcess(["pass", "rm", "--force", pwd]), + ), + ], + ), + ) + + query.add(results) + + def getPasswords(self, otp=False): + passwords = [] + for root, dirnames, filenames in os.walk(PASS_DIR, followlinks=True): + for filename in fnmatch.filter(filenames, self._otp_glob if otp else "*.gpg"): + passwords.append(os.path.join(root, filename.replace(".gpg", "")).replace(PASS_DIR, "")) + + return sorted(passwords, key=lambda s: s.lower()) + + def getPasswordsFromSearch(self, otp_query, otp=False): + passwords = [password for password in self.getPasswords(otp) if otp_query.strip().lower() in password.lower()] + return passwords diff --git a/pomodoro/__init__.py b/pomodoro/__init__.py new file mode 100644 index 00000000..39dc9b35 --- /dev/null +++ b/pomodoro/__init__.py @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2024 Manuel Schneider + +""" +Wiki: [Pomodoro_Technique](https://en.wikipedia.org/wiki/Pomodoro_Technique). +""" + +import threading +import time +from pathlib import Path + +from albert import * + +md_iid = "3.0" +md_version = "2.0" +md_name = "Pomodoro" +md_description = "Set up a Pomodoro timer" +md_license = "MIT" +md_url = "https://github.com/albertlauncher/python/tree/main/pomodoro" +md_authors = "@manuelschneid3r" + + +class PomodoroTimer: + + def __init__(self): + self.isBreak = True + self.timer = None + self.notification = None + self.remainingTillLongBreak = 0 + self.endTime = 0 + self.pomodoroDuration = 0 + self.breakDuration = 0 + self.longBreakDuration = 0 + self.count = 0 + + def timeout(self): + if self.isBreak: + duration = self.pomodoroDuration * 60 + self.timer = threading.Timer(duration, self.timeout) + self.endTime = time.time() + duration + self.notification = Notification("PomodoroTimer", "Let's go to work!") + self.timer.start() + else: + self.remainingTillLongBreak -= 1 + if self.remainingTillLongBreak == 0: + self.remainingTillLongBreak = self.count + self.notification = Notification("PomodoroTimer", "Take a long break (%s min)" % self.longBreakDuration) + duration = self.longBreakDuration * 60 + else: + self.notification = Notification("PomodoroTimer", "Take a short break (%s min)" % self.breakDuration) + duration = self.breakDuration * 60 + self.endTime = time.time() + duration + self.timer = threading.Timer(duration, self.timeout) + self.timer.start() + self.isBreak = not self.isBreak + + def start(self, pomodoroDuration, breakDuration, longBreakDuration, count): + self.stop() + self.pomodoroDuration = pomodoroDuration + self.breakDuration = breakDuration + self.longBreakDuration = longBreakDuration + self.count = count + self.remainingTillLongBreak = count + self.isBreak = True + self.timeout() + + def stop(self): + if self.isActive(): + self.timer.cancel() + self.timer = None + + def isActive(self): + return self.timer is not None + + +class Plugin(PluginInstance, TriggerQueryHandler): + + default_pomodoro_duration = 25 + default_break_duration = 5 + default_longbreak_duration = 15 + default_pomodoro_count = 4 + + def __init__(self): + PluginInstance.__init__(self) + TriggerQueryHandler.__init__(self) + self.pomodoro = PomodoroTimer() + self.iconUrls = [f"file:{Path(__file__).parent}/pomodoro.svg"] + + def defaultTrigger(self): + return 'pomo ' + + def synopsis(self, query): + return '[duration [break duration [long break duration [count]]]]' + + def configWidget(self): + return [ + { + 'type': 'label', + 'text': __doc__.strip(), + 'widget_properties': {'textFormat': 'Qt::MarkdownText'} + } + ] + + def handleTriggerQuery(self, query): + item = StandardItem( + id=self.id(), + iconUrls=self.iconUrls, + ) + + if self.pomodoro.isActive(): + item.text = "Stop Pomodoro" + item.actions = [Action("stop", "Stop", lambda pomo=self.pomodoro: pomo.stop())] + if self.pomodoro.isBreak: + whatsNext = "Pomodoro" + else: + whatsNext = "Long break" if self.pomodoro.remainingTillLongBreak == 1 else "Short break" + item.subtext = "%s at %s" % (whatsNext, time.strftime("%X", time.localtime(self.pomodoro.endTime))) + query.add(item) + + else: + tokens = query.string.split() + if len(tokens) > 4 or not all([t.isdigit() for t in tokens]): + item.text = "Invalid parameters" + item.subtext = "Use %s" % self.synopsis + query.add(item) + else: + p = int(tokens[0]) if len(tokens) > 0 else self.default_pomodoro_duration + b = int(tokens[1]) if len(tokens) > 1 else self.default_break_duration + lb = int(tokens[2]) if len(tokens) > 2 else self.default_longbreak_duration + c = int(tokens[3]) if len(tokens) > 3 else self.default_pomodoro_count + + item.text = "Start Pomodoro" + item.subtext = f"{p} min, break {b} min, long break {lb} min, count {c}" + item.actions = [Action("start", "Start", + lambda _p=p, _b=b, _lb=lb, _c=c: self.pomodoro.start(_p, _b, _lb, _c))] + query.add(item) diff --git a/pomodoro/pomodoro.svg b/pomodoro/pomodoro.svg new file mode 100644 index 00000000..fea164ad --- /dev/null +++ b/pomodoro/pomodoro.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/python_eval/__init__.py b/python_eval/__init__.py new file mode 100644 index 00000000..1acce6c4 --- /dev/null +++ b/python_eval/__init__.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017-2014 Manuel Schneider + +from pathlib import Path + +from albert import * + +md_iid = "3.0" +md_version = "2.0" +md_name = "Python Eval" +md_description = "Evaluate Python code" +md_license = "BSD-3" +md_url = "https://github.com/albertlauncher/python/tree/main/python_eval" +md_authors = "@manuelschneid3r" + + +class Plugin(PluginInstance, TriggerQueryHandler): + + def __init__(self): + PluginInstance.__init__(self) + TriggerQueryHandler.__init__(self) + self.iconUrls = [f"file:{Path(__file__).parent}/python.svg"] + + def synopsis(self, query): + return "" + + def defaultTrigger(self): + return "py " + + def handleTriggerQuery(self, query): + stripped = query.string.strip() + if stripped: + try: + result = eval(stripped) + except Exception as ex: + result = ex + + result_str = str(result) + + query.add(StandardItem( + id=self.id(), + text=result_str, + subtext=type(result).__name__, + inputActionText=query.trigger + result_str, + iconUrls=self.iconUrls, + actions = [ + Action("copy", "Copy result to clipboard", lambda r=result_str: setClipboardText(r)), + Action("exec", "Execute python code", lambda r=result_str: exec(stripped)), + ] + )) diff --git a/Python/python.svg b/python_eval/python.svg similarity index 100% rename from Python/python.svg rename to python_eval/python.svg diff --git a/syncthing/__init__.py b/syncthing/__init__.py new file mode 100644 index 00000000..6fde0df1 --- /dev/null +++ b/syncthing/__init__.py @@ -0,0 +1,182 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2024 Manuel Schneider + +import json +import urllib.error +import urllib.request +from pathlib import Path + +from albert import * + +md_iid = "3.0" +md_version = "3.0" +md_name = "Syncthing" +md_description = "Control the local Syncthing instance." +md_license = "MIT" +md_url = "https://github.com/albertlauncher/python/tree/main/syncthing" +md_authors = "@manuelschneid3r" + + +# https://docs.syncthing.net/dev/rest.html +class Syncthing: + def __init__(self, api_key, base_url="http://localhost:8384"): + self.api_key = api_key + self.base_url = base_url + + def _request(self, method, endpoint, data=None) -> dict: + url = f"{self.base_url}{endpoint}" + headers = { + "X-API-Key": self.api_key, + "Content-Type": "application/json" + } + body = json.dumps(data).encode("utf-8") if data else None + req = urllib.request.Request(url, data=body, headers=headers, method=method) + + try: + with urllib.request.urlopen(req) as resp: + if not (200 <= resp.status < 300): + raise Exception(f"Unexpected status {resp.status}") + content = resp.read().decode() + return json.loads(content) if content else {} + except urllib.error.HTTPError as e: + raise Exception(f"HTTP {e.code}: {e.read().decode()}") + + def _get(self, endpoint): + return self._request("GET", endpoint) + + def _post(self, endpoint, data=None): + return self._request("POST", endpoint, data) + + def _patch(self, endpoint, data=None): + return self._request("PATCH", endpoint, data) + + def config(self): + return self._get('/rest/config') + + def resumeDevice(self, device_id:str): + return self._patch(f'/rest/config/devices/{device_id}', {'paused': False}) + + def pauseDevice(self, device_id:str): + return self._patch(f'/rest/config/devices/{device_id}', {'paused': True}) + + def resumeFolder(self, folder_id:str): + return self._patch(f'/rest/config/folders/{folder_id}', {'paused': False}) + + def pauseFolder(self, folder_id:str): + return self._patch(f'/rest/config/folders/{folder_id}', {'paused': True}) + + def scanFolder(self, folder_id:str): + return self._post(f'/rest/db/scan?folder={folder_id}') + + +class Plugin(PluginInstance, GlobalQueryHandler): + + config_key = 'syncthing_api_key' + icon_urls_active = [f"file:{Path(__file__).parent}/syncthing_active.svg"] + icon_urls_inactive = [f"file:{Path(__file__).parent}/syncthing_inactive.svg"] + + def __init__(self): + PluginInstance.__init__(self) + GlobalQueryHandler.__init__(self) + self.st = Syncthing(self.readConfig(self.config_key, str) or '') + + def defaultTrigger(self): + return 'st ' + + @property + def api_key(self) -> str: + return self.st.api_key + + @api_key.setter + def api_key(self, value: str): + if self.st.api_key != value: + self.st.api_key = value + self.writeConfig(self.config_key, value) + + def configWidget(self): + return [ + { + 'type': 'lineedit', + 'property': 'api_key', + 'label': 'API key', + 'widget_properties': {'tooltip': 'You can find the API key using the web frontend.'} + } + ] + + def handleTriggerQuery(self, query): + try: + super().handleTriggerQuery(query) + except Exception as e: + query.add(StandardItem(id="err", text="Error", subtext=str(e), iconUrls=self.icon_urls_active)) + + def handleGlobalQuery(self, query): + + config = self.st.config() + + devices = dict() + for d in config['devices']: + if not d['name']: + d['name'] = d['deviceID'] + d['_shared_folders'] = {} + devices[d['deviceID']] = d + + folders = dict() + for f in config['folders']: + if not f['label']: + f['label'] = f['id'] + for d in f['devices']: + devices[d['deviceID']]['_shared_folders'][f['id']] = f + folders[f['id']] = f + + results = [] + matcher = Matcher(query.string) + + # create device items + for device_id, d in devices.items(): + device_name = d['name'] + + if match := matcher.match(device_name): + device_folders = ", ".join([f['label'] for f in d['_shared_folders'].values()]) + + actions = [] + if d['paused']: + actions.append(Action("resume", "Resume", lambda did=device_id: self.st.resumeDevice(did))) + else: + actions.append(Action("pause", "Pause", lambda did=device_id: self.st.pauseDevice(did))) + + item = StandardItem( + id=device_id, + text=f"{device_name}", + subtext=f"{'PAUSED · ' if d['paused'] else ''}Device · " + f"Shared: {device_folders if device_folders else 'Nothing'}.", + iconUrls=self.icon_urls_inactive if d['paused'] else self.icon_urls_active, + actions=actions + ) + + results.append(RankItem(item, match)) + + # create folder items + for folder_id, f in folders.items(): + folder_name = f['label'] + if match := matcher.match(folder_name): + folders_devices = ", ".join([devices[d['deviceID']]['name'] for d in f['devices']]) + + actions = [] + if f['paused']: + actions.append(Action("resume", "Resume", lambda fid=folder_id: self.st.resumeFolder(fid))) + else: + actions.append(Action("pause", "Pause", lambda fid=folder_id: self.st.pauseFolder(fid))) + actions.append(Action("open", "Open", lambda p=f['path']: openFile(p))) + actions.append(Action("scan", "Scan", lambda fid=folder_id: self.st.scanFolder(fid))) + + item = StandardItem( + id=folder_id, + text=folder_name, + subtext=f"{'PAUSED · ' if f['paused'] else ''}Folder · {f['path']} · " + f"Shared with {folders_devices if folders_devices else 'nobody'}.", + iconUrls=self.icon_urls_inactive if f['paused'] else self.icon_urls_active, + actions=actions + ) + results.append(RankItem(item, match)) + + return results diff --git a/syncthing/syncthing_active.svg b/syncthing/syncthing_active.svg new file mode 100644 index 00000000..b56b7cbc --- /dev/null +++ b/syncthing/syncthing_active.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/syncthing/syncthing_inactive.svg b/syncthing/syncthing_inactive.svg new file mode 100644 index 00000000..247b4d43 --- /dev/null +++ b/syncthing/syncthing_inactive.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tex_to_unicode/__init__.py b/tex_to_unicode/__init__.py new file mode 100644 index 00000000..629e4584 --- /dev/null +++ b/tex_to_unicode/__init__.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2022 Jonah Lawrence +# Copyright (c) 2024 Manuel Schneider + +import re +import unicodedata +from pathlib import Path +from pylatexenc.latex2text import LatexNodes2Text + +from albert import * + +md_iid = "3.0" +md_version = "2.0" +md_name = "TeX to Unicode" +md_description = "Convert TeX mathmode commands to unicode characters" +md_license = "MIT" +md_url = "https://github.com/albertlauncher/python/tree/main/tex_to_unicode" +md_authors = ["@DenverCoder1", "@manuelschneid3r"] +md_lib_dependencies = "pylatexenc" + + +class Plugin(PluginInstance, TriggerQueryHandler): + + def __init__(self): + PluginInstance.__init__(self) + TriggerQueryHandler.__init__(self) + self.COMBINING_LONG_SOLIDUS_OVERLAY = "\u0338" + self.iconUrls = [f"file:{Path(__file__).parent}/tex.svg"] + + def _create_item(self, text: str, subtext: str, can_copy: bool): + actions = [] + if can_copy: + actions.append( + Action( + "copy", + "Copy result to clipboard", + lambda t=text: setClipboardText(t), + ) + ) + return StandardItem( + id=self.id(), + text=text, + subtext=subtext, + iconUrls=self.iconUrls, + actions=actions, + ) + + def defaultTrigger(self): + return "tex " + + def handleTriggerQuery(self, query): + stripped = query.string.strip() + + if not stripped: + return + + if not stripped.startswith("\\"): + stripped = "\\" + stripped + + # Remove double backslashes (newlines) + stripped = stripped.replace("\\\\", " ") + + # pylatexenc doesn't support \not + stripped = stripped.replace("\\not", "@NOT@") + + n = LatexNodes2Text() + result = n.latex_to_text(stripped) + + if not result: + query.add(self._create_item(stripped, "Type some TeX math", False)) + return + + # success + result = unicodedata.normalize("NFC", result) + result = re.sub(r"@NOT@\s*(\S)", "\\1" + self.COMBINING_LONG_SOLIDUS_OVERLAY, result) + result = result.replace("@NOT@", "") + result = unicodedata.normalize("NFC", result) + query.add(self._create_item(result, "Result", True)) diff --git a/tex_to_unicode/tex.svg b/tex_to_unicode/tex.svg new file mode 100644 index 00000000..86ddf19b --- /dev/null +++ b/tex_to_unicode/tex.svg @@ -0,0 +1,4 @@ + + + + diff --git a/translators/__init__.py b/translators/__init__.py new file mode 100644 index 00000000..45541e8b --- /dev/null +++ b/translators/__init__.py @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2024 Manuel Schneider + +""" +Translates text using the python package translators. See https://pypi.org/project/translators/ +""" + +from locale import getdefaultlocale +from pathlib import Path +from time import sleep + +from albert import * +import translators as ts + +md_iid = "3.0" +md_version = "2.1" +md_name = "Translator" +md_description = "Translate text using online translators" +md_license = "MIT" +md_url = "https://github.com/albertlauncher/python/tree/main/translators" +md_authors = "@manuelschneid3r" +md_lib_dependencies = "translators" + + +class Plugin(PluginInstance, TriggerQueryHandler): + + def __init__(self): + PluginInstance.__init__(self) + TriggerQueryHandler.__init__(self) + + self.iconUrls = [f"file:{Path(__file__).parent}/google_translate.png"] + + self._translator = self.readConfig('translator', str) + if self._translator is None: + self._translator = 'google' + + self._lang = self.readConfig('lang', str) + if self._lang is None: + self._lang = getdefaultlocale()[0][0:2] + + try: + languages = ts.get_languages(self.translator) + self.src_languages = set(languages.keys()) + self.dst_languages = set(languages[self.lang]) + except Exception as e: + warning(str(e)) + + @property + def translator(self): + return self._translator + + @translator.setter + def translator(self, value): + self._translator = value + self.writeConfig('translator', value) + languages = ts.get_languages(self.translator) + self.src_languages = set(languages.keys()) + self.dst_languages = set(languages[self.lang]) + + @property + def lang(self): + return self._lang + + @lang.setter + def lang(self, value): + self._lang = value + self.writeConfig('lang', value) + + def defaultTrigger(self): + return 'tr ' + + def configWidget(self): + return [ + { + 'type': 'label', + 'text': __doc__.strip(), + }, + { + 'type': 'combobox', + 'property': 'translator', + 'label': 'Translator', + 'items': ts.translators_pool + }, + { + 'type': 'lineedit', + 'property': 'lang', + 'label': 'Default language', + } + ] + + def synopsis(self, s): + return "[[from] to] text" + + def handleTriggerQuery(self, query): + stripped = query.string.strip() + if stripped: + for _ in range(50): + sleep(0.01) + if not query.isValid: + return + + if len(splits := stripped.split(maxsplit=2)) == 3 \ + and splits[0] in self.src_languages and splits[1] in self.dst_languages: + src, dst, text = splits + elif len(splits := stripped.split(maxsplit=1)) == 2 and splits[0] in self.src_languages: + src, dst, text = 'auto', splits[0], splits[1] + else: + src, dst, text = 'auto', self.lang, stripped + + try: + translation = ts.translate_text(query_text=text, + translator=self.translator, + from_language=src, + to_language=dst, + timeout=5) + + actions = [] + if havePasteSupport(): + actions.append( + Action( + "paste", "Copy to clipboard and paste to front-most window", + lambda t=translation: setClipboardTextAndPaste(t) + ) + ) + + actions.append( + Action("copy", "Copy to clipboard", + lambda t=translation: setClipboardText(t)) + ) + + query.add(StandardItem( + id=self.id(), + text=translation, + subtext=f"{src.upper()} > {dst.upper()}", + iconUrls=self.iconUrls, + actions=actions + )) + + except Exception as e: + + query.add(StandardItem( + id=self.id(), + text="Error", + subtext=str(e), + iconUrls=self.iconUrls + )) + + warning(str(e)) \ No newline at end of file diff --git a/translators/google_translate.png b/translators/google_translate.png new file mode 100644 index 00000000..63617cf5 Binary files /dev/null and b/translators/google_translate.png differ diff --git a/unit_converter/__init__.py b/unit_converter/__init__.py new file mode 100644 index 00000000..cd26cf22 --- /dev/null +++ b/unit_converter/__init__.py @@ -0,0 +1,464 @@ +# -*- coding: utf-8 -*- + +""" +Unit converter based on the [Pint Python library](https://pint.readthedocs.io/en/stable/). + +Usage examples: +- `convert 180 minutes to hrs` +- `convert 100 km to miles` +- `convert 88 mph to kph` +- `convert 32 degrees F to C` +- `convert 3.14159 rad to degrees` +- `convert 100 USD to EUR` +""" + +import json +import re +import traceback +from datetime import datetime +from pathlib import Path +from typing import Any, Optional +from urllib.error import URLError +from urllib.request import urlopen + +import inflect +import pint +from albert import * + +md_iid = "3.0" +md_version = "1.8" +md_name = "Unit Converter" +md_description = "Convert between units" +md_license = "MIT" +md_url = "https://github.com/albertlauncher/python/tree/main/unit_converter" +md_lib_dependencies = ["pint", "inflect"] +md_authors = ["@DenverCoder1", "@Pete-Hamlin"] + + +class ConversionResult: + """A class to represent the result of a unit conversion""" + + def __init__( + self, + from_amount: float, + from_unit: str, + to_amount: float, + to_unit: str, + dimensionality: str, + source: str = "", + ): + """Initialize the ConversionResult + + Args: + from_amount (float): The amount to convert from + from_unit (str): The unit to convert from + to_amount (float): The resulting amount + to_unit (str): The unit converted to + dimensionality (str): The dimensionality of the result + source (str): The source of the conversion for attribution + """ + self.from_amount = from_amount + self.from_unit = from_unit + self.to_amount = to_amount + self.to_unit = to_unit + self.dimensionality = dimensionality + self.source = source + self.display_names: dict[str, str] = Plugin.config["display_names"] + self.inflect_engine = inflect.engine() + + def __pluralize_unit(self, unit: str) -> str: + """Pluralize the unit + + Args: + unit (str): The unit to pluralize + + Returns: + str: The pluralized unit + """ + # if all characters are uppercase, don't pluralize + if unit.isupper(): + return unit + return self.inflect_engine.plural(unit) + + def __display_unit_name(self, amount: float, unit: str) -> str: + """Display the name of the unit with plural if necessary + + Args: + amount (float): The amount to display + unit (str): The unit to display + + Returns: + str: The name of the unit + """ + unit = self.__pluralize_unit(unit) if amount != 1 else unit + return self.display_names.get(unit, unit) + + @staticmethod + def __format_float(num: float) -> str: + """Format a float to remove trailing zeros and avoid scientific notation + + Args: + num (float): The number to format + + Returns: + str: The formatted number + """ + # format the float to remove trailing zeros and decimal point + precision: int = Plugin.config["precision"] + return f"{num:.{precision}f}".rstrip("0").rstrip(".") + + @property + def formatted_result(self) -> str: + """Return the formatted result amount and unit""" + units = self.__display_unit_name(self.to_amount, self.to_unit) + return f"{self.__format_float(self.to_amount)} {units}" + + @property + def formatted_from(self) -> str: + """Return the formatted from amount and unit""" + units = self.__display_unit_name(self.from_amount, self.from_unit) + result = f"{self.__format_float(self.from_amount)} {units}" + if self.source: + result += f" ({self.source})" + return result + + @property + def icon(self) -> str: + """Return the icon for the result's dimensionality""" + # strip characters from the dimensionality if not alphanumeric or underscore + dimensionality = re.sub(r"\W", "", self.dimensionality) + return f"{dimensionality}.svg" + + def __repr__(self): + """Return the representation of the result""" + return f"{self.formatted_from} = {self.formatted_result}" + + +class UnitConverter: + """Base class for unit converters""" + + def __init__(self): + """Initialize the UnitConverter""" + self.aliases: dict[str, str] = Plugin.config["aliases"] + + def convert(self, amount: float, from_unit: str, to_unit: str) -> ConversionResult: + """Convert a unit to another unit + + Args: + amount (float): The amount to convert + from_unit (str): The unit to convert from + to_unit (str): The unit to convert to + + Returns: + ConversionResult: Object containing information about the conversion result + """ + raise NotImplementedError + + +class StandardUnitConverter(UnitConverter): + """Class to convert standard units of measurement""" + + def __init__(self): + """Initialize the StandardUnitConverter""" + self.units = pint.UnitRegistry() + super().__init__() + + def _get_unit(self, unit: str) -> pint.Unit: + """Check if the unit is a valid unit and return it + If any aliases are found, replace the unit with the alias + If the unit is not valid, check if making it lowercase will fix it + If not, raise the UndefinedUnitError + + Args: + unit (str): The unit to check + + Returns: + pint.Unit: The unit + + Raises: + pint.errors.UndefinedUnitError: If the unit is not valid + """ + unit = self.aliases.get(unit, unit) + if unit in self.units: + # return the unit if it is valid + return self.units.__getattr__(unit) + # check if the lowercase version is a valid unit + return self.units.__getattr__(unit.lower()) + + def convert(self, amount: float, from_unit: str, to_unit: str) -> ConversionResult: + """Convert a unit to another unit + + Args: + amount (float): The amount to convert + from_unit (str): The unit to convert from + to_unit (str): The unit to convert to + + Returns: + ConversionResult: Object containing information about the conversion result + + Raises: + pint.errors.UndefinedUnitError: If the unit is not valid + pint.errors.DimensionalityError: If the units are not compatible + """ + input_unit = self.units.Quantity(amount, self._get_unit(from_unit)) + output_unit = self._get_unit(to_unit) + result = input_unit.to(output_unit) + return ConversionResult( + from_amount=float(amount), + from_unit=str(self._get_unit(from_unit)), + to_amount=result.magnitude, + to_unit=str(result.units), + dimensionality=str(self.units._get_dimensionality(result.units)), + ) + + +class UnknownCurrencyError(Exception): + """Exception to raise when an unknown currency is passed to convert""" + + def __init__(self, currency: str): + """Initialize the UnknownCurrencyError + + Args: + currency (str): The unknown currency + """ + self.currency = currency + super().__init__(f"Unknown currency: {currency}") + + +class CurrencyConverter(UnitConverter): + """Class to convert currencies""" + + API_URL = "https://open.er-api.com/v6/latest/USD" + ATTRIBUTION = "Rates by https://www.exchangerate-api.com" + + def __init__(self): + """Initialize the CurrencyConverter""" + self.last_update = datetime.now() + self.currencies = self._get_currencies() + super().__init__() + + def _get_currencies(self) -> dict[str, float]: + """Get the currencies from the API + + Returns: + dict[str, float]: The currencies + """ + try: + with urlopen(self.API_URL) as response: + data = json.loads(response.read().decode("utf-8")) + if not data or "rates" not in data: + info("No currencies found") + return {} + info(f"Currencies updated") + return data["rates"] + except URLError as error: + warning(f"Error getting currencies: {error}") + return {} + + def get_currency(self, currency: str) -> Optional[str]: + """Get the currency name normalized using aliases and capitalization + + Args: + currency (str): The currency to normalize + + Returns: + Optional[str]: The currency name or None if not found + """ + # update the currencies every 24 hours + if not self.currencies or (datetime.now() - self.last_update).days >= 1: + self.currencies = self._get_currencies() + self.last_update = datetime.now() + currency = self.aliases.get(currency, currency).upper() + return currency if currency in self.currencies else None + + def convert(self, amount: float, from_unit: str, to_unit: str) -> ConversionResult: + """Convert a currency to another currency + + Args: + amount (float): The amount to convert + from_unit (str): The currency to convert from + to_unit (str): The currency to convert to + + Returns: + ConversionResult: Object containing information about the conversion result + + Raises: + UnknownCurrencyError: If the currency is not valid + """ + # get the currency rates + from_currency = self.get_currency(from_unit) + to_currency = self.get_currency(to_unit) + # convert the currency + if from_currency is None: + raise UnknownCurrencyError(from_unit) + if to_currency is None: + raise UnknownCurrencyError(to_unit) + from_rate = self.currencies[from_currency] + to_rate = self.currencies[to_currency] + result = amount * to_rate / from_rate + return ConversionResult( + from_amount=float(amount), + from_unit=from_currency, + to_amount=result, + to_unit=to_currency, + dimensionality="currency", + source=self.ATTRIBUTION, + ) + + +class Plugin(PluginInstance, GlobalQueryHandler): + """The plugin class""" + + config: dict[str, Any] = { + # Maximum number of decimal places for precision + "precision": 12, + # Unit aliases to replace when parsing + # Units may be added here to override the default behavior or create aliases for existing units + # The alias is the key, the string to replace it with is the value + "aliases": { + "sec": "second", + "kph": "km/hour", + "km/h": "km/hour", + "mph": "mile/hour", + "degrees F": "degF", + "degrees C": "degC", + "F": "degF", + "C": "degC", + }, + # Display names for units + # Units may be added here to override the default display names + # The string version of the unit is the key, the display name to replace with is the value + # Both the unpluralized and the pluralized version should be included + "display_names": { + "degree_Celsius": "°C", + "degree_Celsiuses": "°C", + "degree_Fahrenheit": "°F", + "degree_Fahrenheits": "°F", + "mile / hour": "mph", + "mile / hours": "mph", + "kilometer / hour": "km/h", + "kilometer / hours": "km/h", + "kilometer_per_hour": "km/h", + "kilometer_per_hours": "km/h", + }, + } + + def __init__(self): + PluginInstance.__init__(self) + GlobalQueryHandler.__init__(self) + + self.unit_convert_regex = re.compile( + r"(?P-?\d+\.?\d*)\s?(?P.*)\s(?:to|in)\s(?P.*)", + re.I, + ) + self.unit_converter = StandardUnitConverter() + self.currency_converter = CurrencyConverter() + + def defaultTrigger(self): + return "convert " + + def synopsis(self, query): + return " to " + + def handleTriggerQuery(self, query: Query) -> None: + if query_string := query.string.strip(): + items = self.match_query(query_string) + query.add(items) + + def handleGlobalQuery(self, query): + return [RankItem(item=item, score=1) for item in self.match_query(query.string.strip())] + + def match_query(self, query_string: str): + match = self.unit_convert_regex.fullmatch(query_string) + if match: + try: + return self._get_items( + float(match.group("from_amount")), + match.group("from_unit").strip(), + match.group("to_unit").strip(), + ) + except Exception as error: + warning(f"Error: {error}") + tb = "".join(traceback.format_exception(error.__class__, error, error.__traceback__)) + warning(tb) + info("Something went wrong. Make sure you're using the correct format.") + return [] + + @staticmethod + def _create_item(text: str, subtext: str, icon: str = "") -> Item: + """Create an Item from a text and subtext + + Args: + text (str): The text to display + subtext (str): The subtext to display + icon (Optional[str]): The icon to display. If not specified, the default icon will be used + + Returns: + Item: The item to be added to the list of results + """ + icon_path = Path(__file__).parent / "icons" / icon + if not icon or not icon_path.exists(): + warning(f"Icon {icon} does not exist") + icon_path = Path(__file__).parent / "icons" / "unit_converter.svg" + return StandardItem( + id=str(icon_path), + iconUrls=["file:" + str(icon_path)], + text=text, + subtext=subtext, + actions=[ + Action( + id="copy", + text="Copy result to clipboard", + callable=lambda: setClipboardText(text=text), + ) + ], + ) + + def _get_converter(self, from_unit: str, to_unit: str) -> UnitConverter: + """Get the converter to use + + Args: + from_unit (str): The unit to convert from + to_unit (str): The unit to convert to + + Returns: + UnitConverter: The converter to use + """ + if ( + self.currency_converter.get_currency(from_unit) is not None + and self.currency_converter.get_currency(to_unit) is not None + ): + return self.currency_converter + return self.unit_converter + + def _get_items(self, amount: float, from_unit: str, to_unit: str) -> list[Item]: + """Generate the Albert items to display for the query + + Args: + amount (float): The amount to convert from + from_unit (str): The unit to convert from + to_unit (str): The unit to convert to + + Returns: + List[Item]: The list of items to display + """ + try: + converter = self._get_converter(from_unit, to_unit) + result = converter.convert(amount, from_unit, to_unit) + # return the result + return [ + self._create_item( + result.formatted_result, + f"Converted from {result.formatted_from}", + result.icon, + ) + ] + except pint.errors.DimensionalityError as e: + warning(f"DimensionalityError: {e}") + return [self._create_item(f"Unable to convert {amount} {from_unit} to {to_unit}", str(e))] + except pint.errors.UndefinedUnitError as e: + warning(f"UndefinedUnitError: {e}") + return [] + except UnknownCurrencyError as e: + warning(f"UnknownCurrencyError: {e}") + return [] diff --git a/unit_converter/icons/currency.svg b/unit_converter/icons/currency.svg new file mode 100644 index 00000000..51a9f40c --- /dev/null +++ b/unit_converter/icons/currency.svg @@ -0,0 +1,11 @@ + + + + + + + diff --git a/unit_converter/icons/current.svg b/unit_converter/icons/current.svg new file mode 100644 index 00000000..b7cd3ad6 --- /dev/null +++ b/unit_converter/icons/current.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/unit_converter/icons/length.svg b/unit_converter/icons/length.svg new file mode 100644 index 00000000..b3af30a7 --- /dev/null +++ b/unit_converter/icons/length.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/unit_converter/icons/lengthtime.svg b/unit_converter/icons/lengthtime.svg new file mode 100644 index 00000000..edaf5548 --- /dev/null +++ b/unit_converter/icons/lengthtime.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/unit_converter/icons/luminosity.svg b/unit_converter/icons/luminosity.svg new file mode 100644 index 00000000..1f937881 --- /dev/null +++ b/unit_converter/icons/luminosity.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/unit_converter/icons/mass.svg b/unit_converter/icons/mass.svg new file mode 100644 index 00000000..981faac3 --- /dev/null +++ b/unit_converter/icons/mass.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/unit_converter/icons/printing_unit.svg b/unit_converter/icons/printing_unit.svg new file mode 100644 index 00000000..e284337b --- /dev/null +++ b/unit_converter/icons/printing_unit.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/unit_converter/icons/substance.svg b/unit_converter/icons/substance.svg new file mode 100644 index 00000000..5859d59a --- /dev/null +++ b/unit_converter/icons/substance.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/unit_converter/icons/temperature.svg b/unit_converter/icons/temperature.svg new file mode 100644 index 00000000..595e9d53 --- /dev/null +++ b/unit_converter/icons/temperature.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/unit_converter/icons/time.svg b/unit_converter/icons/time.svg new file mode 100644 index 00000000..d8c97c46 --- /dev/null +++ b/unit_converter/icons/time.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/unit_converter/icons/unit_converter.svg b/unit_converter/icons/unit_converter.svg new file mode 100644 index 00000000..e040bd91 --- /dev/null +++ b/unit_converter/icons/unit_converter.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/virtualbox/__init__.py b/virtualbox/__init__.py new file mode 100644 index 00000000..ad39ebbf --- /dev/null +++ b/virtualbox/__init__.py @@ -0,0 +1,119 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2024 Manuel Schneider +""" +This plugin is based on [virtualbox-python](https://pypi.org/project/virtualbox/) and needs the 'vboxapi' module which +is part of the VirtualBox SDK. Some distributions package the SDK, e.g. Arch has +[virtualbox-sdk](https://archlinux.org/packages/extra/x86_64/virtualbox-sdk/). +""" + +import virtualbox +from virtualbox.library import LockType, MachineState + +from albert import * + +md_iid = "3.0" +md_version = "2.0" +md_name = "VirtualBox" +md_description = "Manage your VirtualBox machines" +md_license = "MIT" +md_url = "https://github.com/albertlauncher/python/tree/main/virtualbox" +md_authors = "@manuelschneid3r" +md_lib_dependencies = ['virtualbox'] + + +def startVm(vm): + try: + with virtualbox.Session() as session: + progress = vm.launch_vm_process(session, 'gui', []) + progress.wait_for_completion() + except Exception as e: + warning(str(e)) + + +def acpiPowerVm(vm): + with vm.create_session(LockType.shared) as session: + session.console.power_button() + + +def stopVm(vm): + with vm.create_session(LockType.shared) as session: + session.console.power_down() + + +def saveVm(vm): + with vm.create_session(LockType.shared) as session: + session.machine.save_state() + + +def discardSavedVm(vm): + with vm.create_session(LockType.shared) as session: + session.machine.discard_save_state(True) + + +def resumeVm(vm): + with vm.create_session(LockType.shared) as session: + session.console.resume() + + +def pauseVm(vm): + with vm.create_session(LockType.shared) as session: + session.console.pause() + + +class Plugin(PluginInstance, TriggerQueryHandler): + + def __init__(self): + PluginInstance.__init__(self) + TriggerQueryHandler.__init__(self) + self.iconUrls = ["xdg:virtualbox", ":unknown"] + + def defaultTrigger(self): + return 'vbox ' + + def synopsis(self, query): + return "" + + def configWidget(self): + return [ + { + 'type': 'label', + 'text': __doc__.strip(), + 'widget_properties': { + 'textFormat': 'Qt::MarkdownText' + } + } + ] + + def handleTriggerQuery(self, query): + items = [] + pattern = query.string.strip().lower() + try: + for vm in filter(lambda vm: pattern in vm.name.lower(), virtualbox.VirtualBox().machines): + actions = [] + if vm.state == MachineState.powered_off or vm.state == MachineState.aborted: # 1 # 4 + actions.append(Action("startvm", "Start virtual machine", lambda m=vm: startVm(m))) + if vm.state == MachineState.saved: # 2 + actions.append(Action("restorevm", "Start saved virtual machine", lambda m=vm: startVm(m))) + actions.append(Action("discardvm", "Discard saved state", lambda m=vm: discardSavedVm(m))) + if vm.state == MachineState.running: # 5 + actions.append(Action("savevm", "Save virtual machine", lambda m=vm: saveVm(m))) + actions.append(Action("poweroffvm", "Power off via ACPI event (Power button)", lambda m=vm: acpiPowerVm(m))) + actions.append(Action("stopvm", "Turn off virtual machine", lambda m=vm: stopVm(m))) + actions.append(Action("pausevm", "Pause virtual machine", lambda m=vm: pauseVm(m))) + if vm.state == MachineState.paused: # 6 + actions.append(Action("resumevm", "Resume virtual machine", lambda m=vm: resumeVm(m))) + + items.append( + StandardItem( + id=vm.__uuid__, + text=vm.name, + subtext="{vm.state}".format(vm=vm), + inputActionText=vm.name, + iconUrls=self.iconUrls, + actions=actions + ) + ) + except Exception as e: + warning(str(e)) + + query.add(items) diff --git a/vscode_projects/__init__.py b/vscode_projects/__init__.py new file mode 100644 index 00000000..7ed263c9 --- /dev/null +++ b/vscode_projects/__init__.py @@ -0,0 +1,550 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2024 Sharsie + +import os +import json +from pathlib import Path +from dataclasses import dataclass +from albert import * + +md_iid = "3.0" +md_version = "1.10" +md_name = "VSCode projects" +md_description = "Open VSCode projects" +md_url = "https://github.com/albertlauncher/python/tree/master/vscode_projects" +md_license = "MIT" +md_bin_dependencies = ["code"] +md_authors = ["@Sharsie"] + +@dataclass +class Project: + displayName: str + name: str + path: str + tags: list[str] + + +@dataclass +class SearchResult: + project: Project + # priority is used to sort returned results + priority: int + # sortIndex is a decision maker when two search results have same priority + sortIndex: int + + +@dataclass +class CachedConfig: + projects: list[Project] + mTime: float + + +class Plugin(PluginInstance, TriggerQueryHandler): + # Possible locations for Code configuration + _configStoragePaths = [ + os.path.join(os.environ["HOME"], ".config/Code/storage.json"), + os.path.join(os.environ["HOME"], + ".config/Code/User/globalStorage/storage.json"), + ] + + # Possible locations for Project Manager extension configuration + _configProjectManagerPaths = [ + os.path.join( + os.environ["HOME"], ".config/Code/User/globalStorage/alefragnani.project-manager/projects.json") + ] + + # Indicates whether results from the Recent list in VSCode should be searched + _recentEnabled = True + + # Indicates whether projects from Project Manager extension should be searched + _projectManagerEnabled = False + + # Defines sorting priorities for results + _sortPriority = { + "PMName": 1, + "PMPath": 5, + "PMTag": 10, + "Recent": 15 + } + + # Holds cached data from the json configurations + _configCache: dict[str, CachedConfig] = {} + + # Overrides the command to open projects + _terminalCommand = "" + + # Setting indicating whether results from the Recent list in VSCode should be searched + @property + def recentEnabled(self): + return self._recentEnabled + + @recentEnabled.setter + def recentEnabled(self, value): + self._recentEnabled = value + self.writeConfig("recentEnabled", value) + + # Setting indicating whether projects in Project Manager extension should be searched + @property + def projectManagerEnabled(self): + return self._projectManagerEnabled + + @projectManagerEnabled.setter + def projectManagerEnabled(self, value): + self._projectManagerEnabled = value + self.writeConfig("projectManagerEnabled", value) + + found = False + for p in self._configProjectManagerPaths: + if os.path.exists(p): + found = True + break + + if found == False: + warning( + "Project Manager search was enabled, but configuration file was not found") + notif = Notification( + title=self.name, + text=f"Configuration file was not found for the Project Manager extension. Please make sure the extension is installed." + ) + notif.send() + + # Priority settings for project manager results using name search + @property + def priorityPMName(self): + return self._sortPriority["PMName"] + + @priorityPMName.setter + def priorityPMName(self, value): + self._sortPriority["PMName"] = value + self.writeConfig("priorityPMName", value) + + # Priority settings for project manager results using path search + @property + def priorityPMPath(self): + return self._sortPriority["PMPath"] + + @priorityPMPath.setter + def priorityPMPath(self, value): + self._sortPriority["PMPath"] = value + self.writeConfig("priorityPMPath", value) + + # Priority settings for project manager results using tag search + @property + def priorityPMTag(self): + return self._sortPriority["PMTag"] + + @priorityPMTag.setter + def priorityPMTag(self, value): + self._sortPriority["PMTag"] = value + self.writeConfig("priorityPMTag", value) + + # Priority settings for recently opened files + @property + def priorityRecent(self): + return self._sortPriority["Recent"] + + @priorityRecent.setter + def priorityRecent(self, value): + self._sortPriority["Recent"] = value + self.writeConfig("priorityRecent", value) + + # Setting for custom command when opening resulted items + @property + def terminalCommand(self): + return self._terminalCommand + + @terminalCommand.setter + def terminalCommand(self, value): + self._terminalCommand = value + self.writeConfig("terminalCommand", value) + + def defaultTrigger(self): + return "code " + + def synopsis(self, query): + return "project name or path" + + def __init__(self): + self.iconUrls = [f"file:{Path(__file__).parent}/icon.svg"] + + PluginInstance.__init__(self) + + TriggerQueryHandler.__init__(self) + + configFound = False + + for p in self._configStoragePaths: + if os.path.exists(p): + configFound = True + break + + if not configFound: + warning("Could not find any VSCode configuration directory") + + self._initConfiguration() + + def configWidget(self): + return [ + { + "type": "label", + "text": """Recent files are sorted in order found in the VSCode configuration. +Sort order with Project Manager can be adjusted, lower number = higher priority = displays first. +With all priorities equal, PM results will take precedence over recents.""" + }, + { + "type": "label", + "text": """ +PM extension: https://marketplace.visualstudio.com/items?itemName=alefragnani.project-manager +""" + }, + + { + "type": "checkbox", + "property": "recentEnabled", + "label": "Search in Recent files" + }, + { + "type": "checkbox", + "property": "projectManagerEnabled", + "label": "Search in Project Manager extension" + }, + { + "type": "spinbox", + "property": "priorityPMName", + "label": "Priority: Project Manager entries matched by name", + "widget_properties": { + "minimum": 1, + "maximum": 99, + }, + }, + { + "type": "spinbox", + "property": "priorityPMPath", + "label": "Priority: Project Manager entries matched by path", + "widget_properties": { + "minimum": 1, + "maximum": 99, + }, + }, + { + "type": "spinbox", + "property": "priorityPMTag", + "label": "Priority: Project Manager entries matched by tag", + "widget_properties": { + "minimum": 1, + "maximum": 99, + }, + }, + { + "type": "spinbox", + "property": "priorityRecent", + "label": "Priority: Recent entries", + "widget_properties": { + "minimum": 1, + "maximum": 99, + }, + }, + { + "type": "label", + "text": """ +The way VSCode is opened can be overriden through terminal command. +Terminal will enter the working directory of the project upon selection, execute the command and then close itself. + +Usecase with direnv - To load direnv environment before opening VSCode, enter the following custom command: direnv exec . code . + +Usecase with single VSCode instance - To reuse the VSCode window instead of opening a new one, enter the following custom command: code -r .""" + }, + { + "type": "lineedit", + "property": "terminalCommand", + "label": "Run custom command in the workdir of selected item" + }, + ] + + def _initConfiguration(self): + # Recent search + recentEnabled = self.readConfig('recentEnabled', bool) + if recentEnabled is None: + self._recentEnabled = True + self.writeConfig("recentEnabled", True) + else: + self._recentEnabled = recentEnabled + + # Project Manager search + foundPM = False + for p in self._configProjectManagerPaths: + if os.path.exists(p): + foundPM = True + break + + projectManagerEnabled = self.readConfig('projectManagerEnabled', bool) + if projectManagerEnabled is None: + # If not configured, check if the project manager configuration file exists and if so, enable PM search + if foundPM: + self._projectManagerEnabled = True + self.writeConfig("projectManagerEnabled", True) + else: + self._projectManagerEnabled = False + else: + self._projectManagerEnabled = projectManagerEnabled + + # Priority settings + for p in self._sortPriority: + prio = self.readConfig(f"priority{p}", int) + if prio is None: + self.writeConfig(f"priority{p}", self._sortPriority[p]) + else: + self._sortPriority[p] = prio + + # Terminal command setting + terminalCommand = self.readConfig('terminalCommand', str) + if terminalCommand is not None: + self._terminalCommand = terminalCommand + + def handleTriggerQuery(self, query): + if not query.isValid: + return + + if query.string == "": + return + + matcher = Matcher(query.string) + + results: dict[str, SearchResult] = {} + + if self.recentEnabled: + results = self._searchInRecentFiles(matcher, results) + + if self.projectManagerEnabled: + results = self._searchInProjectManager(matcher, results) + + sortedItems = sorted(results.values(), key=lambda item: "%s_%s_%s" % ( + '{:03d}'.format(item.priority), '{:03d}'.format(item.sortIndex), item.project.name), reverse=False) + + items: list[StandardItem] = [] + for i in sortedItems: + items.append(self._createItem(i.project, query)) + + query.add(items) + + # Creates an item for the query based on the project and plugin settings + def _createItem(self, project: Project, query: Query) -> StandardItem: + actions: list[Action] = [] + + if self.terminalCommand != "": + actions.append( + Action( + id="open-terminal", + text=f"Run terminal command in project's workdir: {self.terminalCommand}", + callable=lambda: runTerminal(f"cd {project.path} && {self.terminalCommand}") + ) + ) + + actions.append( + Action( + id="open-code", + text="Open with VSCode", + callable=lambda: runDetachedProcess( + ["code", project.path]), + ) + ) + + subtext = "" + + if len(project.tags) > 0: + subtext = "<" + ",".join(project.tags) + "> " + + return StandardItem( + id=project.path, + text=project.displayName, + subtext=f"{subtext}{project.path}", + iconUrls=self.iconUrls, + inputActionText=project.displayName, + actions=actions, + ) + + def _searchInRecentFiles(self, matcher: Matcher, results: dict[str, SearchResult]) -> dict[str, SearchResult]: + sortIndex = 1 + + for path in self._configStoragePaths: + c = self._getStorageConfig(path) + for proj in c.projects: + # Resolve sym links to get unique results + resolvedPath = str(Path(proj.path).resolve()) + if matcher.match(proj.name) or matcher.match(proj.path) or matcher.match(resolvedPath): + results[resolvedPath] = self._getHigherPriorityResult( + SearchResult( + project=proj, + priority=self.priorityRecent, + sortIndex=sortIndex + ), + results.get(resolvedPath), + ) + + if results.get(resolvedPath) is not None: + sortIndex += 1 + + return results + + def _searchInProjectManager(self, matcher: Matcher, results: dict[str, SearchResult]) -> dict[str, SearchResult]: + for path in self._configProjectManagerPaths: + c = self._getProjectManagerConfig(path) + for proj in c.projects: + # Resolve sym links to get unique results + resolvedPath = str(Path(proj.path).resolve()) + if matcher.match(proj.name): + results[resolvedPath] = self._getHigherPriorityResult( + SearchResult( + project=proj, + priority=self.priorityPMName, + sortIndex=0 if matcher.match(proj.name).isExactMatch() else 1 + ), + results.get(resolvedPath), + ) + + if matcher.match(proj.path) or matcher.match(resolvedPath): + results[resolvedPath] = self._getHigherPriorityResult( + SearchResult( + project=proj, + priority=self.priorityPMPath, + sortIndex=1 + ), + results.get(resolvedPath), + ) + + for tag in proj.tags: + if matcher.match(tag): + results[resolvedPath] = self._getHigherPriorityResult( + SearchResult( + project=proj, + priority=self.priorityPMTag, + sortIndex=1 + ), + results.get(resolvedPath), + ) + break + + return results + + # Compares the search results to return the one with higher priority + # For nitpickers: higher priorty = lower number + def _getHigherPriorityResult(self, current: SearchResult, prev: SearchResult | None) -> SearchResult: + if prev is None or current.priority < prev.priority or (current.priority == prev.priority and current.sortIndex < prev.sortIndex): + return current + + return prev + + def _getStorageConfig(self, path: str) -> CachedConfig: + c: CachedConfig = self._configCache.get(path, CachedConfig([], 0)) + + if not os.path.exists(path): + return c + + mTime = os.stat(path).st_mtime + + if mTime == c.mTime: + return c + + c.mTime = mTime + + with open(path) as configFile: + # Load the storage json + storageConfig = json.loads(configFile.read()) + + if ( + "lastKnownMenubarData" in storageConfig + and "menus" in storageConfig["lastKnownMenubarData"] + and "File" in storageConfig["lastKnownMenubarData"]["menus"] + and "items" in storageConfig["lastKnownMenubarData"]["menus"]["File"] + ): + # These are all the menu items in File dropdown + for menuItem in storageConfig["lastKnownMenubarData"]["menus"]["File"]["items"]: + # Cannot safely detect proper menu item, as menu item IDs change over time + # Instead we will search all submenus and check for IDs inside the submenu items + if ( + not "id" in menuItem + or not "submenu" in menuItem + or not "items" in menuItem["submenu"] + ): + continue + + for submenuItem in menuItem["submenu"]["items"]: + # Check of submenu item with id "openRecentFolder" and make sure it contains necessarry keys + if ( + not "id" in submenuItem + or submenuItem['id'] != "openRecentFolder" + or not "enabled" in submenuItem + or submenuItem["enabled"] != True + or not "label" in submenuItem + or not "uri" in submenuItem + or not "path" in submenuItem["uri"] + ): + continue + + # Get the full path to the project + recentPath = submenuItem["uri"]["path"] + if not os.path.exists(recentPath): + continue + + displayName = recentPath.split("/")[-1] + + # Inject the project + c.projects.append(Project( + displayName=displayName, + name=displayName, + path=recentPath, + tags=[], + )) + + self._configCache[path] = c + + return c + + def _getProjectManagerConfig(self, path: str) -> CachedConfig: + c = self._configCache.get(path, CachedConfig([], 0)) + + if not os.path.exists(path): + return c + + mTime = os.stat(path).st_mtime + + if mTime == c.mTime: + return c + + c.mTime = mTime + + with open(path) as configFile: + configuredProjects = json.loads(configFile.read()) + + for p in configuredProjects: + # Make sure we have necessarry keys + if ( + not "rootPath" in p + or not "name" in p + or not "enabled" in p + or p["enabled"] != True + ): + continue + + # Grab the path to the project + rootPath = p["rootPath"] + if os.path.exists(rootPath) == False: + continue + + project = Project( + displayName=p["name"], + name=p["name"], + path=rootPath, + tags=[], + ) + + # Search against the query string + if "tags" in p: + for tag in p["tags"]: + project.tags.append(tag) + + c.projects.append(project) + + self._configCache[path] = c + + return c diff --git a/vscode_projects/icon.svg b/vscode_projects/icon.svg new file mode 100644 index 00000000..c453e633 --- /dev/null +++ b/vscode_projects/icon.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wikipedia/__init__.py b/wikipedia/__init__.py new file mode 100644 index 00000000..af92e2f6 --- /dev/null +++ b/wikipedia/__init__.py @@ -0,0 +1,161 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2024 Manuel Schneider + + +from albert import * +from locale import getdefaultlocale +from socket import timeout +from time import sleep +from urllib import request, parse +import json +from pathlib import Path + +md_iid = "3.0" +md_version = "3.0" +md_name = "Wikipedia" +md_description = "Search Wikipedia articles" +md_license = "MIT" +md_url = "https://github.com/albertlauncher/python/tree/main/wikipedia" +md_authors = "@manuelschneid3r" + +class Plugin(PluginInstance, TriggerQueryHandler): + + baseurl = 'https://en.wikipedia.org/w/api.php' + searchUrl = 'https://%s.wikipedia.org/wiki/Special:Search/%s' + user_agent = "org.albert.wikipedia" + limit = 20 + iconUrls = [f"file:{Path(__file__).parent}/wikipedia.png"] + + def __init__(self): + PluginInstance.__init__(self) + TriggerQueryHandler.__init__(self) + + self.fbh = FBH(self) + self.fuzzy = False + + self.local_lang_code = getdefaultlocale()[0] + if self.local_lang_code: + self.local_lang_code = self.local_lang_code[0:2] + else: + self.local_lang_code = 'en' + warning("Failed getting language code. Using 'en'.") + + params = { + 'action': 'query', + 'meta': 'siteinfo', + 'utf8': 1, + 'siprop': 'languages', + 'format': 'json' + } + + get_url = "%s?%s" % (self.baseurl, parse.urlencode(params)) + req = request.Request(get_url, headers={'User-Agent': self.user_agent}) + try: + with request.urlopen(req, timeout=5) as response: + data = json.loads(response.read().decode('utf-8')) + languages = [lang['code'] for lang in data['query']['languages']] + if self.local_lang_code in languages: + self.baseurl = self.baseurl.replace("en", self.local_lang_code) + except timeout: + warning('Error getting languages - socket timed out. Defaulting to EN.') + except Exception as error: + warning('Error getting languages (%s). Defaulting to EN.' % error) + + def extensions(self): + return [self, self.fbh] + + def defaultTrigger(self): + return "wiki " + + def supportsFuzzyMatching(self): + return True + + def setFuzzyMatching(self, enabled: bool): + self.fuzzy = enabled + + def handleTriggerQuery(self, query): + if stripped := query.string.strip(): + + # avoid rate limiting + for _ in range(50): + sleep(0.01) + if not query.isValid: + return + + results = [] + + params = { + 'action': 'opensearch', + 'search': stripped, + 'limit': self.limit, + 'utf8': 1, + 'format': 'json', + 'profile': 'fuzzy' if self.fuzzy else 'normal' + } + get_url = "%s?%s" % (self.baseurl, parse.urlencode(params)) + req = request.Request(get_url, headers={'User-Agent': self.user_agent}) + + with request.urlopen(req) as response: + data = json.loads(response.read().decode('utf-8')) + + for i in range(0, min(self.limit, len(data[1]))): + title = data[1][i] + summary = data[2][i] + url = data[3][i] + results.append( + StandardItem( + id=self.id(), + text=title, + subtext=summary if summary else url, + iconUrls=self.iconUrls, + actions=[ + Action("open", "Open article on Wikipedia", lambda u=url: openUrl(u)), + Action("copy", "Copy URL to clipboard", lambda u=url: setClipboardText(u)) + ] + ) + ) + + if not results: + results.append(self.createFallbackItem(stripped)) + + query.add(results) + else: + query.add( + StandardItem( + id=self.id(), + text=self.name(), + subtext="Enter a query to search on Wikipedia", + iconUrls=self.iconUrls + ) + ) + + def createFallbackItem(self, q: str) -> Item: + return StandardItem( + id=self.id(), + text=self.name(), + subtext="Search '%s' on Wikipedia" % q, + iconUrls=self.iconUrls, + actions=[ + Action("wiki_search", "Search on Wikipedia", + lambda url=self.searchUrl % (self.local_lang_code, q): openUrl(url)) + ] + ) + + +class FBH(FallbackHandler): + + def __init__(self, p: Plugin): + FallbackHandler.__init__(self) + self.plugin = p + + def id(self): + return "wikipedia.fallbacks" + + def name(self): + return md_name + + def description(self): + return md_description + + def fallbacks(self, q :str): + return [self.plugin.createFallbackItem(q)] diff --git a/wikipedia/wikipedia.png b/wikipedia/wikipedia.png new file mode 100644 index 00000000..a2a40bbd Binary files /dev/null and b/wikipedia/wikipedia.png differ diff --git a/x_window_switcher/__init__.py b/x_window_switcher/__init__.py new file mode 100644 index 00000000..00c180b9 --- /dev/null +++ b/x_window_switcher/__init__.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- + +import subprocess +from collections import namedtuple +from albert import * + +md_iid = "3.0" +md_version = "0.6.0" +md_name = "X Window Switcher" +md_description = "Switch X11 Windows" +md_license = "MIT" +md_url = "https://github.com/albertlauncher/python/tree/main/x_window_switcher" +md_bin_dependencies = "wmctrl" +md_authors = ["Ed Perez", "Manuel S.", "dshoreman", "nopsqi"] + +Window = namedtuple("Window", ["wid", "desktop", "wm_class", "host", "wm_name"]) + + +class Plugin(PluginInstance, TriggerQueryHandler): + def __init__(self): + PluginInstance.__init__(self) + TriggerQueryHandler.__init__(self) + + # Check for X session and wmctrl availability + try: + subprocess.check_call(["wmctrl", "-m"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + except FileNotFoundError: + raise Exception("wmctrl not found. Please install wmctrl.") + except subprocess.CalledProcessError: + raise Exception("Unable to communicate with X11 window manager. This plugin requires a running X session.") + + def defaultTrigger(self): + return 'w ' + + def handleTriggerQuery(self, query): + try: + for line in subprocess.check_output(['wmctrl', '-l', '-x']).splitlines(): + win = Window(*parseWindow(line)) + + if win.desktop == "-1": + continue + + win_instance, win_class = win.wm_class.replace(' ', '-').split('.', 1) + + m = Matcher(query.string) + if not query.string or m.match(win_instance + ' ' + win_class + ' ' + win.wm_name): + query.add(StandardItem( + id="%s%s" % (md_name, win.wm_class), + iconUrls=["xdg:%s" % win_instance], + text="%s - Desktop %s" % (win_class.replace('-', ' '), win.desktop), + subtext=win.wm_name, + actions=[Action("switch", + "Switch Window", + lambda w=win: runDetachedProcess(["wmctrl", '-i', '-a', w.wid])), + Action("move", + "Move window to this desktop", + lambda w=win: runDetachedProcess(["wmctrl", '-i', '-R', w.wid])), + Action("close", + "Close the window gracefully.", + lambda w=win: runDetachedProcess(["wmctrl", '-c', w.wid]))] + )) + except subprocess.CalledProcessError as e: + warning(f"Error executing wmctrl: {str(e)}") + + +def parseWindow(line): + win_id, desktop, rest = line.decode().split(None, 2) + win_class, rest = rest.split(' ', 1) + host, title = rest.strip().split(None, 1) + + return [win_id, desktop, win_class, host, title] diff --git a/zeal/__init__.py b/zeal/__init__.py new file mode 100644 index 00000000..1339a7d9 --- /dev/null +++ b/zeal/__init__.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2024 Manuel Schneider + +import albert + +md_iid = "3.0" +md_version = "3.0" +md_name = "Zeal" +md_description = "Search in Zeal docs" +md_license = "MIT" +md_url = "https://github.com/albertlauncher/python/tree/main/zeal" +md_authors = "@manuelschneid3r" +md_bin_dependencies = ['zeal'] + +def createItem(query: str): + return albert.StandardItem( + id=md_name, + text=md_name, + subtext=f"Search '{query}' in Zeal", + iconUrls=["xdg:zeal"], + actions=[albert.Action("zeal", "Search in Zeal", + lambda q=query: albert.runDetachedProcess(['zeal', q]))] + ) + +class FBH(albert.FallbackHandler): + + def id(self): + return "zeal_fbh" + + def name(self): + return md_name + + def description(self): + return md_description + + def fallbacks(self, s): + return [createItem(s)] if s else [] + + +class Plugin(albert.PluginInstance, albert.TriggerQueryHandler): + + def __init__(self): + albert.PluginInstance.__init__(self) + albert.TriggerQueryHandler.__init__(self) + self.fbh = FBH() + + def defaultTrigger(self): + return "z " + + def extensions(self): + return [self, self.fbh] + + def handleTriggerQuery(self, query): + if stripped := query.string.strip(): + query.add(createItem(stripped))