Edit a SkyUI MCM's on-screen text — page titles, tabs, headers, option labels, info text — on Linux, with a GUI. One interface, every MCM style:
| Style | Where the text lives | This editor |
|---|---|---|
| Legacy, script-driven | compiled .pex string table |
patches the pex directly |
| MCM Helper | config.json |
edits the JSON |
Translation-driven ($tokens) |
Interface/Translations/*_english.txt |
resolves tokens and edits the translation file |
Most Skyrim MCMs mix these — e.g. a config.json full of $RespawnSoulslike
tokens whose real words are in a translation .txt. The editor links them
automatically: you see and edit the actual English text, and each change is
written back to whichever file truly holds it. No Papyrus compiler, Creation
Kit, or Windows box.
Implements the mcm-pex-editor stub
from the Linux-Native-Tools project.
A .pex keeps its whole set of strings in a single plaintext string
table, and every reference to a string elsewhere in the file is a u16
index into that table — never a byte offset. So changing a label's text is
just swapping one table entry; nothing else in the file has to move. That is
the single fact this tool rests on, and it's what makes editing safe without
recompiling.
Renaming a page, fixing casing, rewording an info string, correcting a typo — all of these are string-table edits. (Adding new options or changing option logic still needs the Papyrus compiler; that's out of scope here.)
- Fully parses the
.pexformat (header, string table, debug info, user flags, and the entire object/state/function/instruction tree) — not to rewrite the code, but to (a) prove the parse is correct and (b) learn, from the bytecode, how each string is used. - Classifies each string by the SkyUI call it flows into:
SetTitleText→ page title,AddToggleOption/AddSliderOption/… → option label,SetInfoText→ info text,AddHeaderOption→ header. Strings used only as variable/function names are marked identifiers and hidden by default (renaming them would desync bindings). - Edits the string table in a GUI, with a live diff of pending changes.
- Writes back a byte-exact
.pex, keeping a.bak, and refuses to write anything it can't re-parse.
Page names aren't handed to a display call — a legacy MCM's OnPageReset
dispatches on them with string equality (If page == "General"). The tool
finds page names by spotting string literals used in a cmp_eq (opcode 15),
filtering out asset paths and blanks, and tags them Page name.
~80% of Skyrim MCMs use $translation_key tokens instead of literal text (both
in pex and JSON). The editor locates the mod's Interface/Translations/ *_english.txt, resolves each token to its English text, and routes edits there
— the pex/JSON keeps the token, the translation file gets the new words. A
translation .txt can also be opened and edited on its own.
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt # PySide6GUI — opens a .pex, a config.json, or a translation .txt:
./mcm-editor path/to/SomeMod_MCM.pex # or just ./mcm-editor and File > Open
./mcm-editor path/to/MCM/Config/SomeMod/config.jsonInspect / script from the terminal (no Qt needed):
tools/mcmdump.py SomeMod_MCM.pex # list displayed text by role
tools/mcmdump.py config.json --all --json # every field, machine-readable
tools/mcmdump.py SomeMod_MCM.pex --set 356="Enable Sheet Lightning" # patch + .bakThe KEY for --set is the key column shown in a listing (a pex string index,
or a JSON path like /pages/0/content/3/text).
You can create a menu from scratch from a compact spec — small enough for an
assistant to write, forgiving enough that you only name options by their label.
The builder fills in the exact MCM Helper machinery so the result works:
correct valueOptions.sourceType per control, unique id:section binding keys,
slider formatString, enum option lists, and groupControl/groupCondition
wiring for options that reveal a dependent block.
tools/mcmbuild.py --example > spec.json # a starting template
tools/mcmbuild.py spec.json -o "MyMod/MCM/Config/MyMod/config.json"
cat spec.json | tools/mcmbuild.py - --dry-run --json # validate, machine outputSpec (everything but modName and one option is optional):
{
"modName": "MyMod", "displayName": "My Mod", "tokens": false,
"pages": [{"name": "General", "content": [
{"header": "Core"},
{"toggle": "Enable mod", "help": "Turn it on.", "default": true},
{"slider": "Damage", "min": 0.5, "max": 3, "step": 0.1, "default": 1},
{"enum": "Difficulty", "options": ["Easy","Normal","Hard"], "default": 1},
{"keymap": "Hotkey"}, {"input": "Name"}, {"color": "Tint"},
{"text": "Version", "value": "1.0"}, {"empty": true},
{"group": "Advanced", "content": [
{"toggle": "Verbose logging"},
{"slider": "Cache size", "min": 0, "max": 500, "step": 10}
]}
]}]
}tokens: true externalises every label into a translation file with generated
$keys (SkyUI's localisation path); the default inlines literal text.
New pex menus aren't buildable this way — that needs the Papyrus compiler. MCM Helper JSON is the declarative, code-free way to ship a config menu.
mcmbuild validates as it builds; you can also check any menu:
tools/mcmdump.py config.json --validate # or --validate --jsonIt reports, errors first, the things that make a menu look right but misbehave:
a control with no id (its value won't persist), two controls sharing an
id, a slider with min >= max, an empty enum, a groupCondition
pointing at a missing groupControl (a block that never shows), and
unresolved $tokens. Severity is calibrated against real published menus —
patterns that working mods ship are warnings, not errors. In the GUI:
View → Validate menu (Ctrl+Shift+V).
The preview isn't just a picture: toggles flip, sliders drag, enums cycle,
so you can confirm the layout and controls behave before loading the game.
File → New from spec… (Ctrl+N) builds a menu and drops you straight into the
Arrange tab to refine it.
The Arrange tab shows the menu as a page → option tree in document order.
Reorder options within a page, drag them to another page, reorder pages, and
add / remove / duplicate — by drag-and-drop or with the toolbar (Alt+↑/Alt+↓
to move the selection). Edits apply live with full undo/redo (Ctrl+Z/Ctrl+Y);
renaming a $token label in the tree still writes to the translation file.
This works for MCM Helper (JSON) menus, where the layout is data. It does
not work for legacy pex: there the option order and page layout live in the
script's compiled OnPageReset code, so rearranging would mean rewriting
Papyrus bytecode (recompile territory, out of scope). The Arrange tab says so
when you open a pex; use the Fields tab to edit its text.
View → Preview in browser (Ctrl+P) renders the menu the way SkyUI draws it —
pages down the left, options in two columns, help text along the bottom; hover a
row for its help, click a page to switch tabs. The preview reflects your
unsaved edits, so you can proof a rename or reword first. It's faithful for
MCM Helper menus (full page/item structure) and approximate for legacy pex
(pages are detected but options aren't segmented by page). mcmedit/preview.py
renders a self-contained HTML fragment reusable outside the GUI.
A .pex is never hot-swapped: a running session keeps whatever script it
loaded when the save was made. To see edits:
- Cold-load the game (not a mid-session reload of a save made with the old pex).
- SkyUI caches an MCM's title and page list per save (set once in
OnConfigInit). After a cold load, run in the console:to re-register the title and tabs.setstage SKI_ConfigManagerInstance 1 - Row labels and info text regenerate every time a page is opened, so those update on a normal reload.
- ASCII only — Papyrus strings must be ASCII; non-ASCII edits are rejected
(non-ASCII shows up as
•-style garbage in-game). - Byte-exact round-trip — the parser is validated by
parse → emit == identicalover the entire local.pexcorpus (seetests/roundtrip.py); 51,564 / 51,564 files pass. Every write is re-parsed before it hits disk. - Shared strings are flagged — if a table entry is referenced by several call sites (or is also used as an identifier), the GUI/CLI marks it risky, so you don't rename one thing and change five.
mcmedit/pex.py byte-exact .pex reader/writer + string-reference map (core)
mcmedit/mcm.py SkyUI semantic layer: classify pex strings by role
mcmedit/translation.py SkyUI translation .txt reader/writer + mod-tree locator
mcmedit/document.py one interface over pex / JSON / translation, w/ auto-linking
mcmedit/builder.py spec -> valid MCM Helper menu (authoring)
mcmedit/validate.py "will the controls work?" checks
mcmedit/preview.py SkyUI-styled interactive HTML preview
mcmedit/gui.py PySide6 editor
tools/mcmdump.py headless inspect / patch / --validate CLI (all formats)
tools/mcmbuild.py build a new menu from a spec (AI-friendly)
examples/spec.example.json starting-point spec
tests/roundtrip.py ground-truth .pex round-trip over the whole local corpus
tests/test_documents.py edit-routing across pex / JSON+translation / .txt
tests/test_arrange.py structural (reorder/move/add/remove) editing for JSON
tests/test_build.py spec -> build -> validate
The format was modelled on Orvid/Champollion's reader (big-endian = Skyrim, little-endian = Fallout 4+) and then verified against ground truth — the real files — rather than trusted from docs, per project doctrine.