#!/usr/bin/env python3 import json import os import subprocess import sys import logging from pathlib import Path # Setup logging logging.basicConfig( level=logging.INFO, # default level format="%(levelname)s: %(message)s" ) logger = logging.getLogger(__name__) # Optional: pyperclip fallback try: import pyperclip HAVE_PYPERCLIP = True except ImportError: HAVE_PYPERCLIP = False EMOJI_URL = "https://raw.githubusercontent.com/github/gemoji/master/db/emoji.json" class EmojiCache: """Handles downloading and caching the emoji list in XDG_CACHE_HOME.""" def __init__(self): xdg_cache = Path(os.environ.get("XDG_CACHE_HOME", Path.home() / ".cache")) self.cache_dir = xdg_cache / "emoji-picker" self.cache_file = self.cache_dir / "emoji-list.txt" self.cache_dir.mkdir(parents=True, exist_ok=True) logger.debug(f"Cache directory: {self.cache_dir}") def fetch(self): """Download or return cached emoji list.""" if not self.cache_file.exists(): self.download() else: logger.debug(f"Using cached emoji list at {self.cache_file}") return self.cache_file def download(self): try: import requests except ImportError: logger.error("requests module is required to fetch emoji list") sys.exit(1) logger.info("Downloading emoji list...") data = requests.get(EMOJI_URL).json() lines = [] for e in data: emoji = e.get("emoji", "") description = e.get("description", "") category = e.get("category", "") aliases = e.get("aliases", []) tags = e.get("tags", []) line = emoji + " - " + description + " - " + category line = self.add_unique(aliases, line) line = self.add_unique(tags, line) lines.append(line) self.cache_file.write_text("\n".join(lines), encoding="utf-8") logger.info(f"Emoji list cached at {self.cache_file}") @staticmethod def add_unique(words, line): for word in words: if word.lower() not in line.lower(): line += " " + word return line class EmojiPicker: """Shows menu with tofi and copies selected emoji to clipboard.""" def __init__(self, emoji_file): self.emoji_file = Path(emoji_file) def pick(self): try: with self.emoji_file.open("r", encoding="utf-8") as f: choices = f.read().splitlines() result = subprocess.run( [ "tofi", "--fuzzy-match", "true", "--require-match", "true", "--history", "true", "--history-file", str(Path.home() / ".cache/emoji-picker/history.txt") ], input="\n".join(choices), capture_output=True, text=True ) chosen_line = result.stdout.strip() if not chosen_line: logger.debug("No selection made") return emoji = chosen_line.split()[0] # just the emoji self.copy_to_clipboard(emoji) logger.info(f"Copied {emoji} to clipboard") except FileNotFoundError: logger.error("tofi not found!") @staticmethod def copy_to_clipboard(text): if HAVE_PYPERCLIP: pyperclip.copy(text) else: try: subprocess.run(["wl-copy"], input=text.encode(), check=True) except FileNotFoundError: logger.error("wl-copy not found and pyperclip not available") def main(): cache = EmojiCache() emoji_file = cache.fetch() picker = EmojiPicker(emoji_file) picker.pick() if __name__ == "__main__": main()