1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
8pub enum ControlMode {
9 Plain,
11 ControlControl,
13}
14
15impl ControlMode {
16 #[must_use]
18 pub const fn from_count(count: u8) -> Self {
19 if count >= 2 {
20 Self::ControlControl
21 } else {
22 Self::Plain
23 }
24 }
25
26 #[must_use]
28 pub const fn is_control_control(self) -> bool {
29 matches!(self, Self::ControlControl)
30 }
31}
32
33pub const CONTROL_BUFFER_LOW: usize = 512;
35pub const CONTROL_BUFFER_HIGH: usize = 8192;
37pub const CONTROL_WRITE_MINIMUM: usize = 32;
39pub const CONTROL_MAXIMUM_AGE_MS: u64 = 300_000;
41pub const CONTROL_CONTROL_START: &str = "\u{1b}P1000p";
43pub const CONTROL_CONTROL_END: &str = "\u{1b}\\";
45pub const CONTROL_STDIN_EOF_MARKER: &str = "\0rmux-control-eof";
51
52#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
55pub struct ClientTerminalContext {
56 #[serde(default)]
58 pub terminal_features: Vec<String>,
59 #[serde(default)]
61 pub utf8: bool,
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67pub struct ControlModeRequest {
68 pub mode: ControlMode,
70 #[serde(default)]
72 pub client_terminal: ClientTerminalContext,
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
77pub struct ControlModeResponse {
78 pub mode: ControlMode,
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub enum ControlGuardKind {
85 Begin,
87 End,
89 Error,
91}
92
93impl ControlGuardKind {
94 #[must_use]
96 pub const fn as_str(self) -> &'static str {
97 match self {
98 Self::Begin => "begin",
99 Self::End => "end",
100 Self::Error => "error",
101 }
102 }
103}
104
105#[must_use]
107pub fn format_guard_line(
108 kind: ControlGuardKind,
109 time_secs: i64,
110 command_number: u64,
111 flags: u8,
112) -> String {
113 format!(
114 "%{} {} {} {}\n",
115 kind.as_str(),
116 time_secs,
117 command_number,
118 flags
119 )
120}
121
122#[must_use]
124pub fn format_output_line(pane_id: u32, bytes: &[u8]) -> String {
125 format!("%output %{} {}\n", pane_id, octal_escape(bytes))
126}
127
128#[must_use]
130pub fn format_extended_output_line(pane_id: u32, age_ms: u64, bytes: &[u8]) -> String {
131 format!(
132 "%extended-output %{} {} : {}\n",
133 pane_id,
134 age_ms,
135 octal_escape(bytes)
136 )
137}
138
139#[must_use]
141pub fn format_pause_line(pane_id: u32) -> String {
142 format!("%pause %{}\n", pane_id)
143}
144
145#[must_use]
147pub fn format_continue_line(pane_id: u32) -> String {
148 format!("%continue %{}\n", pane_id)
149}
150
151#[must_use]
153pub fn format_exit_line(reason: Option<&str>) -> String {
154 match reason {
155 Some(reason) if !reason.is_empty() => format!("%exit {reason}\n"),
156 _ => "%exit\n".to_owned(),
157 }
158}
159
160#[must_use]
167pub fn octal_escape(bytes: &[u8]) -> String {
168 let mut output = String::with_capacity(bytes.len());
169 let mut offset = 0;
170 while offset < bytes.len() {
171 match std::str::from_utf8(&bytes[offset..]) {
172 Ok(valid) => {
173 push_escaped_text(&mut output, valid);
174 break;
175 }
176 Err(error) if error.valid_up_to() > 0 => {
177 let valid_end = offset + error.valid_up_to();
178 let valid = std::str::from_utf8(&bytes[offset..valid_end])
179 .expect("valid_up_to must describe valid UTF-8");
180 push_escaped_text(&mut output, valid);
181 offset = valid_end;
182 }
183 Err(error) => {
184 let invalid_len = error.error_len().unwrap_or(1);
185 for &byte in &bytes[offset..offset + invalid_len] {
186 push_octal_escape(&mut output, byte);
187 }
188 offset += invalid_len;
189 }
190 }
191 }
192 output
193}
194
195fn push_escaped_text(output: &mut String, text: &str) {
196 for character in text.chars() {
197 if character.is_ascii() {
198 let byte = character as u8;
199 if needs_octal_escape(byte) {
200 push_octal_escape(output, byte);
201 } else {
202 output.push(character);
203 }
204 } else {
205 output.push(character);
206 }
207 }
208}
209
210const fn needs_octal_escape(byte: u8) -> bool {
211 byte < b' ' || byte == b'\\' || byte == 0x7F
212}
213
214fn push_octal_escape(output: &mut String, byte: u8) {
215 output.push('\\');
216 output.push(char::from(b'0' + ((byte >> 6) & 0x7)));
217 output.push(char::from(b'0' + ((byte >> 3) & 0x7)));
218 output.push(char::from(b'0' + (byte & 0x7)));
219}
220
221#[cfg(test)]
222mod tests {
223 use super::{
224 format_exit_line, format_extended_output_line, format_guard_line, format_output_line,
225 octal_escape, ControlGuardKind, ControlMode,
226 };
227
228 #[test]
229 fn count_two_selects_control_control_mode() {
230 assert_eq!(ControlMode::from_count(0), ControlMode::Plain);
231 assert_eq!(ControlMode::from_count(1), ControlMode::Plain);
232 assert_eq!(ControlMode::from_count(2), ControlMode::ControlControl);
233 assert_eq!(ControlMode::from_count(3), ControlMode::ControlControl);
234 }
235
236 #[test]
237 fn octal_escape_matches_tmux_rules_for_control_bytes() {
238 assert_eq!(octal_escape(b"abc"), "abc");
239 assert_eq!(octal_escape(b"a\nb"), "a\\012b");
240 assert_eq!(octal_escape(b"\\\0"), "\\134\\000");
241 assert_eq!(octal_escape(b" "), " ");
242 assert_eq!(octal_escape(b"~"), "~");
243 assert_eq!(octal_escape(b"\x7f"), "\\177");
245 assert_eq!(octal_escape("é".as_bytes()), "é");
246 assert_eq!(octal_escape("hello 👋".as_bytes()), "hello 👋");
247 assert_eq!(octal_escape(b"\x80"), "\\200");
249 assert_eq!(octal_escape(b"\xff"), "\\377");
250 for byte in b' '..b'\x7f' {
252 if byte == b'\\' {
253 continue;
254 }
255 let escaped = octal_escape(&[byte]);
256 assert_eq!(
257 escaped.len(),
258 1,
259 "byte {byte:#04x} should be literal, got {escaped:?}"
260 );
261 }
262 }
263
264 #[test]
265 fn guard_and_output_lines_are_newline_terminated() {
266 assert_eq!(
267 format_guard_line(ControlGuardKind::Begin, 10, 22, 1),
268 "%begin 10 22 1\n"
269 );
270 assert_eq!(format_output_line(7, b"hi\n"), "%output %7 hi\\012\n");
271 assert_eq!(
272 format_extended_output_line(7, 15, b"hi"),
273 "%extended-output %7 15 : hi\n"
274 );
275 assert_eq!(format_exit_line(None), "%exit\n");
276 assert_eq!(
277 format_exit_line(Some("too far behind")),
278 "%exit too far behind\n"
279 );
280 }
281}