Skip to content

The Rust Half

Braden Seaborn edited this page Aug 23, 2026 · 1 revision

The Rust Half

An app's Rust half is a fn pointer in the registry, and not a child process.

Home and File Explorer show what the orchestrator already knows. A pipe in front of that data buys an IPC boundary to talk to ourselves. A tool needs that boundary, because a tool is another team's code on its own release cycle. An app is this code.

type Dispatch = fn(&AppHandle, &CallContext, &str, Option<Value>) -> Result<Value, RpcError>;

The dispatch entry point

pub fn call(
    app: &AppHandle,
    context: &CallContext,
    method: &str,
    params: Option<Value>,
) -> Result<Value, RpcError> {
    match method {
        "notes/list" => encode(&list(app, context)?),
        "notes/write" => {
            let (path, body) = fields(params.as_ref())?;
            write(app, context, &path, &body)?;
            encode(&list(app, context)?)
        }
        _ => Err(RpcError::new(
            METHOD_NOT_FOUND,
            format!("no such method: {method}"),
        )),
    }
}

Three habits show in that block:

  • Return the new state from a mutation. notes/write answers with the list. The frontend redraws from one reply and never guesses what changed.
  • Name the method in the error. no such method: {method} tells the reader which call arrived.
  • Keep the parsing in a helper. fields reads params and returns INVALID_PARAMS with a sentence, and not a code path name.

helve/hello never reaches your code. The shell answers the handshake in ToolWindow.tsx, because the shell is the side that knows the session.

CallContext

pub struct CallContext {
    pub cluster_id: Option<String>,
    pub project: Option<PathBuf>,
}

The context reports where the call came from. cluster_id is the cluster holding the calling surface. project is that cluster's project, already checked against the disk.

Both fields hold None in ordinary states, and not in failure states. A call arrives with no instance id when the shell's own menu raises it. A cluster holds no project until a developer points it at one. Home draws a pick-a-project screen for that state, and File Explorer falls back to the stack root.

When a method has nothing sensible to do without a cluster, ask for one:

let cluster = context.require_cluster()?;

The shell resolves instance_id from event.source against its map of mounted iframes. A frame cannot name itself, which makes the context trustworthy.

Hold no state in the module

files::call holds none. Every method takes its root from the CallContext the caller resolved, which reports where the frame sits, and not which app is in it. An Explorer and a Viewer in one cluster then resolve the same project, while a pair in the next cluster resolve theirs.

Two apps share one dispatch where the data is one thing. viewer and files both point at files::call, because the machine has one filesystem. A second module means a second files/read, a second files/write, and a third chance for the pair to disagree.

State that outlives a restart goes to a file beside settings.json in the OS config directory. apps/tutorial.rs is the short example.

Errors

use helve_rpc::{RpcError, INTERNAL_ERROR, INVALID_PARAMS, METHOD_NOT_FOUND};
Code Use it for
METHOD_NOT_FOUND A method this app does not serve
INVALID_PARAMS Params that are missing, or that name a record the app cannot find
INTERNAL_ERROR A failure inside the app: encoding, the filesystem, the OS

Write the message for the person reading the screen. Name the state, and give the reader a next step:

RpcError::new(
    INVALID_PARAMS,
    format!("no such tutorial: {id}"),
)

Where the rules live

STANDARDS.md §5 covers Rust errors, modules and commands. §1 covers the import layering. src-tauri/src/apps/mod.rs carries the registry, with the reasoning in comments beside the rows.

Clone this wiki locally