Initial public release of Warp.

Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
David Stern
2026-04-28 08:43:33 -05:00
commit 0dbd3d567a
4982 changed files with 1431549 additions and 0 deletions
+163
View File
@@ -0,0 +1,163 @@
use crate::util::parse_ascii_u32;
use lazy_static::lazy_static;
use regex::bytes::Regex;
use std::collections::HashMap;
// The below strings are used as a tag/prefix at the beginning of a response
// from tmux to determine the type of response. These strings must be unique
// and should not contain any special characters (alpha-num, dash, underline,
// and space are acceptable).
// This is because the response is parsed using regex, as well as to avoid
// messing up tmux's shell-like parsing.
const PRIMARY_WINDOW_PANE_PREFIX: &str = "primary window pane";
pub const BACKGROUND_WINDOW_PREFIX: &str = "background window";
#[derive(Clone, Debug)]
pub enum TmuxCommand {
/// Gets the window id and pane id of the primary window pane.
GetPrimaryWindowPane,
/// Runs a command in the background in a new temporary window.
RunInBackgroundWindow {
command_id: String,
current_directory_path: Option<String>,
command: String,
environment_variables: Option<HashMap<String, String>>,
},
/// Refreshes the client in Control Mode, at a target size of rows and cols.
UpdateClientSize { num_rows: usize, num_cols: usize },
/// Configures tmux to automatically terminate any sessions that don't have clients attached.
SetDestroyUnattached,
/// Forces the tmux session to inherit the smallest dimensions of any attached client.
SetWindowSizeToSmallest,
}
fn safe_env_var_name(name: &str) -> bool {
lazy_static! {
static ref SAFE_NAME: Regex = Regex::new(r"^[[:word:]]+$").expect("Invalid regex!");
}
SAFE_NAME.is_match(name.as_bytes())
}
impl TmuxCommand {
pub fn get_command_string(&self) -> String {
// All commands must end with `\n`
match self {
TmuxCommand::GetPrimaryWindowPane => format!(
"list-panes -F \"#{{?pane_active,{PRIMARY_WINDOW_PANE_PREFIX}: ,}}#{{window_id}} #{{pane_id}}\"\n"
),
TmuxCommand::RunInBackgroundWindow {
current_directory_path,
command,
environment_variables,
command_id,
} => {
// We pass the command to tmux wrapped in single quotes. The tmux control mode interface
// interprets escapes in a bash style, so bash-escape any single quotes in the command.
// Note that we should always use bash-style escapes here regardless of the current shell,
// because this command is going to tmux control mode.
let escaped_command = escape_single_quotes(command);
let has_new_line = escaped_command.contains('\n');
debug_assert!(
!has_new_line,
"Tmux control mode commands must take place on one line: `{escaped_command}`"
);
if has_new_line {
log::error!(
"Tmux control mode command contains a newline: `{escaped_command}`"
);
}
// It's highly unreliable to try to strip newlines and make sure commands are still
// valid, so we'd rather ensure no commands have newlines in them.
let escaped_command = escaped_command.replace('\n', r"\n");
let set_directory = if let Some(current_directory_path) = current_directory_path {
let escaped_path = escape_single_quotes(current_directory_path);
format!("-c '{escaped_path}'")
} else {
String::new()
};
let mut set_env_vars = String::new();
for (name, value) in environment_variables.iter().flatten() {
if safe_env_var_name(name) {
set_env_vars.push_str("-e ");
set_env_vars.push_str(name);
set_env_vars.push_str("='");
set_env_vars.push_str(&escape_single_quotes(value));
set_env_vars.push_str("' ");
}
}
// Constructs a tmux command string. Tmux control mode will first parse this string with a sh-like
// parser. The parsed command will be executed in a new interactive terminal session.
// Some things to note:
// - This also prints formatted window info with the new window id and pane id with `-PF "background window: #{window_id} #{pane_id}"`.
// - This must be kept in sync with command output parsing in TmuxPerformer::tmux_message.
// - The output is piped through `cat` to prevent the command from being directly attached
// to a pty (and therefore risking it running in an interactive mode).
// - This sleeps for 1 second after execution to work around tmux bug which clips largs outputs
// when the window exits immediately.
// - There's a newline at the end so control mode will start running the command.
let newline = "\n";
format!(
r#"new-window -d {set_directory} {set_env_vars} -PF "{BACKGROUND_WINDOW_PREFIX}: #{{window_id}} #{{pane_id}}" '(builtin echo -n "^^^{command_id}|||"; {escaped_command}; builtin echo "|||$?\$\$\$")|command cat; command sleep 1'{newline}"#
)
}
TmuxCommand::UpdateClientSize { num_rows, num_cols } => {
format!("refresh-client -C {num_cols},{num_rows}\n")
}
TmuxCommand::SetDestroyUnattached => "set destroy-unattached on\n".to_string(),
TmuxCommand::SetWindowSizeToSmallest => "set window-size smallest\n".to_string(),
}
}
}
pub enum TmuxCommandResponse {
SetPrimaryWindowPane { window_id: u32, pane_id: u32 },
BackgroundWindow { window_id: u32, pane_id: u32 },
}
pub fn parse_command(line: Vec<u8>) -> Option<TmuxCommandResponse> {
lazy_static! {
pub static ref PRIMARY_WINDOW_PANE_REGEX: Regex = {
let pattern = format!(
r"^{PRIMARY_WINDOW_PANE_PREFIX}: @([[:digit:]]+) %([[:digit:]]+)$"
);
Regex::new(&pattern).expect("invalid regex")
};
// Must be kept in sync with the tmux command in TmuxExecutor::execute_command_internal.
pub static ref BACKGROUND_WINDOW_REGEX: Regex = {
let pattern = format!(
r"^{BACKGROUND_WINDOW_PREFIX}: @([[:digit:]]+) %([[:digit:]]+)$"
);
Regex::new(&pattern).expect("invalid regex")
};
}
if let Some(captures) = BACKGROUND_WINDOW_REGEX.captures(&line) {
let window_id: u32 = parse_ascii_u32(&captures[1])
.expect("impossible: encountered non-ASCII digit in ASCII digit pattern");
let pane_id: u32 = parse_ascii_u32(&captures[2])
.expect("impossible: encountered non-ASCII digit in ASCII digit pattern");
return Some(TmuxCommandResponse::BackgroundWindow { window_id, pane_id });
} else if let Some(captures) = PRIMARY_WINDOW_PANE_REGEX.captures(&line) {
let window_id: u32 = parse_ascii_u32(&captures[1])
.expect("impossible: encountered non-ASCII digit in ASCII digit pattern");
let pane_id: u32 = parse_ascii_u32(&captures[2])
.expect("impossible: encountered non-ASCII digit in ASCII digit pattern");
return Some(TmuxCommandResponse::SetPrimaryWindowPane { window_id, pane_id });
}
None
}
/// Tmux control mode uses bash-style escaping, which replaces each single
/// quote with a '"'"' sequence. The first single quote completes the
/// single quoted string to the left, the next three characters: "'" evaluate
/// to a literal single quote in bash/zsh, and then the final single quote
/// starts a new single-quoted string to the right. Effectively, this
/// concatenates the left single-quoted string, a literal single quote char,
/// and the right single-quoted string.
fn escape_single_quotes(command: &str) -> String {
command.replace('\'', r#"'"'"'"#)
}
+67
View File
@@ -0,0 +1,67 @@
pub mod commands;
pub mod parser;
use crate::terminal::event::ExecutedExecutorCommandEvent;
use crate::util::parse_ascii_u32;
use lazy_static::lazy_static;
use regex::bytes::{Regex, RegexBuilder};
pub enum ControlModeEvent {
/// This event is sent when the control mode has started
/// we don't have enough info from the tmux output to know
/// the primary pane or window yet.
Starting,
/// Control Mode will inform us of the primary pane and window,
/// at which point we can safely direct input to the appropriate
/// panel.
ControlModeReady {
primary_window: u32,
primary_pane: u32,
},
/// This event is sent when Control Mode informs us of pane output
/// that is coming from a pane which is not the primary pane.
BackgroundPaneOutput { pane: u32, byte: u8 },
/// This event is sent when Control Mode has been exited.
Exited,
}
pub fn format_input(pane: u32, input: &[u8]) -> String {
let mut formatted = String::new();
for chunk in input.chunks(1000) {
formatted.push_str(&format!("send-keys -Ht %{pane}"));
for byte in chunk {
formatted.push_str(&format!(" {byte:X}"));
}
formatted.push('\n');
}
formatted
}
pub fn parse_generator_output(input: &[u8]) -> Option<ExecutedExecutorCommandEvent> {
lazy_static! {
static ref GENERATOR_OUTPUT_REGEX: Regex =
RegexBuilder::new(r"\^\^\^(.+?)\|\|\|(.*?)\|\|\|(\d+)\$\$\$")
.dot_matches_new_line(true)
.unicode(false)
.build()
.unwrap();
/// tmux adds a carriage return to newlines that it prints, so remove that here.
static ref NEWLINE_REGEX: Regex = Regex::new(r"\r\n").expect("Invalid regex");
}
GENERATOR_OUTPUT_REGEX.captures(input).and_then(|caps| {
let command_id = std::str::from_utf8(&caps[1]).ok()?.to_string();
let output = NEWLINE_REGEX.replace_all(&caps[2], b"\n").to_vec();
let exit_code = parse_ascii_u32(&caps[3])? as usize;
Some(ExecutedExecutorCommandEvent {
command_id,
output,
exit_code,
})
})
}
#[cfg(test)]
#[path = "mod_test.rs"]
mod tests;
+24
View File
@@ -0,0 +1,24 @@
use super::*;
#[test]
fn test_parse_generator_output() {
let input = b"^^^cmd1|||output|||0$$$";
let result = parse_generator_output(input);
assert!(result.is_some());
let event = result.unwrap();
assert_eq!(event.command_id, "cmd1");
assert_eq!(event.output, b"output");
assert_eq!(event.exit_code, 0);
let input = b"^^^cmd2|||multi\nline\noutput|||255$$$";
let result = parse_generator_output(input);
assert!(result.is_some());
let event = result.unwrap();
assert_eq!(event.command_id, "cmd2");
assert_eq!(event.output, b"multi\nline\noutput");
assert_eq!(event.exit_code, 255);
let input = b"invalid input";
let result = parse_generator_output(input);
assert!(result.is_none());
}
+465
View File
@@ -0,0 +1,465 @@
//! This module parses output from tmux control mode.
//!
//! Control mode is a special interface to tmux that allows programs to interact with tmux
//! in a structured manner, receiving information about windows, panes, and the state
//! of the server in a machine-readable format.
//!
//! Refer to the tmux control mode protocol documentation for more details:
//! https://github.com/tmux/tmux/wiki/Control-Mode
use crate::util::AsciiDebug;
#[derive(PartialEq, Eq)]
pub enum TmuxMessage {
/// This is output from a tmux command, like send-keys or new-window. This is different from
/// pane output, which is handled separately.
CommandOutput {
/// Commands can succeed or fail, which is captured by the Result variant, and can have
/// multiple lines of output.
output_lines: Result<Vec<Vec<u8>>, Vec<Vec<u8>>>,
},
WindowClose {
window_id: u32,
},
Exit,
/// Only used in development.
Unknown {
tag: Vec<u8>,
rest: Vec<u8>,
},
ParseError {
message: &'static str,
byte: u8,
},
}
/// Debug impl which formats the byte vecs as readable unicode strings.
impl std::fmt::Debug for TmuxMessage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TmuxMessage::CommandOutput {
output_lines: output,
} => match output {
Ok(v) => {
let output: Vec<_> = v.iter().map(|vec| AsciiDebug(vec)).collect();
f.debug_struct("CommandOutput")
.field("output", &format!("Ok({output:?})"))
.finish()
}
Err(e) => {
let errors: Vec<_> = e.iter().map(|vec| AsciiDebug(vec)).collect();
f.debug_struct("CommandOutput")
.field("output", &format!("Err({errors:?})"))
.finish()
}
},
TmuxMessage::Exit => write!(f, "Exit"),
TmuxMessage::Unknown { tag, rest } => f
.debug_struct("Unknown")
.field("tag", &AsciiDebug(tag))
.field("rest", &AsciiDebug(rest))
.finish(),
TmuxMessage::ParseError { message, byte } => f
.debug_struct("ParseError")
.field("message", message)
.field("byte", byte)
.finish(),
TmuxMessage::WindowClose { window_id } => f
.debug_struct("WindowClose")
.field("window_id", window_id)
.finish(),
}
}
}
/// Performs actions sent by `TmuxControlModeParser`.
///
/// The functions in this trait are called by the parser immediately upon parsing relevant input.
pub trait TmuxControlModeHandler {
/// This is called by the parser whenever a byte of output from a particular pane is parsed.
fn pane_output(&mut self, pane: u32, byte: u8);
/// This is called by the parser for all other tmux messages, as soon as they've been parsed.
/// See `TmuxMessage` for more details.
fn tmux_control_mode_message(&mut self, message: TmuxMessage);
}
#[derive(Debug)]
enum ParserState {
BeginningOfLine,
ReadingTag {
// The longest known tag is 'unlinked-window-renamed', which is 23 characters long.
tag: [u8; 23],
len: u8,
},
TagExit,
TagUnknown {
tag: Vec<u8>, // Only set in debug builds
args: Vec<u8>, // Only set in debug builds
},
TagBegin,
ReadingCommandOutput {
current_line: Vec<u8>,
lines: Vec<Vec<u8>>,
},
TagOutput {
maybe_pane: Option<u32>,
},
ReadingPaneOutput {
pane: u32,
maybe_escape_sequence: Option<EscapeSequence>,
},
TagWindowClose {
maybe_window: Option<u32>,
},
Error,
}
#[derive(Debug)]
struct EscapeSequence {
char: u8,
remaining_digits: u8,
}
/// This parses output from tmux control mode. It expects the output to be well formed and does a
/// best-effort job to identify, report, and recover from errors.
/// Specifically:
/// - The parser correctly parses all well-formed output.
/// - The parser will not crash or infinite loop, whether or not the input is well-formed.
/// - Some mal-formed input may result in an incorrect parse, but most will result in a parse error.
/// - If the parser continues to get input after a parse error, it will attempt to recover after
/// the next newline.
///
/// N.B. When working with control mode output, it's important to understand the distinction
/// between tmux command output (e.g. the output of running the `list-windows` tmux command) with
/// tmux pane output (e.g. the output produced by running `ls` in pane 0).
///
/// For more information on the expected output format, see: https://github.com/tmux/tmux/wiki/Control-Mode
#[derive(Debug)]
pub struct TmuxControlModeParser {
state: ParserState,
}
impl TmuxControlModeParser {
pub fn new() -> Self {
TmuxControlModeParser {
state: ParserState::BeginningOfLine,
}
}
/// The primary interface to the parser. Takes a handler and the next byte of output, and
/// calls one of the handler methods when it parses a byte of pane output or a complete tmux
/// message.
pub fn advance(&mut self, handler: &mut impl TmuxControlModeHandler, byte: u8) {
if byte == b'\r' {
// This should only ever appear directly before a \n and it simplifies parsing
// if we just discard carriage returns and only look for newlines.
return;
}
match &mut self.state {
ParserState::BeginningOfLine => {
// Input prior to this state: ""
// Input parsed in this state: "%"
if byte != b'%' {
report_parse_error(
handler,
"Received non-% character at the beginning of a line",
byte,
);
self.state = ParserState::Error;
return;
}
self.state = ParserState::ReadingTag {
tag: Default::default(),
len: 0,
};
}
ParserState::ReadingTag { tag, len } => {
// Input prior to this state: "%"
// Input parsed in this state: "%[tag]"
let current_len = *len as usize;
if byte == b' ' || byte == b'\n' {
// A space or a new line means we've reached the end of the tag (e.g.
// "%something").
match &tag[..current_len] {
b"begin" => {
self.state = ParserState::TagBegin;
}
b"exit" => {
self.state = ParserState::TagExit;
}
b"output" => {
self.state = ParserState::TagOutput { maybe_pane: None };
}
b"window-close" | b"unlinked-window-close" => {
self.state = ParserState::TagWindowClose { maybe_window: None };
}
_ => {
self.state = ParserState::TagUnknown {
tag: if cfg!(debug_assertions) {
// We only care about the contents of unknown tags in debug
// builds.
tag[..current_len].to_owned()
} else {
Vec::new()
},
args: Vec::new(),
};
}
}
// The parser is now set up to parse the particular message based on the tag we
// matched. That message may be parsed differently depending on whether we've
// encountered a space or a newline, so we call into this method again to
// continue the parse with the same byte.
return self.advance(handler, byte);
}
// Ignore any bits that are longer than our max tag length.
if current_len < tag.len() {
tag[current_len] = byte;
*len += 1;
}
}
ParserState::Error => {
// Input prior to this state: "[any unexpected input]"
// Input parsed in this state: "[any more input]\n"
//
// Discard input until we see a newline and then try to recover.
if byte == b'\n' {
self.state = ParserState::BeginningOfLine;
}
}
ParserState::TagExit => {
// Input prior to this state: "%exit"
// Input parsed in this state: "\n"
if byte != b'\n' {
report_parse_error(handler, "Extraneous byte after %exit", byte);
self.state = ParserState::Error;
return;
}
handler.tmux_control_mode_message(TmuxMessage::Exit);
self.state = ParserState::BeginningOfLine;
}
ParserState::TagUnknown { tag, args } => {
// Input prior to this state: "%[unknown tag]"
// Input parsed in this state: " [rest of the line]\n"
if byte == b'\n' {
if cfg!(debug_assertions) {
handler.tmux_control_mode_message(TmuxMessage::Unknown {
tag: std::mem::take(tag),
rest: std::mem::take(args),
});
}
self.state = ParserState::BeginningOfLine;
return;
}
if cfg!(debug_assertions) {
// We only care about the contents of unknown tags in debug builds.
args.push(byte);
}
}
ParserState::TagBegin => {
// Input prior to this state: "%begin"
// Input parsed in this state: " [seconds from epoch] [unique command number] [flags]\n"
// We don't care about any of the arguments, so discard input until we get a newline.
if byte == b'\n' {
self.state = ParserState::ReadingCommandOutput {
current_line: Vec::new(),
lines: Vec::new(),
};
}
}
ParserState::ReadingCommandOutput {
current_line,
lines,
} => {
// Input prior to this state: "%begin [seconds from epoch] [unique command number] [flags]\n"
// Input parsed in this state: some number of "[command output line]\n" followed by "%end\n" or "%error\n"
if byte == b'\n' {
// Check to see if command output is complete.
if current_line.starts_with(b"%end") {
handler.tmux_control_mode_message(TmuxMessage::CommandOutput {
output_lines: Ok(std::mem::take(lines)),
});
self.state = ParserState::BeginningOfLine;
} else if current_line.starts_with(b"%error") {
handler.tmux_control_mode_message(TmuxMessage::CommandOutput {
output_lines: Err(std::mem::take(lines)),
});
self.state = ParserState::BeginningOfLine;
} else {
// Command output still ongoing -- append to list of lines.
lines.push(std::mem::take(current_line));
}
return;
}
// In the middle of a line.
current_line.push(byte);
}
ParserState::TagOutput { maybe_pane } => {
// Input prior to this state: "%output"
// Input parsed in this state: " %[pane id] "
if let &mut Some(pane) = maybe_pane {
// We're parsing the pane number.
if byte.is_ascii_digit() {
// Got another digit of the pane number.
*maybe_pane = Some(pane * 10 + (byte - b'0') as u32);
} else if byte == b' ' {
// Pane number finished.
self.state = ParserState::ReadingPaneOutput {
pane,
maybe_escape_sequence: None,
};
} else {
report_parse_error(
handler,
"Non-digit character in %output pane number",
byte,
);
self.state = ParserState::Error;
}
} else {
// We haven't started parsing the pane number yet.
if byte == b'%' {
// Pane number starting.
*maybe_pane = Some(0);
} else if byte == b' ' {
// Ignore spaces.
} else {
report_parse_error(handler, "Unexpected character after %output", byte);
}
}
}
ParserState::ReadingPaneOutput {
pane,
maybe_escape_sequence,
} => {
// Input prior to this state: "%output %[pane id] "
// Input parsed in this state: "[escaped command output]\n"
//
// The escaped output format, according to the docs:
// The output has any characters less than ASCII 32 and the \ character replaced
// with their octal equivalent, so \ becomes \134. Otherwise, it is exactly what
// the application running in the pane sent to tmux. It may not be valid UTF-8 and
// may contain escape sequences which will be as expected by tmux (so for
// TERM=screen or TERM=tmux).
if let Some(escape_sequence) = maybe_escape_sequence {
match byte {
b'0'..=b'8' => {
escape_sequence.remaining_digits -= 1;
let octal_digit = byte - b'0';
// Put each octal digit in the right spot.
// The left shift is equivalent to a multiplication by
// (8^remaining_digits).
escape_sequence.char |=
octal_digit << (escape_sequence.remaining_digits * 3);
if escape_sequence.remaining_digits == 0 {
handler.pane_output(*pane, escape_sequence.char);
*maybe_escape_sequence = None;
}
}
_ => {
// Not 0-8 in escape sequence
report_parse_error(
handler,
"Non-octal digit found in output escape sequence",
byte,
);
self.state = ParserState::Error;
}
}
} else {
match byte {
b'\n' => {
// Pane output over
self.state = ParserState::BeginningOfLine;
}
b'\\' => {
// Begin char escape
*maybe_escape_sequence = Some(EscapeSequence {
char: 0,
remaining_digits: 3,
})
}
byte if byte < 32 => {
// All bytes < 32 are supposed to be escaped in output mode.
report_parse_error(
handler,
"Unescaped character < ASCII 32 found in output",
byte,
);
self.state = ParserState::Error;
}
byte => {
// Standard input character
handler.pane_output(*pane, byte);
}
}
}
}
ParserState::TagWindowClose { maybe_window } => {
// Input prior to this state: "%window-close" | "%unlinked-window-close"
// Input parsed in this state: " @[window id]\n"
if let &mut Some(window) = maybe_window {
// We're parsing the window number.
if byte.is_ascii_digit() {
// Got another digit of the pane number.
*maybe_window = Some(window * 10 + (byte - b'0') as u32);
} else if byte == b'\n' {
// Message finished.
handler.tmux_control_mode_message(TmuxMessage::WindowClose {
window_id: window,
});
self.state = ParserState::BeginningOfLine;
} else {
report_parse_error(
handler,
"Non-digit character in %window-close window number",
byte,
);
self.state = ParserState::Error;
}
} else {
// We haven't started parsing the pane number yet.
if byte == b'@' {
// Window number starting.
*maybe_window = Some(0);
} else if byte == b' ' {
// Ignore spaces.
} else {
report_parse_error(
handler,
"Unexpected character after %window-close",
byte,
);
}
}
}
}
}
}
impl Default for TmuxControlModeParser {
fn default() -> Self {
Self::new()
}
}
fn report_parse_error(handler: &mut impl TmuxControlModeHandler, message: &'static str, byte: u8) {
handler.tmux_control_mode_message(TmuxMessage::ParseError { message, byte })
}
#[cfg(test)]
#[path = "parser_test.rs"]
mod tests;
+326
View File
@@ -0,0 +1,326 @@
use super::*;
use std::collections::HashMap;
struct TestHandler {
output: HashMap<u32, Vec<u8>>,
messages: Vec<TmuxMessage>,
}
impl TestHandler {
fn new() -> Self {
TestHandler {
output: HashMap::new(),
messages: Vec::new(),
}
}
}
impl TmuxControlModeHandler for TestHandler {
fn pane_output(&mut self, pane: u32, byte: u8) {
self.output.entry(pane).or_default().push(byte);
}
fn tmux_control_mode_message(&mut self, message: TmuxMessage) {
self.messages.push(message);
}
}
#[test]
fn test_valid_command_output_ok() {
let mut parser = TmuxControlModeParser::new();
let mut handler = TestHandler::new();
let input = b"%begin 1622462330 1\ndummy output\n%end\n";
for &byte in input {
parser.advance(&mut handler, byte);
}
assert_eq!(handler.messages.len(), 1);
assert_eq!(
&handler.messages[0],
&TmuxMessage::CommandOutput {
output_lines: Ok(vec![b"dummy output".to_vec()])
}
);
}
#[test]
fn test_valid_command_output_err() {
let mut parser = TmuxControlModeParser::new();
let mut handler = TestHandler::new();
let input = b"%begin 1622462330 1\ndummy error\n%error\n";
for &byte in input {
parser.advance(&mut handler, byte);
}
assert_eq!(handler.messages.len(), 1);
assert_eq!(
&handler.messages[0],
&TmuxMessage::CommandOutput {
output_lines: Err(vec![b"dummy error".to_vec()])
}
);
}
#[test]
fn test_exit_message() {
let mut parser = TmuxControlModeParser::new();
let mut handler = TestHandler::new();
let input = b"%exit\n";
for &byte in input {
parser.advance(&mut handler, byte);
}
assert_eq!(handler.messages.len(), 1);
assert_eq!(&handler.messages[0], &TmuxMessage::Exit);
}
#[test]
fn test_unknown_message() {
let mut parser = TmuxControlModeParser::new();
let mut handler = TestHandler::new();
let input = b"%unknown something\n";
for &byte in input {
parser.advance(&mut handler, byte);
}
assert_eq!(handler.messages.len(), 1);
assert_eq!(
&handler.messages[0],
&TmuxMessage::Unknown {
tag: b"unknown".to_vec(),
rest: b" something".to_vec(),
}
);
}
#[test]
fn test_error_message() {
let mut parser = TmuxControlModeParser::new();
let mut handler = TestHandler::new();
let input = b"non-percent\n";
for &byte in input {
parser.advance(&mut handler, byte);
}
assert_eq!(handler.messages.len(), 1);
match &handler.messages[0] {
&TmuxMessage::ParseError { message: _, byte } => {
assert_eq!(byte, b'n');
}
_ => panic!("Expected Error message"),
}
}
#[test]
fn test_pane_output() {
let mut parser = TmuxControlModeParser::new();
let mut handler = TestHandler::new();
let input = b"%output %0 here is some output\n";
for &byte in input {
parser.advance(&mut handler, byte);
}
assert_eq!(
handler.output.get(&0),
Some(&b"here is some output".to_vec())
)
}
#[test]
fn test_pane_output_escape_sequence() {
let mut parser = TmuxControlModeParser::new();
let mut handler = TestHandler::new();
let input = b"%output %1 \\1345\\015\\012\\n";
for &byte in input {
parser.advance(&mut handler, byte);
}
assert_eq!(handler.output.get(&1), Some(&b"\\5\r\n".to_vec()))
}
#[test]
fn test_pane_output_multiline() {
let mut parser = TmuxControlModeParser::new();
let mut handler = TestHandler::new();
let input = b"%output %0 first line\\012second line\\012third line\\012\n";
for &byte in input {
parser.advance(&mut handler, byte);
}
assert_eq!(
handler.output.get(&0),
Some(&b"first line\nsecond line\nthird line\n".to_vec())
)
}
#[test]
fn test_pane_output_split() {
let mut parser = TmuxControlModeParser::new();
let mut handler = TestHandler::new();
let input = b"%output %0 one\n%output %0 two\n%output %0 three\n";
for &byte in input {
parser.advance(&mut handler, byte);
}
assert_eq!(handler.output.get(&0), Some(&b"onetwothree".to_vec()))
}
#[test]
fn test_pane_output_incomplete() {
let mut parser = TmuxControlModeParser::new();
let mut handler = TestHandler::new();
let input = b"%output %0 incomplete";
for &byte in input {
parser.advance(&mut handler, byte);
}
assert_eq!(handler.messages.len(), 0);
// Output should be written immediately.
assert_eq!(handler.output.get(&0), Some(&b"incomplete".to_vec()))
}
#[test]
fn test_pane_output_incomplete_escape() {
let mut parser = TmuxControlModeParser::new();
let mut handler = TestHandler::new();
let input = b"%output %0 incomplete\\01";
for &byte in input {
parser.advance(&mut handler, byte);
}
assert_eq!(handler.messages.len(), 0);
// Output should be written as soon as it's ready. Incomplete escape sequences should not be
// written.
assert_eq!(handler.output.get(&0), Some(&b"incomplete".to_vec()))
}
#[test]
fn test_pane_output_invalid_escape_sequence() {
let mut parser = TmuxControlModeParser::new();
let mut handler = TestHandler::new();
let input = b"%output %0 \\a";
for &byte in input {
parser.advance(&mut handler, byte);
}
assert_eq!(handler.messages.len(), 1);
match &handler.messages[0] {
&TmuxMessage::ParseError { message: _, byte } => {
assert_eq!(byte, b'a');
}
_ => panic!("Expected Error message"),
}
}
#[test]
fn test_pane_output_invalid_character() {
// Characters below ASCII 32 should be escaped in the output. Tab is ASCII 9.
let mut parser = TmuxControlModeParser::new();
let mut handler = TestHandler::new();
let input = b"%output %0 \t";
for &byte in input {
parser.advance(&mut handler, byte);
}
assert_eq!(handler.messages.len(), 1);
match &handler.messages[0] {
&TmuxMessage::ParseError { message: _, byte } => {
assert_eq!(byte, b'\t');
}
_ => panic!("Expected Error message"),
}
}
#[test]
fn test_begin_without_end() {
let mut parser = TmuxControlModeParser::new();
let mut handler = TestHandler::new();
let input = b"%begin 1622462330 1\nsome output\n";
for &byte in input {
parser.advance(&mut handler, byte);
}
assert_eq!(handler.messages.len(), 0);
}
#[test]
fn test_example_output() {
let mut parser = TmuxControlModeParser::new();
let mut handler = TestHandler::new();
let input = br#"%begin 1578920019 258 0
%end 1578920019 258 0
%window-add @1
%sessions-changed
%session-changed $1 1
%window-renamed @1 tmux
%output %1 nicholas@yelena:~$
%window-renamed @1 ksh
%exit
"#;
for &byte in input {
parser.advance(&mut handler, byte);
}
assert_eq!(handler.messages.len(), 7);
assert_eq!(
&handler.messages[0],
&TmuxMessage::CommandOutput {
output_lines: Ok(vec![])
}
);
assert_eq!(
&handler.messages[1],
&TmuxMessage::Unknown {
tag: b"window-add".to_vec(),
rest: b" @1".to_vec(),
}
);
assert_eq!(
&handler.messages[2],
&TmuxMessage::Unknown {
tag: b"sessions-changed".to_vec(),
rest: b"".to_vec(),
}
);
assert_eq!(
&handler.messages[3],
&TmuxMessage::Unknown {
tag: b"session-changed".to_vec(),
rest: b" $1 1".to_vec(),
}
);
assert_eq!(
&handler.messages[4],
&TmuxMessage::Unknown {
tag: b"window-renamed".to_vec(),
rest: b" @1 tmux".to_vec(),
}
);
assert_eq!(
&handler.messages[5],
&TmuxMessage::Unknown {
tag: b"window-renamed".to_vec(),
rest: b" @1 ksh".to_vec(),
}
);
assert_eq!(&handler.messages[6], &TmuxMessage::Exit);
assert_eq!(
handler.output.get(&1),
Some(&b"nicholas@yelena:~$".to_vec())
)
}