Architecture
Overview
Prismo is built around a simple idea: plugins emit telemetry, the host normalizes it, and the terminal UI renders the latest view of the system.
Internally, the project is split into a few main parts:
- telemetry ingestion and modeling
- plugin protocol and process hosting
- TUI rendering and interaction
- application runtime wiring
- plugin SDKs and implementations
This structure keeps the UI focused on inspection while making it possible to adapt Prismo to different systems through plugins.
Project Layout
crates/core
This crate owns the core telemetry concepts:
ChannelDescriptorChannelValueChannelSampleTelemetryUpdateRuntimeEventPluginHealthPluginRuntimeStateTelemetryStore
It defines the internal telemetry model used after plugin messages are normalized by the host.
crates/plugin-protocol
This crate defines the external plugin contract:
- protobuf message types
- length-prefixed frame helpers
crates/plugin-host
This crate handles:
- plugin manifest parsing and discovery
- subprocess spawn and supervision
stdin/stdout/stderrtransport- handshake validation
- restart policy handling
- conversion from wire messages into
RuntimeEvent
crates/plugin-sdk/rust
This crate is the Rust SDK for writing subprocess plugins.
crates/plugin-sdk/cpp
This package exposes a small header-only C++ surface for subprocess plugins. It avoids linking Rust into C++ plugins, which lets downstream integrations use the SDK even when their plugin binary already statically links Rust dependencies.
plugins/example-rust
This crate contains the current Rust example plugin implementation. It now runs as a subprocess plugin over the shared wire protocol.
plugins/example-cpp
This target contains the current C++ example plugin implementation. It links against the header-only C++ SDK.
crates/tui
This crate owns:
- pane layout
- key and mouse handling
- focused text cursor behavior
- details and latest-value rendering
- channel tree expansion/collapse state
- scroll offsets and scrollbar rendering
- command/filter prompt rendering
- status bar and help overlay
It renders from StoreSnapshot only. It does not talk directly to plugins.
app
This crate is the executable. It:
- accepts an optional
--pluginsoverride directory - auto-discovers plugins from a sibling
plugins/directory by default - starts plugin subprocesses through
plugin-host - receives
RuntimeEventvalues through a channel - applies normalized telemetry to
TelemetryStore - drives the TUI render loop
- handles clipboard yank via OSC 52
All example plugins go through the same external plugin boundary as real integrations.
Data Flow
At runtime, Prismo looks like this:
plugin subprocess -> stdio + protobuf -> plugin-host -> RuntimeEvent -> app -> TelemetryStore -> StoreSnapshot -> tui
Detailed flow:
- The app resolves the plugin directory from
--pluginsor from a siblingplugins/directory. - The host discovers
prismo-plugin.tomlmanifests in that directory. - The host spawns each discovered plugin subprocess.
- The host sends
Initonstdinand validates the child'sHello. - The host normalizes plugin messages into
RuntimeEventvalues. - The main loop drains pending events with
try_recv. - The store applies descriptors, samples, plugin health, and runtime state.
- The TUI renders from the latest snapshot.
Telemetry Model
The current canonical value types are:
BoolIntegerFloatTextBytesEnum, represented by an integer discriminant and symbolic nameArray, represented by a scalar element type, nesting depth, and recursively nested values
Arrays are homogeneous across the complete value tree. Nested arrays may be ragged, but every leaf must have the declared scalar type and every branch must have the declared nesting depth. Explicit type and depth metadata keeps empty arrays unambiguous. Integer and float arrays keep bounded numeric history keyed by each leaf's full index path.
The store keeps:
- latest sample per channel
- recent numeric history for charting
- per-channel update counts
- the most recent observed inter-sample interval and derived rate
- plugin health snapshots
- total dropped updates caused by missing channel descriptors
Store Behavior
Important store rules:
- channels are keyed by
(plugin_id, path)in aBTreeMap - numeric history retention is capped at 64 samples
- staleness is adaptive: a channel is stale when time since last sample exceeds
3xthe most recent observed interval - channels with only one sample fall back to a
3sinitial stale threshold total_updatescounts applied update batches, not individual samplesdropped_updatesincrements if a sample arrives before its descriptor exists
Plugin Model
Prismo uses a subprocess-first plugin model:
- each plugin runs as a child process
- the host sends
Initonstdin - the plugin writes framed protobuf messages on
stdout - free-form logs go to
stderr
The current Rust and C++ example plugins send:
Helloon startupDeclareChannelsonce- randomized
SampleBatchmessages on each interval tick Healthcontainingemitted_updates
Build System
Bazel is the primary build system for the repository:
MODULE.bazelpins the Bazel module graph. It is also the authoritative repo version; the Cargo workspace package version is intentionally fixed at0.0.0rules_rsprovides the Rust-facing Bazel surface in this repo, including toolchains and Cargo dependency resolutionrules_pythonprovides the hermetic Python runtimetoolchains_llvmprovides the hermetic C++ toolchain used for the in-repo C++ pluginbazel/defs.bzlexposes Prismo-specific packaging rules for downstream Bazel users- each workspace package has its own
BUILD.bazel - plugin manifests are checked into plugin directories. 1st party supported plugins live in the
plugins/directory
The downstream Bazel surface is intentionally small:
prismo_pluginpackages an executable plus its checked-in manifestprismo_bundleassembles the runnable bundle layout and is itself executable viabazel run
The current C++ path intentionally avoids a Rust link dependency:
crates/plugin-sdk/cppexports a header-only C++ SDKplugins/example-cpplinks that SDK into a Bazel-built C++ binary- this keeps C++ plugin binaries compatible with downstream projects that already statically link Rust code
Cargo manifests remain in the repo for dependency metadata and editor/tooling support, not as the primary execution path.
UI Structure
The UI has three focusable regions:
DetailsLatest ValueChannels
The overall layout is:
- wide terminals: details on the left, channels on the right, footer on the bottom
- narrow terminals: details on top, channels below, footer on the bottom
The channels pane is a tree:
- namespaces are derived from dot-separated channel paths
- namespaces can be collapsed individually with
Enter - array-valued channels and nested arrays are expandable indexed tree nodes
- scalar array elements are selectable leaves
- the full tree can be toggled with
z - selecting a namespace or array shows summary details plus its children in the left pane
Renderers:
- numeric values: line chart plus textual summary
- text/integer/bool values: simple text block
- enum values: step chart of the integer discriminant plus the symbolic name
- one-dimensional numeric array nodes: one colored time-series per element
- multidimensional array nodes: flattened descendant values without a combined chart
- numeric array leaves: a standard single-series chart for the selected leaf
- bytes values: hex and ASCII summary
The details pane is intentionally fixed-height and non-scrolling:
- channel details render as a five-line summary block
- namespace details use the same aligned two-column structure
- latest-value content remains the scrollable/expanded pane for long payloads
Input Model
The input model is split by focus:
Channels:j/k, mouse clicks, and mouse wheel choose the active rowDetails: focusable and copyable line-by-line, but intentionally non-scrollingLatest Value: cursor movement and mouse wheel act inside the visible text summary
Copy behavior:
- in
Channels,ycopies the current live channel value - in
DetailsorLatest Value,ycopies the current line under the text cursor
Command/filter behavior:
/opens a persistent filter prompt; a non-empty filter remains visible until clearedEscwhile editing the filter clears it entirely:opens command mode:qis the current quit command
Status and Help
The status bar is intentionally compact:
- left side:
:q quit,: command,? help, current focus, and transient notices - right side: store totals and plugin health
The help overlay is the canonical shortcut reference for the app.
Current Limits
The current implementation does not yet have:
- transport plugins separate from decoders
- multiple instances of the same plugin type
- persistence or replay
- robust clipboard fallback behavior for terminals without OSC 52 support
- responsive/truncated formatting for very large structured payloads beyond the current text renderers