| status | landed |
|---|---|
| summary | Generalising OpTemplates into an op-menu contribution -- any tool can declare itself an alternative for a stock operator type, and a tool that is only in the store can be offered and placed on demand. |
| since | 2026-09-10 |
Owner's idea, 2026-09-10: "each tool can define itself as an OpTemplate that becomes an alternative for the specified optype, like OpTemplates already does. Registry pattern. Tools in the CMS could define this too, so if available in storage they show up as alternatives without being preloaded in the project."
This page pins what already exists, proposes the shape, and lists the three decisions the owner has to make before it is built.
OpTemplates owns the whole mechanism. Four pieces, all inside one package:
- A library.
OPTemplates1, a base whose children are named after operator types (lfoCHOP,moviefileoutTOP, ...) and hold one or more template chains each.Templatesderives{optype: [ops]}from that structure alone. Users keep their own libraries as.toxfiles underPalette/FNSTools/OpTemplates/(theExternaltoggle). - A watcher.
kindergaertner_mymod(a vendored olib component, target/local) firesonCreate(new_ops)for every operator created anywhere, and the callbacks DAT forwards each undocked one toOnNewOp. - An engine.
OnNewOpis gated on theKeyshotkey (alt ctrl). One template:placeOPchaindestroys the fresh op and copies the template in its place, restoring every input and output connection by connector index, carrying docked ops, and re-applying a family-convert parameter. Several templates:popMenuasks which, then places. A template may be a single op, a chain, or abaseCOMPwhoseTEMPLATE_ROOT/TEMPLATE_INtagged children mark where the chain begins. - A contribution.
opmenu_callbacks, published through the package'sFNS_OpMenuRegistryhost:onDecorateLabelappends' >>>'to types that have a template,onMenuItemsaddsEdit Templates....
OpMenuRegistry already merges per-optype contributions. SearchWords
collects onSearchWords() -> {optype: [words]} from every contributor into
one dict, with per-contributor error isolation. That is the exact shape an
alternatives contribution needs, so the protocol extension is one more
callback, not a new registry.
Two primitives for "not preloaded" already ship.
placement: pane(catalog, manifest, installer, updater, both pickers; seetests/test_placement.py): a package that spawns into the network the user is working in, whose presence is the install record and whose instances are frozen at their spawn version. No package declares it yet.FNS_Updater.PlaceCommunityTool(name, target): download a tox, verify size and then hash, place it into a target COMP. Its docstring is explicit that this is a place, not an install.StoreFolder()is the machine-wide flat package store,Palette/FNSTools/store, which is the "storage" in the owner's phrasing.
The manifest is derived live. surfaces, requires and
integrates_with are read off the running project at build time. A new
per-package field can be derived the same way instead of curated.
Add one callback to the OpMenuRegistry callbacks-DAT protocol:
def onAlternatives():
'''{optype: [alternative, ...]} this tool offers when that type is created.
alternative = {'label': str, 'source': COMP | OP | callable, 'help': str}'''
return {}The registry merges it exactly as it merges onSearchWords. The label
decoration and the Edit Templates... item keep working unchanged.
- OpTemplates becomes one contributor. Its
onAlternativesreturns itsTemplatesdict, label = template name, source = the template op. Its library, the external.toxfiles, and the editor stay where they are. Nothing a user has authored moves. - Any tool can contribute. A tool that is a better answer to a stock type
returns
{'moviefileinTOP': [{'label': 'MediaBrowser', 'source': self}]}from its own callbacks DAT, next to its existingFNS_OpMenuRegistryhost. The code stays in the tool, which is the registry scheme's rule. - Placing a tool reuses the
baseCOMPbranch ofplaceOPchain: the tool is copied where the op was and wired through itsTEMPLATE_ROOT/TEMPLATE_INtagged children, or, with no tags, throughin1/out1by name. A tool that wants to be an alternative adopts one of those two conventions and nothing else.
Where the engine lives is the first decision. Two options:
| Engine in the registry | Engine stays in OpTemplates | |
|---|---|---|
| Who must be installed for alternatives to work | the registries (core) | OpTemplates |
| Fits the scheme | yes: a surface's behaviour is the registry's | no: one tool would host every other tool's surface |
| Cost | move OnNewOp, placeOPchain, the picker popup and the watcher into FNS_OpMenuRegistry (about 400 lines plus a vendored component) |
one aggregation call in OnNewOp, plus requires: FNS_OpTemplates on every contributor |
| Risk | the registry masters ship raw and cloneable, so growth has to stay lean; the watcher is third-party olib code and needs a licence check before it moves | a contributor that installs without OpTemplates silently offers nothing |
Recommendation: the registry. A surface that every tool can plug into is what the registries are for, and the second option makes OpTemplates a dependency of tools that have nothing to do with templates.
- Manifest field, derived.
build_manifestcalls each live package'sonAlternatives()at build time and records the keys asalternatives_for: [optype, ...], alongsidesurfaces. A foreign package may curate the list incatalog.json, since there is nothing to reflect. - Gate: only
placement: panepackages. A store-backed pick has to land in the working network as a frozen instance, which is precisely what the pane contract already promises. A package that installs into the toolkit container is not an operator and should not pretend to be one. This rule costs nothing and removes every "is this an install?" question. - Decoration. The registry reads the cached manifest the updater already
keeps in
StoreFolder(), and marks types that have an uninstalled alternative with a second, distinct mark (say' v'beside the' >>>'). The picker lists those under their own heading, "from the store", with the package'stitleand one line ofdescription. - Fetch, then place. If
<StoreFolder>/<name>.toxexists, place it. If not, fetch it the wayPlaceCommunityTooldoes, size then hash against the manifest, then place. Gated (accesstier) packages appear and refuse with the route, which is the picker's existing rule: visible and locked, never hidden. - Nothing installs itself. Placing is a place. No
Installedrecord, no update pass, no toolkit-container copy. If the owner wants a placed instance to become a tracked install later, that is a separate action in the hub, and a separate design.
- Engine home: the registry.
OnNewOp,placeOPchain, the picker popup and the op-create watcher move intoFNS_OpMenuRegistry. OpTemplates keeps the library and the editor and contributes throughonAlternatives. - "Available" means on disk in the store. The owner's rule: once a tool
has been downloaded into
StoreFolder(), it is offered and placed from disk through the same mechanism. So the registry offers the intersection of the cached manifest'salternatives_forand the.toxfiles actually present in the store folder, and placing loads the tox from disk. Nothing is fetched on demand in this version; a package that is in the manifest but not yet downloaded is simply not offered. Fetch-on-demand, if wanted, is a later step on top of this one and changes nothing below it. Places stay places: noInstalledrecord, no update pass. - The
alt ctrlgate stays. A popup on every operator would be intolerable, and the' >>>'mark is how people learn the modifier exists. The only change is that the mark and the popup now cover every contributor.
Pane-placement is the gate, so the first step is declaring it on the tools that genuinely are reusable components: the ones whose whole job is to stand in for a stock operator with a better one. Naming them here before the owner has looked would be guessing.
The external template .tox format, the Edit Templates... editor, and any
form of automatic installation.
Steps 1 to 6 of briefs/2026-09-10-op-alternatives.md, in four commits.
- Protocol.
onAlternatives()is one more callback in the op-menu callbacks-DAT protocol;Alternativesmerges every contributor with the same per-contributor isolation asSearchWords. Each alternative normalises to{label, source, help, contributor, unpack}. A bare op is a library entry and unpacks; a dict naming a tool is placed whole unless it saysunpack. - Engine, in the registry. One
watch_childrenopexecuteDAT onui.panes.current.ownerplus the in-house snapshot logic (immediate children keyed byOP.id); only the/sysglobal's copy is active. The gate is theShortcutalternativespar on the registry'sAlternativespage, defaultalt ctrl. The picker isop.TDResources.PopMenu, live entries first, the store's below a divider. Placement isplaceOPchainported whole for library entries, a whole-COMP copy with the clone-host crash recipe for a tool offering itself, andloadToxfor a store tox. - Store.
alternatives_foris derived per live package at manifest build by calling itsonAlternatives(); a foreign entry may curate it, and so may a live package with no op-menu host (2026-09-10: the content CMS gained an "Alternative for" input; a curated list beside a host is a preflight problem, so a tool never says two different things). The registry offers the intersection of the cached manifest and the toxes on disk inStoreFolder(), skipping packages live in the project, and places from disk. Nothing is fetched, nothing is installed. - One marker.
' >>>'for a type with a live alternative,' >>'for one whose only alternatives are in the store. OpTemplates' own decorator retired. - Multi-type keys (2026-09-10). A key in the returned dict may name several types separated by spaces, or be a list of names; the merge expands it so the registry is still keyed by one type. Before this a tuple key was stringified and offered for nothing, silently.
placeOPchainonly cleanedTEMPLATE_*tags in the destination network; every COMP template placement leftTEMPLATE_ROOT/IN/OUTon the library's own children, and a staleTEMPLATE_OUTis what the next placement wires the outputs to. Forty-six library operators carried them. The port cleans every operator it tagged; the library was stripped once.- The
/sysglobal is a promotion-time copy of the master, not a clone: a child added to the master reaches it only when the master'sVersionis newer at boot. Bumped to 1.1.0 for exactly that. - Every tool's
FNS_OpMenuRegistryhost has cloning on with an empty clone path, so hosts receive the ext by file sync and never receive children. Right for a watcher and a hotkey chain; recorded, not changed.
The question after the move was what an OpTemplates user loses. Nothing they made, and the one setting that would have gone stale is carried for them.
Untouched. The library OPTemplates1, the external .tox libraries under
Palette/FNSTools/OpTemplates/ with the External toggle and its
save/load/overwrite flow, dragging an operator onto the toolbar icon, Edit Templates... in the operator list, and the four quick-launch commands (Open,
Add selection, Save, Refresh). OpTemplates derives Templates from the library
exactly as before and hands that dict to the registry through
onAlternatives(). The registry reads it on demand and caches it per frame, so
a library edit is offered on the next placement with nothing to pulse.
Same from the user's seat. Same alt ctrl / cmd ctrl default, same
in-place swap with inputs and outputs restored, same ' >>>' in the operator
list. Two visible differences: the picker for several templates is TD's shared
TDResources.PopMenu, so it no longer takes the operator family's colour as
its title, and every entry reads name (OpTemplates) because other
contributors share the list now.
Two things changed underneath; both handled in ccd63f19.
Keyswas dead. The gate moved to the registry'sShortcutalternatives, but OpTemplates'Keyspar stayed, and ConfigRegistry kept restoring it at boot. A user who had customised the shortcut would have had it restored into a parameter nothing read.adoptShortcut(), scheduled 90 frames after init, carries a CONSTANTKeysontoShortcutalternativesonce, and only while the registry is still at its default (a registry the user already set wins), then turnsKeysinto a read-only expression that mirrors the registry's par, with help text saying where to edit it. One setting, shown in both places, edited on the registry. Verified three ways: a defaultKeysstays default and mirrors;shift ctrlonKeyslands on the registry and in the live hotkey chain; a later change on the registry flows back intoKeys.- The old watcher still cooked.
kindergaertner_mymodobserved the current pane's children on every change in order to call what is now a no-op. Itsopexec1is inactive; the component stays (next paragraph).
Still carried, on purpose. OpTemplates ships its idle engine until the
owner confirms removal: the vendored kindergaertner_mymod watcher, popMenu
and popMenuConfig, table_pop_templates, the hotkey chain and null_hk
(about 145 operators, all inert), and the original placeOPchain body in
OpTemplateExt from the BULKY GOODS marker down, about 400 lines that
nothing calls now that PlaceTemplate delegates to PlaceAlternative.
kindergaertner_mymod1, which watches the library for OnTemplatesUpdate, is
live and stays either way. OnNewOp stays as a named no-op: forwarding it to
the registry would fire a second placement for the same keypress.
Version pairing. The engine is in the registry, so a working install needs
FNS_OpMenuRegistry 3.1.0 (core, always installed) and FNS_OpTemplates 3.1.0
together. An older registry under the new OpTemplates offers and places
nothing, because OpTemplates no longer holds a watcher. The new registry under
an older OpTemplates works, with the old decorator's second ' >>>' ignored.
A user's own library files carry the leaked tags. placeOPchain left
TEMPLATE_ROOT/IN/OUT on library children for years, so every external .tox
a user saved carries them too. Harmless until a stale TEMPLATE_OUT sits on
the wrong operator; the ported engine strips every tag it touches on both
sides, so a library heals as it is used. A one-shot strip on library load would
close it entirely and is not built.
-
The mark was never reaching the dialog.
_markAlternativeshung offDecorateLabel, but the dialog's chain stage (OpMenuMods'script_inject_callbacks) resolvesDecoratorsonce per cook and applies them itself; it never callsDecorateLabel. With OpTemplates' own decorator retired, nothing appended' >>>'for anyone, old templates included, whileDecorateLabel('noiseCHOP', 'Noise')kept answering'Noise >>>'in tests. Verify the surface, not the method: the stage now calls the publicMarkAlternatives(optype, label)per row. -
A pane switch read as a hundred creations. The watcher follows
ui.panes.current.owner, and there is no tick on the switch itself, so its first tick after entering a network diffed against the previous network's snapshot and handed every child to the engine (measured: 100 from one navigation into/). Harmless with the modifier up, but the obvious fix, "a changed owner only re-baselines", swallows the very creation that caused that first tick (verified: the first op created after entering a network was never offered). The watcher now keeps a watermark, the highest OP id seen at its last tick; ids only grow within a session, so on a changed owner the candidates are the children above the watermark: the op just created, and nothing that already existed. Verified: enter a network holding two older ops, create one, exactly one offer. -
A tool inside itself. Testing
/randomCHOP1's alternative from inside/randomCHOP1asked the engine to copy the COMP into its own network.PlaceAlternativerefuses when the fresh op sits inside the source. -
A copied host is registered hidden. A host copied out of another tool keeps that tool's Registration values (
Canonicalname,Displayed, help URL).Displayedoff drops the entry from_activeNames(), and with it every hook the tool defines, alternatives included.StampHostis the route; a copied host needsDisplayedon and its own canonical name. -
A placed copy published itself. A tool offering itself is copied whole, hosts included, and the copy's hosts autoregistered on init: each placement added one more alternative for the type, and with a canonical name set, the copy took over the original's entry (three placements, the entry pointing at the last copy). A placed copy is an instance, not a tool:
_placeCompnow switches Autoregister off on every registry host inside the copy before its extensions init. The store path is unchanged, since a store alternative is only offered for a package that is not live in the project, so there is no original to displace. -
A chain landed corner-first.
ui.pasteOPs(parent, x, y)anchors the pasted group's corner at the point, so the render template put its box SOP where the user had clicked and the Render TOP a thousand units to the right; it read as "a disconnected box". Inherited from OpTemplates, which pasted the same way. The real defect was the lost wire (next bullet); for the layout the owner chose the leftmost operator, the start of the chain, exactly under the cursor, which is what the paste roughly did and is now exact. -
A wire vanished on a name collision. Placing the render template at
/, where abox1already exists, pasted the box asbox2with its wire intoattribcreate1gone; in an empty network the same placement was fully wired. A bareui.copyOPs/pasteOPswith the same collision keeps the wire, cooking on or off, so the trigger inside the engine is not isolated. The engine now records the library's own wiring by index before the copy (aTEMPLATE_IDX_ntag per op, cleaned on both sides) and reconnects any input the paste left empty, so a rename can never cost a connection whatever the cause. -
Hosts carried the Alternatives page. Every stamped host is a copy of the master, so each carried its own
Shortcutalternatives, read by nothing, and the hotkey manager listed five 'alt ctrl' rows for one setting. Hosts now shed the page at init alongside the engine ops.
The eight tools that offer themselves (ChopBank, ChopProcess, ConstantCHOP,
ExpressionPOP, FeedbackDisplace, RandomCHOP, PrismTOP, ThresholdColor) keep
the operator types in an alternatives table beside their callbacks DAT:
type | label | help, one row per type. onAlternatives() is the same
reader in all eight. The content CMS's "Alternative for" field reads the
live list through FNS_CMS (/api/altread) and, where the table exists,
rewrites it and PI-saves the tool (/api/altwrite). Before this the field
was empty for every one of them, because the list was code and the field
only showed the catalog. OpTemplates computes its list from its library,
has no table, and shows read-only. A tool with no op-menu host keeps the
catalog value as before.
- Which real tool becomes the first contributor after OpTemplates. Owner's call.
- Fetch-on-demand for a package in the manifest but not yet in the store.
- OpTemplates' idle engine, listed under "Still carried" above, stays until the owner confirms its removal.