Skip to content

Commit 970f98b

Browse files
[emoji] New generic and platform agnostic emoji implementation
1 parent 2cf4cc1 commit 970f98b

File tree

1 file changed

+182
-0
lines changed

1 file changed

+182
-0
lines changed

emoji/__init__.py

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
# -*- coding: utf-8 -*-
2+
3+
import json
4+
import re
5+
import threading
6+
import urllib.request
7+
from itertools import product
8+
from locale import getdefaultlocale
9+
from pathlib import Path
10+
11+
from albert import *
12+
13+
md_iid = '2.0'
14+
md_version = "2.0"
15+
md_name = "Emoji"
16+
md_description = "Find and copy emojis by name"
17+
md_license = "MIT"
18+
md_url = "https://github.com/albertlauncher/python/tree/master/emoji"
19+
20+
21+
class Plugin(PluginInstance, IndexQueryHandler):
22+
23+
def __init__(self):
24+
IndexQueryHandler.__init__(self,
25+
id=md_id,
26+
name=md_name,
27+
description=md_description,
28+
defaultTrigger=':',
29+
synopsis='<emoji name>')
30+
PluginInstance.__init__(self, extensions=[self])
31+
self.thread = None
32+
33+
def finalize(self):
34+
if self.thread.is_alive():
35+
self.thread.join()
36+
37+
def updateIndexItems(self):
38+
if self.thread and self.thread.is_alive():
39+
self.thread.join()
40+
self.thread = threading.Thread(target=self.update_index_items_task)
41+
self.thread.start()
42+
43+
def update_index_items_task(self):
44+
45+
def download_file(url: str, path: str) -> bool:
46+
debug(f"Downloading {url}.")
47+
headers = {'User-Agent': 'Mozilla/5.0'} # otherwise github returns html
48+
request = urllib.request.Request(url, headers=headers)
49+
with urllib.request.urlopen(request, timeout=3) as response:
50+
if response.getcode() == 200:
51+
debug(f"Success. Storing to {path}.")
52+
with open(path, 'wb') as file:
53+
file.write(response.read())
54+
else:
55+
raise RuntimeError(f"Failed to download {url}. Status code: {response.getcode()}")
56+
57+
def get_fully_qualified_emojis(cache_path: str) -> list:
58+
"""Returns fully qualified emoji strings"""
59+
60+
def convert_to_unicode_char(hex_code: str):
61+
return chr(int(hex_code, 16))
62+
63+
def convert_to_unicode_str(hex_codes: str):
64+
hex_list = hex_codes.split()
65+
return ''.join([convert_to_unicode_char(hex_code) for hex_code in hex_list])
66+
67+
path = cache_path / 'emoji_list.txt'
68+
if not path.is_file():
69+
info("Fetching emoji list.")
70+
url = 'https://unicode.org/Public/emoji/latest/emoji-test.txt'
71+
download_file(url, path)
72+
73+
# components = set()
74+
fully_qualified = []
75+
76+
with path.open("r") as f:
77+
78+
emoji_list_re_str = r"""
79+
^
80+
(?P<codepoints> .*\S)
81+
\s*;\s*
82+
(?P<status> \S+)
83+
\s*\#\s*
84+
(?P<emoji> \S+)
85+
\s*
86+
(?P<version> E\d+.\d+)
87+
\s*
88+
(?P<name> [^:]+)
89+
(?: : \s* (?P<modifier> .+))?
90+
\n
91+
$
92+
"""
93+
94+
line_re = re.compile(emoji_list_re_str, re.VERBOSE)
95+
for line in f:
96+
if match := line_re.match(line):
97+
if match.group("status") == "fully-qualified":
98+
fully_qualified.append(convert_to_unicode_str(match.group("codepoints")))
99+
100+
return fully_qualified
101+
102+
def get_annotations(cache_path: str) -> dict:
103+
104+
# determine locale
105+
106+
if lang := getdefaultlocale()[0]:
107+
lang = lang[0:2]
108+
else:
109+
warning("Failed getting locale. There will be no localized emoji aliases.")
110+
lang = 'en'
111+
112+
# fetch localized cldr annotations 'full'
113+
114+
path_full = cache_path / 'emoji_annotations_full.json'
115+
if not path_full.is_file():
116+
url = 'https://raw.githubusercontent.com/unicode-org/cldr-json/main/cldr-json/' \
117+
'cldr-annotations-full/annotations/%s/annotations.json' % lang
118+
download_file(url, path_full)
119+
120+
# fetch localized cldr annotations 'derived'
121+
122+
path_derived = cache_path / 'emoji_annotations_derived.json'
123+
if not path_derived.is_file():
124+
url = 'https://raw.githubusercontent.com/unicode-org/cldr-json/main/cldr-json/' \
125+
'cldr-annotations-derived-full/annotationsDerived/%s/annotations.json' % lang
126+
download_file(url, path_derived)
127+
128+
# open, read, parse, merge, return
129+
130+
with path_full.open("r", encoding='utf-8') as file_full, \
131+
path_derived.open("r", encoding='utf-8') as file_derived:
132+
json_full = json.load(file_full)['annotations']['annotations']
133+
json_derived = json.load(file_derived)['annotationsDerived']['annotations']
134+
return json_full | json_derived
135+
emojis = get_fully_qualified_emojis(self.cacheLocation)
136+
annotations = get_annotations(self.cacheLocation)
137+
138+
def remove_redundancy(sentences):
139+
sets_of_words = [set(sentence.lower().split()) for sentence in sentences]
140+
unique = []
141+
for sow, sentence in zip(sets_of_words, sentences):
142+
for other_sow in sets_of_words:
143+
if sow != other_sow:
144+
if all([any([oword.startswith(word) for oword in other_sow]) for word in sow]):
145+
break
146+
else:
147+
unique.append(sentence)
148+
return unique
149+
150+
index_items = []
151+
for emoji in emojis:
152+
try:
153+
ann = annotations[emoji]
154+
except KeyError:
155+
try:
156+
non_rgi_emoji = emoji.replace('\uFE0F', '')
157+
ann = annotations[non_rgi_emoji]
158+
except KeyError as e:
159+
debug(f"Found no translation for {e}. Emoji will not be available.")
160+
continue
161+
162+
title = ann['tts'][0]
163+
aliases = remove_redundancy([title.replace(':', '').replace(',', ''), *ann['default']])
164+
165+
item = StandardItem(
166+
id=emoji,
167+
text=title.capitalize(),
168+
subtext=", ".join([a.capitalize() for a in aliases]),
169+
iconUrls=[f"gen:?text={emoji}"],
170+
actions=[
171+
Action(
172+
"copy",
173+
"Copy to clipboard",
174+
lambda emj=emoji: setClipboardText(emj),
175+
),
176+
]
177+
)
178+
179+
for alias in aliases:
180+
index_items.append(IndexItem(item=item, string=alias))
181+
182+
self.setIndexItems(index_items)

0 commit comments

Comments
 (0)