ZyrisDocs
Addon crate guides

zyris-terminal

Register a shell addon, choose exec or interactive PTYs, interpret output and manage session lifetimes.

zyris-terminal provides PtyTerminal, implementing terminal v3. It supports two different workflows: one-shot commands and interactive terminal sessions. It does not require a graphical desktop.

Dependencies and registration

[dependencies]
zyris = { git = "https://github.com/attacca-cc/zyris-protocol", features = ["caps"] }
zyris-terminal = { git = "https://github.com/attacca-cc/zyris-protocol" }
use std::time::Duration;
use zyris::caps::TerminalServer;
use zyris::{Node, NodeKind};
use zyris_terminal::PtyTerminal;

fn terminal_node() -> zyris::Result<Node> {
    let terminal = PtyTerminal::rooted("/srv/work")
        .with_idle_timeout(Duration::from_secs(600));
    Node::builder()
        .name("shell")
        .kind(NodeKind::Service)
        .capability(TerminalServer(terminal))
        .build()
}

Add a TLS provider for wss:// and use Connecting to bring the node online. Async calls need an application Tokio runtime. The idle sweeper starts on the first PTY open, not in the constructor.

Pick the right workflow

WorkflowToolsUse when
One-shotexecA command finishes and you want stdout, stderr and an exit status.
Interactive, unaryopen → read / screen / write → closeA shell, REPL or TUI must remain alive between calls.
Interactive, streamingopen_stream + write / resize / closeA typed client can receive raw terminal bytes continuously.

One-shot commands without quoting problems

use std::time::Duration;
use zyris::caps::{Terminal, TerminalClient};
use zyris::Connection;

async fn git_status(conn: &Connection) -> zyris::Result<()> {
    let terminal: TerminalClient = conn.wait_capability(Duration::from_secs(5)).await?;
    let result = terminal.exec(
        None,
        Some(vec!["git".into(), "status".into(), "--short".into()]),
        None,
        Some(10_000),
        None,
        None,
        None,
    ).await?;
    println!("exit={} timed_out={}", result.exit_code, result.timed_out);
    println!("{}", result.stdout);
    Ok(())
}
exec argumentMeaning
commandA shell string. Unix defaults to /bin/sh -c; Windows uses cmd /C.
argvA nonempty program/argument vector, with no shell interpretation.
cwdOptional working directory, relative to the addon root or an absolute path.
timeout_msExecution deadline in milliseconds. Set it explicitly for predictable behavior.
stdinOptional text written to the child; stdin is then closed.
envEntries override the host process environment; this is not a clean environment.
shellUnix shell override in command mode. Ignored in Windows command mode.

Supply exactly one of command and argv. Prefer argv unless you need pipes, redirects or other shell syntax. For a PowerShell script, use argv = ["powershell.exe", "-NoProfile", "-NonInteractive", "-Command", "-"] and put the script in stdin.

Timeout kills the process tree and returns partial output with timed_out = true and exit code -1. Stdout and stderr are each capped at 1 MiB; check their separate truncation flags. Surplus output is drained and discarded so a full pipe does not leave the child blocked.

Interactive sessions

use std::time::Duration;
use zyris::caps::{Settle, Terminal, TerminalClient};
use zyris::Connection;

async fn inspect_shell(conn: &Connection) -> zyris::Result<()> {
    let terminal: TerminalClient = conn.wait_capability(Duration::from_secs(5)).await?;
    let opened = terminal.open(None, 100, 30).await?;
    let result = terminal.screen(
        opened.pty.clone(),
        None,
        Some(Settle { quiet_ms: 0, timeout_ms: 1000 }),
    ).await;
    let cleanup = terminal.close(opened.pty).await;
    let screen = result?;
    cleanup?;
    println!("{}", screen.lines.join("\n"));
    Ok(())
}

open takes an optional shell, columns and rows, returning PtyOpened { pty }. The default shell uses the host's SHELL on Unix, falling back to /bin/sh; Windows uses its platform default.

  • read(pty, input, settle) writes optional text first, then returns new sanitized text. Control sequences are stripped except newlines and tabs.
  • screen(pty, input, settle) returns the current terminal grid, cursor and shell exit code. It does not consume read output. Use it for full-screen programs.
  • write(pty, data: Blob) sends terminal input bytes. resize(pty, cols, rows) updates the PTY.
  • open_stream returns a head with the PTY handle and raw PtyChunk items, including escape codes. An agent's unary caller should use open plus read instead.
  • close(pty) releases the session. Close explicitly even if you drop a streaming subscriber.

Interpret output correctly

FieldMeaning
PtyRead.moreMore bytes remain buffered; another read can retrieve them.
PtyRead.droppedBytes were lost to ring-buffer overflow; another read cannot recover them.
PtyRead.exitedNone while the shell lives; the exit code after it exits.
PtyScreen.linesThe rendered grid, not an append-only output log.

Settle defaults to 300ms of quiet with a 5000ms wait ceiling. Hitting that ceiling does not prove a command finished. An echoed input line or a quiet output interval is not a completion signal; inspect program output and shell state. quiet_ms = 0 snapshots immediately.

Limits and isolation

There are at most eight simultaneous PTYs; the next open fails with too_many_ptys. A session idle for ten minutes is eligible for sweeping by default. Disconnect does not immediately close all sessions; the idle sweep is the fallback, so do not promise instantaneous cleanup on wire loss.

rooted chooses a working directory, not a sandbox. Commands retain the host user's privileges and inherited environment. Use OS-level isolation and application authorization for untrusted callers. Restricting cwd alone cannot restrict what a shell or executable accesses.

Reference

Source and API · Published crate · Released rustdoc

On this page