use std::ffi::OsStr;
use std::sync::Arc;
use std::time::Duration;
use crate::formats::TmuxText;
use crate::{Command, CommandChain, Error};
use super::{CaptureOptions, CapturedLine, POLL_INTERVAL, Pane, PaneWait, contains};
impl Pane {
#[cfg(feature = "control-mode")]
pub async fn stream_output(&self) -> Result<crate::control::PaneOutput, Error> {
self.stream_output_with_limits(crate::ControlLimits::default())
.await
}
#[cfg(feature = "control-mode")]
pub async fn stream_output_with_limits(
&self,
limits: crate::ControlLimits,
) -> Result<crate::control::PaneOutput, Error> {
let window = self.window().await?.ok_or_else(|| Error::ObjectGone {
kind: crate::ObjectKind::Pane,
id: self.id().to_string(),
})?;
let server = crate::Server::from_core(Arc::clone(&self.core));
let (sender, events) =
crate::control::ControlMode::attach_with_limits(&server, window.session_id(), limits)
.await?
.split();
if let Err(error) = sender.watch_only(std::slice::from_ref(self.id())).await {
drop(sender);
return Err(events.shutdown_after_error(error).await);
}
Ok(crate::control::PaneOutput::new(
self.id().clone(),
events,
sender,
))
}
pub async fn capture(&self) -> Result<Vec<TmuxText>, Error> {
self.capture_with(CaptureOptions::visible()).await
}
pub async fn capture_with(&self, options: CaptureOptions) -> Result<Vec<TmuxText>, Error> {
let version = self.core.format_version().await?;
let needs_wrap_trim = options.needs_wrap_trim();
let command = options.lower(self.id().as_ref(), &version)?;
let target = command.target().map(OsStr::to_os_string);
let result = self.core.execute(command).await?;
if !result.success() {
return Err(Error::from_refused_result(
"capture-pane",
&result,
target.as_deref(),
));
}
let lines = split_lines(result.stdout());
Ok(if needs_wrap_trim {
lines.into_iter().map(trim_wrap_padding).collect()
} else {
lines
})
}
pub async fn capture_lines(&self, options: CaptureOptions) -> Result<Vec<CapturedLine>, Error> {
crate::Server::from_core(Arc::clone(&self.core))
.require("capture-pane -F", crate::version::since::CAPTURE_LINE_FLAGS)
.await?;
Ok(self
.capture_with(options.line_flags())
.await?
.into_iter()
.map(|row| CapturedLine::parse(&row))
.collect())
}
pub async fn wait_for_text(
&self,
needle: impl AsRef<[u8]>,
within: Duration,
) -> Result<PaneWait, Error> {
let needle = needle.as_ref();
self.look_until(within, |text, _| contains(text, needle))
.await
}
pub async fn wait_for_quiet(
&self,
quiet_for: Duration,
within: Duration,
) -> Result<PaneWait, Error> {
let mut last_change = tokio::time::Instant::now();
let mut previous: Option<Vec<u8>> = None;
self.look_until(within, move |text, now| {
if previous.as_deref() == Some(text) {
return now.duration_since(last_change) >= quiet_for;
}
previous = Some(text.to_vec());
last_change = now;
false
})
.await
}
pub async fn wait_until(
&self,
within: Duration,
mut settled: impl FnMut(&[TmuxText]) -> bool,
) -> Result<PaneWait, Error> {
self.look_until(within, |text, _| {
let lines: Vec<TmuxText> = split_lines(text)
.into_iter()
.map(trim_wrap_padding)
.collect();
settled(&lines)
})
.await
}
async fn look_until(
&self,
within: Duration,
mut settled: impl FnMut(&[u8], tokio::time::Instant) -> bool,
) -> Result<PaneWait, Error> {
let options = CaptureOptions::history().join_wrapped();
let target = self.id().to_string();
let version = self.core.format_version().await?;
let look = CommandChain::new(
Command::new("display-message")
.arg("-p")
.arg("-t")
.arg(&target)
.arg("-F")
.arg("#{pane_dead}"),
)
.then(options.lower(&target, &version)?);
let deadline = tokio::time::Instant::now().checked_add(within);
loop {
let result = match self.core.execute_chain(look.clone()).await {
Ok(result) => result,
Err(error) if is_gone(&error) => return Ok(PaneWait::Dead),
Err(error) => return Err(error),
};
if !result.success() {
let error =
Error::from_refused_result("capture-pane", &result, Some(OsStr::new(&target)));
return if is_gone(&error) {
Ok(PaneWait::Dead)
} else {
Err(error)
};
}
let (dead, text) = split_look(result.stdout());
let now = tokio::time::Instant::now();
if settled(text, now) {
return Ok(PaneWait::Arrived);
}
if dead {
return Ok(PaneWait::Dead);
}
if deadline.is_some_and(|deadline| tokio::time::Instant::now() >= deadline) {
return Ok(PaneWait::TimedOut);
}
tokio::time::sleep(POLL_INTERVAL.min(within)).await;
}
}
}
fn is_gone(error: &Error) -> bool {
error.is_object_gone() || matches!(error, Error::ServerGone { .. })
}
fn split_lines(stdout: &[u8]) -> Vec<TmuxText> {
let stdout = stdout.strip_suffix(b"\n").unwrap_or(stdout);
if stdout.is_empty() {
return Vec::new();
}
stdout
.split(|byte| *byte == b'\n')
.map(|line| TmuxText::from(line.to_vec()))
.collect()
}
fn trim_wrap_padding(line: TmuxText) -> TmuxText {
let bytes = line.as_bytes();
let content_len = bytes.len() - bytes.iter().rev().take_while(|&&byte| byte == b' ').count();
if content_len == bytes.len() {
line
} else {
TmuxText::from(bytes[..content_len].to_vec())
}
}
fn split_look(stdout: &[u8]) -> (bool, &[u8]) {
match stdout.iter().position(|byte| *byte == b'\n') {
Some(end) => (&stdout[..end] == b"1", &stdout[end + 1..]),
None => (stdout == b"1", &[]),
}
}
#[cfg(test)]
mod tests {
use super::split_look;
#[test]
fn a_look_separates_the_flag_from_the_capture() {
assert_eq!(
split_look(b"0\nfirst\nsecond\n"),
(false, &b"first\nsecond\n"[..])
);
assert_eq!(split_look(b"1\nfirst\n"), (true, &b"first\n"[..]));
assert_eq!(split_look(b"0\n"), (false, &b""[..]));
assert_eq!(split_look(b""), (false, &b""[..]));
}
}