Skip to main content

rmux_client/
lib.rs

1#![deny(missing_docs)]
2
3//! Blocking local client for the RMUX detached RPC protocol.
4//!
5//! This crate provides the transport layer for sending [`rmux_proto::Request`]
6//! frames and receiving [`rmux_proto::Response`] frames over a blocking
7//! local stream. It also exposes nested-session detection through the `$RMUX`
8//! environment variable and raw-terminal lifecycle management for attach-mode
9//! clients.
10
11#[cfg(unix)]
12pub mod attach;
13#[cfg(windows)]
14#[path = "attach_windows.rs"]
15pub mod attach;
16mod attach_lock_state;
17pub mod auto_start;
18pub(crate) mod commands;
19pub mod connection;
20pub mod control;
21mod endpoint_cleanup;
22pub mod nested;
23pub(crate) mod shell_quote;
24pub(crate) mod upgrade;
25
26#[cfg(unix)]
27pub use attach::attach_terminal_with_initial_bytes_and_resize_geometry;
28#[cfg(windows)]
29pub use attach::attach_terminal_with_initial_bytes_and_windows_console_key;
30pub use attach::{
31    attach_terminal, attach_terminal_with_initial_bytes, attach_with_terminal, drive_attach_stream,
32    AttachError, RawTerminal,
33};
34pub use auto_start::{
35    ensure_server_running, ensure_server_running_with_config,
36    ensure_server_running_with_config_outcome, AutoStartConfig, AutoStartConfigSelection,
37    AutoStartError, EnsuredServerConnection, ServerConnectionProvenance, INTERNAL_DAEMON_FLAG,
38};
39#[cfg(windows)]
40pub use commands::server::connect_for_server_shutdown;
41pub use commands::server::StartServerError;
42pub use commands::window::SplitWindowOptions;
43pub use connection::{
44    connect, connect_or_absent, default_socket_path, resolve_socket_path,
45    resolve_tmux_compatible_socket_path, socket_path_for_label, AttachSessionUpgrade,
46    AttachTransition, ConnectResult, Connection, ControlModeUpgrade, ControlTransition,
47};
48pub use control::{drive_control_mode, drive_control_mode_with_stdio};
49pub use endpoint_cleanup::wait_for_server_endpoint_cleanup;
50pub use nested::{
51    detect_context, detect_parent, ensure_nested_context, require_nested_context, ClientContext,
52    ClientContextParent, NestedContextError,
53};
54
55use rmux_proto::RmuxError;
56use std::fmt;
57
58/// Client-side errors for transport and protocol failures.
59#[derive(Debug)]
60pub enum ClientError {
61    /// An I/O error occurred on the local client stream.
62    Io(std::io::Error),
63    /// A protocol framing or encoding error occurred.
64    Protocol(RmuxError),
65    /// Entering or restoring raw terminal mode failed.
66    Attach(AttachError),
67    /// The server closed the connection before sending a complete response frame.
68    UnexpectedEof,
69}
70
71impl fmt::Display for ClientError {
72    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
73        match self {
74            Self::Io(error) => write!(formatter, "i/o error: {error}"),
75            Self::Protocol(error) => write!(formatter, "protocol error: {error}"),
76            Self::Attach(error) => write!(formatter, "attach error: {error}"),
77            Self::UnexpectedEof => formatter
78                .write_str("server closed connection before a complete response frame arrived"),
79        }
80    }
81}
82
83impl std::error::Error for ClientError {
84    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
85        match self {
86            Self::Io(error) => Some(error),
87            Self::Protocol(error) => Some(error),
88            Self::Attach(error) => Some(error),
89            Self::UnexpectedEof => None,
90        }
91    }
92}
93
94impl From<std::io::Error> for ClientError {
95    fn from(error: std::io::Error) -> Self {
96        Self::Io(error)
97    }
98}
99
100impl From<RmuxError> for ClientError {
101    fn from(error: RmuxError) -> Self {
102        Self::Protocol(error)
103    }
104}
105
106impl From<AttachError> for ClientError {
107    fn from(error: AttachError) -> Self {
108        Self::Attach(error)
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use std::error::Error as _;
115    use std::io;
116
117    use super::{AttachError, ClientError};
118
119    #[test]
120    fn client_error_wraps_attach_errors() {
121        let error = ClientError::from(AttachError::Io(io::Error::other("dup failed")));
122
123        assert!(
124            matches!(error, ClientError::Attach(AttachError::Io(_))),
125            "attach errors should preserve their variant information"
126        );
127        assert_eq!(
128            error.to_string(),
129            expected_attach_error_display("dup failed")
130        );
131        assert!(
132            error.source().is_some(),
133            "wrapped attach error should chain"
134        );
135    }
136
137    #[cfg(unix)]
138    fn expected_attach_error_display(message: &str) -> String {
139        format!("attach error: terminal descriptor operation failed: {message}")
140    }
141
142    #[cfg(windows)]
143    fn expected_attach_error_display(message: &str) -> String {
144        format!("attach error: terminal console operation failed: {message}")
145    }
146
147    #[cfg(not(any(unix, windows)))]
148    fn expected_attach_error_display(message: &str) -> String {
149        format!("attach error: terminal descriptor operation failed: {message}")
150    }
151}