Files
ichmagmaus111andClaude Opus 5.5 0622057a4f Ghostgram: open-source release
Ghostgram for iOS, rebuilt from scratch on Telegram iOS 13.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 17:57:09 +01:00

167 lines
6.5 KiB
Python

#!/usr/bin/env python3
"""Generates GGIconPackMapTable.generated.swift from the icon pack map data and checks it.
The data is `icon_pack_map.tsv` (Android drawable name <TAB> iOS asset name, one pair per line)
and `icon_pack_blacklist.txt` (Android names a pack may never replace, one per line). An `.icons`
pack (exteraGram / Telegram-for-Android format) names its icons by Android drawable, so the table
says which iOS asset each of them replaces.
Every iOS asset name is checked against the asset catalogs of the app: an asset upstream renamed
or removed is an error (the table must follow upstream, never silently go stale).
python3 Ghostgram/Features/Interface/IconPacks/map/generate_icon_map.py
python3 Ghostgram/Features/Interface/IconPacks/map/generate_icon_map.py --check # no writing
Bootstrap only (the TSV is committed): `--import-markdown <XTGIconPackMap.md>` rewrites the TSV
from exteraGram's map document (rows of the "Not mapped" section are skipped).
"""
import argparse
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
REPO = os.path.abspath(os.path.join(HERE, "..", "..", "..", "..", ".."))
TSV = os.path.join(HERE, "icon_pack_map.tsv")
BLACKLIST = os.path.join(HERE, "icon_pack_blacklist.txt")
OUTPUT = os.path.join(HERE, "..", "..", "Sources", "IconPacks", "GGIconPackMapTable.generated.swift")
CATALOGS = [
os.path.join(REPO, "submodules", "TelegramUI", "Images.xcassets"),
]
def import_markdown(path):
pairs = []
section = None
with open(path, "r", encoding="utf-8") as handle:
for line in handle:
line = line.rstrip("\n")
if line.startswith("## "):
section = line[3:].strip()
continue
if section is None or section.lower().startswith("not mapped"):
continue
if not line.startswith("| ") or line.startswith("| ---") or line.startswith("| Android drawable"):
continue
cells = [cell.strip() for cell in line.strip().strip("|").split("|")]
if len(cells) < 4:
continue
android, ios = cells[0], cells[3]
if not android or not ios or " " in android:
continue
pairs.append((android, ios))
seen = set()
with open(TSV, "w", encoding="utf-8") as handle:
handle.write("# Android drawable\tiOS asset — generated once from exteraGram's XTGIconPackMap.md, edited here since\n")
for android, ios in pairs:
if (android, ios) in seen:
continue
seen.add((android, ios))
handle.write("%s\t%s\n" % (android, ios))
print("imported %d pairs" % len(seen))
def read_pairs():
pairs = []
with open(TSV, "r", encoding="utf-8") as handle:
for number, line in enumerate(handle, 1):
line = line.rstrip("\n")
if not line or line.startswith("#"):
continue
parts = line.split("\t")
if len(parts) != 2 or not parts[0] or not parts[1]:
raise SystemExit("icon_pack_map.tsv:%d: expected '<android>\\t<ios asset>'" % number)
pairs.append((parts[0], parts[1]))
return pairs
def read_blacklist():
names = []
with open(BLACKLIST, "r", encoding="utf-8") as handle:
for line in handle:
line = line.strip()
if line and not line.startswith("#"):
names.append(line)
return names
def asset_exists(name):
for catalog in CATALOGS:
base = os.path.join(catalog, name)
for extension in (".imageset", ".symbolset"):
if os.path.isdir(base + extension):
return True
return False
def swift_string(value):
return '"' + value.replace("\\", "\\\\").replace('"', '\\"') + '"'
def render(pairs, blacklist):
by_asset = {}
for android, ios in pairs:
by_asset.setdefault(ios, [])
if android not in by_asset[ios]:
by_asset[ios].append(android)
lines = [
"// GENERATED by Ghostgram/Features/Interface/IconPacks/map/generate_icon_map.py from",
"// icon_pack_map.tsv and icon_pack_blacklist.txt. Do not edit by hand.",
"",
"import Foundation",
"",
"enum GGIconPackMapTable {",
" /// iOS asset → the Android drawable names an `.icons` pack may use for it.",
" static let androidNamesByAsset: [String: [String]] = [",
]
for ios in sorted(by_asset):
names = ", ".join(swift_string(name) for name in by_asset[ios])
lines.append(" %s: [%s]," % (swift_string(ios), names))
lines.append(" ]")
lines.append("")
lines.append(" /// Android names a pack may never replace (structural pieces, splash art, animated vectors).")
lines.append(" static let blacklist: Set<String> = [")
for name in sorted(set(blacklist)):
lines.append(" %s," % swift_string(name))
lines.append(" ]")
lines.append("}")
lines.append("")
return "\n".join(lines)
def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--check", action="store_true", help="only check, do not write")
parser.add_argument("--import-markdown", default=None, help="rewrite the TSV from XTGIconPackMap.md")
arguments = parser.parse_args(argv)
if arguments.import_markdown:
import_markdown(arguments.import_markdown)
pairs = read_pairs()
blacklist = read_blacklist()
missing = sorted(set(ios for _, ios in pairs if not asset_exists(ios)))
for name in missing:
print("error: asset not found in the catalogs: %s" % name)
content = render([(a, i) for a, i in pairs if i not in missing], blacklist)
output = os.path.abspath(OUTPUT)
existing = None
if os.path.exists(output):
with open(output, "r", encoding="utf-8") as handle:
existing = handle.read()
if arguments.check:
if existing != content:
print("error: %s is stale, run generate_icon_map.py" % output)
return 1
return 1 if missing else 0
if existing != content:
with open(output, "w", encoding="utf-8") as handle:
handle.write(content)
print("generate_icon_map.py: updated %s" % os.path.relpath(output, REPO))
else:
print("generate_icon_map.py: up to date")
print("%d pairs, %d assets, %d blacklisted, %d missing assets" % (len(pairs), len(set(i for _, i in pairs)), len(set(blacklist)), len(missing)))
return 1 if missing else 0
if __name__ == "__main__":
sys.exit(main())