#!/usr/bin/env python3 """Generate the data tables of crates/ghostty-macos/src/app/menu_spec/generated.rs from Sources/App/macOS/MainMenu.xib of the Swift app (no ibtool needed). Usage: scripts/gen-macos-menu-spec.py [path/to/MainMenu.xib] > generated.rs""" import sys, pathlib import xml.etree.ElementTree as ET root = pathlib.Path(__file__).resolve().parent.parent default = root.parent / "ghostty/macos/Sources/App/macOS/MainMenu.xib" xib = pathlib.Path(sys.argv[1]) if len(sys.argv) > 1 else default r = ET.parse(xib).getroot() ids = {} # object id -> customObject class for c in r.find("objects").findall("customObject"): ids[c.get("id")] = c.get("customClass") outlets = {} # menuItem id -> property for c in r.find("objects").findall("customObject"): for o in c.iter("outlet"): if o.get("property", "").startswith("menu"): outlets[o.get("destination")] = o.get("property") used = [] def camel(p): return p[0].upper() + p[1:] def rs(s): return '"' + s.replace("\\", "\\\\").replace('"', '\\"') + '"' def mods(item): m = item.find("modifierMask") if m is None: return "None" # IB default: command parts = [n for n in ("shift", "control", "option", "command") if m.get(n) == "YES"] if not parts: return "Some(Mods::NONE)" return "Some(Mods(" + " | ".join("Mods::" + p.upper() + ".0" for p in parts) + "))" def target(t): if t == "-1": return "Target::FirstResponder" if t == "bbz-4X-AYv": return "Target::AppDelegate" raise SystemExit("unknown target " + str(t)) def menu(m, ind): pad = " " * ind sysm = {"apple": "Some(SystemMenu::Apple)", "window": "Some(SystemMenu::Window)", "help": "Some(SystemMenu::Help)", "main": "Some(SystemMenu::Main)"}.get(m.get("systemMenu"), "None") mo = outlets.get(m.get("id")) if mo: used.append(mo) mo = ("Some(Outlet::" + camel(mo) + ")") if mo else "None" out = [f"MenuSpec {{ title: {rs(m.get('title'))}, system_menu: {sysm}, outlet: {mo}, items: &["] items = m.find("items") for i in (items.findall("menuItem") if items is not None else []): if i.get("isSeparatorItem") == "YES": out.append(f"{pad} ItemSpec::Separator,") continue act = i.find("connections/action") action = "None" if act is not None: action = f"Some(Action {{ selector: {rs(act.get('selector'))}, target: {target(act.get('target'))} }})" outlet = outlets.get(i.get("id")) if outlet: used.append(outlet) sub = i.find("menu") out.append(f"{pad} ItemSpec::Item(Item {{") out.append(f"{pad} title: {rs(i.get('title'))},") out.append(f"{pad} key_equivalent: {rs(i.get('keyEquivalent', ''))},") out.append(f"{pad} modifiers: {mods(i)},") out.append(f"{pad} action: {action},") out.append(f"{pad} outlet: {('Some(Outlet::' + camel(outlet) + ')') if outlet else 'None'},") if sub is not None: out.append(f"{pad} submenu: Some(&" + menu(sub, ind + 2) + "),") else: out.append(f"{pad} submenu: None,") out.append(f"{pad} }}),") out.append(f"{pad}] }}") return "\n".join(out) top = r.find("objects").find("menu") body = menu(top, 1) print("//! GENERATED by scripts/gen-macos-menu-spec.py from MainMenu.xib; do not edit.") print("//!") print("//! Every menu item of the xib: title, key equivalent, modifier mask, action") print("//! selector + target (`-1` first responder / the AppDelegate) and the AppDelegate") print("//! outlet it is connected to.") print() print("use super::{Action, Item, ItemSpec, MenuSpec, Mods, Outlet, SystemMenu, Target};") print() print("/// The main menu (`systemMenu=\"main\"`).") print("pub const MAIN_MENU: MenuSpec = " + body + ";") print() print("/// All outlets connected in the xib (the Swift names).") print("pub const CONNECTED_OUTLETS: &[Outlet] = &[") for o in sorted(set(used)): print(f" Outlet::{camel(o)},") print("];")