Split protocol and leaf surfaces into crates

Move the protocol runtime into unshell-protocol and remote shell leaf code into unshell-leaves so endpoint and TUI roles can compile independently without circular dependencies.
This commit is contained in:
Michael Mikovsky
2026-04-26 12:39:06 -06:00
parent 74f08333ae
commit d4100d0604
41 changed files with 435 additions and 195 deletions
Generated
+19 -1
View File
@@ -1440,11 +1440,21 @@ name = "unshell"
version = "0.1.0"
dependencies = [
"chrono",
"portable-pty",
"rkyv",
"static_init",
"thiserror 2.0.18",
"unshell-leaves",
"unshell-macros",
"unshell-protocol",
]
[[package]]
name = "unshell-leaves"
version = "0.1.0"
dependencies = [
"portable-pty",
"rkyv",
"unshell",
]
[[package]]
@@ -1456,6 +1466,14 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "unshell-protocol"
version = "0.1.0"
dependencies = [
"rkyv",
"unshell-macros",
]
[[package]]
name = "ush-obfuscate"
version = "0.1.0"
+8 -1
View File
@@ -5,6 +5,8 @@ members = [
"ush-obfuscate",
"base62",
"unshell-macros",
"unshell-protocol",
"unshell-leaves",
"treetest",
]
resolver = "2"
@@ -27,6 +29,8 @@ quote = "1.0.45"
proc-macro2 = "1.0.106"
portable-pty = "0.9.0"
unshell = { path = "." }
unshell-protocol = { path = "./unshell-protocol" }
unshell-leaves = { path = "./unshell-leaves" }
# ush-obfuscate = { path = "./ush-obfuscate" }
# base62 = { path = "./base62" }
@@ -50,7 +54,10 @@ chrono = { workspace = true, optional = true }
# ush-obfuscate = { workspace = true }
static_init = { workspace = true }
unshell-macros = { path = "./unshell-macros" }
portable-pty = { workspace = true }
unshell-protocol = { workspace = true }
[dev-dependencies]
unshell-leaves = { workspace = true, features = ["endpoint"] }
[[example]]
name = "leaf_derive"
+1 -3
View File
@@ -4,15 +4,13 @@
//! example over TCP, feeds inbound frames into the `ProcedureRuntime`, and flushes any resulting
//! protocol frames back to the controller.
#[path = "../../src/leaf/remote_shell/mod.rs"]
mod remote_shell;
use std::error::Error;
use std::net::TcpStream;
use std::sync::mpsc::RecvTimeoutError;
use std::time::Duration;
use unshell::protocol::tree::Ingress;
use unshell_leaves::remote_shell;
fn main() -> Result<(), Box<dyn Error>> {
let mut stream = TcpStream::connect(remote_shell::LISTEN_ADDR)?;
+1 -3
View File
@@ -3,13 +3,11 @@
//! This binary listens for the endpoint example, opens one remote shell session, sends a few
//! commands, and prints returned hook data until the shell closes.
#[path = "../../src/leaf/remote_shell/mod.rs"]
mod remote_shell;
use std::error::Error;
use std::net::TcpListener;
use unshell::protocol::tree::{Endpoint, EndpointOutcome, Ingress, LocalEvent};
use unshell_leaves::remote_shell;
fn main() -> Result<(), Box<dyn Error>> {
let listener = TcpListener::bind(remote_shell::LISTEN_ADDR)?;
@@ -5,13 +5,11 @@
//! a shell process, so it is the easiest place to see how the endpoint and leaf metadata fit
//! together.
#[path = "../../src/leaf/remote_shell/mod.rs"]
mod remote_shell;
use std::error::Error;
use unshell::protocol::tree::{EndpointOutcome, LocalEvent, ProtocolEndpoint};
use unshell::protocol::{INTROSPECTION_PROCEDURE_ID, LeafIntrospection};
use unshell_leaves::remote_shell;
fn main() -> Result<(), Box<dyn Error>> {
let mut endpoint = ProtocolEndpoint::new(
-1
View File
@@ -1 +0,0 @@
pub mod remote_shell;
-159
View File
@@ -1,159 +0,0 @@
//! Stateful remote shell leaf used by the protocol examples.
//!
//! # Design
//!
//! The leaf owns all live hook sessions explicitly in `sessions`. Each entry in
//! that map is one `ProcedureOpen`, keyed by the caller-owned hook identity.
//! The protocol runtime still owns packet validation and transport close state,
//! while the procedure session owns application resources such as the spawned
//! shell process.
//!
//! This keeps the storage obvious:
//! - the leaf owns session maps
//! - the procedure type owns one hook conversation
//! - the runtime routes later `Data` and `Fault` packets automatically
mod errors;
mod session;
mod transport;
use std::collections::BTreeMap;
use unshell::Leaf;
use unshell::protocol::tree::{
Call, HookKey, Procedure, ProcedureEffect, ProcedureRuntime, ProcedureStore, ProtocolEndpoint,
};
pub use errors::ShellLeafError;
pub use session::ProcedureOpen;
pub use transport::LISTEN_ADDR;
/// Leaf state for the remote shell example.
///
/// The map is explicit on purpose. Stateful procedures are easier to debug when
/// the leaf clearly owns its live sessions instead of relying on generated hidden
/// enums or side tables.
#[derive(Default, Leaf)]
#[leaf(leaf_name = "remote_shell")]
pub struct RemoteShellLeaf {
sessions: BTreeMap<HookKey, ProcedureOpen>,
}
impl ProcedureStore<ProcedureOpen> for RemoteShellLeaf {
fn procedure_sessions(&mut self) -> &mut BTreeMap<HookKey, ProcedureOpen> {
&mut self.sessions
}
}
impl Procedure<RemoteShellLeaf> for ProcedureOpen {
type Error = ShellLeafError;
type Input = ();
fn open(_leaf: &mut RemoteShellLeaf, call: Call<Self::Input>) -> Result<Self, Self::Error> {
let hook_key = call.response_hook.ok_or(ShellLeafError::MissingHook)?;
ProcedureOpen::spawn(hook_key.return_path, hook_key.hook_id, call.procedure_id)
}
fn on_data(
_leaf: &mut RemoteShellLeaf,
session: &mut Self,
data: unshell::protocol::tree::IncomingData,
) -> Result<ProcedureEffect, Self::Error> {
session.on_data(data)
}
fn on_fault(
_leaf: &mut RemoteShellLeaf,
_session: &mut Self,
_fault: unshell::protocol::tree::IncomingFault,
) -> Result<(), Self::Error> {
Ok(())
}
fn poll(
_leaf: &mut RemoteShellLeaf,
session: &mut Self,
) -> Result<ProcedureEffect, Self::Error> {
session.poll()
}
fn close(_leaf: &mut RemoteShellLeaf, mut session: Self) -> Result<(), Self::Error> {
session.terminate()
}
}
/// Returns the example endpoint path used by both shell binaries.
pub fn agent_path() -> Vec<String> {
path(&["agent"])
}
/// Builds the controller endpoint used by the receiver example.
#[allow(dead_code)]
pub fn build_controller_endpoint() -> ProtocolEndpoint {
ProtocolEndpoint::new(
Vec::new(),
None,
vec![unshell::protocol::tree::ChildRoute::registered(agent_path())],
Vec::new(),
)
}
/// Builds the stateful shell runtime used by the endpoint example.
#[allow(dead_code)]
pub fn build_agent_runtime() -> ProcedureRuntime<RemoteShellLeaf, ProcedureOpen> {
let endpoint = ProtocolEndpoint::new(
agent_path(),
Some(Vec::new()),
Vec::new(),
vec![unshell::protocol::tree::LeafSpec {
name: RemoteShellLeaf::protocol_leaf_name(),
procedures: vec![ProcedureOpen::protocol_procedure_id()],
}],
);
ProcedureRuntime::new(endpoint, RemoteShellLeaf::default())
}
/// Returns the canonical leaf id used by the receiver example.
#[allow(dead_code)]
pub fn shell_leaf_name() -> String {
RemoteShellLeaf::protocol_leaf_name()
}
/// Returns the opening `procedure_id` used to create one shell session.
#[allow(dead_code)]
pub fn shell_open_procedure() -> String {
ProcedureOpen::protocol_procedure_id()
}
/// Encodes the empty opening payload used by the shell example.
#[allow(dead_code)]
pub fn shell_open_payload() -> Vec<u8> {
unshell::protocol::tree::encode_call_reply(&()).expect("unit shell open payload should encode")
}
#[allow(dead_code)]
pub fn send_forward(
stream: &mut std::net::TcpStream,
outcome: unshell::protocol::tree::EndpointOutcome,
) -> std::io::Result<()> {
transport::send_forward(stream, outcome)
}
#[allow(dead_code)]
pub fn write_frames(
stream: &mut std::net::TcpStream,
frames: &[unshell::protocol::FrameBytes],
) -> std::io::Result<()> {
transport::write_frames(stream, frames)
}
#[allow(dead_code)]
pub fn spawn_frame_reader(
stream: std::net::TcpStream,
) -> std::sync::mpsc::Receiver<std::io::Result<unshell::protocol::FrameBytes>> {
transport::spawn_frame_reader(stream)
}
fn path(parts: &[&str]) -> Vec<String> {
parts.iter().map(|part| (*part).to_owned()).collect()
}
+4 -1
View File
@@ -18,7 +18,10 @@ pub extern crate alloc;
extern crate self as unshell;
pub mod logger;
pub mod protocol;
/// Re-export the protocol crate behind the historical `unshell::protocol` path so
/// proc-macro output and downstream code do not need a second migration.
pub use unshell_protocol as protocol;
pub use unshell_macros::{Leaf, Procedure, procedures};
+28
View File
@@ -0,0 +1,28 @@
[package]
name = "unshell-leaves"
version.workspace = true
edition.workspace = true
description = "Application-layer UnShell leaves and client surfaces"
[features]
default = []
endpoint = ["dep:portable-pty"]
tui = []
[dependencies]
rkyv = { workspace = true }
portable-pty = { workspace = true, optional = true }
unshell = { workspace = true }
[lints.rust]
elided_lifetimes_in_paths = "warn"
future_incompatible = { level = "warn", priority = -1 }
nonstandard_style = { level = "warn", priority = -1 }
rust_2018_idioms = { level = "warn", priority = -1 }
rust_2021_prelude_collisions = "warn"
semicolon_in_expressions_from_macros = "warn"
unsafe_op_in_unsafe_fn = "warn"
unused_import_braces = "warn"
unused_lifetimes = "warn"
trivial_casts = "allow"
missing_docs = "warn"
+79
View File
@@ -0,0 +1,79 @@
//! Application-layer leaves and user-facing surfaces built on top of the UnShell
//! protocol runtime.
//!
//! Each leaf module always exports its shared protocol-facing types. Role-specific
//! implementations are selected with the crate-wide `endpoint` and `tui`
//! features, and can optionally be re-exported behind one stable alias.
use unshell::protocol::DataMessage;
/// Re-exports one role-specific type behind a stable public alias.
///
/// This keeps consumers on a single name such as `RemoteShell` while still
/// compiling only the role implementation needed by the current binary.
#[macro_export]
macro_rules! role_leaf {
(
$(#[$meta:meta])*
$vis:vis type $alias:ident {
endpoint => $endpoint:path,
tui => $tui:path $(,)?
}
) => {
#[cfg(all(feature = "endpoint", feature = "tui"))]
compile_error!(concat!(
"`",
stringify!($alias),
"` can only alias one concrete role at a time; enable either `endpoint` or `tui`, not both"
));
#[cfg(feature = "endpoint")]
$(#[$meta])*
$vis type $alias = $endpoint;
#[cfg(all(not(feature = "endpoint"), feature = "tui"))]
$(#[$meta])*
$vis type $alias = $tui;
};
}
/// Minimal leaf-specific TUI contract.
///
/// The initial implementation intentionally stays transport-agnostic. A CLI can
/// feed validated protocol `DataMessage` values into a leaf TUI and ask it for a
/// textual frame without depending on a specific rendering crate yet.
pub trait LeafTui {
/// Returns the canonical protocol leaf name this UI understands.
fn leaf_name(&self) -> String;
/// Applies one inbound hook payload to the local UI state.
fn handle_data(&mut self, message: &DataMessage) -> Result<(), TuiError>;
/// Produces the current textual frame for the leaf.
fn render(&self) -> String;
}
/// Lightweight error used by the leaf TUI surface.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TuiError {
message: String,
}
impl TuiError {
/// Creates one UI-surface error from owned text.
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
}
}
}
impl core::fmt::Display for TuiError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(&self.message)
}
}
impl core::error::Error for TuiError {}
pub mod remote_shell;
@@ -0,0 +1,96 @@
//! PTY-backed endpoint implementation for the remote shell leaf.
mod errors;
mod session;
mod transport;
use std::collections::BTreeMap;
use unshell::Leaf;
use unshell::protocol::tree::{
Call, HookKey, Procedure, ProcedureEffect, ProcedureRuntime, ProcedureStore, ProtocolEndpoint,
};
pub use errors::ShellLeafError;
pub use session::ProcedureOpen;
pub use transport::{LISTEN_ADDR, send_forward, spawn_frame_reader, write_frames};
use super::{OpenRequest, agent_path};
/// Leaf state for the remote shell endpoint runtime.
///
/// The endpoint keeps each live shell session in an explicit map keyed by the
/// caller-owned hook identity. That makes ownership and cleanup of hook-backed
/// shell processes easy to inspect during debugging.
#[derive(Default, Leaf)]
#[leaf(leaf_name = "remote_shell")]
pub struct RemoteShellEndpoint {
sessions: BTreeMap<HookKey, ProcedureOpen>,
}
impl ProcedureStore<ProcedureOpen> for RemoteShellEndpoint {
fn procedure_sessions(&mut self) -> &mut BTreeMap<HookKey, ProcedureOpen> {
&mut self.sessions
}
}
impl Procedure<RemoteShellEndpoint> for ProcedureOpen {
type Error = ShellLeafError;
type Input = OpenRequest;
fn open(_leaf: &mut RemoteShellEndpoint, call: Call<Self::Input>) -> Result<Self, Self::Error> {
let hook_key = call.response_hook.ok_or(ShellLeafError::MissingHook)?;
ProcedureOpen::spawn(hook_key.return_path, hook_key.hook_id, call.procedure_id)
}
fn on_data(
_leaf: &mut RemoteShellEndpoint,
session: &mut Self,
data: unshell::protocol::tree::IncomingData,
) -> Result<ProcedureEffect, Self::Error> {
session.on_data(data)
}
fn on_fault(
_leaf: &mut RemoteShellEndpoint,
_session: &mut Self,
_fault: unshell::protocol::tree::IncomingFault,
) -> Result<(), Self::Error> {
Ok(())
}
fn poll(
_leaf: &mut RemoteShellEndpoint,
session: &mut Self,
) -> Result<ProcedureEffect, Self::Error> {
session.poll()
}
fn close(_leaf: &mut RemoteShellEndpoint, mut session: Self) -> Result<(), Self::Error> {
session.terminate()
}
}
/// Builds the controller endpoint used by the receiver example.
pub fn build_controller_endpoint() -> ProtocolEndpoint {
ProtocolEndpoint::new(
Vec::new(),
None,
vec![unshell::protocol::tree::ChildRoute::registered(agent_path())],
Vec::new(),
)
}
/// Builds the stateful shell runtime used by the endpoint example.
pub fn build_agent_runtime() -> ProcedureRuntime<RemoteShellEndpoint, ProcedureOpen> {
let endpoint = ProtocolEndpoint::new(
agent_path(),
Some(Vec::new()),
Vec::new(),
vec![unshell::protocol::tree::LeafSpec {
name: RemoteShellEndpoint::protocol_leaf_name(),
procedures: vec![ProcedureOpen::protocol_procedure_id()],
}],
);
ProcedureRuntime::new(endpoint, RemoteShellEndpoint::default())
}
@@ -1,9 +1,12 @@
use std::fmt;
use std::io;
/// Error produced by the remote shell endpoint implementation.
#[derive(Debug)]
pub enum ShellLeafError {
/// Underlying PTY or I/O failure.
Io(io::Error),
/// Shell open requires a response hook so the session can stream bytes back.
MissingHook,
}
@@ -1,13 +1,9 @@
//! Per-hook remote shell session lifecycle.
//!
//! A session opens one PTY-backed shell process and then translates protocol hook
//! traffic into stdin writes and stdout/stderr chunks. The close model is
//! intentionally two-sided:
//! - peer end: the caller sets `end_hook`, so no more stdin is accepted
//! - local end: the shell process exits and the PTY reader drains completely
//!
//! Only after both conditions are observed does the session emit its final empty
//! `end_hook` packet back through the protocol runtime.
//! A session opens one PTY-backed shell process and translates protocol hook
//! traffic into stdin writes and stdout or stderr chunks. Close is intentionally
//! two-sided: the peer signals input completion with `end_hook`, while the local
//! side closes only after the child exits and the PTY reader drains.
use std::io::{self, Read, Write};
use std::process::Command;
@@ -15,18 +11,18 @@ use std::sync::mpsc::{self, Receiver, SyncSender, TryRecvError};
use std::thread;
use portable_pty::{CommandBuilder, ExitStatus, PtySize, native_pty_system};
use unshell::Procedure;
use unshell::protocol::tree::{IncomingData, OutgoingData, ProcedureEffect};
use unshell::Procedure;
use super::RemoteShellEndpoint;
use super::errors::ShellLeafError;
/// Per-hook shell session created by the `open` procedure.
///
/// The procedure type is also the stored session type. This keeps the mapping
/// between protocol procedure and hook state direct and easy to inspect.
/// The procedure type is also the stored session type so the mapping between
/// one opening procedure and one live hook remains direct and visible.
#[derive(Procedure)]
#[procedure(leaf = RemoteShellLeaf, name = "open")]
#[procedure(leaf = RemoteShellEndpoint, name = "open")]
pub struct ProcedureOpen {
/// Spawned PTY child process.
pub(super) child: Box<dyn portable_pty::Child + Send>,
@@ -56,8 +52,6 @@ enum OutputEvent {
ReaderClosed,
}
use super::RemoteShellLeaf;
impl ProcedureOpen {
pub(super) fn spawn(
return_path: Vec<String>,
@@ -173,9 +167,7 @@ impl ProcedureOpen {
}
// Peer end means no more stdin from the caller. Keep the process alive so
// any buffered PTY output can drain through the normal poll path. On Unix
// we also send SIGHUP so an interactive shell treats this like terminal
// hangup instead of waiting forever on the still-open PTY master.
// buffered PTY output can drain through the normal poll path.
self.stdin_tx.take();
self.signal_process_group("-HUP");
Ok(ProcedureEffect::default())
@@ -6,10 +6,11 @@ use std::thread;
use unshell::protocol::FrameBytes;
use unshell::protocol::tree::EndpointOutcome;
/// TCP listen address used by the remote shell examples.
pub const LISTEN_ADDR: &str = "127.0.0.1:4444";
const MAX_FRAME_BYTES: usize = 1024 * 1024;
#[allow(dead_code)]
/// Writes the forwarded frame produced by one endpoint outcome.
pub fn send_forward(stream: &mut TcpStream, outcome: EndpointOutcome) -> io::Result<()> {
match outcome {
EndpointOutcome::Forward { frame, .. } => write_frames(stream, &[frame]),
@@ -17,6 +18,7 @@ pub fn send_forward(stream: &mut TcpStream, outcome: EndpointOutcome) -> io::Res
}
}
/// Writes one or more framed packets onto the example TCP stream.
pub fn write_frames(stream: &mut TcpStream, frames: &[FrameBytes]) -> io::Result<()> {
for frame in frames {
let frame_len = u32::try_from(frame.len()).map_err(|_| {
@@ -29,6 +31,7 @@ pub fn write_frames(stream: &mut TcpStream, frames: &[FrameBytes]) -> io::Result
Ok(())
}
/// Spawns the example frame reader that lifts prefixed frames off the TCP stream.
pub fn spawn_frame_reader(mut stream: TcpStream) -> Receiver<io::Result<FrameBytes>> {
let (tx, rx) = mpsc::sync_channel(64);
@@ -65,10 +68,7 @@ fn read_frame(stream: &mut TcpStream) -> io::Result<Option<FrameBytes>> {
));
}
let mut bytes = vec![0u8; frame_len];
match stream.read_exact(&mut bytes) {
Ok(()) => {}
Err(error) => return Err(error),
}
stream.read_exact(&mut bytes)?;
let mut frame = FrameBytes::with_capacity(bytes.len());
frame.extend_from_slice(&bytes);
+92
View File
@@ -0,0 +1,92 @@
//! Remote shell leaf and its user-facing surfaces.
//!
//! The module always exports the protocol contract for the leaf. Role-specific
//! implementations live behind crate-wide features:
//! - `endpoint` builds the PTY-backed runtime leaf
//! - `tui` builds a placeholder client-side TUI surface
use rkyv::{Archive, Deserialize, Serialize};
#[cfg(feature = "endpoint")]
mod endpoint;
#[cfg(feature = "tui")]
mod tui;
#[cfg(feature = "endpoint")]
pub use endpoint::{
LISTEN_ADDR, RemoteShellEndpoint, ShellLeafError, build_agent_runtime,
build_controller_endpoint, send_forward, spawn_frame_reader, write_frames,
};
#[cfg(feature = "tui")]
pub use tui::RemoteShellTui;
use unshell::protocol::tree::encode_call_reply;
/// Open-request payload for the remote shell leaf.
///
/// The shell currently needs no structured arguments, but a named payload type is
/// easier for downstream code to discover than a bare `()`.
#[derive(Archive, Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq)]
pub struct OpenRequest;
crate::role_leaf! {
/// Feature-selected remote shell surface.
pub type RemoteShell {
endpoint => endpoint::RemoteShellEndpoint,
tui => tui::RemoteShellTui,
}
}
/// Returns the example endpoint path used by the remote shell samples.
pub fn agent_path() -> Vec<String> {
path(&["agent"])
}
/// Returns the canonical leaf id used by endpoint and TUI code.
#[cfg(feature = "endpoint")]
pub fn shell_leaf_name() -> String {
RemoteShellEndpoint::protocol_leaf_name()
}
/// Returns the canonical opening `procedure_id` for the shell leaf.
#[cfg(feature = "endpoint")]
pub fn shell_open_procedure() -> String {
endpoint::ProcedureOpen::protocol_procedure_id()
}
/// Encodes the empty open-request payload used by the shell example.
#[cfg(all(not(feature = "endpoint"), feature = "tui"))]
pub fn shell_leaf_name() -> String {
RemoteShellTui::protocol_leaf_name()
}
/// Returns the canonical opening `procedure_id` for the shell leaf.
#[cfg(all(not(feature = "endpoint"), feature = "tui"))]
pub fn shell_open_procedure() -> String {
let mut procedure_id = shell_leaf_name();
procedure_id.push_str(".open");
procedure_id
}
/// Encodes the empty open-request payload used by the shell example.
#[cfg(not(any(feature = "endpoint", feature = "tui")))]
pub fn shell_leaf_name() -> String {
String::from("remote_shell")
}
/// Returns the canonical opening `procedure_id` for the shell leaf.
#[cfg(not(any(feature = "endpoint", feature = "tui")))]
pub fn shell_open_procedure() -> String {
let mut procedure_id = shell_leaf_name();
procedure_id.push_str(".open");
procedure_id
}
/// Encodes the empty open-request payload used by the shell example.
pub fn shell_open_payload() -> Vec<u8> {
encode_call_reply(&OpenRequest).expect("remote shell open payload should encode")
}
fn path(parts: &[&str]) -> Vec<String> {
parts.iter().map(|part| (*part).to_owned()).collect()
}
+43
View File
@@ -0,0 +1,43 @@
//! Placeholder client-side TUI surface for the remote shell leaf.
//!
//! The first application-layer consumer will be a CLI and later a full GUI. This
//! stub keeps the leaf-specific interpretation point in place without forcing a
//! rendering-library decision yet.
use std::string::String;
use std::vec::Vec;
use unshell::Leaf;
use unshell::protocol::DataMessage;
use crate::{LeafTui, TuiError};
/// Stub TUI surface for the remote shell leaf.
#[derive(Default, Leaf)]
#[leaf(leaf_name = "remote_shell")]
pub struct RemoteShellTui {
transcript: Vec<u8>,
}
impl RemoteShellTui {
/// Returns a short explanation of the current stub status.
pub fn status_line(&self) -> &'static str {
"remote shell TUI stub: rendering is placeholder-only for now"
}
}
impl LeafTui for RemoteShellTui {
fn leaf_name(&self) -> String {
Self::protocol_leaf_name()
}
fn handle_data(&mut self, message: &DataMessage) -> Result<(), TuiError> {
self.transcript.extend_from_slice(&message.data);
Ok(())
}
fn render(&self) -> String {
let body = String::from_utf8_lossy(&self.transcript);
format!("{}\n\n{}", self.status_line(), body)
}
}
+25
View File
@@ -0,0 +1,25 @@
[package]
name = "unshell-protocol"
version.workspace = true
edition.workspace = true
description = "Wire protocol, framing, validation, and endpoint runtime for UnShell"
[lib]
doctest = false
[dependencies]
rkyv = { workspace = true }
unshell-macros = { path = "../unshell-macros" }
[lints.rust]
elided_lifetimes_in_paths = "warn"
future_incompatible = { level = "warn", priority = -1 }
nonstandard_style = { level = "warn", priority = -1 }
rust_2018_idioms = { level = "warn", priority = -1 }
rust_2021_prelude_collisions = "warn"
semicolon_in_expressions_from_macros = "warn"
unsafe_op_in_unsafe_fn = "warn"
unused_import_braces = "warn"
unused_lifetimes = "warn"
trivial_casts = "allow"
missing_docs = "warn"
+20
View File
@@ -0,0 +1,20 @@
//! # UnShell Protocol
//!
//! The protocol crate owns the wire types, framing, validation helpers, and the
//! small tree runtime used by endpoint implementations.
#![no_std]
pub extern crate alloc;
#[allow(unused_extern_crates)]
extern crate self as unshell;
/// Keep the historical nested path so existing imports and proc-macro output can
/// continue to target `unshell::protocol::...` while the implementation lives in
/// its own crate.
pub mod protocol;
pub use protocol::*;
#[cfg(test)]
pub use unshell_macros::{Leaf, Procedure, procedures};