Skip to main content

rmux_client/commands/
config.rs

1use rmux_proto::request::{SetHookMutationRequest, ShowHooksRequest};
2use rmux_proto::types::OptionScopeSelector;
3use rmux_proto::{
4    HookLifecycle, HookName, OptionName, PaneTarget, Request, Response, ScopeSelector,
5    SetEnvironmentMode, SetEnvironmentRequest, SetHookRequest, SetOptionByNameRequest,
6    SetOptionMode, SetOptionRequest, ShowEnvironmentRequest, ShowOptionsRequest, SourceFileRequest,
7};
8use std::path::PathBuf;
9
10use crate::{connection::Connection, ClientError};
11
12impl Connection {
13    /// Sends a `set-option` request over the detached RPC channel.
14    pub fn set_option(
15        &mut self,
16        scope: ScopeSelector,
17        option: OptionName,
18        value: String,
19        mode: SetOptionMode,
20    ) -> Result<Response, ClientError> {
21        let request = SetOptionRequest {
22            scope,
23            option,
24            value,
25            mode,
26        };
27        rmux_core::validate_option_mutation(
28            request.option,
29            &request.scope,
30            request.mode,
31            &request.value,
32        )?;
33        self.roundtrip(&Request::SetOption(request))
34    }
35
36    /// Sends a string-keyed `set-option` request over the detached RPC channel.
37    #[allow(clippy::too_many_arguments)]
38    pub fn set_option_by_name(
39        &mut self,
40        scope: OptionScopeSelector,
41        name: String,
42        value: Option<String>,
43        mode: SetOptionMode,
44        only_if_unset: bool,
45        unset: bool,
46        unset_pane_overrides: bool,
47    ) -> Result<Response, ClientError> {
48        let request = SetOptionByNameRequest {
49            scope,
50            name,
51            value,
52            mode,
53            only_if_unset,
54            unset,
55            unset_pane_overrides,
56            format: false,
57            format_target: None,
58        };
59        rmux_core::validate_option_name_mutation(
60            &request.name,
61            &request.scope,
62            request.mode,
63            request.value.as_deref(),
64            request.unset,
65        )?;
66        self.roundtrip(&Request::SetOptionByName(Box::new(request)))
67    }
68
69    /// Sends a `set-environment` request over the detached RPC channel.
70    pub fn set_environment(
71        &mut self,
72        scope: ScopeSelector,
73        name: String,
74        value: String,
75        mode: Option<SetEnvironmentMode>,
76        hidden: bool,
77        format: bool,
78    ) -> Result<Response, ClientError> {
79        self.roundtrip(&Request::SetEnvironment(Box::new(SetEnvironmentRequest {
80            scope,
81            name,
82            value,
83            mode,
84            hidden,
85            format,
86        })))
87    }
88
89    /// Sends a `set-hook` request over the detached RPC channel.
90    pub fn set_hook(
91        &mut self,
92        scope: ScopeSelector,
93        hook: HookName,
94        command: String,
95        lifecycle: HookLifecycle,
96    ) -> Result<Response, ClientError> {
97        self.roundtrip(&Request::SetHook(SetHookRequest {
98            scope,
99            hook,
100            command,
101            lifecycle,
102        }))
103    }
104
105    /// Sends an extended `set-hook` mutation over the detached RPC channel.
106    #[allow(clippy::too_many_arguments)]
107    pub fn set_hook_mutation(
108        &mut self,
109        scope: ScopeSelector,
110        hook: HookName,
111        command: Option<String>,
112        lifecycle: HookLifecycle,
113        append: bool,
114        unset: bool,
115        run_immediately: bool,
116        index: Option<u32>,
117    ) -> Result<Response, ClientError> {
118        self.roundtrip(&Request::SetHookMutation(SetHookMutationRequest {
119            scope,
120            hook,
121            command,
122            lifecycle,
123            append,
124            unset,
125            run_immediately,
126            index,
127        }))
128    }
129
130    /// Sends a `show-options` request over the detached RPC channel.
131    pub fn show_options(
132        &mut self,
133        scope: OptionScopeSelector,
134        name: Option<String>,
135        value_only: bool,
136        include_inherited: bool,
137        quiet: bool,
138    ) -> Result<Response, ClientError> {
139        self.roundtrip(&Request::ShowOptions(ShowOptionsRequest {
140            scope,
141            name,
142            value_only,
143            include_inherited,
144            quiet,
145        }))
146    }
147
148    /// Sends a `show-environment` request over the detached RPC channel.
149    pub fn show_environment(
150        &mut self,
151        scope: ScopeSelector,
152        name: Option<String>,
153        hidden: bool,
154        shell_format: bool,
155    ) -> Result<Response, ClientError> {
156        self.roundtrip(&Request::ShowEnvironment(ShowEnvironmentRequest {
157            scope,
158            name,
159            hidden,
160            shell_format,
161        }))
162    }
163
164    /// Sends a `show-hooks` request over the detached RPC channel.
165    pub fn show_hooks(
166        &mut self,
167        scope: ScopeSelector,
168        window: bool,
169        pane: bool,
170        hook: Option<HookName>,
171    ) -> Result<Response, ClientError> {
172        self.roundtrip(&Request::ShowHooks(ShowHooksRequest {
173            scope,
174            window,
175            pane,
176            hook,
177        }))
178    }
179
180    /// Sends a `source-file` request over the detached RPC channel.
181    #[allow(clippy::too_many_arguments)]
182    pub fn source_file(
183        &mut self,
184        paths: Vec<String>,
185        quiet: bool,
186        parse_only: bool,
187        verbose: bool,
188        expand_paths: bool,
189        target: Option<PaneTarget>,
190        stdin: Option<String>,
191    ) -> Result<Response, ClientError> {
192        self.roundtrip_without_read_timeout(&Request::SourceFile(Box::new(SourceFileRequest {
193            paths,
194            quiet,
195            parse_only,
196            verbose,
197            expand_paths,
198            target,
199            caller_cwd: current_working_directory(),
200            stdin,
201        })))
202    }
203}
204
205fn current_working_directory() -> Option<PathBuf> {
206    std::env::current_dir().ok()
207}
208
209#[cfg(all(test, unix))]
210mod tests {
211    use std::io::{self, Read};
212    use std::os::unix::net::UnixStream;
213
214    use rmux_proto::{OptionName, RmuxError, ScopeSelector, SessionName, SetOptionMode};
215
216    use super::Connection;
217    use crate::ClientError;
218
219    #[test]
220    fn set_option_rejects_invalid_requests_before_writing_to_the_socket() {
221        let (client_stream, mut server_stream) = UnixStream::pair().expect("create stream pair");
222        server_stream
223            .set_nonblocking(true)
224            .expect("set read end nonblocking");
225        let mut connection = Connection::new(client_stream).expect("connection");
226
227        let error = connection
228            .set_option(
229                ScopeSelector::Session(SessionName::new("alpha").expect("valid session")),
230                OptionName::DefaultTerminal,
231                "tmux-256color".to_owned(),
232                SetOptionMode::Replace,
233            )
234            .expect_err("invalid request should fail");
235
236        assert!(matches!(
237            error,
238            ClientError::Protocol(RmuxError::InvalidSetOption(message))
239                if message == "default-terminal is only supported at global scope"
240        ));
241
242        let mut buffer = [0_u8; 1];
243        let read_error = server_stream
244            .read(&mut buffer)
245            .expect_err("validation should happen before any bytes are written");
246        assert_eq!(read_error.kind(), io::ErrorKind::WouldBlock);
247    }
248}