1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
//! Build a throwaway session, use it, and leave nothing behind.
//!
//! ```console
//! $ cargo run --example scratch
//! ```
use std::time::Duration;
use libtmux::test::unique_name;
use libtmux::{NewWindowOptions, PaneWait, Server, SplitDirection, SplitOptions};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// An example must not build sessions on whatever server the reader
// happens to be using, so this one gets a socket of its own. More than one
// libtmux runs on a developer's machine, so it goes in a directory this
// one owns rather than straight into the temporary directory.
let root = std::path::Path::new("/tmp/libtmux-rs-dev");
std::fs::create_dir_all(root)?;
let socket = root.join(format!("{}.sock", unique_name("libtmux-scratch")));
let server = Server::builder().socket_path(&socket).build()?;
// The scope kills the session whether the body succeeds or fails, so a
// failure partway through does not leave a session behind.
println!("server on {}", socket.display());
let output = server
.with_session(unique_name("scratch").as_str(), async |session| {
println!(" session {} created", session.id());
let window = session
.new_window(NewWindowOptions::new("work").command("sh"))
.await?;
println!(" window {} running sh", window.id());
window
.split(SplitOptions::new(SplitDirection::Below).command("sh"))
.await?;
println!(" split it: {} panes", window.panes().await?.len());
// A window always has an active pane, but saying so with a panic
// would be a worse example than handling it.
let Some(pane) = window.active_pane().await? else {
return Ok(0);
};
println!(" typing into {}", pane.id());
pane.send_line("printf 'hello from tmux\\n'").await?;
// tmux runs the shell asynchronously, so wait for the output
// rather than sleeping and hoping. The outcome is checked because
// a wait that reached its deadline still returns successfully.
match pane.wait_for_text("hello", Duration::from_secs(5)).await? {
PaneWait::Arrived => println!(" the pane printed it"),
other => println!(" gave up: {other:?}"),
}
// region: capture
let lines = pane.capture().await?;
for line in lines.iter().filter(|line| !line.as_bytes().is_empty()) {
println!(" | {}", line.to_string_lossy());
}
// endregion
Ok::<_, Box<dyn std::error::Error>>(lines.len())
})
.await?;
println!("captured {output} lines, then the scope killed the session");
// The lenient form is the one that answers this question. The scope killed
// the only session, so tmux exited with it, and the loud form reports that
// as the failure it is rather than as the empty listing this is asking for.
assert!(
server.sessions().await.unwrap_or_default().is_empty(),
"the scope cleaned up",
);
println!(
"sessions left behind: {}",
server.sessions().await.unwrap_or_default().len()
);
server.shutdown().await?;
// tmux does not unlink its socket when the server exits, so whatever named
// one owns removing it. Leaving it behind is invisible until /tmp fills up.
std::fs::remove_file(&socket)?;
Ok(())
}