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
64pub const MAX_INITIAL_CONTROL_COMMANDS: usize = 1024;
67
68#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
71pub struct ControlModeRequest {
72 pub mode: ControlMode,
74 #[serde(default)]
76 pub client_terminal: ClientTerminalContext,
77 #[serde(default)]
82 pub initial_command_count: u32,
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
87pub struct ControlModeResponse {
88 pub mode: ControlMode,
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum ControlGuardKind {
95 Begin,
97 End,
99 Error,
101}
102
103impl ControlGuardKind {
104 #[must_use]
106 pub const fn as_str(self) -> &'static str {
107 match self {
108 Self::Begin => "begin",
109 Self::End => "end",
110 Self::Error => "error",
111 }
112 }
113}
114
115#[must_use]
117pub fn format_guard_line(
118 kind: ControlGuardKind,
119 time_secs: i64,
120 command_number: u64,
121 flags: u8,
122) -> String {
123 format!(
124 "%{} {} {} {}\n",
125 kind.as_str(),
126 time_secs,
127 command_number,
128 flags
129 )
130}
131
132#[must_use]
134pub fn format_output_line(pane_id: u32, bytes: &[u8]) -> String {
135 format!("%output %{} {}\n", pane_id, octal_escape(bytes))
136}
137
138#[must_use]
140pub fn format_extended_output_line(pane_id: u32, age_ms: u64, bytes: &[u8]) -> String {
141 format!(
142 "%extended-output %{} {} : {}\n",
143 pane_id,
144 age_ms,
145 octal_escape(bytes)
146 )
147}
148
149#[must_use]
151pub fn format_pause_line(pane_id: u32) -> String {
152 format!("%pause %{}\n", pane_id)
153}
154
155#[must_use]
157pub fn format_continue_line(pane_id: u32) -> String {
158 format!("%continue %{}\n", pane_id)
159}
160
161#[must_use]
163pub fn format_exit_line(reason: Option<&str>) -> String {
164 match reason {
165 Some(reason) if !reason.is_empty() => format!("%exit {reason}\n"),
166 _ => "%exit\n".to_owned(),
167 }
168}
169
170#[must_use]
177pub fn octal_escape(bytes: &[u8]) -> String {
178 let mut output = String::with_capacity(bytes.len());
179 let mut offset = 0;
180 while offset < bytes.len() {
181 match std::str::from_utf8(&bytes[offset..]) {
182 Ok(valid) => {
183 push_escaped_text(&mut output, valid);
184 break;
185 }
186 Err(error) if error.valid_up_to() > 0 => {
187 let valid_end = offset + error.valid_up_to();
188 let valid = std::str::from_utf8(&bytes[offset..valid_end])
189 .expect("valid_up_to must describe valid UTF-8");
190 push_escaped_text(&mut output, valid);
191 offset = valid_end;
192 }
193 Err(error) => {
194 let invalid_len = error.error_len().unwrap_or(1);
195 for &byte in &bytes[offset..offset + invalid_len] {
196 push_octal_escape(&mut output, byte);
197 }
198 offset += invalid_len;
199 }
200 }
201 }
202 output
203}
204
205fn push_escaped_text(output: &mut String, text: &str) {
206 for character in text.chars() {
207 if character.is_ascii() {
208 let byte = character as u8;
209 if needs_octal_escape(byte) {
210 push_octal_escape(output, byte);
211 } else {
212 output.push(character);
213 }
214 } else {
215 output.push(character);
216 }
217 }
218}
219
220const fn needs_octal_escape(byte: u8) -> bool {
221 byte < b' ' || byte == b'\\'
222}
223
224fn push_octal_escape(output: &mut String, byte: u8) {
225 output.push('\\');
226 output.push(char::from(b'0' + ((byte >> 6) & 0x7)));
227 output.push(char::from(b'0' + ((byte >> 3) & 0x7)));
228 output.push(char::from(b'0' + (byte & 0x7)));
229}
230
231#[cfg(test)]
232mod tests {
233 use super::{
234 format_exit_line, format_extended_output_line, format_guard_line, format_output_line,
235 octal_escape, ControlGuardKind, ControlMode,
236 };
237
238 #[test]
239 fn count_two_selects_control_control_mode() {
240 assert_eq!(ControlMode::from_count(0), ControlMode::Plain);
241 assert_eq!(ControlMode::from_count(1), ControlMode::Plain);
242 assert_eq!(ControlMode::from_count(2), ControlMode::ControlControl);
243 assert_eq!(ControlMode::from_count(3), ControlMode::ControlControl);
244 }
245
246 #[test]
247 fn octal_escape_matches_tmux_rules_for_control_bytes() {
248 assert_eq!(octal_escape(b"abc"), "abc");
249 assert_eq!(octal_escape(b"a\nb"), "a\\012b");
250 assert_eq!(octal_escape(b"\\\0"), "\\134\\000");
251 assert_eq!(octal_escape(b" "), " ");
252 assert_eq!(octal_escape(b"~"), "~");
253 assert_eq!(octal_escape(b"\x7f"), "\x7f");
255 assert_eq!(octal_escape("é".as_bytes()), "é");
256 assert_eq!(octal_escape("hello 👋".as_bytes()), "hello 👋");
257 assert_eq!(octal_escape(b"\x80"), "\\200");
259 assert_eq!(octal_escape(b"\xff"), "\\377");
260 for byte in b' '..b'\x7f' {
262 if byte == b'\\' {
263 continue;
264 }
265 let escaped = octal_escape(&[byte]);
266 assert_eq!(
267 escaped.len(),
268 1,
269 "byte {byte:#04x} should be literal, got {escaped:?}"
270 );
271 }
272 }
273
274 #[test]
275 fn octal_escape_covers_every_ascii_control_byte() {
276 for byte in 0_u8..b' ' {
277 let escaped = octal_escape(&[byte]);
278 assert_eq!(
279 escaped,
280 format!("\\{byte:03o}"),
281 "control byte {byte:#04x} should be octal escaped"
282 );
283 }
284 assert_eq!(octal_escape(b"\\"), "\\134");
285 }
286
287 #[test]
288 fn guard_and_output_lines_are_newline_terminated() {
289 assert_eq!(
290 format_guard_line(ControlGuardKind::Begin, 10, 22, 1),
291 "%begin 10 22 1\n"
292 );
293 assert_eq!(format_output_line(7, b"hi\n"), "%output %7 hi\\012\n");
294 assert_eq!(
295 format_extended_output_line(7, 15, b"hi"),
296 "%extended-output %7 15 : hi\n"
297 );
298 assert_eq!(format_exit_line(None), "%exit\n");
299 assert_eq!(
300 format_exit_line(Some("too far behind")),
301 "%exit too far behind\n"
302 );
303 }
304}