Files
Modus/src/window/spotlight/plugins/emoji.py
T

225 lines
7.3 KiB
Python

import contextlib
from pathlib import Path
from typing import Any
from urllib.request import Request, urlopen
from fabric.utils import GLib, logger
import shared.data as data
from utils.functions import copy_text, read_json_file, trigger_paste_shortcut
from window.spotlight.api import PluginContext, SearchResult, SpotlightPlugin
class EmojiPlugin(SpotlightPlugin):
id = "emoji"
name = "Emoji"
icon = "face-smile-symbolic"
keywords = ["emoji", "em"]
searchable = True
priority = 70
def __init__(self, context: PluginContext):
super().__init__(context)
self._emoji_data: dict[str, dict[str, Any]] = {}
def initialize(self) -> None:
self._ensure_data()
def cleanup(self) -> None:
self._emoji_data.clear()
def release_memory(self) -> None:
self._emoji_data.clear()
def _ensure_data(self):
if self._emoji_data:
return
cldr = self._load_cldr_annotations()
self._emoji_data = cldr or {}
def detect(self, text: str) -> bool:
s = text.strip()
if not s:
return False
for ch in s:
cp = ord(ch)
if (
0x1F600 <= cp <= 0x1F64F
or 0x1F300 <= cp <= 0x1F5FF
or 0x1F680 <= cp <= 0x1F6FF
or 0x1F1E0 <= cp <= 0x1F1FF
or 0x2600 <= cp <= 0x27BF
or 0xFE00 <= cp <= 0xFE0F
or 0x1F900 <= cp <= 0x1F9FF
or cp == 0x200D
or cp == 0x20E3
or 0xE0020 <= cp <= 0xE007F
):
return True
return False
def search(self, query: str, token: Any) -> list[SearchResult]:
q = query.strip()
if not q:
return []
for prefix in ("emoji ", "em "):
if q.lower().startswith(prefix):
q = q[len(prefix) :].strip()
break
if q.lower() in ("emoji", "em"):
q = ""
if not q:
return []
self._ensure_data()
if not self._emoji_data:
logger.warning("[Emoji] No emoji data available")
return []
return self._query_emojis(q)
def _query_emojis(self, text: str) -> list[SearchResult]:
query = text.lower().strip()
scored: list[tuple[str, dict, float]] = []
for emoji_char, info in self._emoji_data.items():
relevance = 0.0
name = info.get("name", "").lower()
keywords = info.get("keywords", [])
if query == emoji_char:
relevance = 1.0
elif query == name:
relevance = 0.95
elif name.startswith(query):
relevance = 0.9
elif query in name:
relevance = 0.8
elif any(query == k.lower() for k in keywords):
relevance = 0.7
elif any(query in k.lower() for k in keywords):
relevance = 0.6
if relevance > 0:
scored.append((emoji_char, info, relevance))
if not scored:
return []
scored.sort(key=lambda x: x[2], reverse=True)
results: list[SearchResult] = []
for emoji_char, info, relevance in scored[:20]:
name = info.get("name", "Unknown").replace("_", " ").title()
keyword_list = info.get("keywords", [])
results.append(
SearchResult(
id=emoji_char,
title=emoji_char,
subtitle=name,
icon_name="",
score=relevance * 100,
render_type="emoji",
action=lambda e=emoji_char: self._copy_to_clipboard(e),
plugin_name=self.name,
plugin_id=self.id,
metadata={
"emoji": emoji_char,
"name": name,
"keywords": keyword_list,
},
)
)
return results
def _copy_to_clipboard(self, emoji_char: str) -> None:
copy_text(emoji_char)
GLib.timeout_add(50, lambda: (trigger_paste_shortcut(0), False)[1])
def handle_external(self, command: str, args: str) -> None:
pass
def _load_cldr_annotations(self) -> dict[str, dict[str, Any]] | None:
try:
lang = "en"
annotations_path = self._get_annotations_path(lang)
if not annotations_path.exists():
url = (
"https://raw.githubusercontent.com/unicode-org/cldr-json/main/"
f"cldr-json/cldr-annotations-full/annotations/{lang}/annotations.json"
)
logger.info(f"[Emoji] Downloading CLDR annotations from {url}")
if not self._download_annotations(url, annotations_path):
logger.error("[Emoji] Failed to download CLDR annotations")
return None
raw = self._read_annotations_file(annotations_path)
if raw is None:
logger.error("[Emoji] Failed to read annotations file")
return None
result = self._normalize_annotations(raw) or None
if result:
logger.debug(f"[Emoji] Normalized {len(result)} emojis")
else:
logger.error("[Emoji] Normalization returned empty")
return result
except Exception as e:
logger.error(f"[Emoji] Error loading CLDR annotations: {e}")
return None
def _get_annotations_path(self, lang: str) -> Path:
cache_dir = Path(data.CACHE_DIR) / "emoji"
cache_dir.mkdir(parents=True, exist_ok=True)
return cache_dir / f"annotations_full_{lang}.json"
def _download_annotations(self, url: str, dest: Path) -> bool:
headers = {"User-Agent": "Mozilla/5.0"}
try:
req = Request(url, headers=headers)
with urlopen(req, timeout=10) as response:
if response.status == 200:
dest.write_bytes(response.read())
return True
return False
except Exception as e:
logger.error(f"[Emoji] Download error: {e}")
return False
def _read_annotations_file(self, path: Path) -> dict[str, Any] | None:
raw = read_json_file(str(path))
if raw:
return raw
with contextlib.suppress(OSError):
path.unlink()
return None
def _normalize_annotations(self, raw: dict[str, Any]) -> dict[str, dict[str, Any]]:
annotations = (
raw.get("annotations", {}).get("annotations", {})
if isinstance(raw, dict)
else {}
)
if not annotations:
return {}
normalized: dict[str, dict[str, Any]] = {}
for emoji_char, info in annotations.items():
if not isinstance(info, dict):
continue
tts_list = info.get("tts", []) or []
default_list = info.get("default", []) or []
title = tts_list[0] if tts_list else ""
normalized[emoji_char] = {
"name": title,
"keywords": default_list,
}
return {k: v for k, v in normalized.items() if v.get("name")}