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