Docs Core
FNS_Console
The toolkit's web front, served from inside your project: every tool's settings on one page, the install picker, and any tab a tool contributes. The raw master, promoted to /sys.
◈ Core
The console is the one place to look at and steer an installed toolkit
from a browser: every tool's settings on a single scrollable page, the
install picker, and any tab a tool decides to contribute. It is served
from inside your project on 127.0.0.1, only while you are looking at it,
and it holds no tool knowledge of its own -- every request is routed to
the component that owns the answer.
Opening it
From the toolkit root's FNSTools parameter page:
| Pulse | Lands on |
|---|---|
| Open Settings | the Settings tab |
| Pick Tools | the Install & remove tab |
From the FNS_Console component itself: Open Console. From Python:
op.FNS_CONSOLE.Open() # Settings
op.FNS_CONSOLE.Open(tab='tools') # Install & remove
op.FNS_CONSOLE.Open(tab='ColorUI') # a contributed tab, by canonical name
op.FNS_CONSOLE.Open(panel=False) # force the system browser
It shows in the toolkit root's webBrowser panel when the root has one
(the bootstrap ships one), else in your system browser. The panel handles
everything the page does, file dialogs included. The tab is the URL
fragment (#settings, #tools, #t-<name>), so a reload keeps it and a
link can deep-link to it.
The tabs
- Settings -- every registered tool as a section card (sticky header,
parameters grouped by page), a jump list with scroll-spy on the left,
search across all tools. Writing a value goes through
FNS_ConfigRegistry's own filters and persistence, so the page can never disagree with the components. The header carries scope (global/project -- see FNS_ConfigRegistry), Export (writes<config dir>/exports/FNStools_config_<stamp>.jsonserver-side and offers a download) and Import (applies a document to installed tools now and keeps sections for tools you install later). - Install & remove -- the
FNS_Installerpicker, served through the console. Unchecking an installed tool removes it; its settings are kept for the next install. Installing decides what is in the project; the config layer always re-applies on top. In the toolkit's own source checkout this tab is locked (it would remove authored masters). - Contributed tabs -- anything a tool registers (below). They sit after a divider, sorted by their declared order; the two built-ins are the console's own and always shown.
Exposure: shipped dormant, enabled by the install
Every tool artifact ships with Expose to Console off, and the toolkit's installer turns it on as it lands the package. One artifact therefore serves two lives:
| How the tool arrived | Console exposure | Its own panel |
|---|---|---|
| dropped into a project on its own (a standalone plugin) | off -- nothing raises a console | works as always |
installed by FNS_Installer (bootstrap, picker, Textport rail) |
on, flipped once at install | off while exposed -- the console is the UI |
updated by FNS_Updater |
untouched -- your choice persists | follows your choice |
Why not simply ship it on: a registry host bootstraps its own /sys
global when none exists, which is right for a toolbar button (it adds
a capability) and wrong for the console, whose exposure removes a local
surface. A standalone ColorUI shipped exposed would raise a console nobody
asked for and switch off its own panel. The rule, for any future surface
with that property: a host whose exposure takes a local surface away
ships dormant and is enabled by the install rail, never by bootstrapping
itself.
The flag is the tool's own Expose to Console parameter (its Registry page), which the config registry persists -- so after the install's one decision, your choice roams across projects and survives updates. If you drag a toolkit package into a project by hand instead of installing it, it arrives in local mode: flip Expose on the tool's page.
Managing tabs
The ⋯ button at the end of the tab bar lists every contributed tab, hidden ones included, with a show/hide checkbox. The choice is written back to the contributing tool's Shown in Console parameter, so it persists with the tool and roams with its Registry page like any other setting -- the console keeps no list of its own. A tool decides its own default the same way, and can stay out of the console entirely (below).
How it runs
FNS_Console is a registry in the FNS sense (it sits on RegistryBase
like the toolbar, navbar and config registries):
- The in-project master
FNSTools/FNS_Consolepromotes a global copy to/sys/FNS_Registries/FNS_Console, reachable asop.FNS_CONSOLE. The global is the API owner; the master and every host forward to it. - The surface is the server. The global owns a Web Server DAT
(
console_server) that is created on demand, bound to the first free port in 36710-36759, switched on byOpen, and switched off after ten idle minutes. It is never saved into the.toeor any package --/sysis rebuilt on every project open. Several open projects each run their own console on their own port. - The page (
console_page) and the request dispatcher (console_server_callbacks) ride along from the master; the global pulls them from the master if a promoted copy predates them. - Routing:
/api/*goes toFNS_ConfigRegistry'sUi*methods (the paths are the registry's original ones -- the TDXLPP launcher reads/api/stateand/api/set);/toolsand the picker's endpoints go toFNS_Installer.ServeRequest;/t/<tab>/...goes to the tool that contributed the tab. Without the config package the Settings tab says so; without an installer the Install tab says so. - Everything is
127.0.0.1only. - The root's
webBrowserpanel (where the console and the picker show in TD) renders only while someone can see it: the component's own Render Only While Window Open / Render Only While Viewer Active watchers follow the floating window and the node's Viewer Active flag, and the console and installer switch it on as they open it. Closed, its Web Render cooks nothing.
Contributing a tab
A tool contributes a tab the same way it contributes a toolbar button or
its settings: it carries a stamped FNS_Console host. The host's
Registration page:
| Par | Meaning |
|---|---|
| Tool COMP | the contributing tool; defaults to the parent (the host lives inside the tool) |
| Canonical Name | URL-safe (letters, digits, _ -); the tab lives at /t/<name>/. Empty = the tool's name |
| Tab Page | a text DAT holding the tab's HTML. Served verbatim in an iframe under /t/<name>/ |
| Tab API | optional Python DAT defining onConsoleRequest(action, method, body) |
| Local Browser | optional: the tool's own web browser panel rendering the same page. While the tab is exposed the console serves the page and switches that browser's Active off -- a renderer nobody looks at would keep a CEF process and a texture alive -- and switches it back on when exposure ends (Expose off, a failed registration, the host removed). A tool that wants finer control implements OnConsoleExposure(exposed) on its extension; the host then calls that instead of touching Active |
| Expose to Console (Auto-register) | the tool's own decision: on = publish the tab while the component exists; off = local only -- the tool keeps its own interface and contributes nothing |
| Register / Status | publish once regardless of Expose; read-only status |
| Shown in Console (Displayed) | on the bar or hidden. The console's tab manager writes here, so a user's show/hide persists with the tool |
| Tab Label / Tab Order | what the tab bar shows, and where. Built-ins sit at 0 and 10; contributions default to 50 and sort by order, then label |
These are mirrored onto the tool's own Registry page (prefix Cs),
so a tool like ColorUI exposes, hides or goes local-only from its own
parameters without anyone opening the host.
Two rules that follow from how it is served:
- The page is self-contained. Inline its CSS and JS -- there is no
asset route. Relative URLs resolve under
/t/<name>/, sofetch('api/x')reaches your API; a root-relative/api/...would reach the console's API instead. - DAT parameters take sibling names. An OP parameter resolves from
the host's parent network, i.e. the tool. Write
webui_html, not../webui_html(that looks one level too high).
The API DAT's single function:
def onConsoleRequest(action, method, body):
"""action = the path after /t/<name>/api/, method = 'GET'|'POST',
body = parsed JSON of a POST, else None. Return anything JSON-able."""
What a tool contributes is a web re-expression of what it owns -- tables, parameters, state -- talking to the same extension its panel talks to. A TouchDesigner panel itself cannot be embedded in a browser.
Stamping the host from Python (the master does the copy safely):
op.FNS.op('FNS_Console').ext.ConsoleRegistryExt.StampHost(
op.FNS.op('MyTool'), canonical_name='MyTool', autoregister=True,
par_values={'Tabpage': 'my_page', 'Tabapi': 'my_api',
'Tablabel': 'My Tool', 'Taborder': 30})
Or register directly, without a host, from your own extension:
op.FNS_CONSOLE.RegisterTab(comp, 'MyTool', page=op('my_page'),
api_dat=op('my_api'), label='My Tool', order=30)
A direct registration is ephemeral (the global is rebuilt every open);
re-register on your onStart. A host re-publishes itself.
Worked example -- ColorUI
ColorUI's panel already is a web page (webui_html, rendered in its own
Web Render browser, talking to TD through document.title rewrites up
and executeJavaScript down). Its console tab is the same page with
a second transport, picked at load:
const SERVED = /^https?:$/.test(location.protocol);
const Bridge = SERVED ? HttpBridge : TitleBridge; // same post()/ack() surface
Served, HttpBridge POSTs every command to api/cmd and gets the fresh
state back, and polls api/state every two seconds so edits made in the
in-TD panel show up. The extension side is two methods over the code the
panel already used:
def ConsoleState(self):
return {'ok': True, 'state': self._statePayload()}
def ConsoleCommand(self, msg):
self._toast_sink = [] # toasts a command raises ride back
try:
self._dispatch(msg) # the title bridge's vocabulary
finally:
toasts, self._toast_sink = self._toast_sink, None
return {'ok': True, 'state': self._statePayload(), 'toasts': toasts}
and the whole console_api DAT:
def onConsoleRequest(action, method, body):
ext = parent.ColorUI.ext.ExtColorUI
if action == 'state':
return ext.ConsoleState()
if action == 'cmd' and method == 'POST':
return ext.ConsoleCommand(body)
return {'ok': False, 'why': 'unknown action: %s %s' % (method, action)}
Host pars: Tab Page webui_html, Tab API console_api, Local Browser
webBrowser, label ColorUI, order 20.
ColorUI goes one step further and keeps its renderer off whenever
nobody can see it: a Panel Execute DAT (watch_viewer) on the panel
value winopen -- "1 if panel is open as a floating window" -- feeds
SyncLocalBrowser, whose single rule is Active = viewer open AND not
served by the console. The console's hand-off (OnConsoleExposure)
and the Expose parameter call the same rule, so there is one owner and
no flicker. Consequences: with the viewer closed the Web Render cooks
nothing at all; Open UI opens the console's tab while served, else the
panel (and the renderer with it); JavaScript pushes and reload-kicks are
skipped while the renderer is off.
HTTP API
All on the console's port, 127.0.0.1 only.
| Route | Method | Answers with |
|---|---|---|
/ |
GET | the console page |
/api/tabs |
GET | {ok, tabs:[{name, label, order, builtin, displayed, url?, api?, source?}]} -- hidden contributions included |
/api/tabs/display |
POST {name, displayed} |
{ok, name, displayed, persisted}; built-ins refuse |
/api/state |
GET | every registered tool with its exposed parameters (+ scope, project, installer) |
/api/set |
POST {tool, par, value} |
{ok, val} -- validated against what /api/state exposes |
/api/export |
GET | {ok, document, tools, saved_to} -- the document is also written to <config dir>/exports/ |
/api/import |
POST (a config document) | {ok, applied:[...], deferred:[...], scope} |
/api/scope |
GET / POST {value, mode} |
{ok, scope}; global requires mode push or adopt |
/tools, /manifest.js, /selection, /status, /install |
— | forwarded to FNS_Installer |
/t/<name>/ |
GET | the contributed tab's page |
/t/<name>/api/<action> |
GET / POST | the tab's onConsoleRequest |
Python API
On op.FNS_CONSOLE (any host or the master forwards):
| Call | Does |
|---|---|
Open(tab=None, panel=True) |
serve + show; returns {ok, url, shown} |
Close() |
stop serving now |
Url() |
the URL while serving, else None |
Tabs(include_hidden=False) |
the ordered tab list; hidden contributions only when asked |
SetTabDisplayed(name, bool) |
show/hide a contribution, written back to its host's Displayed par |
RegisterTab(comp, name, page=, api_dat=, label=, order=, displayed=True) / UnregisterTab(name) |
contributions without a host |
ServeTab(name, subpath, method, body) |
what the dispatcher calls for /t/... |
op.FNS_CONFIGREGISTRY.OpenSettingsUI(tab, panel) still works -- it
forwards here.
Developing on it
- Sources are Embody-externalized under
FNSTools/FNS_Console/(ConsoleRegistryExt.py,console_server_callbacks.py,console_page.html,pre_release.py). A disk edit hot-syncs into the master and into the/sysglobal (its DATs keep the file binding), so the running console picks it up on the next request -- reload the page. - Structural changes under
FNSTools(a new host, a new DAT) must be persisted the project's way or a reload reverts them:pi.Addfor new components,pi.Saveon the changed suspects,pi.Save(op.FNS)last, then save the project. - Promoted globals live in
/sys/FNS_Registries; afindChildrenfrom/does not descend there -- search that container explicitly. - The pre-release hook scrubs registration state, the shortcut, cloning,
the external-tox binding and any server DAT, so a shipped
FNS_Consoleis inert until it promotes in the user's project.
Troubleshooting
| Symptom | Cause / fix |
|---|---|
| "no free port in 36710-36759" | fifty consoles open, or that block is reserved on the machine (netsh interface ipv4 show excludedportrange protocol=tcp); close some, or change UI_PORTS |
| Page stops answering after ~10 minutes | by design -- the server idles out; pulse Open again |
| Settings tab says the config package is missing | FNS_ConfigRegistry is not installed or not yet promoted |
| Install tab says there is no installer | the project has no FNS_Installer; drop the bootstrap or the bare installer |
| Install tab shows Locked (source checkout) | you are in the toolkit's own source root; point Install Into at a scratch container |
| A contributed tab is missing | host status shows why (Registration Status): no page DAT, a name that is not URL-safe, or a name clashing with a built-in. Remember sibling names for the DAT pars |
A tab's fetch('/api/...') hits the wrong thing |
root-relative paths reach the console; use relative api/... |
| Register pulse "did nothing" in a script | parameter callbacks fire at frame end -- read the status on the next frame |