use std::time::Duration;
use tokio::io::{AsyncBufRead, AsyncBufReadExt as _, AsyncWriteExt as _};
use tokio::process::ChildStdin;
use super::Event;
use crate::{Error, PaneId, TmuxText, WindowId};
pub(super) async fn read_line<R: AsyncBufRead + Unpin + ?Sized>(
stdout: &mut R,
pending: &mut Vec<u8>,
limit: usize,
) -> Result<Option<Line>, Error> {
read_line_within(stdout, pending, limit, None).await
}
pub(super) async fn read_line_within<R: AsyncBufRead + Unpin + ?Sized>(
stdout: &mut R,
pending: &mut Vec<u8>,
limit: usize,
within: Option<u64>,
) -> Result<Option<Line>, Error> {
let allowance = limit.saturating_sub(pending.len()).saturating_add(1);
let read = {
let mut bounded = tokio::io::AsyncReadExt::take(&mut *stdout, allowance as u64);
bounded
.read_until(b'\n', pending)
.await
.map_err(Error::control_mode)?
};
if read == 0 && pending.is_empty() {
return Ok(None);
}
if pending.len() > limit {
pending.clear();
return Err(Error::control_mode_frame_too_large("line", limit));
}
let bytes = pending.strip_suffix(b"\n").unwrap_or(pending);
let line = match within {
Some(number) => Line::parse_within_block(bytes, number),
None => Line::parse(bytes),
};
pending.clear();
Ok(Some(line))
}
pub(super) async fn write_line(stdin: &mut ChildStdin, line: &str) -> Result<(), Error> {
stdin
.write_all(line.as_bytes())
.await
.map_err(Error::control_mode)?;
stdin.write_all(b"\n").await.map_err(Error::control_mode)?;
stdin.flush().await.map_err(Error::control_mode)?;
Ok(())
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(super) enum Line {
BlockStart(u64),
BlockEnd { number: u64, succeeded: bool },
Event(Event),
Text(TmuxText),
}
impl Line {
pub(super) fn parse_within_block(line: &[u8], number: u64) -> Self {
match Self::parse(line) {
end @ Self::BlockEnd { number: found, .. } if found == number => end,
_ => Self::Text(TmuxText::from_bytes(line)),
}
}
pub(super) fn parse(line: &[u8]) -> Self {
let text = || Self::Text(TmuxText::from_bytes(line));
let Some(rest) = line.strip_prefix(b"%") else {
return text();
};
let (name, arguments) = split_once(rest, b' ');
let Ok(name) = std::str::from_utf8(name) else {
return text();
};
Self::framing(name, arguments, line)
.or_else(|| Self::about_output(name, arguments, line))
.or_else(|| Self::about_a_session(name, arguments, line))
.or_else(|| Self::about_a_window(name, arguments, line))
.or_else(|| Self::about_the_server(name, arguments, line))
.unwrap_or_else(|| {
Self::Event(Event::Other {
name: name.to_owned(),
rest: TmuxText::from_bytes(arguments),
})
})
}
fn framing(name: &str, arguments: &[u8], line: &[u8]) -> Option<Self> {
if !matches!(name, "begin" | "end" | "error") {
return None;
}
let number = std::str::from_utf8(arguments).ok().and_then(|arguments| {
arguments
.split_whitespace()
.nth(1)
.and_then(|value| value.parse().ok())
});
Some(match (name, number) {
("begin", Some(number)) => Self::BlockStart(number),
(_, Some(number)) => Self::BlockEnd {
number,
succeeded: name == "end",
},
(_, None) => Self::Text(TmuxText::from_bytes(line)),
})
}
fn about_output(name: &str, arguments: &[u8], line: &[u8]) -> Option<Self> {
let text = || Self::Text(TmuxText::from_bytes(line));
Some(match name {
"output" => {
let (pane, bytes) = split_once(arguments, b' ');
parsed(pane).map_or_else(text, |pane| {
Self::Event(Event::Output {
pane,
bytes: unescape_output(bytes),
})
})
}
"extended-output" => {
let (pane, rest) = split_once(arguments, b' ');
let (age, rest) = split_once(rest, b' ');
let bytes = rest.strip_prefix(b": ").unwrap_or(rest);
match (parsed(pane), parsed::<u64>(age)) {
(Some(pane), Some(age)) => Self::Event(Event::ExtendedOutput {
pane,
age: Duration::from_millis(age),
bytes: unescape_output(bytes),
}),
_ => text(),
}
}
"pause" => pane_event(arguments, text, |pane| Event::Paused { pane }),
"continue" => pane_event(arguments, text, |pane| Event::Continued { pane }),
"pane-mode-changed" => {
pane_event(arguments, text, |pane| Event::PaneModeChanged { pane })
}
_ => return None,
})
}
fn about_a_session(name: &str, arguments: &[u8], line: &[u8]) -> Option<Self> {
let text = || Self::Text(TmuxText::from_bytes(line));
Some(match name {
"session-changed" => {
let (session, _) = split_once(arguments, b' ');
parsed(session).map_or_else(text, |session| {
Self::Event(Event::SessionChanged { session })
})
}
"session-renamed" => {
let (session, new_name) = split_once(arguments, b' ');
parsed(session).map_or_else(text, |session| {
Self::Event(Event::SessionRenamed {
session,
name: TmuxText::from_bytes(new_name),
})
})
}
"session-window-changed" => {
let (session, window) = split_once(arguments, b' ');
match (parsed(session), parsed(window)) {
(Some(session), Some(window)) => {
Self::Event(Event::SessionWindowChanged { session, window })
}
_ => text(),
}
}
"sessions-changed" => Self::Event(Event::SessionsChanged),
_ => return None,
})
}
fn about_a_window(name: &str, arguments: &[u8], line: &[u8]) -> Option<Self> {
let text = || Self::Text(TmuxText::from_bytes(line));
Some(match name {
"window-add" => window_event(arguments, text, |window| Event::WindowAdded { window }),
"window-close" => {
window_event(arguments, text, |window| Event::WindowClosed { window })
}
"unlinked-window-add" => window_event(arguments, text, |window| {
Event::UnlinkedWindowAdded { window }
}),
"unlinked-window-close" => window_event(arguments, text, |window| {
Event::UnlinkedWindowClosed { window }
}),
"window-renamed" | "unlinked-window-renamed" => {
let (window, new_name) = split_once(arguments, b' ');
parsed(window).map_or_else(text, |window| {
let new_name = TmuxText::from_bytes(new_name);
Self::Event(if name == "window-renamed" {
Event::WindowRenamed {
window,
name: new_name,
}
} else {
Event::UnlinkedWindowRenamed {
window,
name: new_name,
}
})
})
}
"window-pane-changed" => {
let (window, pane) = split_once(arguments, b' ');
match (parsed(window), parsed(pane)) {
(Some(window), Some(pane)) => {
Self::Event(Event::WindowPaneChanged { window, pane })
}
_ => text(),
}
}
"layout-change" => {
let (window, rest) = split_once(arguments, b' ');
let (layout, rest) = split_once(rest, b' ');
let (visible_layout, flags) = split_once(rest, b' ');
parsed(window).map_or_else(text, |window| {
Self::Event(Event::LayoutChanged {
window,
layout: TmuxText::from_bytes(layout),
visible_layout: TmuxText::from_bytes(visible_layout),
flags: TmuxText::from_bytes(flags),
})
})
}
_ => return None,
})
}
fn about_the_server(name: &str, arguments: &[u8], line: &[u8]) -> Option<Self> {
let text = || Self::Text(TmuxText::from_bytes(line));
Some(match name {
"client-detached" => Self::Event(Event::ClientDetached {
client: TmuxText::from_bytes(arguments),
}),
"client-session-changed" => {
let (client, rest) = split_once(arguments, b' ');
let (session, session_name) = split_once(rest, b' ');
parsed(session).map_or_else(text, |session| {
Self::Event(Event::ClientSessionChanged {
client: TmuxText::from_bytes(client),
session,
name: TmuxText::from_bytes(session_name),
})
})
}
"paste-buffer-changed" => Self::Event(Event::PasteBufferChanged {
name: TmuxText::from_bytes(arguments),
}),
"paste-buffer-deleted" => Self::Event(Event::PasteBufferDeleted {
name: TmuxText::from_bytes(arguments),
}),
"subscription-changed" => Self::subscription(arguments).unwrap_or_else(text),
"config-error" => Self::Event(Event::ConfigError {
message: TmuxText::from_bytes(arguments),
}),
"message" => Self::Event(Event::Message {
message: TmuxText::from_bytes(arguments),
}),
"exit" => Self::Event(Event::Exit {
reason: (!arguments.is_empty()).then(|| TmuxText::from_bytes(arguments)),
}),
_ => return None,
})
}
fn subscription(arguments: &[u8]) -> Option<Self> {
let (name, rest) = split_once(arguments, b' ');
let (session, rest) = split_once(rest, b' ');
let (window, rest) = split_once(rest, b' ');
let (index, rest) = split_once(rest, b' ');
let (pane, rest) = split_once(rest, b' ');
Some(Self::Event(Event::SubscriptionChanged {
name: TmuxText::from_bytes(name),
session: parsed(session)?,
window: named(window),
index: named(index),
pane: named(pane),
value: TmuxText::from_bytes(rest.strip_prefix(b": ").unwrap_or(rest)),
}))
}
}
fn named<T: std::str::FromStr>(field: &[u8]) -> Option<T> {
if field == b"-" {
return None;
}
parsed(field)
}
fn parsed<T: std::str::FromStr>(field: &[u8]) -> Option<T> {
std::str::from_utf8(field).ok()?.parse().ok()
}
fn pane_event(
arguments: &[u8],
text: impl FnOnce() -> Line,
build: impl FnOnce(PaneId) -> Event,
) -> Line {
let (pane, _) = split_once(arguments, b' ');
parsed(pane).map_or_else(text, |pane| Line::Event(build(pane)))
}
fn window_event(
arguments: &[u8],
text: impl FnOnce() -> Line,
build: impl FnOnce(WindowId) -> Event,
) -> Line {
let (window, _) = split_once(arguments, b' ');
parsed(window).map_or_else(text, |window| Line::Event(build(window)))
}
fn split_once(bytes: &[u8], byte: u8) -> (&[u8], &[u8]) {
bytes
.iter()
.position(|found| *found == byte)
.map_or((bytes, [].as_slice()), |index| {
(&bytes[..index], &bytes[index + 1..])
})
}
pub(super) fn unescape_output(source: &[u8]) -> Vec<u8> {
let mut bytes = Vec::with_capacity(source.len());
let mut index = 0;
while index < source.len() {
if source[index] != b'\\' {
bytes.push(source[index]);
index += 1;
continue;
}
match source.get(index + 1..index + 4) {
Some(digits) if digits.iter().all(|digit| (b'0'..=b'7').contains(digit)) => {
let value = digits
.iter()
.fold(0_u32, |value, digit| value * 8 + u32::from(digit - b'0'));
if let Ok(byte) = u8::try_from(value) {
bytes.push(byte);
index += 4;
continue;
}
bytes.push(source[index]);
index += 1;
}
_ => {
if source.get(index + 1) == Some(&b'\\') {
bytes.push(b'\\');
index += 2;
} else {
bytes.push(source[index]);
index += 1;
}
}
}
}
bytes
}