Files
amethyst/tools/emoji-data/generate.py
Claude 8158adf1ea feat: shared Unicode emoji picker for composers and reaction choices
Desktop has no system emoji keyboard, so there was no way to type an emoji
there. The composer's emoji button now opens a picker with the Unicode palette
(categories, name search, skin tones) next to the existing NIP-30 custom emoji
tab, in the note, comment, DM, public chat, Concord and Nest composers.

- commons/unicodeEmoji: UnicodeEmojiCatalog parses the palette and searches it.
- commonsUI: the palette file (Unicode emoji-test.txt up to Emoji 15.0, built by
  tools/emoji-data/generate.py) and the picker. Cells draw in the system's color
  emoji font on desktop, since Skia's fallback picks monochrome DejaVu Sans on Linux.
- The reaction choices screen gets the picker in its field and is now registered
  in the shared nav host, so desktop can open it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QEZ92TehbWvUhrJfrX2kie
2026-10-15 15:36:00 +00:00

98 lines
3.5 KiB
Python

#!/usr/bin/env python3
"""Builds the Unicode emoji palette that the shared emoji picker reads.
Input is Unicode's emoji-test.txt (UTS #51), which lists every RGI emoji in
CLDR keyboard order, by group. Output is a small tab-separated file under
commonsUI's compose resources:
#<license header lines>
@<group name>
<emoji>\t<name>[\t<5 skin-tone variants, space separated>]
Only fully-qualified emoji up to --max-version are kept, so the palette does not
offer glyphs the desktop's system emoji font (Segoe UI Emoji, Apple Color Emoji,
Noto Color Emoji) is still too old to draw. Skin-tone variants are folded into
their base emoji: an entry carries the five uniform-tone variants
("<name>: light skin tone", ...) when all five exist, and every other tone
combination is left out. The Component group (bare skin tones, hair) is dropped.
Usage:
curl -sSO https://unicode.org/Public/emoji/latest/emoji-test.txt
python3 tools/emoji-data/generate.py emoji-test.txt \
commonsUI/src/commonMain/composeResources/files/emoji/emoji.tsv
"""
import argparse
import re
TONES = ["light", "medium-light", "medium", "medium-dark", "dark"]
LINE = re.compile(r"^([0-9A-F ]+?)\s*;\s*([a-z-]+)\s*#\s*(\S+)\s+E(\d+\.\d+)\s+(.+)$")
LICENSE = [
"Generated by tools/emoji-data/generate.py from Unicode emoji-test.txt, version {version}.",
"Copyright (c) 1991-2026 Unicode, Inc. Distributed under the Unicode License v3:",
"https://www.unicode.org/license.txt",
]
def version_key(v):
major, minor = v.split(".")
return (int(major), int(minor))
def main():
parser = argparse.ArgumentParser()
parser.add_argument("source")
parser.add_argument("output")
parser.add_argument("--max-version", default="15.0")
args = parser.parse_args()
max_version = version_key(args.max_version)
groups = [] # [(name, [(emoji, name)])]
by_name = {} # name -> emoji, for the tone lookup
source_version = "?"
group = None
with open(args.source, encoding="utf-8") as f:
for raw in f:
line = raw.rstrip("\n")
if line.startswith("# Version:"):
source_version = line.split(":", 1)[1].strip()
continue
if line.startswith("# group:"):
name = line.split(":", 1)[1].strip()
group = None if name == "Component" else (name, [])
if group:
groups.append(group)
continue
m = LINE.match(line)
if not m or group is None:
continue
_, status, emoji, version, name = m.groups()
if status != "fully-qualified" or version_key(version) > max_version:
continue
by_name[name] = emoji
group[1].append((emoji, name))
out = ["# " + l.format(version=source_version) for l in LICENSE]
count = 0
for group_name, entries in groups:
out.append("@" + group_name)
for emoji, name in entries:
if " skin tone" in name:
continue # folded into its base entry below, or dropped
tones = [by_name.get(f"{name}: {t} skin tone") for t in TONES]
row = [emoji, name]
if all(tones):
row.append(" ".join(tones))
out.append("\t".join(row))
count += 1
with open(args.output, "w", encoding="utf-8") as f:
f.write("\n".join(out) + "\n")
print(f"wrote {count} emoji in {len(groups)} groups to {args.output}")
if __name__ == "__main__":
main()