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        include_inherited: bool,
135    ) -> Result<Response, ClientError> {
136        self.roundtrip(&Request::ShowOptions(ShowOptionsRequest {
137            scope,
138            name,
139            value_only,
140            include_inherited,
141        }))
142    }
143
144    /// Sends a `show-environment` request over the detached RPC channel.
145    pub fn show_environment(
146        &mut self,
147        scope: ScopeSelector,
148        name: Option<String>,
149        hidden: bool,
150        shell_format: bool,
151    ) -> Result<Response, ClientError> {
152        self.roundtrip(&Request::ShowEnvironment(ShowEnvironmentRequest {
153            scope,
154            name,
155            hidden,
156            shell_format,
157        }))
158    }
159
160    /// Sends a `show-hooks` request over the detached RPC channel.
161    pub fn show_hooks(
162        &mut self,
163        scope: ScopeSelector,
164        window: bool,
165        pane: bool,
166        hook: Option<HookName>,
167    ) -> Result<Response, ClientError> {
168        self.roundtrip(&Request::ShowHooks(ShowHooksRequest {
169            scope,
170            window,
171            pane,
172            hook,
173        }))
174    }
175
176    /// Sends a `source-file` request over the detached RPC channel.
177    #[allow(clippy::too_many_arguments)]
178    pub fn source_file(
179        &mut self,
180        paths: Vec<String>,
181        quiet: bool,
182        parse_only: bool,
183        verbose: bool,
184        expand_paths: bool,
185        target: Option<PaneTarget>,
186        stdin: Option<String>,
187    ) -> Result<Response, ClientError> {
188        self.roundtrip_without_read_timeout(&Request::SourceFile(SourceFileRequest {
189            paths,
190            quiet,
191            parse_only,
192            verbose,
193            expand_paths,
194            target,
195            caller_cwd: current_working_directory(),
196            stdin,
197        }))
198    }
199}
200
201fn current_working_directory() -> Option<PathBuf> {
202    std::env::current_dir().ok()
203}
204
205#[cfg(all(test, unix))]
206mod tests {
207    use std::io::{self, Read};
208    use std::os::unix::net::UnixStream;
209
210    use rmux_proto::{OptionName, RmuxError, ScopeSelector, SessionName, SetOptionMode};
211
212    use super::Connection;
213    use crate::ClientError;
214
215    #[test]
216    fn set_option_rejects_invalid_requests_before_writing_to_the_socket() {
217        let (client_stream, mut server_stream) = UnixStream::pair().expect("create stream pair");
218        server_stream
219            .set_nonblocking(true)
220            .expect("set read end nonblocking");
221        let mut connection = Connection::new(client_stream).expect("connection");
222
223        let error = connection
224            .set_option(
225                ScopeSelector::Session(SessionName::new("alpha").expect("valid session")),
226                OptionName::DefaultTerminal,
227                "tmux-256color".to_owned(),
228                SetOptionMode::Replace,
229            )
230            .expect_err("invalid request should fail");
231
232        assert!(matches!(
233            error,
234            ClientError::Protocol(RmuxError::InvalidSetOption(message))
235                if message == "default-terminal is only supported at global scope"
236        ));
237
238        let mut buffer = [0_u8; 1];
239        let read_error = server_stream
240            .read(&mut buffer)
241            .expect_err("validation should happen before any bytes are written");
242        assert_eq!(read_error.kind(), io::ErrorKind::WouldBlock);
243    }
244}