diff --git a/README.md b/README.md
index 5cdd45d..da8e1ff 100644
--- a/README.md
+++ b/README.md
@@ -141,7 +141,8 @@ Hyperware processes can handle four types of requests, specified by attributes:
|-----------|-------------|
| `#[local]` | Handles local (same-node) requests |
| `#[remote]` | Handles remote (cross-node) requests |
-| `#[http]` | Handles ALL HTTP requests (GET, POST, PUT, DELETE, etc.) |
+| `#[http]` | Handles ALL HTTP requests (GET, POST, PUT, DELETE, etc.) |
+| `#[terminal]` | Handles terminal requests from the system terminal |
| `#[eth]` | Handles Ethereum subscription updates from your RPC provider |
These attributes can be combined to make a handler respond to multiple request types:
@@ -164,9 +165,83 @@ fn handle_any_http(&mut self) -> String {
fn get_status(&mut self) -> String {
format!("Status: {}", self.counter)
}
+
+#[terminal]
+fn handle_terminal_command(&mut self, command: String) -> String {
+ match command.as_str() {
+ "status" => format!("Counter: {}", self.counter),
+ "reset" => {
+ self.counter = 0;
+ "Counter reset".to_string()
+ }
+ _ => "Unknown command".to_string()
+ }
+}
+```
+
+#### Messaging Terminal Handlers
+
+To send messages to terminal handlers from the Hyperdrive terminal, use the `m` command:
+
+```bash
+m - message a process
+Usage: m
+Arguments:
+ hns address e.g. some-node.os@process:pkg:publisher.os
+ json payload wrapped in single quotes, e.g. '{"foo": "bar"}'
+Options:
+ -a, --await await the response, timing out after SECONDS
+Example:
+ m -a 5 our@foo:bar:baz '{"some payload": "value"}'
+ - this will await the response and print it out
+ m our@foo:bar:baz '{"some payload": "value"}'
+ - this one will not await the response or print it out
```
-The function arguments and the return values _have_ to be serializable with `Serde`.
+For terminal handlers, the JSON body should match the generated request enum variant. Here's a complete example:
+
+**Handler Implementation:**
+```rust
+#[terminal]
+fn handle_terminal_command(&mut self, command: String) {
+ match command.as_str() {
+ "status" => kiprintln!("Counter: {}", self.counter),
+ "reset" => {
+ self.counter = 0;
+ kiprintln!("Counter reset");
+ },
+ "increment" => {
+ self.counter += 1;
+ kiprintln!("Counter incremented to: {}", self.counter);
+ },
+ "help" => kiprintln!("Available commands: status, reset, increment, help"),
+ _ => kiprintln!("Unknown command. Type 'help' for available commands.")
+ }
+}
+```
+
+**Messaging the Terminal Handler:**
+```bash
+# Get current status (output will be printed to terminal)
+m our@my-process:my-package:my-publisher.os '{"HandleTerminalCommand": "status"}'
+
+# Reset the counter (output will be printed to terminal)
+m our@my-process:my-package:my-publisher.os '{"HandleTerminalCommand": "reset"}'
+
+# Increment counter (output will be printed to terminal)
+m our@my-process:my-package:my-publisher.os '{"HandleTerminalCommand": "increment"}'
+
+# Get help (output will be printed to terminal)
+m our@my-process:my-package:my-publisher.os '{"HandleTerminalCommand": "help"}'
+```
+
+**Important Notes:**
+- Terminal handlers should **not** have a return value
+- All output should be handled with `kiprintln!()` or logging functions
+
+```
+
+The function arguments _have_ to be serializable with `Serde`, but return values are not used.
#### HTTP Method Support and Smart Routing
@@ -600,6 +675,18 @@ impl AsyncRequesterState {
self.request_count
}
+ #[terminal]
+ fn handle_terminal(&mut self, command: String) -> String {
+ match command.as_str() {
+ "stats" => format!("Requests processed: {}", self.request_count),
+ "clear" => {
+ self.request_count = 0;
+ "Request count cleared".to_string()
+ }
+ _ => "Available commands: stats, clear".to_string()
+ }
+ }
+
#[ws]
fn websocket(&mut self, channel_id: u32, message_type: WsMessageType, blob: LazyLoadBlob) {
// Process WebSocket messages
@@ -1406,6 +1493,9 @@ async fn get_user(&mut self, id: u64) -> User { ... }
#[local]
#[remote]
fn update_settings(&mut self, settings: Settings, apply_now: bool) -> bool { ... }
+
+#[terminal]
+fn execute_command(&mut self, cmd: String) -> String { ... }
```
The macro generates these enums:
@@ -1414,11 +1504,13 @@ The macro generates these enums:
enum Request {
GetUser(u64),
UpdateSettings(Settings, bool),
+ ExecuteCommand(String),
}
enum Response {
GetUser(User),
UpdateSettings(bool),
+ ExecuteCommand(String),
}
```
diff --git a/src/lib.rs b/src/lib.rs
index 166528e..a28e3a3 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -46,6 +46,7 @@ struct FunctionMetadata {
is_local: bool, // Has #[local] attribute
is_remote: bool, // Has #[remote] attribute
is_http: bool, // Has #[http] attribute
+ is_terminal: bool, // Has #[terminal] attribute
is_eth: bool, // Has #[eth] attribute
http_methods: Vec, // HTTP methods this handler accepts (GET, POST, etc.)
http_path: Option, // Specific path this handler is bound to (optional)
@@ -57,6 +58,7 @@ enum HandlerType {
Local,
Remote,
Http,
+ Terminal,
Eth,
}
@@ -65,6 +67,7 @@ struct HandlerGroups<'a> {
local: Vec<&'a FunctionMetadata>,
remote: Vec<&'a FunctionMetadata>,
http: Vec<&'a FunctionMetadata>,
+ terminal: Vec<&'a FunctionMetadata>,
// New group for combined handlers (used for local messages that can also use remote handlers)
local_and_remote: Vec<&'a FunctionMetadata>,
}
@@ -80,6 +83,8 @@ impl<'a> HandlerGroups<'a> {
// Collect HTTP handlers
let http: Vec<_> = metadata.iter().filter(|f| f.is_http).collect();
+ // Collect terminal handlers
+ let terminal: Vec<_> = metadata.iter().filter(|f| f.is_terminal).collect();
// Create a combined list of local and remote handlers for local messages
// We first include all local handlers, then add remote handlers that aren't already covered
@@ -98,6 +103,7 @@ impl<'a> HandlerGroups<'a> {
local,
remote,
http,
+ terminal,
local_and_remote,
}
}
@@ -108,6 +114,7 @@ struct HandlerDispatch {
local: proc_macro2::TokenStream,
remote: proc_macro2::TokenStream,
http: proc_macro2::TokenStream,
+ terminal: proc_macro2::TokenStream,
local_and_remote: proc_macro2::TokenStream,
}
@@ -370,6 +377,7 @@ fn clean_impl_block(impl_block: &ItemImpl) -> ItemImpl {
&& !attr.path().is_ident("local")
&& !attr.path().is_ident("remote")
&& !attr.path().is_ident("eth")
+ && !attr.path().is_ident("terminal")
&& !attr.path().is_ident("ws")
&& !attr.path().is_ident("ws_client")
});
@@ -683,6 +691,27 @@ fn validate_eth_handler(method: &syn::ImplItemFn) -> syn::Result<()> {
Ok(())
}
+/// Validate the terminal handler signature
+fn validate_terminal_handler(method: &syn::ImplItemFn) -> syn::Result<()> {
+ // Ensure first param is &mut self
+ if !has_valid_self_receiver(method) {
+ return Err(syn::Error::new_spanned(
+ &method.sig,
+ "Terminal handler must take &mut self as their first parameter",
+ ));
+ }
+
+ // Validate return type (must be unit)
+ if !matches!(method.sig.output, ReturnType::Default) {
+ return Err(syn::Error::new_spanned(
+ &method.sig.output,
+ "Terminal handlers must not return a value. Use kiprintln!() for output instead of return values.",
+ ));
+ }
+
+ Ok(())
+}
+
//------------------------------------------------------------------------------
// Method Analysis Functions
//------------------------------------------------------------------------------
@@ -716,11 +745,12 @@ fn analyze_methods(
let has_remote = has_attribute(method, "remote");
let has_eth = has_attribute(method, "eth");
let has_ws = has_attribute(method, "ws");
+ let has_terminal = has_attribute(method, "terminal");
let has_ws_client = has_attribute(method, "ws_client");
// Handle init method
if has_init {
- if has_http || has_local || has_remote || has_eth || has_ws || has_ws_client {
+ if has_http || has_local || has_remote || has_eth || has_ws || has_ws_client || has_terminal {
return Err(syn::Error::new_spanned(
method,
"#[init] cannot be combined with other attributes",
@@ -743,7 +773,7 @@ fn analyze_methods(
// Handle WebSocket method
if has_ws {
- if has_http || has_local || has_remote || has_eth || has_init || has_ws_client {
+ if has_http || has_local || has_remote || has_eth || has_init || has_ws_client || has_terminal {
return Err(syn::Error::new_spanned(
method,
"#[ws] cannot be combined with other attributes",
@@ -765,7 +795,7 @@ fn analyze_methods(
// Handle WebSocket client method
if has_ws_client {
- if has_http || has_local || has_remote || has_eth || has_init || has_ws {
+ if has_http || has_local || has_remote || has_eth || has_init || has_ws || has_terminal {
return Err(syn::Error::new_spanned(
method,
"#[ws_client] cannot be combined with other attributes",
@@ -787,7 +817,7 @@ fn analyze_methods(
// Handle ETH method
if has_eth {
- if has_http || has_local || has_remote || has_init || has_ws || has_ws_client {
+ if has_http || has_local || has_remote || has_init || has_ws || has_ws_client || has_terminal {
return Err(syn::Error::new_spanned(
method,
"#[eth] cannot be combined with other attributes",
@@ -807,10 +837,14 @@ fn analyze_methods(
// Continue with regular processing for function metadata
}
- // Handle request-response methods (local, remote, http - NOT eth)
- if has_http || has_local || has_remote {
+ // Handle request-response methods (local, remote, http, terminal - NOT eth)
+ if has_http || has_local || has_remote || has_terminal {
+ // Validate terminal handlers specifically
+ if has_terminal {
+ validate_terminal_handler(method)?;
+ }
validate_request_response_function(method)?;
- let metadata = extract_function_metadata(method, has_local, has_remote, has_http, false);
+ let metadata = extract_function_metadata(method, has_local, has_remote, has_http, has_terminal, false);
// Parameter-less HTTP handlers can optionally specify a path, but it's not required
// They can use get_path() and get_method() to handle requests dynamically
@@ -824,7 +858,7 @@ fn analyze_methods(
if function_metadata.is_empty() {
return Err(syn::Error::new(
proc_macro2::Span::call_site(),
- "You must specify at least one handler with #[remote], #[local] or #[http] attribute. Without any handlers, this hyperprocess wouldn't respond to any requests.",
+ "You must specify at least one handler with #[remote], #[local], #[terminal] or #[http] attribute. Without any handlers, this hyperprocess wouldn't respond to any requests.",
));
}
@@ -878,6 +912,7 @@ fn extract_function_metadata(
is_local: bool,
is_remote: bool,
is_http: bool,
+ is_terminal: bool,
is_eth: bool,
) -> FunctionMetadata {
let ident = method.sig.ident.clone();
@@ -922,6 +957,7 @@ fn extract_function_metadata(
is_local,
is_remote,
is_http,
+ is_terminal,
is_eth,
http_methods,
http_path,
@@ -1053,6 +1089,7 @@ fn generate_handler_dispatch(
HandlerType::Local => "No local handlers defined but received a local request",
HandlerType::Remote => "No remote handlers defined but received a remote request",
HandlerType::Http => "No HTTP handlers defined but received an HTTP request",
+ HandlerType::Terminal => "No terminal handlers defined but received a terminal request",
HandlerType::Eth => "No ETH handlers defined but received an ETH request",
};
return quote! {
@@ -1064,6 +1101,7 @@ fn generate_handler_dispatch(
HandlerType::Local => "local",
HandlerType::Remote => "remote",
HandlerType::Http => "http",
+ HandlerType::Terminal => "terminal",
HandlerType::Eth => "eth",
};
@@ -1121,6 +1159,14 @@ fn generate_response_handling(
resp.send().unwrap();
}
}
+ HandlerType::Terminal => {
+ quote! {
+ // Instead of wrapping in HPMResponse enum, directly serialize the result
+ let resp = hyperware_process_lib::Response::new()
+ .body(serde_json::to_vec(&result).unwrap());
+ resp.send().unwrap();
+ }
+ }
HandlerType::Eth => {
quote! {
// Instead of wrapping in HPMResponse enum, directly serialize the result
@@ -1947,6 +1993,8 @@ fn generate_message_handlers(
generate_local_message_handler(self_ty, local_and_remote_request_match_arms);
let remote_message_handler =
generate_remote_message_handler(self_ty, remote_request_match_arms);
+ let terminal_message_handler =
+ generate_terminal_message_handler(self_ty, &handler_arms.terminal);
let eth_message_handler =
generate_eth_message_handler(self_ty, eth_method_call);
@@ -1987,10 +2035,44 @@ fn generate_message_handlers(
#local_message_handler
#remote_message_handler
+ #terminal_message_handler
#eth_message_handler
}
}
+/// Generate terminal message handler
+fn generate_terminal_message_handler(
+ self_ty: &Box,
+ match_arms: &proc_macro2::TokenStream,
+) -> proc_macro2::TokenStream {
+ quote! {
+ /// Handle terminal messages
+ fn handle_terminal_message(state: *mut #self_ty, message: hyperware_process_lib::Message) {
+ hyperware_process_lib::logging::debug!("Processing terminal message from: {:?}", message.source());
+ match serde_json::from_slice::(message.body()) {
+ Ok(request) => {
+ unsafe {
+ #match_arms
+ hyperware_process_lib::hyperapp::maybe_save_state(&mut *state);
+ }
+ },
+ Err(e) => {
+ let raw_body = String::from_utf8_lossy(message.body());
+ hyperware_process_lib::logging::error!(
+ "Failed to deserialize terminal request into HPMRequest enum.\n\
+ Error: {}\n\
+ Source: {:?}\n\
+ Body: {}\n\
+ \n\
+ 💡 This usually means the message format doesn't match any of your #[terminal] handlers.",
+ e, message.source(), raw_body
+ );
+ }
+ }
+ }
+ }
+}
+
/// Generate ETH message handler
fn generate_eth_message_handler(
self_ty: &Box,
@@ -2199,6 +2281,8 @@ fn generate_component_impl(
hyperware_process_lib::Message::Request { .. } => {
if message.is_local() && message.source().process == "http-server:distro:sys" {
handle_http_server_message(&mut state, message);
+ } else if message.is_local() && message.source().package_id().to_string() == "terminal:sys" {
+ handle_terminal_message(&mut state, message);
} else if message.is_local() && message.source().process == "http-client:distro:sys" {
handle_websocket_client_message(&mut state, message);
} else if message.is_local() && message.source().process == "eth:distro:sys" {
@@ -2282,6 +2366,7 @@ pub fn hyperprocess(attr: TokenStream, item: TokenStream) -> TokenStream {
remote: generate_handler_dispatch(&handlers.remote, self_ty, HandlerType::Remote),
// HTTP dispatch arms are only generated for handlers with parameters.
http: generate_handler_dispatch(&http_handlers_with_params, self_ty, HandlerType::Http),
+ terminal: generate_handler_dispatch(&handlers.terminal, self_ty, HandlerType::Terminal),
// Generate dispatch for combined local and remote handlers
local_and_remote: generate_handler_dispatch(
&handlers.local_and_remote,