528 lines
15 KiB
Rust
528 lines
15 KiB
Rust
//! Wire protocol envelopes and error types for Galaxy local control.
|
|
use serde::de::DeserializeOwned;
|
|
use serde::{Deserialize, Serialize};
|
|
use uuid::Uuid;
|
|
|
|
pub use crate::catalog::{
|
|
ActionImplementationStatus, ActionKind, ActionMetadata, ActionParameterSpec, ActionResultSpec,
|
|
PROTOCOL_VERSION, TargetScope,
|
|
};
|
|
pub use crate::selectors::{
|
|
PaneSelector, PaneTarget, SessionSelector, SessionTarget, TabSelector, TabTarget,
|
|
TargetSelector, WindowSelector, WindowTarget,
|
|
};
|
|
|
|
/// Common layout direction values accepted by pane and tab mutations.
|
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum Direction {
|
|
Left,
|
|
Right,
|
|
Up,
|
|
Down,
|
|
Previous,
|
|
Next,
|
|
}
|
|
|
|
/// Tab type accepted by `tab.create` and `window.create`.
|
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum TabType {
|
|
Terminal,
|
|
Agent,
|
|
Default,
|
|
}
|
|
|
|
/// Mode accepted by `tab.activate`.
|
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum TabActivationMode {
|
|
Target,
|
|
Previous,
|
|
Next,
|
|
Last,
|
|
}
|
|
|
|
/// Mode accepted by `tab.close`.
|
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum TabCloseMode {
|
|
Target,
|
|
Active,
|
|
Others,
|
|
RightOf,
|
|
}
|
|
|
|
/// Empty parameters for actions whose catalog parameter spec is `none`.
|
|
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct EmptyParams {}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct ActionNameParams {
|
|
pub action: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct BindingNameParams {
|
|
pub binding_name: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct BooleanValueParams {
|
|
pub value: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct ColorValueParams {
|
|
pub color: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct DirectionParams {
|
|
pub direction: Direction,
|
|
}
|
|
|
|
/// Parameters for opening a file in Galaxy's app/editor state.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct FileOpenParams {
|
|
pub path: String,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub line: Option<u32>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub column: Option<u32>,
|
|
#[serde(default)]
|
|
pub new_tab: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct KeyParams {
|
|
pub key: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct KeyValueParams {
|
|
pub key: String,
|
|
pub value: serde_json::Value,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct NamespaceParams {
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub namespace: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct PageQueryParams {
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub page: Option<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub query: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct QueryParams {
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub query: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct RenameParams {
|
|
pub title: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct ResizeParams {
|
|
pub direction: Direction,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub amount: Option<u32>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct TabActivateParams {
|
|
pub mode: TabActivationMode,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct TabCloseParams {
|
|
pub mode: TabCloseMode,
|
|
}
|
|
|
|
/// Parameters for `tab.create` and `window.create` shell/profile options.
|
|
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct TabCreateParams {
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub tab_type: Option<TabType>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub shell: Option<String>,
|
|
}
|
|
|
|
/// Parameters for submitting a command to an idle terminal session.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct TerminalExecuteParams {
|
|
pub command: String,
|
|
}
|
|
|
|
/// Parameters for interrupting the current command in a terminal session.
|
|
///
|
|
/// `block_id` is required as a compare-and-swap guard so a delayed request
|
|
/// cannot interrupt a newer command.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct TerminalInterruptParams {
|
|
pub block_id: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct TextParams {
|
|
pub text: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct ThemeNameParams {
|
|
pub theme_name: String,
|
|
}
|
|
|
|
pub type KeybindingGetParams = BindingNameParams;
|
|
pub type KeybindingListParams = EmptyParams;
|
|
pub type SettingGetParams = KeyParams;
|
|
pub type SettingListParams = NamespaceParams;
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct ActionListResult {
|
|
pub actions: Vec<ActionMetadata>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct ActionInspectResult {
|
|
pub action: ActionMetadata,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct ActiveTargetChain {
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub instance_id: Option<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub window_id: Option<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub tab_id: Option<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub pane_id: Option<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub session_id: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct ThemeSummary {
|
|
pub name: String,
|
|
pub is_current: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct ThemeListResult {
|
|
pub themes: Vec<ThemeSummary>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct ThemeStateResult {
|
|
pub name: String,
|
|
pub follow_system_theme: bool,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub light_theme: Option<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub dark_theme: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct AppearanceStateResult {
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub theme: Option<String>,
|
|
pub follow_system_theme: bool,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub light_theme: Option<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub dark_theme: Option<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub font_size: Option<u32>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub ui_zoom_percent: Option<u32>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct SettingSummary {
|
|
pub key: String,
|
|
pub value: serde_json::Value,
|
|
pub value_type: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct SettingListResult {
|
|
pub settings: Vec<SettingSummary>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct SettingGetResult {
|
|
pub setting: SettingSummary,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct KeybindingSummary {
|
|
pub name: String,
|
|
pub description: String,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub group: Option<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub keystroke: Option<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub normalized_keystroke: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct KeybindingListResult {
|
|
pub keybindings: Vec<KeybindingSummary>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct KeybindingGetResult {
|
|
pub keybinding: KeybindingSummary,
|
|
}
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SurfaceSummary {
|
|
pub name: String,
|
|
pub is_available: bool,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub unavailable_reason: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct SurfaceListResult {
|
|
pub surfaces: Vec<SurfaceSummary>,
|
|
}
|
|
|
|
/// Snapshot of the active command block in a terminal session.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct TerminalStatusResult {
|
|
pub action: ActionKind,
|
|
pub session_id: String,
|
|
pub active_block_id: String,
|
|
pub is_executing: bool,
|
|
pub is_command_pending: bool,
|
|
pub is_long_running: bool,
|
|
pub is_agent_in_control: bool,
|
|
pub is_idle: bool,
|
|
/// Elapsed wall-clock time for the active running command.
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub running_for_ms: Option<u64>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub command_summary: Option<String>,
|
|
}
|
|
|
|
/// Typed success payloads for catalog actions that need stable structured data.
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
#[serde(tag = "type", rename_all = "snake_case")]
|
|
pub enum ControlResult {
|
|
Acknowledgement { action: ActionKind },
|
|
Metadata { data: serde_json::Value },
|
|
Content { data: serde_json::Value },
|
|
}
|
|
|
|
/// Top-level request sent by a local-control client to a Galaxy instance.
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct RequestEnvelope {
|
|
pub protocol_version: u32,
|
|
pub request_id: Uuid,
|
|
#[serde(default)]
|
|
pub target: TargetSelector,
|
|
pub action: Action,
|
|
}
|
|
|
|
impl RequestEnvelope {
|
|
pub fn new(action: Action) -> Self {
|
|
Self {
|
|
protocol_version: PROTOCOL_VERSION,
|
|
request_id: Uuid::new_v4(),
|
|
target: TargetSelector::default(),
|
|
action,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Requested action and action-specific JSON parameters.
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct Action {
|
|
pub kind: ActionKind,
|
|
#[serde(default)]
|
|
pub params: serde_json::Value,
|
|
}
|
|
|
|
impl Action {
|
|
pub fn new(kind: ActionKind) -> Self {
|
|
Self {
|
|
kind,
|
|
params: serde_json::Value::Object(Default::default()),
|
|
}
|
|
}
|
|
|
|
pub fn with_params<T: Serialize>(kind: ActionKind, params: T) -> Result<Self, ControlError> {
|
|
Ok(Self {
|
|
kind,
|
|
params: serde_json::to_value(params).map_err(|err| {
|
|
ControlError::with_details(
|
|
ErrorCode::InvalidParams,
|
|
format!("failed to serialize {} parameters", kind.as_str()),
|
|
err.to_string(),
|
|
)
|
|
})?,
|
|
})
|
|
}
|
|
|
|
pub fn params_as<T: DeserializeOwned>(&self) -> Result<T, ControlError> {
|
|
serde_json::from_value(self.params.clone()).map_err(|err| {
|
|
ControlError::with_details(
|
|
ErrorCode::InvalidParams,
|
|
format!("failed to decode {} parameters", self.kind.as_str()),
|
|
err.to_string(),
|
|
)
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Top-level response returned by a Galaxy instance for a control request.
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
pub struct ResponseEnvelope {
|
|
pub protocol_version: u32,
|
|
pub request_id: Uuid,
|
|
pub response: ControlResponse,
|
|
}
|
|
|
|
impl ResponseEnvelope {
|
|
pub fn ok(request_id: Uuid, data: serde_json::Value) -> Self {
|
|
Self {
|
|
protocol_version: PROTOCOL_VERSION,
|
|
request_id,
|
|
response: ControlResponse::Ok { data },
|
|
}
|
|
}
|
|
|
|
pub fn error(request_id: Uuid, error: ControlError) -> Self {
|
|
Self {
|
|
protocol_version: PROTOCOL_VERSION,
|
|
request_id,
|
|
response: ControlResponse::Error { error },
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Success or error payload for a control response.
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
#[serde(tag = "status", rename_all = "snake_case")]
|
|
pub enum ControlResponse {
|
|
Ok { data: serde_json::Value },
|
|
Error { error: ControlError },
|
|
}
|
|
|
|
/// Error envelope used when a request cannot be decoded into a full request envelope.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct ErrorResponseEnvelope {
|
|
pub protocol_version: u32,
|
|
pub error: ControlError,
|
|
}
|
|
|
|
impl ErrorResponseEnvelope {
|
|
pub fn new(error: ControlError) -> Self {
|
|
Self {
|
|
protocol_version: PROTOCOL_VERSION,
|
|
error,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Structured error returned by local-control protocol and transport layers.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)]
|
|
#[error("{code}: {message}")]
|
|
pub struct ControlError {
|
|
pub code: ErrorCode,
|
|
pub message: String,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub details: Option<String>,
|
|
}
|
|
|
|
impl ControlError {
|
|
pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
|
|
Self {
|
|
code,
|
|
message: message.into(),
|
|
details: None,
|
|
}
|
|
}
|
|
|
|
pub fn with_details(
|
|
code: ErrorCode,
|
|
message: impl Into<String>,
|
|
details: impl Into<String>,
|
|
) -> Self {
|
|
Self {
|
|
code,
|
|
message: message.into(),
|
|
details: Some(details.into()),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Stable error code surfaced to CLI clients and automation.
|
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum ErrorCode {
|
|
LocalControlDisabled,
|
|
UnauthorizedLocalClient,
|
|
InsufficientPermissions,
|
|
ProtocolVersionUnsupported,
|
|
InvalidRequest,
|
|
InvalidSelector,
|
|
InvalidParams,
|
|
NoInstance,
|
|
AmbiguousInstance,
|
|
AmbiguousTarget,
|
|
StaleTarget,
|
|
TargetStateConflict,
|
|
MissingTarget,
|
|
TransportUnavailable,
|
|
BridgeUnavailable,
|
|
UnsupportedAction,
|
|
NotAllowlisted,
|
|
Internal,
|
|
}
|
|
|
|
impl std::fmt::Display for ErrorCode {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
let value = serde_json::to_value(self).map_err(|_| std::fmt::Error)?;
|
|
let Some(value) = value.as_str() else {
|
|
return Err(std::fmt::Error);
|
|
};
|
|
f.write_str(value)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "protocol_tests.rs"]
|
|
mod tests;
|