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]
168pub fn octal_escape(bytes: &[u8]) -> String {
169 let mut output = String::with_capacity(bytes.len());
170 for &byte in bytes {
171 if (b' '..0x7F).contains(&byte) && byte != b'\\' {
172 output.push(byte as char);
173 } else {
174 output.push('\\');
175 output.push(char::from(b'0' + ((byte >> 6) & 0x7)));
176 output.push(char::from(b'0' + ((byte >> 3) & 0x7)));
177 output.push(char::from(b'0' + (byte & 0x7)));
178 }
179 }
180 output
181}
182
183#[cfg(test)]
184mod tests {
185 use super::{
186 format_exit_line, format_extended_output_line, format_guard_line, format_output_line,
187 octal_escape, ControlGuardKind, ControlMode,
188 };
189
190 #[test]
191 fn count_two_selects_control_control_mode() {
192 assert_eq!(ControlMode::from_count(0), ControlMode::Plain);
193 assert_eq!(ControlMode::from_count(1), ControlMode::Plain);
194 assert_eq!(ControlMode::from_count(2), ControlMode::ControlControl);
195 assert_eq!(ControlMode::from_count(3), ControlMode::ControlControl);
196 }
197
198 #[test]
199 fn octal_escape_matches_tmux_rules_for_control_bytes() {
200 assert_eq!(octal_escape(b"abc"), "abc");
201 assert_eq!(octal_escape(b"a\nb"), "a\\012b");
202 assert_eq!(octal_escape(b"\\\0"), "\\134\\000");
203 assert_eq!(octal_escape(b" "), " ");
204 assert_eq!(octal_escape(b"~"), "~");
205 assert_eq!(octal_escape(b"\x7f"), "\\177");
207 assert_eq!(octal_escape(b"\x80"), "\\200");
208 assert_eq!(octal_escape(b"\xff"), "\\377");
209 for byte in b' '..b'\x7f' {
211 if byte == b'\\' {
212 continue;
213 }
214 let escaped = octal_escape(&[byte]);
215 assert_eq!(
216 escaped.len(),
217 1,
218 "byte {byte:#04x} should be literal, got {escaped:?}"
219 );
220 }
221 }
222
223 #[test]
224 fn guard_and_output_lines_are_newline_terminated() {
225 assert_eq!(
226 format_guard_line(ControlGuardKind::Begin, 10, 22, 1),
227 "%begin 10 22 1\n"
228 );
229 assert_eq!(format_output_line(7, b"hi\n"), "%output %7 hi\\012\n");
230 assert_eq!(
231 format_extended_output_line(7, 15, b"hi"),
232 "%extended-output %7 15 : hi\n"
233 );
234 assert_eq!(format_exit_line(None), "%exit\n");
235 assert_eq!(
236 format_exit_line(Some("too far behind")),
237 "%exit too far behind\n"
238 );
239 }
240}