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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
//! React to what a tmux server does, as it does it.
//!
//! ```console
//! $ cargo run --example watch --features control-mode,test-support
//! ```
//!
//! Every other API in this crate spawns a tmux process per command. Control
//! mode opens one connection and keeps it: commands go down it and tmux
//! reports what happens on the server as it happens.
#![allow(clippy::print_stdout, reason = "an example")]
use std::time::Duration;
use libtmux::control::{ControlMode, Event};
use libtmux::test::unique_name;
use libtmux::{Command, Server};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// An example must not watch whatever server the reader happens to be
// using, so this one gets a socket of its own under a root this workspace
// owns.
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-watch")));
let server = Server::builder().socket_path(&socket).build()?;
let session = server.new_session(unique_name("watched").as_str()).await?;
let control = match ControlMode::attach(&server, session.id()).await {
Ok(control) => control,
Err(error @ libtmux::Error::UnsupportedCapability { .. }) => {
println!("{error}");
server.kill().await?;
server.shutdown().await?;
std::fs::remove_file(&socket)?;
return Ok(());
}
Err(error) => return Err(error.into()),
};
let (commands, mut events) = control.split();
println!("one connection to {}", socket.display());
// The watcher gets its own task. Sending and watching are separate
// handles precisely so neither has to wait for the other, and a loop that
// only reads is the shape that keeps the connection moving.
let watcher = tokio::spawn(async move {
let mut seen = Vec::new();
while let Some(event) = events.next_event().await {
match event? {
Event::WindowAdded { window } => {
println!(" <- window {window} appeared");
seen.push(window.to_string());
}
Event::WindowRenamed { window, name } => {
println!(" <- window {window} is now {}", name.to_string_lossy());
seen.push(window.to_string());
}
Event::Exit { .. } => break,
// tmux reports far more than this; an event nobody matched is
// still worth counting rather than dropping silently.
_ => continue,
}
if seen.len() == 2 {
break;
}
}
Ok::<_, libtmux::Error>((seen, events))
});
// Meanwhile, drive the server down the same connection. These spawn no
// processes: they are lines on the socket the watcher is reading.
println!(" -> new-window (a line on that connection, not a process)");
commands
.send(Command::new("new-window").arg("-d").arg("-n").arg("built"))
.await?;
println!(" -> rename-window");
commands
.send(
Command::new("rename-window")
.arg("-t")
.arg("built")
.arg("renamed"),
)
.await?;
// The watcher's own `event?` can end this early -- a connection error is
// as reachable here as tmux exiting cleanly. Collecting its outcome
// first, rather than propagating it immediately with `?`, means the
// teardown below still runs on that path instead of leaking the
// throwaway server and its socket file.
let outcome: Result<usize, Box<dyn std::error::Error>> = async {
let (seen, events) = tokio::time::timeout(Duration::from_secs(10), watcher).await???;
println!(
"{} events arrived while those commands were being sent, on the same socket",
seen.len()
);
drop(commands);
events.shutdown().await?;
Ok(seen.len())
}
.await;
server.kill().await?;
server.shutdown().await?;
// tmux does not unlink its socket when the server exits, so whatever named
// one owns removing it.
std::fs::remove_file(&socket)?;
outcome?;
Ok(())
}