Wizards — the PyMOL wizard surface
Map of PyMOL’s wizard machinery. Scope:packages/engine/modules/pymol/wizard/ (every file),
packages/engine/modules/pymol/wizarding.py,
packages/engine/layer1/Wizard.{h,cpp}, the wizard prompt renderer in packages/engine/layer1/Ortho.cpp, the pop-up menu
renderer in packages/engine/layer4/PopUp.cpp, wizard event dispatch sites in packages/engine/layer1/SceneMouse.cpp,
packages/engine/layer3/Seeker.cpp, packages/engine/layer3/Executive.cpp, packages/engine/layer5/PyMOL.cpp, and the Wizard menubar in
packages/engine/modules/pymol/_gui.py.
Everything below is grounded in file:line read out of packages/engine/, which is unmodified
upstream. Where something could not be found, the text says so.
Where the port stands. The protocol described here is implemented and shipping:
packages/bridge/tenmol_bridge/panels/wizards.py (snapshot/menu/event/exec, mask gating,
callable-submenu resolution), packages/protocol/src/topics/wizard.ts (wire shapes),
apps/web/src/features/wizards/ (WizardPanel, WizardPrompt, WizardPopupMenu,
WizardKeyCapture, ColorCodedText, service.ts, useWizard.ts).
0. TL;DR
A wizard is a plain Python object living on a stack inside the C++ core (CWizard::Wiz, packages/engine/layer1/Wizard.cpp:69). It is not a widget. The C++ core pulls a
declarative panel description and a declarative prompt out of it by calling five
Python methods, and pushes events into it by calling ~10 more.
That is why there is no per-wizard code in the port: the protocol is implemented once
(<WizardPanel/> + <WizardPrompt/> + <WizardPopupMenu/>), and all 26 bundled wizards —
plus the builder wizards in packages/engine/modules/pmg_qt/builder.py and any third-party
plugin wizard — render through it.
The five “render” methods (confirmed against source):
The event methods (all invoked via
WizardCallPython, which first does
PyObject_HasAttrString — packages/engine/layer1/Wizard.cpp:162 — so every method is optional):
Python-side mirror of the mask constants:
packages/engine/modules/pymol/wizard/__init__.py:6-15.
C-side mirror: packages/engine/layer1/Wizard.cpp:49-58. They agree exactly.
1. The base class
packages/engine/modules/pymol/wizard/__init__.py:4 — class Wizard.
__init__(self, _self=cmd)—:22. Setsself.menu = {},self.prompt = None,self.panel = None,self.cmd = _self, thenself._validate_instance(). Becauseget_panel()(:52) defaults toreturn self.panelandget_prompt()(:49) defaults toreturn self.prompt, a wizard can be purely declarative by just assigningself.panel— this is exactly whatopenvr.py:92-97does._validate_instance()—:38. Createspymol.session.wizard_storage[str(self.__class__)]and exposes it asself.session. This is the per-wizard-class persistence dict that survives across wizard launches and is saved in the session file.measurement.py:70(default_mode),:87(default_object_mode),:142(meas_count) use it.__getstate__—:29: stripscmdbefore pickling.__reduce__—:34.migrate_session(version)—:17; onlyfilter.py:28overrides it.get_event_mask()default —:56:pick + select(= 3).get_menu(tag)default —:91: dict lookup inself.menu.- All
do_*defaults returnNone(:58-86),cleanup()is a no-op (:88).
Session persistence
packages/engine/modules/pymol/wizarding.py:176 session_save_wizard double-pickles
cmd.get_wizard_stack() into session['wizard'].
:182 session_restore_wizard unpickles with chempy.io.pkl, reattaches wiz.cmd = _self,
calls wiz.migrate_session(version), then cmd.set_wizard_stack(wizards).
Registered as session tasks in packages/engine/modules/pymol/cmd.py:50-54.
2. The panel wire format (get_panel)
Parsed in packages/engine/layer1/Wizard.cpp:227-252. Each element must be a list of ≥3 items
(:241), and only the first three are read:
packages/engine/layer1/Wizard.cpp:44-47):
Panel height =
internal_gui_control_size * NLine + 4 via OrthoReshapeWizard
(packages/engine/layer1/Wizard.cpp:254-259); 0 lines → panel collapses to height 0 (:258).
OrthoReshapeWizard itself: packages/engine/layer1/Ortho.cpp:2464. Layout position (between the
Executive/object panel and the ButMode panel): packages/engine/layer1/Ortho.cpp:2286-2300.
Interaction detail worth cloning: pressing a button highlights it while held
(I->Pressed, packages/engine/layer1/Wizard.cpp:490-494), drag-off cancels the highlight
(CWizard::drag, :519-548), and the command only fires on release inside the row
(:568-580). React: onPointerDown sets pressed, onPointerUp inside fires.
The code string is PyMOL command-language, executed by PParse server-side
(packages/engine/layer1/Wizard.cpp:575). It is not JavaScript and must never be evaluated client-side.
In practice it is almost always cmd.get_wizard().<method>(...), cmd.set_wizard(),
replace_wizard demo,reps (demo.py:42), or a raw command string
(sculpting.py:175: cmd.set("sculpting",{"off":"on"}.get(cmd.get("sculpting"),0))).
Text markup
Both panel text and prompt text carry inline color codes\RGB where R,G,B are digits
0-9 mapped to d/9.0 floats, plus \--- = reset to default color.
Implementation: TextStartsWithColorCode packages/engine/layer1/Text.cpp:507, TextSetColorFromCode
packages/engine/layer1/Text.cpp:530. Consumers: Wizard.cpp:670-681 (draw_text),
Ortho.cpp:2165 and :2237 (prompt), PopUp.cpp:194 (menu width calc).
Live users: cleanup.py:133 "\\999Ligand:\\000 ", appearance.py:39-52
('\\900red'…), command.py:146, renaming.py:12, pseudoatom.py:13,
annotation.py:86-93. message.py:7 even has a regex _nuke_color_re to strip them
for console echo.
Ported as the shared \RGB parser in apps/web/src/features/wizards/colorCodes.ts, rendered by
apps/web/src/features/wizards/ColorCodedText.tsx.
3. The prompt wire format (get_prompt)
WizardRefresh calls get_prompt() (packages/engine/layer1/Wizard.cpp:205), converts the returned list
to a NUL-separated char VLA (PConvPyListToStringVLA, :207) and hands it to
OrthoSetWizardPrompt (packages/engine/layer1/Ortho.cpp:314).
Rendered by OrthoDrawWizardPrompt — packages/engine/layer1/Ortho.cpp:2124. Behaviour driven by the
global setting wizard_prompt_mode (packages/engine/layer1/SettingInfo.h:461, default 1):
0→ prompt suppressed entirely (Ortho.cpp:2142).1→ text plus opaque backdrop rectangle inWizardBackColor(0.2,0.2,0.2 —Ortho.cpp:2692-2694);Ortho.cpp:2193-2218.2→ text only, no backdrop, still offset bycWizardTopMargin/cWizardLeftMargin(15/15,Ortho.cpp:200-201).3→ text only, flush to top-left (rect.top -= 1; rect.left = 1,Ortho.cpp:2186-2190).
WizardTextColor = (0.2, 1.0, 0.2) — Ortho.cpp:2695-2697; forced to
black when internal_gui_mode != Default (Ortho.cpp:2145).
The prompt is anchored to the top-left of the viewport and shifted down by the sequence
viewer height when the seq viewer is on top (Ortho.cpp:2180-2184).
It is drawn twice — once in Ortho.cpp:2022, once in Scene.cpp:3463 with the comment
“ugly hack necessitated because wizard…”. React only needs one overlay.
4. The pop-up menu wire format (get_menu)
CWizard::click on a type-3 row calls get_menu(code) with the row’s code as the tag
(packages/engine/layer1/Wizard.cpp:501), and if non-None opens PopUpNew(G, x, my, x, y, false, menuList, nullptr)
(:507).
Parsed in packages/engine/layer4/PopUp.cpp:231-248:
Third element semantics:
str→ a PyMOL command; on releasePLog+PParse(PopUp.cpp:471-473).list→ nested submenu (I->Sub[a],:247); opens on hover/click (:459).- callable → lazy submenu.
SubGetItem(PopUp.cpp:88-105) detects a non-list, calls it with no arguments, memoises the result in place. The bridge replicates this inpanels/wizards.py::_encode_menu: a menu payload can only be serialized after resolving callables.
mutagenesis.py:130-133 (ARG…/LYS…/HIS… grouped rotamer submenus), openvr.py:15-29
(“Store…” → F1..F12), measurement.py:130-137 (neighbor_submenu, whose entries 133/134
are themselves lists built eagerly from get_names). I did not find a wizard in
packages/engine/modules/pymol/wizard/ that uses the callable form — that path is exercised by
packages/engine/modules/pymol/menu.py. Honest caveat: the bridge still must handle it because
get_menu is user-extensible.
Note command.py:87 overrides get_menu itself (does not just read self.menu) and
regenerates values from cmd.auto_arg shortcuts on every open (:100-103) — proof that
get_menu must be called on demand, per open, never cached from a snapshot.
5. Lifecycle & refresh
cmd.wizard(name, *arg, **kwd)—packages/engine/modules/pymol/wizarding.py:62. Legacy alias:name == 'distance'is rewritten to'measurement'(:88-89).name=None→set_wizard()(dismiss all,:83-85)._wizard()—wizarding.py:30:__import__('pymol.wizard.'+name)(:35), class name isname.capitalize()(:41), instantiated with_self=cmdinjected into kwargs (:44-46). ATypeErrorbecomespymol.CmdException(:47-49); aWizardError(wizarding.py:27) is swallowed and replaced by aMessagewizard showing the error (:50-52). Thencmd.set_wizard(wiz, replace)andcmd.do("_ refresh_wizard")(:54-55).cmd.replace_wizard(...)—wizarding.py:94, same butreplace=1. Used bydemo.py:42-53.cmd.set_wizard(wizard=None, replace=0)—wizarding.py:110→_cmd.set_wizard→CmdSetWizardpackages/engine/layer4/Cmd.cpp:2801→WizardSetpackages/engine/layer1/Wizard.cpp:264. Push/pop semantics: ifwizisNone/Py_Noneorreplaceis set, pop the top and call itscleanup()(:268-279); then if a wizard was supplied, push it (:280-282); finallyWizardRefresh(:283). It is a stack — nested wizards are supported.cmd.get_wizard()—wizarding.py:156→WizardGetWizard.cpp:784(top of stack).cmd.get_wizard_stack()/cmd.set_wizard_stack()—wizarding.py:166/:120→Wizard.cpp:791/:805.cmd.refresh_wizard()—wizarding.py:130→WizardRefreshWizard.cpp:195. Synchronous re-pull of prompt + event mask + panel. Nearly every wizard method ends with this call.cmd.dirty_wizard()—wizarding.py:146→WizardDirtyWizard.cpp:94: sets a flag so the nextWizardUpdate(:101) does a refresh. Used byannotation.py:11,14,17.WizardUpdate(Wizard.cpp:101) runs once per render pass fromExecutiveDrawNow(packages/engine/layer3/Executive.cpp:11555) and additionally firesdo_dirty/do_frame/do_state/do_position/do_viewby comparing againstLastUpdated*cached values (:106-125).cmd.reinitialize()clears the stack:WizardSet(G, nullptr, false)atpackages/engine/layer3/Executive.cpp:16637.- Python API export list:
packages/engine/modules/pymol/api.py:283-290. - Only
refresh_wizardandreplace_wizardare registered as command-language keywords (packages/engine/modules/pymol/keywords.py:223,228) —wizarditself is auto-registered elsewhere;cmd.wizardis definitely reachable as a command (demo.py:429self.cmd.do("_ wizard")).
WizardRefresh is C pulling from
Python, so nothing on the Python side is notified. The bridge therefore wraps
cmd.refresh_wizard / set_wizard / set_wizard_stack / dirty_wizard and bumps a version
counter (packages/bridge/tenmol_bridge/panels/wizards.py, install() / bump()); the client
polls the cheap wizards.probe and only pulls wizards.snapshot when the version moved. That
is safe because every wizard calls self.cmd.refresh_wizard() after mutating state, and it
is necessary because get_panel/get_prompt have side effects (see §9.3 rules 4 and 5).
6. Where pick/select events come from
This is the single biggest architectural collision with a client-side WebGL viewport, because all pick/select dispatch currently happens in C++ during mouse handling.- Atom pick,
cButModePickAtom:packages/engine/layer1/SceneMouse.cpp:468→WizardDoPick(G, 0, state). - Atom pick, editor path:
packages/engine/layer1/SceneMouse.cpp:427→WizardDoPick(G, 0, state). - Bond pick / torsion:
packages/engine/layer1/SceneMouse.cpp:543→WizardDoPick(G, 1, state);:545is the non-bond fallback. - Click-select (
cButModeSeleSetetc.):packages/engine/layer1/SceneMouse.cpp:135and:357→WizardDoSelect(G, selName, state). - Rectangle/box select:
packages/engine/layer3/Executive.cpp:7563→WizardDoSelect(G, selName). - Sequence-viewer select:
packages/engine/layer3/Seeker.cpp:150and:231→WizardDoSelect(G, selName). - Keyboard:
packages/engine/layer5/PyMOL.cpp:2356→WizardDoKey;:2369→WizardDoSpecial. - Scene changed:
packages/engine/layer1/Scene.cpp:4812→WizardDoScene. - View/position:
packages/engine/layer1/Scene.cpp:4675-4676→WizardDoPosition/WizardDoView.
WizardDoPick and WizardDoSelect both first call do_pick_state(state+1)
(Wizard.cpp:189, :324) — a 1-based state index of the picked object, which only
measurement.py:292 implements (to remember which state each of the 4 picks came from,
used at measurement.py:317,332,355,381).
Both also PLog the exact Python replay line:
"cmd.get_wizard().do_pick(1)" / "cmd.get_wizard().do_pick(0)" (Wizard.cpp:319-321) and
"cmd.get_wizard().do_select('''%s''')" (Wizard.cpp:185).
That log line is the replay contract the web path reproduces: the browser, after a
client-side ray pick, tells the bridge which atom was hit; the bridge does the equivalent of
the C editor work (cmd.edit(...) / cmd.select("pk1", ...)) then calls
cmd.get_wizard().do_pick_state(state) followed by cmd.get_wizard().do_pick(bondFlag)
(packages/bridge/tenmol_bridge/panels/wizards.py::event).
The C++ layer honours the event mask before calling Python
(CWizard::isEventType, Wizard.cpp:140); the bridge re-implements that gate, because
without it do_pick would reach wizards that never asked for picks (e.g. annotation.py:7
masks scene|state|frame only, pseudoatom.py:17 masks key only, command.py:149-152
returns 0 unless in text-input mode).
7. Per-wizard catalogue
Launch names come from_wizard()’s name.capitalize() rule (wizarding.py:41), so
wizard pair_fit → class Pair_fit, wizard nucmutagenesis → Nucmutagenesis.
Menubar entries (packages/engine/modules/pymol/_gui.py:834-864): Appearance, Measurement,
Mutagenesis▸(Protein | Nucleic Acids), Pair Fitting, ─, Density, Filter, Sculpting, ─,
Label, Charge, ─, Demo▸(11 demos + End Demonstration).
Context-menu launches: packages/engine/modules/pymol/menu.py:1163, 1194, 1234, 1268, 1298, 1314, 1401, 1437, 1454, 1845 (all cmd.wizard("renaming", …)) and menu.py:1655
(cmd.wizard("pseudoatom","label",pos=[…])).
7.1 measurement.py — Measurement (475 lines)
Purpose: interactive distance / angle / dihedral / H-bond / neighbor measurements.
Replaces the legacy distance wizard (wizarding.py:88).
Panel (:195-203), 6 rows:
[1,'Measurement',''][3, mode_name[mode], 'mode'][3, object_mode_name[object_mode], 'object_mode'][2,'Delete Last Object','cmd.get_wizard().delete_last()'][2,'Delete All Measurements','cmd.get_wizard().delete_all()'][2,'Done','cmd.set_wizard()']
mode (:77-84), built from modes (:14-23) with labels from mode_name
(:24-33): Distances(pairs), Distances to Rings(rings), Angles(angle),
Dihedrals(dihed), Polar Neighbors(polar), Heavy Neighbors(heavy),
Neighbors(neigh), Polar Contacts(hbond).
The three neighbor modes get a submenu built by neighbor_submenu (:130-137):
in all objects, in object▸(live object list, neighbor_objects :108-117, capped at 25),
in selection▸(live selection list, neighbor_selections :119-128, capped at 25),
in other objects, in same object. All call set_neighbor_target(mode, target) (:163).
Menu object_mode (:89-94): “Merge With Previous”(merge), “Replace Previous”(overwr),
“Create New Object”(append) (:47-51).
State machine: self.status 0→1→2→3 (:65), meaning “how many atoms picked”.
pairs/rings/hbond need 2, angle needs 3, dihed needs 4 (:319-426).
neigh|polar|heavy are one-shot (:427-469). Prompt text is status-driven (:242-264)
and is worded using the current mouse_selection_mode (“first atom”/“first residue”/…,
get_selection_name :223-240 maps 0..6 → atom/residue/chain/segment/object/molecule/C-alpha
plus the selection-expander prefix br./bc./bs./bo./bm./bca.).
Picking: do_pick :307 — first re-expands pk1 by the mouse-selection-mode code
(:310), rejects bonds (:311-313), stores each pick as _mw0.._mw3, mirrors into _indicate_mw and enables it for visual feedback (:322-323). do_select :295maps selections into picks bycmd.select(“pk1”, name+” and not _mw*“). do_pick_state :292records per-pick state intoself.pick_state[status]`.
Event mask: pick|select|dirty (:101-106) — dirty only to notice
mouse_selection_mode changes and re-render the prompt (do_dirty :472-475).
cmd calls: unpick, get_setting_float(neighbor_cutoff, heavy_neighbor_cutoff,
polar_neighbor_cutoff, h_bond_cutoff_center — :60-63), set("mouse_selection_mode",…)
(:97,184,212), deselect, get_names("public_objects"/"public_selections"), get_type,
select, delete, enable, dist(...,state1=,state2=) (:335), distance(mode=4) for
rings (:358), angle (:384), dihedral (:404), dist(mode=2,cutoff=hbond_cutoff)
(:422), get_selection_state('?pk1') (:332), refresh_wizard.
Object naming: measure%02d via get_name (:144-159), counter persisted in
self.session['meas_count'] (:142).
7.2 mutagenesis.py — Mutagenesis (protein) (730 lines)
Purpose: swap a residue for a rotamer library entry, with bump (strain) scoring.
Refuses to run inside a movie: raises WizardError if get_movie_length() > 0 (:47-48),
which wizarding.py:50-52 turns into a Message wizard.
Panel (:276-295), 10 rows:
[1,'Mutagenesis',''], [3,'No Mutation'|'Mutate to X','mode'],
[3,'N-Cap: …','n_cap'], [3,'C-Cap: …','c_cap'], [3, hyd_name[hyd],'hyd'],
[3, rep_name[rep],'rep'], [3, dep_name[dep],'dep'],
[2,'Apply','cmd.get_wizard().apply()'], [2,'Clear','cmd.get_wizard().clear()'],
[2,'Done','cmd.set_wizard()'].
Side effect inside get_panel: forces mouse_selection_mode=1 (residue) every refresh (:278-279).
Menus:
mode(:93-135) — “No change” + every residue insc_bb_ind.pklplusGLY, ALA, HID, HIE, HIP, ARGN, LYSN, ASPH, GLUH(:74-84); protonation variants are regrouped into nested submenusARG…,LYS…,HIS…,GLU…,ASP…by the loop at:113-133.NT_/CT_variants are generated (:81-84) but commented out of the menu (:102-107) — they’re still reachable viaset_mode.n_cap(:180-184): Open / NH3+ / Acetyl (:165-170).c_cap(:186-190): Open / COO- / Amine / N-methyl (:172-178).hyd(:192-196): Hydrogens: Current / Add & Retain / Remove (:157-163).rep(:198-202): Show Lines / Sticks / Spheres / Dots (:138-150).dep(:204-209): Backbone Depen. Rotamers / Backbone Indep. Rotamers (:152-155).
self.status 0 = nothing picked, 1 = mutagenizing (:61).
If pk1 already exists at launch it auto-enters status 1 (:211-218).
Picking: do_pick :715 — rejects bonds, select(_mute_sel,"(byres pk1)"), enable,
do_library(). do_select :695 — ignores clicks that land on the preview object
(:698-701), otherwise same path. do_state(state) :678 — syncs the bump object,
prints Rotamer n/N, strain=… (:684-686), and rebuilds polar-contact distances
_tmp_hbonds (:689-693). Event mask pick|select|state (:297-298).
do_library() (:410-676) is the heavy lifter — the single most complex piece of
cmd orchestration in the whole wizard tree:
feedback push/disable, iterate to build the residue label, select("_seeker_hilight"),
set('auto_zoom',0), frame(0), center(animate=-1), fragment(), remove hydrogens,
alter identity transfer via self.space, pair_fit of CA/CB/C/N (:479-493),
iterate_state/alter_state carbonyl + OXT + amide-H fixing (:496-531),
editor.attach_amino_acid for caps (:539,541,557), h_fix, phi_psi lookup into
the backbone-dependent library with 10°/20°/60° binning (:578-589), per-rotamer
create + set_dihedral + set_title("%1.1f%%") (:599-616), then bump checking with
sculpt_activate + sculpt_iterate per state (:642-654) and
set("sculpt_vdw_vis_mode",1) (:645).
Data files: $PYMOL_DATA/chempy/sidechains/sc_bb_ind.pkl (:58) and sc_bb_dep.pkl
(:223-224).
apply() (:326-399): two branches. Mutation branch (:340-374) does
create/set_title/color/alter/select neighbor/remove/create merged/bond/set_geometry/h_fix,
then frame(1). Conformation-only branch (:375-398) uses push_undo + update.
Objects/selections it owns: _mutate_sel, _bump_check, mutation, _tmp_mut,
_tmp_mut_sele, _tmp_obj2, _tmp_sele1, _tmp_sele2, _tmp_hbonds (:10-19) —
plus _seeker_hilight (:323).
7.3 nucmutagenesis.py — Mutagenesis (nucleic acids) (426 lines)
Same shape, simpler. Also refuses movies (:82-84).
Panel (:137-158): [1,'Mutagenesis',''], [3,'Mutate to X','mode'],
[3,'Auto Center: ON|OFF','auto_center'], [3, _rep_name[rep],'rep'],
[2,'Apply',…], [2,'Clear',…], [2,'Done','cmd.set_wizard()']. Forces
mouse_selection_mode=1 on every refresh (:147-148).
Menus: mode (:99-104) = Adenine/Cytosine/Guanine/Thymine/Uracil (:72-78 → ATP/CTP/GTP/TTP/UTP);
rep (:106-112) = lines/sticks/spheres/dots (:21-26);
auto_center (:114-117) = ON / OFF.
State machine: Status.NO_SELECTION=0 / Status.MUTAGENIZING=1 (:15-17),
prompts at :127-134.
Picking: do_pick (:160) rejects bonds, then do_select('byres pk1') (:170).
do_select (:173) selects _mutate_sel, unpick, _do_mutation(), delete,
refresh_wizard, deselect. No get_event_mask override → default pick|select.
Chemistry: purine/pyrimidine chi-dihedral atom sets (:33-36), _base_types table
(:38-54), _transfer_dihedral uses get_dihedral + set_dihedral (:221-235),
_update_reps uses show_as(rep+' lines', '?_tmp_mut') (:216-218).
apply() (:406-426) builds an inverse selection of _sugar_phos_atoms (:64-70),
removes the old base, fuses the fragment, then alters resn with a D prefix when
O2' is absent (DNA detection, :420-421).
7.4 pair_fit.py — Pair Fitting (190 lines)
Purpose: superpose two objects by user-picked atom pairs.
Panel (:28-36): [1,'Pair Fitting',''],
[2,'Fit %d Pairs' % n_pair,'cmd.get_wizard().fit()'],
[2,'Delete Last Pair','…remove_last()'], [2,'Redraw','…update_dashes()'],
[2,'Clear','…clear()'], [2,'Done','cmd.set_wizard()']. No menus.
State machine: status 0 = expecting mobile atom, 1 = expecting target atom (:21).
Prompts 'Pick the mobile atom...' / 'Pick the target atom...' (:53-61), plus an
appended self.message line carrying errors or the RMS result.
Picking: do_pick :152. Validates that each mobile atom is in the same object as
previous mobiles (check_same_object :126) and that the target is in a different
object (check_different_object :134). Selections are named _pf_s_%02db (mobile) and
_pf_s_%02da (target) — the trailing letter is how get_sele_list(mode=) (:67-75)
separates them. do_select :142 maps a selection to a pick via cmd.edit(...).
cmd calls: pair_fit(*args) (:93) after push_undo (:92); dashes drawn as
dist(name,a,b,width=7,length=0.05,gap=0.05) + hide('label') (:121-122);
set/get_setting_int("mouse_selection_mode") (:24-25, restored :40).
Bug worth not cloning: this module calls the module-level cmd rather than
self.cmd throughout (:22-25, :40-51, :79-122, :143-190), so it ignores _self.
7.5 appearance.py — Appearance (233 lines)
Purpose: click-to-restyle. A 3-dropdown “verb / object / scope” machine.
Panel (:163-183) is dynamic: always [1,'Appearance Wizard',''] +
[3, mode_dict[mode][0], 'mode']; then either [3, color_dict[color][0],'color']
(modes 0,1) or [3, what_dict[what][0],'what'] (modes 2,3,4) or
[1,'Atoms',''] (mode 5); then [3, scope_dict[scope][0],'scope'] and
[2,'Done','cmd.set_wizard()']. Good canonical test case for a dynamic React panel.
Menus:
mode(:74-83): Color, Color (elem c), ─, Toggle, Show, Hide. (mode_dict:15-22also defines5: Selectbut its menu row is commented out at:82.)what(:85-101): Lines, Nonbonded, Sticks, Ribbon, Cartoon, ─, Labels, ─, Dots, Spheres, NB Spheres, ─, Mesh, Surface (what_dict:24-36).color(:103-119): 14 colors, each label carrying its own\RGBswatch code (color_dict:38-53): red, green, blue, yellow, magenta, cyan, salmon, lime, pink, slate, violet, orange, marine, hotpink.scope(:121-130): By Atom, By Residue, By Chain, By Segment, By Object, ─, By Molecule (scope_dict:55-62, mapping to selection operators'',byres,bychain,bysegment,byobject,bymol).
do_pick :186 and do_select :205 both synthesize a command string
mode + '("what","(scope pk1)")' and run it through self.cmd.do(cmmd, log=0) (:192).
The verbs are _ cmd.color, _ util.color_carbon, _ cmd.toggle, _ cmd.show,
_ cmd.hide, _ cmd.select (:15-22).
Persistence: module-level saved_mode/scope/what/color (:7-10), written back in
cleanup() (:224-233). undo() exists but only prints “no undo!” (:156-157) and its
panel row is commented out (:180).
7.6 cleanup.py — Cleanup / szybki (168 lines)
Purpose: run OpenEye szybki on a ligand and pull the minimized coords back.
Hard dependency: auto_configure() (:13-34) searches $OE_DIR / $OEDIR for
bin/szybki[.exe]; if not found __init__ raises pymol.CmdException (:52-54), so the
wizard never opens.
Panel (:126-137): [1,'Cleanup',''], [2,'Run','…run()'], [2,'Undo','…undo()'],
[2,'Redo','…redo()'], [3,"\\999Ligand:\\000 "+ligand,'ligand'],
[2,'Refresh','…update()'], [2,'Done','cmd.set_wizard()'].
A Target: popup row exists but is commented out (:134).
Menus: ligand and target rebuilt by update_menus() (:38-46) from
get_names("public_objects"); target also gets a (none) entry (:40).
Picking: do_pick :164 — if no ligand yet, adopt get_object_list("pkmol")[0].
No get_event_mask override → pick|select. Enters edit_mode() on construction (:65).
run() (:88-116): save('ligand_inp.mol') → busy-wait for the file →
cmd.system("szybki -i … -o …") → busy-wait ≤1 s → load('ligand_out.mol') →
alter(ID=index) → fit(matchmaker=2) → update(matchmaker=2) → delete →
sculpt_deactivate → sculpt_purge. Undo/redo are implemented as object copies
_w_cleanup_undo / _w_cleanup_redo (:10-11, :67-86).
7.7 density.py — Density Map (256 lines)
Purpose: roving isomesh around a picked atom/residue, up to 3 maps.
Panel (:159-176), 14 rows: title, Update Maps, Zoom, Next Res. (PgDown),
Previous Res. (PgUp), [3,"Radius: %3.1f A",'radius'], then three
Map N:/@ X sigma popup pairs (map0/level0, map1/level1, map2/level2),
[3, track label, 'track'], Done. Calls update_map_menus() inside get_panel
(:160) — menus must be re-derived on every refresh.
Menus: radius (:28-37): 4/5/6/8/10/15/20/50 Å.
level0..2 from the level_menu lambda (:43-48): 1.0/1.5/2.0/3.0/5.0/-3.0 sigma.
map0..2 (:79-87): live list of object:map objects + (none).
track (:54-59): Track & Zoom / Track & Center / Track & Set Origin / Track Off.
Keybindings: set_key('pgup', … next_res(d=-1)) and set_key('pgdn', … next_res())
(:68-69), unbound in cleanup() (:185-186).
Picking: do_pick :197 → select("_dw","pk1") + update_maps(); do_select :191
same from a named selection. Both are no-ops when track == 2 (origin mode).
Event mask pick|select|position (:205-206); do_position() (:208-210) re-runs
update_maps(zoom=0) whenever the camera center moves and no _dw selection exists —
this is the actual “roving” behaviour.
update_maps() (:107-143): for each slot, isomesh('w{n}_{map}', map, level, sele, radius, state=1), coloring new meshes blue/white/magenta (:126-131), then
zoom(animate=0.67) / center(animate=0.67) / origin() per track (:134-142).
next_res(d=±1) (:212-256, “Donated by Tom Lee”): walks to the neighbouring residue’s
CA / C1* / C1’ atom, handles sequence gaps by enumerating the chain (:236-248), labels the
new residue " %s %s/%s/" % (resn,chain,resi) (:253).
7.8 filter.py — Filter (402 lines)
Purpose: triage a multi-state (docked-compound) object into Accept/Reject/Defer.
Panel (:228-254), 12 rows: [1,'Filtering Wizard',''],
[3, browse label, 'browse'], [3,'Object: %s','object'],
[2,'Accept (F1)',…], [2,'Reject (F2)',…], [2,'Defer (F3)',…],
[2,'Forward (->)',…], [2,'Back (<-)',…], [3,'Create Filtered Object','create'],
[2,'Save %s.txt' % object,'…save()'], [2,'Refresh','cmd.refresh_wizard()'],
[2,'Done','cmd.set_wizard()']. update_object_menu() is called from inside get_panel
(:236).
Menus: browse (:82-89): Browse All / Accepted / Rejected / Deferred / Remaining.
create (:91-96): Accepted / Rejected / Deferred.
object (:138-141): all object:molecule with count_states>1, plus None.
Keybindings (:101-105): F1=accept, F2=reject, F3=defer, →=forward, ←=backward;
restored in cleanup() (:398-402, note left/right are restored to cmd.backward/
cmd.forward, not to None).
Prompt (:260-277): two lines — "obj: N accepted, N rejected, N deferred, N remaining"
and "<state>/<total> <title>: Accept|Reject|Defer" (or …? when undecided).
Identifier format '%d/%d %s' % (state, tota, get_title(...)) (get_ident :256-258).
Picking: do_pick :115 → do_select('pk1'); do_select :107 resolves the clicked
atom to its owning object and makes it the filter target. do_state :119 just refreshes.
Event mask pick|select|state (:122-123).
Browse filtering (set_browse :143-179) drives the movie: cmd.mset() for all, or
cmd.mset(' '.join(states)) + cmd.rewind() for a subset.
create_object(what) (:339-347) builds <obj>_Accept etc. via repeated cmd.create.
save() (:349-389) writes a TSV report next to the cwd, falling back to ~.
Persistence: module-level static_dict, default_object, default_browse
(:10-18), saved in cleanup() (:391-396). migrate_session (:28-58) remaps
pre-1.7.0.0 title-keyed dicts to the new state/total title identifiers.
7.9 label.py — Labeling (101 lines)
Purpose: click an atom to toggle a formatted label on it.
Panel (:70-77): [1,'Labeling',''], [3,'Mode: '+mode_names[mode],'mode'],
[2,'Messages: On|Off','cmd.get_wizard().toggle_messages()'], [2,'Done','cmd.set_wizard()'].
Menu mode (:36-43) — 9 python-format templates (mode_names :14-24):
{resn}-{resi}, {onelettercode}{resi}, {chain}/{resn}\{resi}, /`{resi}/{name}`, ////`{resi}/{name}`, , , , `.
Prompt (:52-64): suppressed when messages is off; otherwise the full atom identifier
plus B = … XYZ = … … ….
Picking: do_pick :79 → do_select('pk1'); do_select :83 uses
iterate_state(-1, 'first ?sele', 'self.atom = (model,segi,chain,resn,resi,name,alt,b,x,y,z,label)')
(:85-87, field list at :26-28), and toggles: if the atom already has a label it sets
'', otherwise it formats the template (one-letter code via
pymol.exporting._resn_to_aa, :6, :96). Forces mouse_selection_mode=0 at construction
(:34). Event mask pick|select (:45-46).
7.10 charge.py — Charge (296 lines)
Purpose: inspect / move / zero partial_charge on atoms and residues.
Panel (:53-59): [1,'Charge Wizard',''], [3, mode_name[mode],'mode'],
[2,'Clear','cmd.get_wizard().clear()'], [2,'Done','cmd.set_wizard()'].
Menu mode (:41-46), 9 entries (modes :12-22, labels :27-37):
Show(labchg), Add(addchg), Copy(cpychg), Zero(zrochg), Move & Zero (atom)(mzochg),
Move & Zero (resi)(cbachg), Move & Remove (atom)(movchg), Move & Remove (resi)(rbachg),
Get Total Charge(sumchg).
State machine: status 0 = pick source, 1 = pick destination (:30). Each mode has its
own two prompt strings (:79-122), several of which interpolate self.partial_charge.
get_prompt also side-computes and prepends “Total charge on the residue is …” by
iterate("(byres wcharge)") whenever the wcharge selection exists (:124-128) — i.e.
get_prompt has side effects and issues cmd calls; the bridge must not call it
speculatively.
Picking: do_pick :142 — a long per-mode dispatch (:146-294) using
iterate/alter/label/remove/select("wcharge",…)/enable. Residue modes build a
name -> charge dict and transfer only atoms present in both residues (:167-206).
sumchg sums over (pkmol) (:287-294). Enters edit_mode() at construction (:50).
No get_event_mask override → pick|select, though do_select is not implemented.
Persistence: module-level default_mode (:5, saved :61-64).
7.11 sculpting.py — Sculpting (227 lines)
Purpose: pick a center atom, auto-partition the object into free / fixed / excluded
shells, and turn on the real-time sculpting engine.
Panel (:169-180): [1,'Sculpting',''], [3, mode_name[mode],'mode'],
[3,'Radius: %3.1f A','radius'], [3,'Cushion: %3.1f A','cushion'],
[2,'Toggle Sculpting','cmd.set("sculpting",{"off":"on"}.get(cmd.get("sculpting"),0))'],
[2,'Toggle Bumps','cmd.set("sculpt_vdw_vis_mode",{0:1}.get(int(cmd.get("sculpt_vdw_vis_mode")),0))'],
[2,'Relocate','cmd.get_wizard().free_all()'], [2,'Done','cmd.set_wizard()'].
Note the two Toggle rows carry raw inline Python, not a get_wizard() call —
proof the panel code is a general command string.
Menus: mode (:83-87) — only One Residue(ligand_rx) and Residue Shells(by_resi)
are listed (modes :67-72); by_atom / ligand_re are defined in mode_name (:74-81)
but commented out. radius (:89-97): 4/5/6/8/10/15/20 Å.
cushion (:99-107): 2/3/4/6/8/10/12 Å.
Constructor side effects (:39-62): forces edit_mode(1) if not already editing
(remembering how to restore, :41-45), saves and sets sculpt_vdw_vis_mode,
sculpt_deactivate("all"), set("sculpting"), unmask("all").
State machine: NO_SELECTIONS=0 / HAVE_SELECTIONS=1 (:22-23).
Prompt only exists in state 0: 'Please pick the center atom...' (:207-213).
Picking: do_pick :215 — only accepts the first pick; returns 0 afterwards so the
pick falls through to normal editing. Also push_undo per object (:221-222).
update_selections() (:126-161) is the core: builds sclpt_wz_free, sclpt_wz_fix,
sclpt_wz_excl selections from sclpt_wz_cent using the x; (within) operator, then
protect/deprotect, flag('exclude', …), color('grey'), mask/unmask,
zoom(animate=1), util.cbac/util.cbag, disables the helper selections, and
sculpt_activate(obj) after push_undo(obj).
cleanup() (:196-205) restores edit mode, sculpt_vdw_vis_mode, sculpting=0,
then clear() which unmasks/deprotects/deactivates everything and deletes sclpt_wz_*.
Dead code: set_object_mode (:163-167) references self.object_modes, which is never
defined — calling it raises AttributeError. Not reachable from the panel.
7.12 message.py — Message (36 lines)
Purpose: the generic modal notice. Used by the error path in wizarding.py:50-52 and by
pymol/__init__.py:271.
Constructor: Message(*arg, dismiss=1, _self=cmd) (:11); positional args are flattened
into a list of lines (:14-18) and echoed to the console with \RGB codes stripped
(:19-20) using _nuke_color_re (:7).
Panel (:27-36): with dismiss=1 → [1,'Message',''], [2,'Dismiss','cmd.set_wizard()'];
with dismiss=0 → [] (empty panel, prompt-only overlay). The hasattr guard at :28
exists for un-pickling older sessions.
Invoked from the command line as wizard message, <text>, dismiss=0
(see stereodemo.py:18 for a live example).
7.13 demo.py — Demo (456 lines)
Purpose: the built-in feature tour.
Panel (:39-54), 13 rows, every one of them replace_wizard demo,<name>:
Representations(reps), Cartoon Ribbons(cartoon), Roving Detail(roving),
Roving Density(roving_density), Transparency(trans), Ray Tracing(ray),
Sculpting(sculpt), Scripted Animation(anime), Electrostatics(elec), CGOs(cgo),
Molscript/R3D Input(raster3d), End Demonstration(finish).
No menus. Prompt = self.message (:34-37).
Behaviour: __init__(name=None) (:12-32) — on a named launch it first calls
<last>(cleanup=1) to tear down the previous demo (:20-22), then runs the new demo body
on a daemon thread (:26-28), and pulls a hint string out of
DemoInfo.message_dict (:67-76; keys roving, roving_density, elec, sculpt —
these are the mouse-instruction overlays). Last demo name persists in module-level
saved (:4, :32).
DemoInfo (:62) bodies each take cleanup=0|1 and are pure cmd scripts:
reps (:100), raster3d (:174), cgo (:190), anime (:228), roving (:277),
roving_density (:308), cartoon (:352), elec (:371), trans (:391),
ray (:410), finish (:428 → cmd.do("_ wizard")), sculpt (:431).
Deprecated bodies rep_old (:78) and anime_old (:204) are unreferenced.
Assets: $PYMOL_DATA/demo/{pept.pdb, pept.pkl, il2.pdb, 1tii.pdb, 1hpv.r3d, cgo03.py} and
$TUT/1hpv.pdb.
7.14 stereodemo.py — Stereodemo (441 lines)
Purpose: the “kiosk” demo used for stereo hardware, launched as
wizard stereodemo[,name[,mono]].
Panel (:46-72), 16 rows including five [1,…] section headers —
“Structural Biology”, “Drug Discovery”, “Presentation Graphics”, “Bioinformatics”,
“Science Education”, “Configuration” — with buttons:
X-ray Crystallography(roving_density), Electron Tomography(electomo),
Medicinal Chemistry(medchem), Computational Chemistry(electro),
Molecular Animation(animate), Multiprocessor Raytracing(ray),
Structure Alignments(structure), Homology Modeling(homology),
Interactive Modeling(sculpt), Toggle Fullscreen(cmd.full_screen()),
Toggle Stereo 3D(cmd.stereo("off" if cmd.get_setting_int("stereo") else "on")).
This is the best example of a grouped/sectioned panel for the React renderer.
Constructor (:30-38): cmd.full_screen("off"), cmd.stereo("on") unless mono,
then launch(name) (default "cartoon").
launch() (:10-27) tears down the previous demo, cmd.delete("all"), pushes a
wizard message, Please wait while the %s example loads..., dismiss=0 notice (:18), then
runs the demo on a daemon thread (:22-24). Demo bodies at :101-441 load .pse sessions
via get_sess (:93-99).
7.15 benchmark.py — Benchmark (368 lines)
Panel (:349-367), 17 rows: Run All / Run GL / Run CPU / Updates / Smooth Lines /
Jagged Lines / Dots / Sticks / Surface / Spheres / Cartoon / Blits /
Surface Calculation / Mesh Calculation / Ray Tracing / End Demonstration.
All buttons call cmd.get_wizard().delay_launch("<name>") (:340-347), which does
reinitialize() (configure :22-23), viewport(640,480), disables all feedback except
python output, and runs on a daemon thread (:346-348). Results go to stdout via
report() (:15-17). No menus; prompt = self.message (:336-338).
7.16 box.py — Box (445 lines)
Purpose: draw an editable CGO box/plane/quad from four draggable pseudo-atoms.
Panel (:384-395): [1,'Box Wizard',''], [3, mode_name[mode],'mode'],
[2,'Change Name','cmd.get_wizard().edit_name()'],
[2,'Copy Box','cmd.get_wizard().edit_name(copying=1)'],
[2,'Toggle Points','cmd.get_wizard().toggle_points()'],
[2,'Auto-Position (50%)','…auto_position(0.75)'],
[2,'Auto-Position (99%)','…auto_position(0.99)'], [2,'Done','cmd.set_wizard()'].
(An Update row is commented out at :392.)
Menu mode (:56-60): Box / Walls / Plane / Quad (:41-53).
Text-entry sub-state: edit_name() (:85-89) sets self.editing_name = 1, which
(a) flips get_event_mask to include event_mask_key (:398-403) and
(b) changes get_prompt to "Enter box name: " + new_name (:440-445).
do_key (:423-437) implements a hand-rolled line editor: backspace (8/127),
printable (k>32), Enter (10/13) commits and defaults to "box" when blank.
This inline text-input pattern is shared with renaming.py, pseudoatom.py and
command.py — implement it once in React as a <WizardTextInput/>.
Live view coupling: event mask always includes event_mask_scene (:398-403);
do_scene() (:405-418) diffs the four pseudo-atom coordinates and rebuilds the CGO when
they move. auto_position(fract,size) (:91-…) computes box placement from
get_setting_float("field_of_view") and get_view(). do_pick is an explicit no-op (:420-421).
Geometry built with chempy.models.Indexed + pymol.cgo (:7-11), pseudo-atom template at
:14-18.
7.17 command.py — Command (178 lines)
Purpose: a generic wizard that introspects any PyMOL command and renders one popup
row per keyword argument. Docstring: “Generic wizard for any PyMOL command.” (:6-8).
This is the closest existing thing to the generic contract we want in React.
Panel (:119-141) — three shapes:
- no command chosen →
[3,'Select a command...','_commands'],[2,'Done','cmd.set_wizard()']. - text-input active →
[1,'Input',''],[2,'Apply', …apply_input()],[2,'Cancel ', …set_input_arg()]. - normal →
[1,'<Command> Wizard','']+ one[3,'<arg>: <value>', '<arg>']row per parameter +[2,'Run', …run()]+[2,'Done','cmd.set_wizard()'].
set_command (:60-64) as
[title, separator, 'Enter value...']; get_menu is overridden (:87-104) and
- for the magic tag
'_commands'returns the full keyword list frompymol.keywords.get_command_keywords()(:88-95); - otherwise splices live auto-completion values from
self.cmd.auto_arginto the menu (:100-103,set_menu_values:81-85, top 20 keywords).
inspect.signature(self.func) (:44), stops at the first non
POSITIONAL_OR_KEYWORD parameter (:53-54), skips _-prefixed and quiet (:57,
ignored_args :11). Defaults captured at :70-76.
Text input: set_input_arg (:154-160), apply_input (:162-164),
do_key (:166-178) with a numeric-only mode (:173). Event mask is key only while
inputting, else 0 (:149-152).
Reference cycle handling: stores itself on pymol.stored under a generated name
(:30-32) so panel commands can be stored._wizardN.run() (:33, :135-139); removed in
cleanup (:116-117). __getstate__ blanks shortcut (:13-16) because the shortcut
objects aren’t picklable. Runs commands through cmd.async_ when async_ is set (:111-112).
7.18 distance.py — Distance (193 lines) — LEGACY
Superseded by measurement.py; cmd.wizard("distance") is rewritten to measurement
(wizarding.py:88-89), so this module is reachable only as cmd.wizard("Distance")… no —
it is effectively unreachable via cmd.wizard. Kept for API/session compatibility.
Panel (:89-97): [1,'Distance Measurement',''], [3, mode_name,'mode'],
[3, object_mode_name,'object_mode'], Delete Last, Delete All, Done.
Menus: mode (:46-50) Polar/Heavy/Neighbors/Pairwise Distances;
object_mode (:64-68) Replace Previous / Create New.
do_pick :148, do_select :142. Objects named dist%02d, module-global dist_count
(:14). Also calls module-level cmd rather than self.cmd throughout.
7.19 dragging.py — Dragging (106 lines)
Purpose: the panel that appears while the mouse is in “drag” editor scheme.
Panel (:82-106) — two shapes:
- dragging atoms:
[1,'Dragging %d atoms in',''],[1,'object "<obj>"',''],[2,'Undo (CTRL-Z)','cmd.undo()'],[2,'Redo (CTRL-A)','cmd.redo()'],[2,'Indicate','cmd.get_wizard().indicate()'],[2,'Done','cmd.set_wizard()']. - dragging an object matrix:
[1,'Dragging matrix for',''],[1,'object "<obj>"',''],[2,'Reset','cmd.reset(object="<obj>")'],[2,'Done','cmd.set_wizard()']. get_panel()can returnNone(:104-105) when the wizard has gone invalid — the React renderer must tolerateNone/[].
check_valid() (:46-56) polls cmd.get_editor_scheme() != 3 and does
self.cmd.do("_ cmd.set_wizard()") — i.e. a wizard can dismiss itself from an event
handler. Event mask pick|dirty (:58-59); do_dirty (:42-44) is what drives the check.
cleanup() (:72-80) calls cmd.drag(), disables _drag, restores button_mode
and cmd.mouse().
Latent bug: self.old_button_mode is only assigned when constructed with an argument
(:21-22), so cleanup at :78 can raise AttributeError.
7.20 openvr.py — Openvr (97 lines)
Purpose: the in-headset VR menu. Launched automatically by the core when stereo mode
switches to OpenVR: PParse(G, "cmd.set_wizard_stack()") then PParse(G, "wizard openvr")
at packages/engine/layer1/Scene.cpp:1126-1128.
Purely declarative — assigns self.panel directly (:92-97), relying on the base
get_panel (wizard/__init__.py:52):
[1,'OpenVR Menu',''], [3,'Scene','scene'], [3,'Wizard','wizard'], [3,'VR GUI','gui'].
Menus:
scene(:9-30): Next / Previous / ─ / Append / Store…▸(F1..F12) — a nested list submenu.wizard(:31-36): Measurement / Mutagenesis / Density — i.e. a wizard that launches other wizards (stack push).gui(:37-91): 7 presets (Old Defaults, Spatial Opaque, Spatial Semi-Transparent, Spatial Transparent, Overlay, Responsive Overlay, Responsive Spatial), each a semicolon-joinedset openvr_gui_*command string.
7.21 annotation.py — Annotation (102 lines)
Purpose: show per-state SD-file annotations as a prompt overlay.
Panel (:31-35): [1,'Annotation',''], [2,'Dismiss','cmd.set_wizard()'].
No picking at all — event mask is scene|state|frame (:7-8); each of
do_scene/do_frame/do_state (:10-17) just calls self.cmd.dirty_wizard(), forcing a
refresh on the next update pass.
get_prompt (:19-29) reads pymol.session.annotation[obj][state] for every enabled
object (get_names('objects',1)).
Companion loader load_annotated_sdf(filename, object, state, discrete) (:42-102) parses
an SD file with chempy.sdf.SDF, read_molstrs each record, and stores colored annotation
lines (\955, \595, \559 — :86-93).
7.22 pseudoatom.py — Pseudoatom (45 lines)
Purpose: inline text entry to create a labeled pseudoatom at a 3D position. Launched
from the viewport context menu: menu.py:1655
cmd.wizard("pseudoatom","label",pos=[x,y,z]).
Panel (:42-45): a single [2,'Cancel','cmd.set_wizard()'] row — no title row.
Prompt (:38-40): r'Label text: \888' + text + "_" (fake caret).
Event mask: key only (:17-18). do_key (:20-36): 8/127 backspace,
27 escape→set_wizard(), 32 space, >32 append, 10/13 commit →
get_unused_name(text[:14].lower(), 0) + cmd.pseudoatom(obj, pos=…, label=…) +
set_wizard().
7.23 renaming.py — Renaming (49 lines)
Purpose: inline rename of an object / selection / group / scene. Launched from 10 places
in packages/engine/modules/pymol/menu.py (see §7 header list).
Constructor Renaming(old_name, mode='object') (:9).
Panel (:45-49): [1,'Renaming',''], [2,'Cancel','cmd.set_wizard()'].
Prompt (:41-43): 'Renaming \\999<old>\\--- to: \\999' + new_name + "_".
Event mask: key only (:17-18). do_key (:20-39): space becomes _ (:26-27);
Enter runs set_name <old>,<new> for mode='object' (:31-33) or
scene <old>,rename,new_key=<new> for mode='scene' (:34-36).
Uses module-level cmd, not self.cmd (:24-38).
7.24 security.py — Security (45 lines)
Purpose: the “this session file contains movie commands” consent gate.
Prompt (:15-34): a 17-line fixed warning block, also echoed to the console at
construction (:12-13).
Panel (:37-45): [1,'Assume Movie Risks?',''], [2,'accept','cmd.accept()'],
[1,'',''] (spacer), [2,'decline','cmd.decline()'], [1,'',''],
[2,'mdump','cmd.mdump()']. Note the empty [1,'',''] spacer rows — the React renderer
must reserve vertical space for them.
7.25 toggle.py — Toggle (42 lines)
Purpose: kiosk-style toggles plus an optional message overlay.
Panel (:30-42): [2,'Toggle Fullscreen','cmd.full_screen()'],
[2,'Toggle Stereo 3D','cmd.stereo("off" if cmd.get_setting_int("stereo") else "on")'],
[2,'Toggle Message','cmd.get_wizard().toggle()'], [2,'Dismiss','cmd.set_wizard()'].
The panel is edited in place: when there is no message, row index 2 is deleted (:40-41).
Prompt is self.message or None (:23-28).
Bug worth noting: __init__ (:8-17) never calls Wizard.__init__, so self.cmd,
self.menu, self.panel and self.session do not exist; it falls back to the module-level
cmd at :21. A generic bridge must not assume wiz.cmd exists.
8. Related wizards outside packages/engine/modules/pymol/wizard/
packages/engine/modules/pmg_qt/builder.py defines 19 more Wizard subclasses that use the
identical panel/prompt/do_pick contract, driven by the Builder GUI:
ActionWizard (:39), CleanWizard (:89), SculptWizard (:134),
RepeatableActionWizard (:228), ReplaceWizard (:266), AttachWizard (:302),
BioPolymerWizard (:368), AminoAcidWizard (:473), NucleicAcidWizard (:494),
ValenceWizard (:514), ChargeWizard (:566), InvertWizard (:607),
BondWizard (:646), UnbondWizard (:700), HydrogenWizard (:743),
RemoveWizard (:808), AtomFlagWizard (:844), FixAtomWizard (:982),
RestAtomWizard (:986). They call self.cmd.set_wizard(self, replace=1) (:56, :254)
to swap themselves onto the stack, so they render through the same generic renderer with no
builder-specific code. See docs/builder.md for the surface that drives them and
apps/web/src/features/builder/ for the port.
9. The generic contract, as ported
9.1 Bridge RPCs
Served bypackages/bridge/tenmol_bridge/panels/wizards.py, granted by
packages/bridge/tenmol_bridge/policy/grants/wp-16.py, typed in
packages/protocol/src/topics/wizard.ts (WIZARD_RPC), called from
apps/web/src/features/wizards/service.ts. Each is a thin wrapper over a confirmed API:
wizards.probe and only
pulls wizards.snapshot when the version moved. That is forced by §9.3 rules 4 and 5:
get_panel() and get_prompt() have side effects, so they cannot be called speculatively.
9.2 Components (apps/web/src/features/wizards/)
WizardPanel.tsx— renderssnapshot.panel. One row per type:0spacer,1label,2button (press state on pointerdown,wizards.exec_code(code)on pointerup-inside),3popup button (opensWizardPopupMenufed bywizards.menu(code)— fetched on open, never cached, becausedensity.py:160,filter.py:236,command.py:87rebuild menus live). Row height mirrorsinternal_gui_control_size(packages/engine/layer1/Wizard.cpp:255).WizardPrompt.tsx— top-left viewport overlay, honourswizard_prompt_mode0/1/2/3 (packages/engine/layer1/Ortho.cpp:2136-2190), default text colorrgb(51,255,51)and backdroprgb(51,51,51)(packages/engine/layer1/Ortho.cpp:2692-2697).WizardPopupMenu.tsx— codes 0/1/2 → separator / item / title (packages/engine/layer4/PopUp.cpp:293-320); nestedsubmenuopens on hover; leaf runswizards.exec_code(command).ColorCodedText.tsx+colorCodes.ts— the\RGB/\---parser (packages/engine/layer1/Text.cpp:507-548).WizardsPanel.tsx— the stack;snapshot.depth > 1means nested wizards (packages/engine/layer1/Wizard.cpp:280-282).WizardKeyCapture.tsx— synthesizesdo_key(k,x,y,mod)from real keystrokes for the four wizards that implement a hand-rolled line editor (box.py:423,renaming.py:20,pseudoatom.py:20,command.py:166). ASCII codes are what they compare against: 8/127 backspace, 10/13 enter, 27 escape, 32 space,>32printable.WizardLauncher.tsx— the Wizard menubar (_gui.py:834-864) fed bywizards.catalog.
9.3 Rules the renderer obeys (each derived from real source)
- Panel may be
Noneor[].dragging.py:104-105returnsNone;message.py:36returns[]. C treats both as height 0 (Wizard.cpp:254-259). - Panel rows must have ≥3 elements; extras are ignored (
Wizard.cpp:241). - Text is truncated:
WordTypefor panel text,OrthoLineTypefor code (Wizard.cpp:243-246). Web can render full text; just don’t rely on truncation. get_panel()has side effects.mutagenesis.py:278,nucmutagenesis.py:147changemouse_selection_mode;density.py:160andfilter.py:236rebuild menus. Call it exactly once per refresh, never speculatively.get_prompt()has side effects too —charge.py:124-128runscmd.iterate.- Buttons fire on release, popups on press (
Wizard.cpp:481-511vs:552-583). - Menus are dynamic — always re-fetch on open.
- Menu third element may be a callable — resolve server-side (
PopUp.cpp:88-105). - Gate events on the mask before dispatching; refresh the mask on every snapshot
because it changes with wizard sub-state (
box.py:398-403,command.py:149-152). do_pick_state(state)fires beforedo_pick/do_select, withstate+1(Wizard.cpp:189,:324).- All wizard methods are optional — probe with
hasattrsemantics (Wizard.cpp:162), never assume presence. - Wizards can dismiss themselves from a handler (
dragging.py:52) and can push other wizards (openvr.py:31-36,demo.py:42-53) — the client reconciles from the pushed snapshot, not from optimistic local state.
10. Wizards that stay out of reach
Three bundled wizards cannot run in this deployment, for reasons in their own source rather than in the port:cleanup.py—__init__raisespymol.CmdExceptionunless an OpenEyeszybkibinary is found under$OE_DIR/$OEDIR(:13-34,:52-54), so the wizard never opens.openvr.py— launched only by the core when stereo mode switches to OpenVR (packages/engine/layer1/Scene.cpp:1126-1128); there is no OpenVR stereo path here.benchmark.py— measures GL blit and raytrace throughput of the local GL window (:349-367); the numbers describe the bridge’s offscreen context, not the browser.
distance.py is dead by upstream’s own rewrite: cmd.wizard("distance") is rewritten to
measurement (wizarding.py:88-89). It is kept for session compatibility only.