#![cfg(feature = "test-support")]
#[path = "support/versions.rs"]
mod versions;
use std::sync::Arc;
use std::time::Duration;
use libtmux::test::{TestServer, retry_until};
use libtmux::{ChannelWait, Command, NewWindowOptions, SplitDirection, SplitOptions};
#[tokio::test]
async fn a_listed_buffer_name_passes_back_unchanged() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let mut expected = vec![libtmux::TmuxText::from("a=b c")];
server
.set_buffer(Some("a=b c"), "framed")
.await
.expect("buffer is stored");
let multiline = server
.cmd(
Command::new("set-buffer")
.arg("-b")
.arg("two\nlines")
.arg("--")
.arg("x"),
)
.await
.expect("tmux answers");
if multiline.success() {
expected.push(libtmux::TmuxText::from("two\nlines"));
}
let mut names = server.buffer_names().await.expect("names");
names.sort_by(|left, right| left.as_bytes().cmp(right.as_bytes()));
assert_eq!(names, expected);
let safe_delete = versions::supports(server, &libtmux::since::NAMED_BUFFER_IDENTITY).await;
for name in &names {
assert!(
server.buffer(name).await.expect("read").is_some(),
"{name:?}"
);
if safe_delete {
server.delete_buffer(name).await.expect("buffer is deleted");
} else {
assert!(matches!(
server.delete_buffer(name).await,
Err(libtmux::Error::UnsupportedCapability { .. })
));
assert!(
server
.buffer(name)
.await
.expect("retained buffer")
.is_some()
);
}
}
assert_eq!(
server.buffer_names().await.expect("names").is_empty(),
safe_delete
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn buffers_hold_exact_bytes_and_report_absence() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
assert!(server.buffer_names().await.expect("names").is_empty());
assert!(
server.buffer("absent").await.expect("read").is_none(),
"an unknown buffer is absent rather than an error",
);
server
.set_buffer(Some("payload"), "line one\nline two\ttabbed")
.await
.expect("buffer is stored");
assert_eq!(
server.buffer("payload").await.expect("read"),
Some(b"line one\nline two\ttabbed".to_vec()),
);
let names = server.buffer_names().await.expect("names");
assert_eq!(names, ["payload"]);
if versions::supports(server, &libtmux::since::NAMED_BUFFER_IDENTITY).await {
server
.delete_buffer("payload")
.await
.expect("buffer is deleted");
assert!(server.buffer("payload").await.expect("read").is_none());
assert!(
server.delete_buffer("payload").await.is_err(),
"deleting a buffer that is gone is a failure, not a silent success",
);
}
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn flag_shaped_buffer_data_and_channels_stay_literal() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
server
.set_buffer(Some("-literal-buffer"), "-literal-payload")
.await
.expect("flag-shaped buffer data stays literal");
assert_eq!(
server.buffer("-literal-buffer").await.expect("read"),
Some(b"-literal-payload".to_vec()),
);
server
.lock_channel("-literal-lock")
.await
.expect("a flag-shaped channel locks");
server
.unlock_channel("-literal-lock")
.await
.expect("a flag-shaped channel unlocks");
server
.signal_channel("-literal-signal")
.await
.expect("a flag-shaped channel signals");
assert_eq!(
server
.wait_for_channel("-literal-signal", Duration::from_secs(5))
.await
.expect("a flag-shaped channel waits"),
ChannelWait::Signalled,
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn key_bindings_can_be_added_and_removed() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
server
.bind_key("prefix", "Y", "display-message bound")
.await
.expect("key is bound");
let listed = server
.key_bindings(Some("prefix"))
.await
.expect("bindings are listed");
assert!(
listed
.iter()
.any(|line| line.contains(" Y ") && line.contains("display-message")),
"the new binding appears in tmux's own listing",
);
server
.unbind_key("prefix", "Y")
.await
.expect("key is unbound");
let listed = server
.key_bindings(Some("prefix"))
.await
.expect("bindings are listed");
assert!(!listed.iter().any(|line| line.contains(" Y ")));
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn real_tmux_compat_key_bindings_read_as_fields() {
use libtmux::{Error, since};
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let version = server
.capabilities()
.await
.expect("capabilities")
.tmux_version()
.clone();
if !version.has_behavior(&since::LIST_KEYS_FORMAT) {
let refused = server
.typed_key_bindings(None)
.await
.expect_err("list-keys -F is 3.7 and later");
assert!(
matches!(refused, Error::UnsupportedCapability { .. }),
"{refused:?}"
);
guard.shutdown().await.expect("tmux fixture shuts down");
return;
}
for command in [
Command::new("bind-key")
.arg("-r")
.arg("-N")
.arg("a\nnote")
.arg("-T")
.arg("sp ace")
.arg("\"")
.arg("display-message 'two words'"),
Command::new("bind-key")
.arg("-T")
.arg("solo")
.arg("M-'")
.arg("display-message a=b ; display-message c"),
] {
let bound = server.cmd(command).await.expect("bind-key runs");
assert!(bound.success(), "{:?}", bound.stderr_lossy());
}
let spaced = server
.typed_key_bindings(Some("sp ace"))
.await
.expect("bindings decode");
assert_eq!(spaced.len(), 1);
assert_eq!(spaced[0].table(), "sp ace");
assert_eq!(spaced[0].key(), "\"");
assert_eq!(spaced[0].command(), "display-message \"two words\"");
assert_eq!(
spaced[0].note().map(libtmux::TmuxText::as_bytes),
Some(b"a\nnote".as_slice())
);
assert!(spaced[0].repeats());
let solo = server
.typed_key_bindings(Some("solo"))
.await
.expect("bindings decode");
assert_eq!(solo.len(), 1, "a one-binding table is listed");
assert_eq!(solo[0].key(), "M-'");
assert_eq!(
solo[0].command(),
r"display-message a=b \; display-message c"
);
assert_eq!(solo[0].note(), None);
assert!(!solo[0].repeats());
let all = server
.typed_key_bindings(None)
.await
.expect("bindings decode");
let lines = server
.key_bindings(None)
.await
.expect("bindings are listed");
assert_eq!(all.len(), lines.len());
let command = solo[0].command().as_str().expect("the command is UTF-8");
assert!(
lines
.iter()
.any(|line| line.contains("-T solo") && line.ends_with(command)),
"{lines:?}",
);
assert!(
server
.typed_key_bindings(Some("no-such-table"))
.await
.expect("bindings decode")
.is_empty()
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn formats_expand_against_a_target() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server.new_session("formatted").await.expect("session");
let pane = session
.panes()
.await
.expect("panes")
.into_iter()
.next()
.expect("one pane");
let name = server
.format(Some(&pane), "#{session_name}")
.await
.expect("format expands");
assert_eq!(name.as_bytes(), b"formatted");
let combined = server
.format(Some(&pane), "#{session_name}:#{session_windows}")
.await
.expect("format expands");
assert_eq!(combined.as_bytes(), b"formatted:1");
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn sourcing_a_file_applies_its_commands() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let directory = tempfile::tempdir().expect("temporary directory");
let config = directory.path().join("extra.conf");
std::fs::write(&config, "set-option -s @sourced yes\n").expect("config is written");
server.source_file(&config).await.expect("file is sourced");
assert_eq!(
server.typed_option("@sourced").await.expect("read"),
Some(libtmux::OptionValue::from("yes")),
"the sourced option is set",
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[derive(Debug, PartialEq)]
enum Failure {
Deliberate,
}
#[tokio::test]
async fn scoped_operations_clean_up_after_success_and_failure() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let seen = server
.with_session("scoped", async |session| {
Ok::<_, libtmux::Error>(session.id().to_string())
})
.await
.expect("the scope runs and the operation succeeds");
assert!(seen.starts_with('$'));
assert!(server.sessions().await.expect("sessions").is_empty());
let outcome = server
.with_session("failing", async |_session| {
Err::<(), Failure>(Failure::Deliberate)
})
.await;
assert!(matches!(
outcome,
Err(libtmux::ScopeError::Operation(Failure::Deliberate))
));
assert!(
server.sessions().await.expect("sessions").is_empty(),
"cleanup runs even when the operation failed",
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn scoped_windows_and_panes_nest() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server.new_session("nested").await.expect("session");
let before = session.windows().await.expect("windows").len();
let pane_count = session
.with_window(
NewWindowOptions::new("temporary").command("sleep 300"),
async |window| {
window
.with_pane(
SplitOptions::new(SplitDirection::Below).command("sleep 300"),
async |_pane| {
Ok::<_, libtmux::Error>(window.panes().await.expect("panes").len())
},
)
.await
},
)
.await
.expect("both scopes run and the operation succeeds");
assert_eq!(pane_count, 2, "the scoped pane existed inside its scope");
assert_eq!(
session.windows().await.expect("windows").len(),
before,
"the scoped window is gone afterwards",
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn scoped_operations_clean_up_after_cancellation() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server().clone();
let anchor = server.new_session("anchor").await.expect("session");
let window = anchor
.active_window()
.await
.expect("window lookup")
.expect("window");
let ready = Arc::new(tokio::sync::Barrier::new(4));
let session_scope = tokio::spawn({
let server = server.clone();
let ready = Arc::clone(&ready);
async move {
server
.with_session("cancelled-session", async move |_session| {
ready.wait().await;
std::future::pending::<Result<(), libtmux::Error>>().await
})
.await
}
});
let window_scope = tokio::spawn({
let anchor = anchor.clone();
let ready = Arc::clone(&ready);
async move {
anchor
.with_window("cancelled-window", async move |_window| {
ready.wait().await;
std::future::pending::<Result<(), libtmux::Error>>().await
})
.await
}
});
let pane_scope = tokio::spawn({
let window = window.clone();
let ready = Arc::clone(&ready);
async move {
window
.with_pane(SplitDirection::Below, async move |_pane| {
ready.wait().await;
std::future::pending::<Result<(), libtmux::Error>>().await
})
.await
}
});
tokio::time::timeout(Duration::from_secs(5), ready.wait())
.await
.expect("all scoped objects are created");
session_scope.abort();
window_scope.abort();
pane_scope.abort();
for scope in [session_scope, window_scope, pane_scope] {
assert!(scope.await.expect_err("scope is aborted").is_cancelled());
}
let cleanup = retry_until(Duration::from_secs(5), async || {
matches!(server.sessions().await, Ok(objects) if objects.len() == 1)
&& matches!(anchor.windows().await, Ok(objects) if objects.len() == 1)
&& matches!(window.panes().await, Ok(objects) if objects.len() == 1)
})
.await;
let sessions = server.sessions().await.expect("sessions").len();
let windows = anchor.windows().await.expect("windows").len();
let panes = window.panes().await.expect("panes").len();
guard.shutdown().await.expect("tmux fixture shuts down");
assert!(
cleanup.is_ok(),
"aborted scopes left sessions={sessions}, windows={windows}, panes={panes}",
);
}
#[tokio::test]
async fn shell_commands_run_through_tmux_and_report_their_output() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let outcome = server.run_shell("printf 'first\\nsecond\\n'").await;
let Ok(lines) = outcome else {
let error = outcome.expect_err("refused");
assert!(
matches!(&error, libtmux::Error::CapabilityDefective { capability, .. }
if *capability == "run-shell output"),
"an affected release refuses with the reason, got {error:?}",
);
assert_eq!(error.kind(), libtmux::ErrorKind::UnsupportedVersion);
guard.shutdown().await.expect("tmux fixture shuts down");
return;
};
assert_eq!(
lines
.iter()
.map(libtmux::TmuxText::as_bytes)
.collect::<Vec<_>>(),
[&b"first"[..], &b"second"[..]],
);
assert!(server.run_shell("true").await.expect("runs").is_empty());
server
.spawn_shell("false")
.await
.expect("tmux accepts a background command whatever it later does");
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn wait_for_channels_lock_and_release() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
server
.lock_channel("gate")
.await
.expect("channel is locked");
server
.unlock_channel("gate")
.await
.expect("channel is unlocked");
server
.signal_channel("gate")
.await
.expect("channel is signalled");
assert_eq!(
server
.wait_for_channel("gate", Duration::from_secs(5))
.await
.expect("waiting is not an error"),
ChannelWait::Signalled,
"a signal with nobody waiting is kept, not dropped",
);
assert_eq!(
server
.wait_for_channel("gate", Duration::from_millis(300))
.await
.expect("running out of time is not an error"),
ChannelWait::TimedOut,
"the latch releases one wait, not every later one",
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn real_tmux_compat_cancelling_a_queued_lock_wedges_the_channel() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
server
.lock_channel("wedge")
.await
.expect("the first lock is uncontested");
let cancelled = tokio::time::timeout(
libtmux::test::scaled(Duration::from_secs(2)),
server.lock_channel("wedge"),
)
.await;
assert!(
cancelled.is_err(),
"the second lock is genuinely contested and queues",
);
let wedged = tokio::time::timeout(
libtmux::test::scaled(Duration::from_millis(800)),
server.lock_channel("wedge"),
)
.await;
assert!(
wedged.is_err(),
"cancelling the queued locker corrupts the channel for good, per the defect this pins",
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn a_wait_blocks_until_something_signals_the_channel() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let signaller = server.clone();
let signalling = tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(400)).await;
signaller
.signal_channel("ready")
.await
.expect("channel is signalled");
});
let started = std::time::Instant::now();
let outcome = server
.wait_for_channel("ready", Duration::from_secs(10))
.await
.expect("waiting is not an error");
let waited = started.elapsed();
signalling.await.expect("the signalling task finishes");
assert_eq!(outcome, ChannelWait::Signalled);
assert!(
waited >= Duration::from_millis(300),
"the wait returned in {waited:?}, so it did not wait for the signal",
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn client_operations_need_a_client_and_report_when_there_is_none() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
server.new_session("headless").await.expect("session");
assert!(server.clients().await.expect("clients list").is_empty());
assert!(
server.display_popup(None, "true").await.is_err(),
"a popup needs a client with a terminal",
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[cfg(feature = "control-mode")]
#[tokio::test]
async fn a_client_reports_itself_while_it_is_attached() {
use libtmux::control::ControlMode;
use std::time::Duration;
let guard = TestServer::builder().start().await.expect("tmux starts");
let Some(guard) = versions::control_fixture(guard).await else {
return;
};
let server = guard.server();
let first = server.new_session("client-one").await.expect("session");
let second = server.new_session("client-two").await.expect("session");
let control = ControlMode::attach(server, first.id())
.await
.expect("control mode attaches");
retry_until(Duration::from_secs(10), async || {
server
.clients()
.await
.is_ok_and(|clients| !clients.is_empty())
})
.await
.expect("the attached client is listed");
let mut client = server.clients().await.expect("clients").remove(0);
assert!(client.is_control_mode(), "this client attached with -C");
assert!(!client.name().as_bytes().is_empty());
assert!(client.pid() > 0);
client
.switch_to(&second)
.await
.expect("the client switches session");
retry_until(Duration::from_secs(10), async || {
second
.refreshed()
.await
.is_ok_and(|session| session.is_attached())
})
.await
.expect("the second session reports the client");
client.refresh().await.expect("the client still exists");
client.detach().await.expect("the client detaches");
retry_until(Duration::from_secs(10), async || {
server
.clients()
.await
.is_ok_and(|clients| clients.is_empty())
})
.await
.expect("the detached client stops being listed");
let _ = control.shutdown().await;
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[cfg(feature = "control-mode")]
#[tokio::test]
async fn server_operations_reject_foreign_handles() {
use libtmux::control::ControlMode;
use libtmux::{Chooser, Error, ErrorKind};
macro_rules! error_kind {
($future:expr) => {
match $future.await {
Err(error @ Error::ServerMismatch { .. }) => error.kind(),
Err(error) => panic!("foreign handle returned {error:?}"),
Ok(_) => panic!("foreign handle was accepted"),
}
};
}
let left_guard = TestServer::builder().start().await.expect("tmux starts");
let right_guard = TestServer::builder().start().await.expect("tmux starts");
let left = left_guard.server();
let right = right_guard.server();
let left_session = left.new_session("left").await.expect("left session");
let right_session = right.new_session("right").await.expect("right session");
let right_pane = right_session.panes().await.expect("right panes").remove(0);
assert_eq!(
error_kind!(left.format(Some(&right_pane), "#{session_name}")),
ErrorKind::InvalidInput
);
if !versions::supports_control(right).await {
left_guard.shutdown().await.expect("left fixture closes");
right_guard.shutdown().await.expect("right fixture closes");
return;
}
let control = ControlMode::attach(right, right_session.id())
.await
.expect("control mode attaches");
retry_until(Duration::from_secs(10), async || {
right
.clients()
.await
.is_ok_and(|clients| !clients.is_empty())
})
.await
.expect("the foreign client is listed");
let foreign_client = right.clients().await.expect("right clients").remove(0);
let kinds = [
error_kind!(left.format(Some(&right_pane), "#{session_name}")),
error_kind!(left.display_popup(Some(&foreign_client), "true")),
error_kind!(left.display_menu(
Some(&foreign_client),
"menu",
[libtmux::MenuItem::new("Item", "i", "display-message item")],
)),
error_kind!(left.command_prompt(
Some(&foreign_client),
Some("question"),
"display-message answer",
)),
error_kind!(left.choose(Chooser::Tree, Some(&foreign_client))),
error_kind!(left.find_window(Some(&foreign_client), "needle")),
error_kind!(left.display_panes(Some(&foreign_client))),
error_kind!(foreign_client.switch_to(&left_session)),
];
assert_eq!(kinds, [ErrorKind::InvalidInput; 8]);
let _ = control.shutdown().await;
right_guard
.shutdown()
.await
.expect("right tmux fixture shuts down");
left_guard
.shutdown()
.await
.expect("left tmux fixture shuts down");
}
#[tokio::test]
async fn pane_modes_enter_and_leave() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server.new_session("modes").await.expect("session");
let mut pane = session
.panes()
.await
.expect("panes")
.into_iter()
.next()
.expect("one pane");
assert!(!pane.is_in_mode(), "a fresh pane has no mode open");
pane.copy_mode().await.expect("copy mode is entered");
pane.refresh().await.expect("the pane still exists");
assert!(pane.is_in_mode(), "copy mode is a pane mode");
pane.exit_mode().await.expect("the mode is cancelled");
pane.refresh().await.expect("the pane still exists");
assert!(!pane.is_in_mode(), "cancelling leaves the mode");
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn a_pane_leaves_a_mode_that_is_not_copy_mode() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server.new_session("clocked").await.expect("session");
let mut pane = session
.panes()
.await
.expect("panes")
.into_iter()
.next()
.expect("one pane");
pane.clock_mode().await.expect("clock mode is entered");
pane.refresh().await.expect("the pane still exists");
assert!(pane.is_in_mode(), "clock mode is a pane mode");
pane.exit_mode().await.expect("clock mode is left");
pane.refresh().await.expect("the pane still exists");
assert!(
!pane.is_in_mode(),
"a mode that is not copy mode is left as well"
);
pane.exit_mode()
.await
.expect("leaving no mode at all is not a failure");
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn edge_flags_follow_the_split_that_made_them() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server.new_session("edges").await.expect("session");
let window = session
.active_window()
.await
.expect("windows")
.expect("a session has a window");
let only = window.panes().await.expect("panes").remove(0);
assert!(only.is_at_left(), "an undivided pane touches both edges");
assert!(only.is_at_right(), "an undivided pane touches both edges");
only.split(SplitOptions::new(SplitDirection::Right))
.await
.expect("the pane splits sideways");
let panes = window.panes().await.expect("panes");
assert_eq!(panes.len(), 2, "the split produced a second pane");
let (left, right) = (&panes[0], &panes[1]);
assert!(left.is_at_left(), "the original keeps the left edge");
assert!(!left.is_at_right(), "and gives up the right one");
assert!(right.is_at_right(), "the new pane takes the right edge");
assert!(!right.is_at_left(), "and does not hold the left one");
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn a_global_window_option_is_read_globally() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server.new_session("options").await.expect("session");
let window = session
.active_window()
.await
.expect("windows")
.expect("a session has a window");
server
.set_global_window_option("main-pane-width", "123")
.await
.expect("the global window option is set");
window
.set_option("main-pane-width", "456")
.await
.expect("the window takes a value of its own");
let read = server
.typed_global_window_option("main-pane-width")
.await
.expect("the option is readable");
assert_eq!(
read,
Some(libtmux::OptionValue::from("123")),
"the global read is not answered by the window's own value"
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn a_pane_pipes_until_it_is_told_to_stop() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server.new_session("piped").await.expect("session");
let pane = session.panes().await.expect("panes").remove(0);
let mut pane = pane;
assert!(!pane.is_piped(), "nothing yet");
let sink_dir = tempfile::Builder::new()
.prefix("libtmux-pipe-")
.tempdir()
.expect("a directory of its own");
let sink = sink_dir.path().join("seen");
pane.pipe(Some(format!("cat >{}", sink.display())))
.await
.expect("the pane pipes");
pane.refresh().await.expect("the pane still exists");
assert!(pane.is_piped(), "now piping");
pane.send_keys("printf 'piped\n'").await.expect("keys");
pane.send_key_names(["Enter"]).await.expect("enter");
retry_until(Duration::from_secs(10), async || {
std::fs::read(&sink).is_ok_and(|seen| seen.windows(5).any(|word| word == b"piped"))
})
.await
.expect("what the pane printed reaches the pipe");
pane.pipe(None::<String>).await.expect("the pipe stops");
pane.refresh().await.expect("the pane still exists");
assert!(!pane.is_piped(), "the same call with no command stops it");
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn a_pane_reports_its_own_terminal() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server.new_session("ttys").await.expect("session");
let pane = session.panes().await.expect("panes").remove(0);
let reported = pane.tty().as_str().expect("a tty path is text").to_owned();
let asked = server
.format(Some(&pane), "#{pane_tty}")
.await
.expect("tmux answers for the same pane");
assert_eq!(
reported,
asked.as_str().expect("a tty path is text"),
"the accessor and tmux name the same terminal"
);
assert!(reported.starts_with("/dev/"), "and it is a device path");
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn window_flags_each_read_their_own_field() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server.new_session("flags").await.expect("session");
let window = session
.active_window()
.await
.expect("windows")
.expect("a session has a window");
server
.set_global_window_option("monitor-activity", "on")
.await
.expect("activity is monitored");
session
.new_window(NewWindowOptions::new("elsewhere"))
.await
.expect("a second window takes the focus");
let panes = window.panes().await.expect("panes");
panes[0].send_keys("printf 'noise\n'").await.expect("keys");
panes[0].send_key_names(["Enter"]).await.expect("enter");
let mut window = window;
retry_until(Duration::from_secs(10), async || {
window.refresh().await.is_ok() && window.has_activity()
})
.await
.expect("the background window records activity");
assert!(
!window.has_bell(),
"nothing rang a bell, so the two flags differ and a swap would show"
);
for (reported, format) in [
(window.has_activity(), "#{window_activity_flag}"),
(window.has_bell(), "#{window_bell_flag}"),
] {
let asked = server
.cmd(
Command::new("display-message")
.arg("-p")
.arg("-t")
.arg(window.id().to_string())
.arg(format),
)
.await
.expect("tmux answers");
let asked = asked.stdout_lossy().trim() == "1";
assert_eq!(reported, asked, "{format} agrees with the accessor");
}
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn a_buffer_pastes_into_a_pane() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server.new_session("pasted").await.expect("session");
let pane = session.panes().await.expect("panes").remove(0);
server
.set_buffer(Some("greeting"), "printf 'pasted-here\n'")
.await
.expect("the buffer is set");
pane.paste_buffer(Some("greeting"))
.await
.expect("the buffer pastes");
pane.send_key_names(["Enter"]).await.expect("enter");
retry_until(Duration::from_secs(10), async || {
pane.capture().await.is_ok_and(|lines| {
lines
.iter()
.any(|line| line.as_bytes().windows(11).any(|w| w == b"pasted-here"))
})
})
.await
.expect("what was pasted runs in the pane");
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[cfg(feature = "query")]
#[tokio::test]
async fn searching_a_window_finds_the_pane_that_matches() {
use libtmux::query::Filterable as _;
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server.new_session("searched").await.expect("session");
let window = session
.active_window()
.await
.expect("windows")
.expect("a session has a window");
let panes = window.panes().await.expect("panes");
let wanted = panes[0].id().to_string();
let fields = libtmux::Pane::filter_fields();
let found = window
.search_panes(fields.pane_id.eq(wanted.as_str()))
.await
.unwrap_or_default();
assert_eq!(found.len(), 1, "the pane that matches is returned");
assert_eq!(found[0].id().to_string(), wanted);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[cfg(feature = "control-mode")]
#[tokio::test]
async fn a_client_reports_its_own_terminal_and_type() {
use libtmux::control::ControlMode;
let guard = TestServer::builder().start().await.expect("tmux starts");
let Some(guard) = versions::control_fixture(guard).await else {
return;
};
let server = guard.server();
let session = server.new_session("client-fields").await.expect("session");
let control = ControlMode::attach(server, session.id())
.await
.expect("control mode attaches");
retry_until(Duration::from_secs(10), async || {
server
.clients()
.await
.is_ok_and(|clients| !clients.is_empty())
})
.await
.expect("the attached client is listed");
let client = server.clients().await.expect("clients").remove(0);
let name = client.name().to_string_lossy().into_owned();
assert_ne!(
client.tty().as_str().expect("client text"),
client.term_name().as_str().expect("client text"),
"the two fields differ here, so a swap between them would show"
);
for (reported, format) in [
(client.tty(), "#{client_tty}"),
(client.term_name(), "#{client_termname}"),
] {
let asked = server
.cmd(
Command::new("display-message")
.arg("-p")
.arg("-t")
.arg(&name)
.arg(format),
)
.await
.expect("tmux answers for this client");
assert_eq!(
reported.as_str().expect("client text"),
asked.stdout_lossy().trim(),
"{format} agrees with the accessor"
);
}
assert!(
!client.is_readonly(),
"a client that attached without -r may act"
);
client.redraw().await.expect("the client redraws");
let _ = control.shutdown().await;
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[cfg(all(feature = "control-mode", feature = "query"))]
#[tokio::test]
async fn timestamps_are_system_times_that_agree_with_their_handles() {
use std::time::{SystemTime, UNIX_EPOCH};
use libtmux::control::ControlMode;
use libtmux::query::Filterable as _;
use libtmux::{Availability, Client, Session, Window};
let since_epoch = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("the clock is past 1970");
let before = UNIX_EPOCH + Duration::from_secs(since_epoch.as_secs());
let guard = TestServer::builder().start().await.expect("tmux starts");
let Some(guard) = versions::control_fixture(guard).await else {
return;
};
let server = guard.server();
let mut session = server.new_session("clock").await.expect("session");
let control = ControlMode::attach(server, session.id())
.await
.expect("control mode attaches");
retry_until(Duration::from_secs(10), async || {
server
.clients()
.await
.is_ok_and(|clients| !clients.is_empty())
})
.await
.expect("the attached client is listed");
session.refresh().await.expect("session refreshes");
let window = session
.active_window()
.await
.expect("window lookup")
.expect("a window");
let client = server.clients().await.expect("clients").remove(0);
let generation = server.generation().await.expect("generation");
let after = SystemTime::now();
let seconds = |time: SystemTime| {
let offset = time.duration_since(UNIX_EPOCH).expect("after 1970");
i64::try_from(offset.as_secs()).expect("fits tmux's seconds")
};
let sessions = Session::filter_fields();
let last_attached = session.last_attached().expect("a client attached");
for (label, time, handle) in [
(
"session_created",
session.created(),
session.get(sessions.session_created),
),
(
"session_last_attached",
last_attached,
session.get(sessions.session_last_attached),
),
(
"window_activity",
window.last_activity(),
window.get(Window::filter_fields().window_activity),
),
(
"client_created",
client.created(),
client.get(Client::filter_fields().client_created),
),
] {
assert!(
before <= time && time <= after,
"{label} is {time:?}, outside {before:?}..={after:?}",
);
assert_eq!(
handle,
Availability::Available(seconds(time)),
"{label}'s handle reads the same moment as Unix seconds",
);
}
let started = generation.start_time();
assert!(
before <= started && started <= after,
"start_time is {started:?}, outside {before:?}..={after:?}",
);
assert!(
generation
.to_string()
.ends_with(&format!(" started {}", seconds(started))),
"Display prints the same moment as Unix seconds",
);
let _ = control.shutdown().await;
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn dispatch_only_commands_are_accepted() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server.new_session("dispatch").await.expect("session");
let pane = session.panes().await.expect("panes").remove(0);
pane.send_prefix().await.expect("the prefix key is sent");
server
.clear_prompt_history()
.await
.expect("the prompt history is cleared");
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn unlinking_a_gone_link_does_not_call_a_live_window_gone() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let home = server.new_session("home").await.expect("session");
let elsewhere = server.new_session("elsewhere").await.expect("session");
let shared = home
.new_window(NewWindowOptions::new("shared"))
.await
.expect("a window to share");
let shared_id = shared.id().to_string();
let index = shared.index();
shared
.link_to(&elsewhere, None)
.await
.expect("the window is linked into a second session");
let mut handles = Vec::new();
for _ in 0..2 {
handles.push(
home.windows()
.await
.expect("windows")
.into_iter()
.find(|window| window.index() == index)
.expect("the shared window is linked here"),
);
}
let second = handles.pop().expect("two handles");
let first = handles.pop().expect("two handles");
first.unlink().await.expect("the link is removed");
let alive = server
.windows()
.await
.expect("windows")
.into_iter()
.any(|window| window.id().to_string() == shared_id);
assert!(alive, "the window survives losing one of its links");
let again = home
.windows()
.await
.expect("windows")
.into_iter()
.find(|window| window.index() == index);
assert!(
again.is_none(),
"the session no longer holds a window at that index"
);
let refused = second.unlink().await.expect_err("the link is already gone");
assert!(
!refused.is_object_gone(),
"a missing link is not a missing object; the window is still alive. got {refused:?}"
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn a_malformed_pane_variable_is_not_being_outside_tmux() {
use libtmux::ServerConfigurationErrorKind as Kind;
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
server.new_session("env").await.expect("session");
let absent = libtmux::Pane::from_env_value(server, None::<std::ffi::OsString>)
.await
.expect_err("nothing set the variable");
assert!(
matches!(
&absent,
libtmux::Error::InvalidServerConfiguration {
kind: Kind::NotInsideTmux,
..
}
),
"an unset variable means this process was not started by tmux, got {absent:?}"
);
for wrong in ["%abc", "@0", "%-1", "#{pane_id}", ""] {
let malformed = libtmux::Pane::from_env_value(server, Some(wrong))
.await
.expect_err("the variable is set and does not name a pane");
assert!(
matches!(
&malformed,
libtmux::Error::InvalidServerConfiguration {
kind: Kind::MalformedTmuxVariable,
..
}
),
"{wrong:?} is a variable written wrongly, not an absent one, got {malformed:?}"
);
}
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn a_chooser_opens_in_a_pane_and_a_popup_needs_a_client() {
use libtmux::Chooser;
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server.new_session("nobody").await.expect("session");
assert!(
server.clients().await.expect("clients").is_empty(),
"nothing is attached"
);
let mut pane = session.panes().await.expect("panes").remove(0);
assert!(!pane.is_in_mode(), "the pane starts in no mode");
server
.choose(Chooser::Tree, None)
.await
.expect("a chooser needs no client");
pane.refresh().await.expect("the pane still exists");
assert!(
pane.is_in_mode(),
"the chooser is open in the pane, which is what the Ok meant"
);
let refused = server
.display_popup(None, "true")
.await
.expect_err("a popup draws on a client and there is none");
assert_eq!(refused.kind(), libtmux::ErrorKind::Refused);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn a_dead_pane_says_so_however_the_question_is_asked() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server.new_session("gone").await.expect("session");
let window = session
.active_window()
.await
.expect("windows")
.expect("a session has a window");
let doomed = window
.panes()
.await
.expect("panes")
.remove(0)
.split(SplitOptions::new(SplitDirection::Below))
.await
.expect("a second pane to kill");
doomed
.set_option("@marker", "here")
.await
.expect("the user option is set");
assert_eq!(
doomed.typed_option("@marker").await.expect("readable"),
Some(libtmux::OptionValue::from("here")),
);
let asking = window
.panes()
.await
.expect("panes")
.into_iter()
.find(|pane| pane.id() == doomed.id())
.expect("the doomed pane is listed");
doomed.kill().await.expect("the pane is killed");
let doomed = asking;
let by_capture = doomed.capture().await.expect_err("the pane is gone");
assert!(
by_capture.is_object_gone(),
"capture says so: {by_capture:?}"
);
let by_option = doomed
.typed_option("remain-on-exit")
.await
.expect_err("the pane is gone");
assert!(
by_option.is_object_gone(),
"a built-in option says so too: {by_option:?}"
);
let by_user_option = doomed
.typed_option("@marker")
.await
.expect_err("the pane is gone, not the option unset");
assert!(
by_user_option.is_object_gone(),
"a user option must not report a gone pane as an unset option: {by_user_option:?}"
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn a_dead_session_is_not_an_unset_variable() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
server.new_session("keeper").await.expect("session");
let doomed = server.new_session("doomed").await.expect("session");
doomed
.set_environment("FOO", "bar")
.await
.expect("the variable is set");
assert!(
doomed.environment("FOO").await.expect("readable").is_some(),
"the positive control: it really is set"
);
assert!(
doomed
.environment("NEVER_SET")
.await
.expect("readable")
.is_none(),
"a name tmux does not hold is still None"
);
let asking = server
.sessions()
.await
.expect("sessions")
.into_iter()
.find(|session| session.id() == doomed.id())
.expect("the doomed session is listed");
doomed.kill().await.expect("the session is killed");
let refused = asking
.environment("FOO")
.await
.expect_err("the session is gone, not the variable unset");
assert!(
refused.is_object_gone(),
"a gone session must not read as an unset variable: {refused:?}"
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn interactive_commands_need_a_client() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
server.new_session("interactive").await.expect("session");
let panes_mode = versions::supports(
server,
&libtmux::ReleaseVersion::new(3, 8, libtmux::ReleaseSuffix::RELEASE_CANDIDATE),
)
.await;
assert_eq!(server.display_panes(None).await.is_ok(), panes_mode);
assert!(
server
.display_menu(
None,
"menu",
[libtmux::MenuItem::new("Item", "i", "kill-pane")]
)
.await
.is_err(),
);
assert!(
server
.command_prompt(None, Some("question"), "kill-pane")
.await
.is_err(),
);
server
.choose(libtmux::Chooser::Tree, None)
.await
.expect("tmux accepts a chooser with no client");
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn retry_until_waits_for_tmux_rather_than_sleeping() {
use libtmux::test::{retry_until, scaled, unique_name};
use std::time::Duration;
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let name = unique_name("retry");
let session = server.new_session(name.as_str()).await.expect("session");
assert_ne!(unique_name("retry"), name);
session
.windows()
.await
.expect("windows")
.into_iter()
.next()
.expect("one window")
.split(SplitOptions::new(SplitDirection::Below).command("sleep 300"))
.await
.expect("pane is created");
retry_until(Duration::from_secs(5), async || {
session.panes().await.is_ok_and(|panes| panes.len() == 2)
})
.await
.expect("the second pane appears");
let waited = retry_until(Duration::from_millis(50), async || false)
.await
.expect_err("a condition that never holds times out");
assert_eq!(waited.waited(), scaled(Duration::from_millis(50)));
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn a_scoped_raw_command_targets_its_own_object() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let first = server.new_session("first").await.expect("first session");
let second = server.new_session("second").await.expect("second session");
for (session, expected) in [(&first, "first"), (&second, "second")] {
let named = session
.cmd(
Command::new("display-message")
.arg("-p")
.arg("#{session_name}"),
)
.await
.expect("the command runs");
assert_eq!(named.stdout_lossy().trim(), expected);
}
let window = second
.active_window()
.await
.expect("active window")
.expect("a session has a window");
let pane = window
.active_pane()
.await
.expect("active pane")
.expect("a window has a pane");
pane.cmd(
Command::new("send-keys")
.arg("--")
.arg("echo scoped")
.arg("Enter"),
)
.await
.expect("keys are sent");
let seen = retry_until(Duration::from_secs(5), async || {
pane.capture().await.is_ok_and(|lines| {
lines
.iter()
.any(|line| String::from_utf8_lossy(line.as_bytes()).contains("scoped"))
})
})
.await;
assert!(seen.is_ok(), "the keys reached this pane, not another");
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn formatting_returns_text_and_displaying_returns_nothing() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server.new_session("split").await.expect("session");
let window = session
.active_window()
.await
.expect("active window")
.expect("a session has a window");
assert_eq!(
session
.format("#{session_name}")
.await
.expect("format")
.to_string_lossy(),
"split",
);
assert_eq!(
window
.format("#{window_id}")
.await
.expect("format")
.to_string_lossy(),
window.id().to_string(),
);
assert!(
session
.format("#{?0,yes,}")
.await
.expect("format")
.as_bytes()
.is_empty(),
);
let pane = window
.active_pane()
.await
.expect("active pane")
.expect("a window has a pane");
assert_eq!(
pane.format("#{pane_id}")
.await
.expect("format")
.to_string_lossy(),
pane.id().to_string(),
);
session.display("hello").await.expect("display");
window.display("hello").await.expect("display");
pane.display("hello").await.expect("display");
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn prompt_history_is_available_on_every_supported_release() {
let guard = TestServer::new().await.expect("tmux starts");
assert!(
guard
.server()
.prompt_history(libtmux::PromptKind::Command)
.await
.expect("prompt history reads")
.is_empty()
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn the_server_access_list_names_its_owner_and_refuses_to_unseat_them() {
let mut builder = TestServer::builder();
if let Some(executable) = std::env::var_os("LIBTMUX_TEST_TMUX") {
builder = builder.tmux_executable(executable);
}
let guard = builder.start().await.expect("tmux starts");
let server = guard.server();
let rules = server.access_rules().await.expect("access list");
let owner = rules.first().expect("the owner is listed");
assert_eq!(rules.len(), 1);
assert_eq!(owner.mode(), libtmux::AccessMode::Write);
assert_eq!(owner.principal(), libtmux::Principal::User);
assert!(!owner.name().is_empty());
let user = owner.name().to_owned();
for attempt in [
server
.grant_access(&user, libtmux::AccessMode::ReadOnly)
.await,
server.revoke_access(&user).await,
] {
let error = attempt.expect_err("tmux refuses to change the owner");
assert!(
error.to_string().contains("owns the server"),
"the refusal says why: {error}",
);
}
assert_eq!(server.access_rules().await.expect("access list"), rules);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn real_tmux_compat_capture_line_flags_mark_prompts_when_the_shell_emits_them() {
use libtmux::{CaptureOptions, since};
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server.new_session("prompts").await.expect("session");
let pane = session.panes().await.expect("panes").remove(0);
let supported = server
.capabilities()
.await
.expect("capabilities")
.tmux_version()
.has_behavior(&since::CAPTURE_LINE_FLAGS);
if !supported {
let refused = pane.capture_lines(CaptureOptions::visible()).await;
assert!(
matches!(
refused.as_ref().map_err(libtmux::Error::kind),
Err(libtmux::ErrorKind::UnsupportedVersion),
),
"an older tmux reports the version rather than returning nothing: {refused:?}",
);
guard.shutdown().await.expect("tmux fixture shuts down");
return;
}
pane.send_keys(
r"printf '\033]133;A\007'; echo THE-PROMPT; printf '\033]133;C\007'; echo the-output",
)
.await
.expect("keys are sent");
pane.send_key_names(["Enter"]).await.expect("Enter is sent");
tokio::time::sleep(Duration::from_millis(600)).await;
let lines = pane
.capture_lines(CaptureOptions::history())
.await
.expect("the capture succeeds");
let prompt = lines
.iter()
.position(|line| line.starts_prompt)
.expect("the prompt mark is reported");
let output = lines
.iter()
.position(|line| line.starts_output)
.expect("the output mark is reported");
assert!(prompt < output, "the prompt precedes its output");
assert_eq!(
lines[prompt].text.to_string_lossy().trim(),
"THE-PROMPT",
"the mark lands on the line the shell was drawing",
);
assert_eq!(lines[output].text.to_string_lossy().trim(), "the-output");
assert!(
lines
.iter()
.all(|line| !line.text.to_string_lossy().starts_with("- ")),
"no line keeps tmux's flag column in its text",
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn a_suspended_client_is_not_reported_gone() {
use libtmux::since;
use std::process;
struct KillOnDrop(process::Child);
impl Drop for KillOnDrop {
fn drop(&mut self) {
drop(self.0.kill());
drop(self.0.wait());
}
}
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server.new_session("suspendable").await.expect("session");
let hides_stopped = server
.capabilities()
.await
.expect("capabilities")
.tmux_version()
.has_behavior(&since::CLIENTS_HIDE_STOPPED);
let child = process::Command::new("tmux")
.arg("-S")
.arg(guard.socket_path())
.arg("-C")
.arg("attach")
.arg("-t")
.arg("suspendable")
.stdin(process::Stdio::piped())
.stdout(process::Stdio::null())
.stderr(process::Stdio::null())
.spawn()
.expect("a control-mode client attaches");
let _client_process = KillOnDrop(child);
retry_until(Duration::from_secs(10), async || {
server
.clients()
.await
.is_ok_and(|clients| !clients.is_empty())
})
.await
.expect("the attached client is listed");
let client = server.clients().await.expect("clients").remove(0);
client.suspend().await.expect("the client suspends");
if !hides_stopped {
let same = client
.refreshed()
.await
.expect("the client is still listed");
assert_eq!(same.name(), client.name());
assert!(
!server.clients().await.expect("clients").is_empty(),
"an older tmux lists a suspended client",
);
guard.shutdown().await.expect("tmux fixture shuts down");
return;
}
retry_until(Duration::from_secs(10), async || {
server
.clients()
.await
.is_ok_and(|clients| clients.is_empty())
})
.await
.expect("the suspended client leaves the listing");
let Err(error) = client.refreshed().await else {
panic!("the suspended client is not in the listing to be found")
};
assert!(
matches!(error, libtmux::Error::ClientSuspended { .. }),
"a suspended client resolves and says so: {error:?}",
);
assert!(
!error.is_object_gone(),
"this is what a caller consults before discarding the handle",
);
assert!(
error.is_transient(),
"the same client handle works once the process resumes",
);
assert!(
!session.refreshed().await.expect("session").is_attached(),
"a suspended client stops counting toward its session",
);
}
#[tokio::test]
async fn a_capture_can_keep_the_spaces_tmux_would_trim() {
use libtmux::CaptureOptions;
fn printed(lines: Vec<libtmux::TmuxText>) -> String {
lines
.into_iter()
.map(|line| line.to_string_lossy().into_owned())
.find(|line| line.starts_with("AB"))
.expect("the printed line is captured")
}
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server
.new_session(
libtmux::NewSessionOptions::new("trailing").command("printf 'AB \\n'; exec /bin/sh"),
)
.await
.expect("session");
let pane = session.panes().await.expect("panes").remove(0);
retry_until(Duration::from_secs(10), async || {
pane.capture()
.await
.is_ok_and(|lines| lines.iter().any(|l| l.to_string_lossy().starts_with("AB")))
})
.await
.expect("the pane prints");
let trimmed = printed(pane.capture().await.expect("default capture"));
let exact = printed(
pane.capture_with(CaptureOptions::visible().trailing_spaces())
.await
.expect("capture keeping trailing spaces"),
);
assert_eq!(trimmed, "AB", "tmux trims trailing spaces without `-N`");
assert!(
exact.starts_with("AB "),
"`-N` keeps what the program printed: {exact:?}",
);
assert!(
exact.len() > trimmed.len(),
"the exact capture is longer: {exact:?} vs {trimmed:?}",
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn trimming_blank_cells_is_refused_below_the_release_that_has_it() {
use libtmux::{CaptureOptions, since};
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server.new_session("blankcells").await.expect("session");
let pane = session.panes().await.expect("panes").remove(0);
let supported = server
.capabilities()
.await
.expect("capabilities")
.tmux_version()
.has_behavior(&since::CAPTURE_TRIM_BLANK_CELLS);
let asked = pane
.capture_with(CaptureOptions::visible().trim_blank_cells())
.await;
if supported {
asked.expect("3.4 and later accept -T");
} else {
assert!(
matches!(
asked.as_ref().map_err(libtmux::Error::kind),
Err(libtmux::ErrorKind::UnsupportedVersion),
),
"an older tmux reports the version rather than dispatching a flag \
it will reject: {asked:?}",
);
}
pane.capture_with(CaptureOptions::visible().trailing_spaces())
.await
.expect("-N works on every supported release");
pane.capture_with(CaptureOptions::visible().pending_escape())
.await
.expect("-P works on every supported release");
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn waiting_survives_scrollback_and_a_line_wider_than_the_pane() {
use libtmux::PaneWait;
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server.new_session("waits").await.expect("session");
let pane = session.panes().await.expect("panes").remove(0);
let wide = "W".repeat(400);
pane.send_keys(format!("printf '{wide}\\n'"))
.await
.expect("keys are sent");
pane.send_key_names(["Enter"]).await.expect("it runs");
assert_eq!(
pane.wait_for_text(&wide, Duration::from_secs(10))
.await
.expect("waiting is not an error"),
PaneWait::Arrived,
"a line wider than the pane is matched across its wrap",
);
for _ in 0..60 {
pane.send_keys("printf 'filler\\n'").await.expect("keys");
pane.send_key_names(["Enter"]).await.expect("it runs");
}
assert_eq!(
pane.wait_for_text(&wide, Duration::from_secs(10))
.await
.expect("waiting is not an error"),
PaneWait::Arrived,
"text that scrolled off the screen is still found",
);
assert_eq!(
pane.wait_for_text("nothing prints this", Duration::from_millis(400))
.await
.expect("a deadline is not an error"),
PaneWait::TimedOut,
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn waiting_ends_when_the_pane_dies_rather_than_at_the_deadline() {
use libtmux::{NewWindowOptions, PaneWait};
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server.new_session("dying").await.expect("session");
server
.cmd(
Command::new("set-option")
.arg("-g")
.arg("remain-on-exit")
.arg("on"),
)
.await
.expect("panes remain after exit");
let window = session
.new_window(NewWindowOptions::new("shortlived").command("true"))
.await
.expect("window");
let pane = window.panes().await.expect("panes").remove(0);
let started = std::time::Instant::now();
let outcome = pane
.wait_for_text("never printed", Duration::from_secs(20))
.await
.expect("waiting is not an error");
let waited = started.elapsed();
assert_eq!(outcome, PaneWait::Dead);
assert!(
waited < Duration::from_secs(15),
"a dead pane ends the wait early rather than at the deadline: {waited:?}",
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn a_predicate_wait_arrives_or_runs_to_its_deadline() {
use libtmux::PaneWait;
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server.new_session("predicate").await.expect("session");
let pane = session.panes().await.expect("panes").remove(0);
let wide = "P".repeat(300);
pane.send_line(format!("printf '%s\\n' {wide} {wide}"))
.await
.expect("keys are sent");
let outcome = pane
.wait_until(Duration::from_secs(10), |lines| {
lines.iter().filter(|line| **line == *wide).count() == 2
})
.await
.expect("waiting is not an error");
assert_eq!(outcome, PaneWait::Arrived, "both printed lines were seen");
let within = Duration::from_millis(400);
let started = std::time::Instant::now();
let outcome = tokio::time::timeout(
within * 10,
pane.wait_until(within, |lines| lines.iter().any(|line| *line == "absent")),
)
.await
.expect("an unmet wait ends near its deadline, not long after it")
.expect("a deadline is not an error");
let waited = started.elapsed();
assert_eq!(outcome, PaneWait::TimedOut);
assert!(
waited >= within,
"an unmet wait answers at its deadline, not before: {waited:?}",
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn a_predicate_wait_ends_when_the_pane_dies() {
use libtmux::PaneWait;
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server.new_session("dying").await.expect("session");
server
.cmd(
Command::new("set-option")
.arg("-g")
.arg("remain-on-exit")
.arg("on"),
)
.await
.expect("panes remain after exit");
let window = session
.new_window(NewWindowOptions::new("shortlived").command("true"))
.await
.expect("window");
let pane = window.panes().await.expect("panes").remove(0);
let started = std::time::Instant::now();
let outcome = pane
.wait_until(Duration::from_secs(20), |_| false)
.await
.expect("waiting is not an error");
let waited = started.elapsed();
assert_eq!(outcome, PaneWait::Dead);
assert!(
waited < Duration::from_secs(15),
"a dead pane ends the wait early rather than at the deadline: {waited:?}",
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[cfg(feature = "control-mode")]
#[tokio::test]
async fn a_predicate_wait_runs_over_a_control_connection() {
use libtmux::PaneWait;
use libtmux::control::ControlMode;
let guard = TestServer::builder().start().await.expect("tmux starts");
let Some(guard) = versions::control_fixture(guard).await else {
return;
};
let session = guard.server().new_session("routed").await.expect("session");
let (sender, events) = ControlMode::attach(guard.server(), session.id())
.await
.expect("a control connection")
.split();
let routed = guard
.server()
.over_control_mode(&sender)
.await
.expect("a routed handle");
let pane = routed.panes().await.expect("panes").remove(0);
pane.send_line("printf 'ROUTED-%s\\n' 1")
.await
.expect("keys are sent");
let outcome = pane
.wait_until(Duration::from_secs(10), |lines| {
lines.iter().any(|line| *line == "ROUTED-1")
})
.await
.expect("waiting is not an error");
assert_eq!(outcome, PaneWait::Arrived);
events.shutdown().await.expect("the connection closes");
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn a_quiet_below_the_polling_interval_still_costs_one() {
use libtmux::PaneWait;
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server.new_session("settling").await.expect("session");
let pane = session.panes().await.expect("panes").remove(0);
let started = std::time::Instant::now();
let settled = pane
.wait_for_quiet(Duration::from_millis(5), Duration::from_secs(20))
.await
.expect("waiting is not an error");
let waited = started.elapsed();
assert_eq!(settled, PaneWait::Arrived, "an idle pane is quiet");
assert!(
waited >= Duration::from_millis(120),
"a 5ms quiet cannot be seen sooner than the 120ms interval: {waited:?}",
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn breaking_out_the_only_pane_moves_its_window() {
use libtmux::NewWindowOptions;
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server.new_session("lonely").await.expect("session");
let window = session
.new_window(NewWindowOptions::new("lone"))
.await
.expect("window");
let pane = window.panes().await.expect("panes").remove(0);
assert_eq!(window.panes().await.expect("panes").len(), 1, "one pane");
let before = window.index();
let window_id = window.id().clone();
pane.break_out().await.expect("tmux accepts the command");
let after = window
.refreshed()
.await
.expect("the window is still there, by id");
assert_eq!(after.id(), &window_id, "the window keeps its identity");
assert_ne!(
after.index(),
before,
"the window moved to a free index rather than nothing happening",
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn a_window_acts_on_itself_after_its_index_moved() {
use libtmux::NewWindowOptions;
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server.new_session("renumber").await.expect("session");
let mut moved = session
.new_window(NewWindowOptions::new("moved"))
.await
.expect("window");
let moved_id = moved.id().clone();
let cached = moved.index();
let pane = moved.panes().await.expect("panes").remove(0);
pane.break_out().await.expect("tmux accepts the command");
let after = moved.refreshed().await.expect("the window still exists");
assert_ne!(after.index(), cached, "the window moved");
let intruder = session
.new_window(NewWindowOptions::new("intruder").index(cached))
.await
.expect("a window takes the vacated slot");
let intruder_id = intruder.id().clone();
assert_eq!(intruder.index(), cached, "the slot is occupied again");
assert_ne!(intruder_id, moved_id, "by a different window");
moved.select().await.expect("select succeeds");
let active = session
.active_window()
.await
.expect("active window")
.expect("a session always has one");
assert_eq!(
active.id(),
&moved_id,
"select acted on the handle's window, not on whatever took its index",
);
assert_ne!(
active.id(),
&intruder_id,
"and not on the window occupying its old index",
);
guard.shutdown().await.expect("tmux fixture shuts down");
}
#[tokio::test]
async fn a_name_outside_ascii_survives_the_listing() {
let guard = TestServer::builder().start().await.expect("tmux starts");
let server = guard.server();
let session = server.new_session("日本語").await.expect("session");
assert_eq!(session.name().as_bytes(), "日本語".as_bytes());
let listed = server.sessions().await.expect("sessions");
assert_eq!(listed[0].name().as_bytes(), "日本語".as_bytes());
guard.shutdown().await.expect("tmux fixture shuts down");
}