use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ControlMode {
Plain,
ControlControl,
}
impl ControlMode {
#[must_use]
pub const fn from_count(count: u8) -> Self {
if count >= 2 {
Self::ControlControl
} else {
Self::Plain
}
}
#[must_use]
pub const fn is_control_control(self) -> bool {
matches!(self, Self::ControlControl)
}
}
pub const CONTROL_BUFFER_LOW: usize = 512;
pub const CONTROL_BUFFER_HIGH: usize = 8192;
pub const CONTROL_WRITE_MINIMUM: usize = 32;
pub const CONTROL_MAXIMUM_AGE_MS: u64 = 300_000;
pub const CONTROL_CONTROL_START: &str = "\u{1b}P1000p";
pub const CONTROL_CONTROL_END: &str = "\u{1b}\\";
pub const CONTROL_STDIN_EOF_MARKER: &str = "\0rmux-control-eof";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct ClientTerminalContext {
#[serde(default)]
pub terminal_features: Vec<String>,
#[serde(default)]
pub utf8: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ControlModeRequest {
pub mode: ControlMode,
#[serde(default)]
pub client_terminal: ClientTerminalContext,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct ControlModeResponse {
pub mode: ControlMode,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ControlGuardKind {
Begin,
End,
Error,
}
impl ControlGuardKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Begin => "begin",
Self::End => "end",
Self::Error => "error",
}
}
}
#[must_use]
pub fn format_guard_line(
kind: ControlGuardKind,
time_secs: i64,
command_number: u64,
flags: u8,
) -> String {
format!(
"%{} {} {} {}\n",
kind.as_str(),
time_secs,
command_number,
flags
)
}
#[must_use]
pub fn format_output_line(pane_id: u32, bytes: &[u8]) -> String {
format!("%output %{} {}\n", pane_id, octal_escape(bytes))
}
#[must_use]
pub fn format_extended_output_line(pane_id: u32, age_ms: u64, bytes: &[u8]) -> String {
format!(
"%extended-output %{} {} : {}\n",
pane_id,
age_ms,
octal_escape(bytes)
)
}
#[must_use]
pub fn format_pause_line(pane_id: u32) -> String {
format!("%pause %{}\n", pane_id)
}
#[must_use]
pub fn format_continue_line(pane_id: u32) -> String {
format!("%continue %{}\n", pane_id)
}
#[must_use]
pub fn format_exit_line(reason: Option<&str>) -> String {
match reason {
Some(reason) if !reason.is_empty() => format!("%exit {reason}\n"),
_ => "%exit\n".to_owned(),
}
}
#[must_use]
pub fn octal_escape(bytes: &[u8]) -> String {
let mut output = String::with_capacity(bytes.len());
for &byte in bytes {
if (b' '..0x7F).contains(&byte) && byte != b'\\' {
output.push(byte as char);
} else {
output.push('\\');
output.push(char::from(b'0' + ((byte >> 6) & 0x7)));
output.push(char::from(b'0' + ((byte >> 3) & 0x7)));
output.push(char::from(b'0' + (byte & 0x7)));
}
}
output
}
#[cfg(test)]
mod tests {
use super::{
format_exit_line, format_extended_output_line, format_guard_line, format_output_line,
octal_escape, ControlGuardKind, ControlMode,
};
#[test]
fn count_two_selects_control_control_mode() {
assert_eq!(ControlMode::from_count(0), ControlMode::Plain);
assert_eq!(ControlMode::from_count(1), ControlMode::Plain);
assert_eq!(ControlMode::from_count(2), ControlMode::ControlControl);
assert_eq!(ControlMode::from_count(3), ControlMode::ControlControl);
}
#[test]
fn octal_escape_matches_tmux_rules_for_control_bytes() {
assert_eq!(octal_escape(b"abc"), "abc");
assert_eq!(octal_escape(b"a\nb"), "a\\012b");
assert_eq!(octal_escape(b"\\\0"), "\\134\\000");
assert_eq!(octal_escape(b" "), " ");
assert_eq!(octal_escape(b"~"), "~");
assert_eq!(octal_escape(b"\x7f"), "\\177");
assert_eq!(octal_escape(b"\x80"), "\\200");
assert_eq!(octal_escape(b"\xff"), "\\377");
for byte in b' '..b'\x7f' {
if byte == b'\\' {
continue;
}
let escaped = octal_escape(&[byte]);
assert_eq!(
escaped.len(),
1,
"byte {byte:#04x} should be literal, got {escaped:?}"
);
}
}
#[test]
fn guard_and_output_lines_are_newline_terminated() {
assert_eq!(
format_guard_line(ControlGuardKind::Begin, 10, 22, 1),
"%begin 10 22 1\n"
);
assert_eq!(format_output_line(7, b"hi\n"), "%output %7 hi\\012\n");
assert_eq!(
format_extended_output_line(7, 15, b"hi"),
"%extended-output %7 15 : hi\n"
);
assert_eq!(format_exit_line(None), "%exit\n");
assert_eq!(
format_exit_line(Some("too far behind")),
"%exit too far behind\n"
);
}
}