Appendix: Common Tasks
Add a new file parser — see the I/O Pipelines chapter, §6.1. Not summarised here: this entry used to give four steps that left out registering the
FormatHandler, which is what actually makes a file load.Add a keyboard shortcut
- Define the key mapping in
ui/shell/src/keyboard.rs. - Handle the action inside
viewer_loop/key_actions/(updateKeyActionContextif new state is required). - Update the User Guide’s shortcut tables.
- Define the key mapping in
Create a new viewer panel. A panel is a section of the left rail, not a window of its own. The three steps this entry used to give were true and left out every way the work actually goes wrong, so they are spelled out here with the guard that catches each one.
- Add the module and give it a section. A module under
ui/shell/src/panels/, declared and re-exported inpanels/mod.rs, plus aUnifiedTabvariant (viewer/core/src/ui_state/analysis.rs) listed insections_for_mode(panels/unified.rs). Two matches there are exhaustive, so a new variant fails the compile until it has an accent colour and a body; what nothing forces issections_for_mode, which returns a different list for edit mode and for view mode. A variant in neither list compiles, runs, and never appears. - Return an action; do not reach out of the panel. Add a field to
UnifiedPanelActionsand handle it inviewer_loop/runtime/redraw/panels/mod.rs. A panel that needs a menu action callsdispatch_menu_action, which does the frame swap around it;only_one_dispatcher_builds_a_menu_action_contextscans the sources and fails on a second dispatcher, because the landing card grew one and lost the tab focus with it. - Know which half of the state you are writing. The panel is handed a full clone of
UiStateandsync_ui_state_from_temp(ui/shell/src/orchestration/sync.rs) copies it back, but not uniformly:- Copied whole, so a new field needs nothing:
analysis,measurements,trajectory,chrome.panel,overlay,style,markup.annotations. - Copied field by name, so a new field is silently discarded every frame and reads as a checkbox that will not stick:
viewport.view,selection,placement,edit,console,export, andmarkup.labels(one field of it,atom_label_overrides). Add the copy and extend the matching exhaustive-literal guard in that file (every_view_setting_survives_the_per_frame_clone,edit_settings_survive_the_per_frame_clone,camera_settings_survive_the_per_frame_clone), which then fails the compile rather than the behaviour.selection,console,exportandmarkup.labelshave no such guard yet; a new field there is on you. - Reconciled by an exhaustive destructure:
runtime, throughRuntimeUiState::merge_frame_edits, which classifies every field (50 today) as the frame’s, this copy’s, or session-gated. A new field there fails to compile until it is classified. - Not copied back at all:
summaries,bundle,settings,dialogs,requests. The last two are deliberate (seePendingRequests), so a dialog can close itself.
- Copied whole, so a new field needs nothing:
- A panel that loads something writes both copies through one call. A companion file, a volumetric dataset or an appearance bundle has to land on the canonical
UiStateas well as the frame’s, because the rest of the frame and the scene rebuild read it there. Useorchestration::frame_edit::edit_both;no_site_writes_the_two_copies_by_handscansui/shell/srcfor the hand-written pair and names the offender. A toast is the one thing that does not belong in it:push_toastappends, so applying it to both copies raises the message twice. A panel that only draws meets none of this. - If it changes what is drawn, say so twice. Set
render_data_changed = trueon the actions, and make sure the new input is represented inRenderCacheKey(ui/shell/src/render_cache/key.rs) — otherwise the cache hits and returns the previous build, which is the inert-toggle bug this repo has shipped more than once. The key’s own doc comment lists the two compiler-enforced safeguards and the one gap they leave, which is forgetting to add the field at all; mirror a test inrender_cache/tests.rswhen you wire a new render-affecting control. One specific trap: do not hash a raw colour float that a widget writes back.color_edit_button_rgbround-trips through HSV and moves a channel by one ulp with no interaction, soSurfaceEntry::current_hashquantises colour to eight bits per channel. - If it should survive a session, add the field to
ui/shell/src/session/types.rswith a#[serde(default)], write it insave.rsand read it inrestore.rs. A newUnifiedTabalso needs an arm inunified_tab_from_str, or a session reopens on a different section than it was saved on. That one is enforced:every_unified_tab_round_trips_through_its_name(ui/shell/src/session/helpers.rs) walks the variants as a chain through an exhaustive match, so a new one fails the test’s compile rather than going unchecked.
Gone, and listed because older notes still mention it: the loop no longer keeps mirror copies of view flags, chrome flags, measurement state, selection, the summary caches or the status line.
UiStateis the one owner of each, so a panel no longer has to write a value in two places to make it take effect.- Add the module and give it a section. A module under
Expose a new CLI subcommand
- Update
automation/cli/src/cli/mod.rswith the Clap definition (enum Commands). - Implement the handler in a module under
automation/cli/src/handlers/commands/, declared and re-exported inhandlers/commands/mod.rs, and add its arm to thematchonCommandsinautomation/cli/src/app.rs::run. - Implement the logic using
OrbitronServicesinstead of re-implementing loaders. - Add user-facing documentation and tests (ideally under
automation/cli/testsor as integration binaries). A test launches the binary throughcommon::orbitron()(automation/cli/tests/support/common.rs), neverCommand::new(cargo_bin!("orbitron")): the helper pointsORBITRON_CONFIGat a path nothing creates, so the run gets built-in defaults instead of your ownconfig.toml.automation/cli/tests/config_isolation.rsscans the test sources and fails on a direct call site.
- Update
Expose functionality in the Python bridge
- Add public methods to the
OrbitronorScenepyclass (RustPyOrbitron/PyScene) inextensions/python-bridge/src/python/(orbitron.rs,scene.rs). - Reuse existing service helpers; avoid duplicating logic already present in the CLI.
- Update docstrings and provide a short example in the User Guide or Python README.
- Rebuild through the PEP 517 wrapper so viewer assets are staged:
conda run --no-capture-output -n orbitron-dev python -m pip install -e extensions/python-bridge. Add a focused test underextensions/python-bridge/tests.
- Add public methods to the
Add an analysis module shared by CLI/TUI
- Implement the core logic under
automation/cli/src/handlers/(the analyze handler lives athandlers/commands/analyze.rs; follow existing modules for geometry/orbitals). - Expose the module through the
AnalyzeCommandsenum and handler. - If the TUI should display the data, add a helper in
automation/tui/src/analysis.rsand render it inside the appropriate panel. - Capture representative fixtures in regression tests.
- Implement the core logic under
By following the patterns described in this guide and reusing existing abstractions, new features remain consistent with the rest of the Orbitron workspace while minimising regressions.