libtmux Reference MCP Search

Attach and send keys

Edit this page on GitHub

The task: get a handle on a tmux session, find its active pane, type a command into it, and read back what printed. This is the round trip nearly every real program built on libtmux starts from — an agent that runs a command and checks the result, a test harness driving a CLI, a dashboard polling a long-running process. “Attach” here means obtaining a live handle from your program, not a terminal takeover — see Attaching to tmux for that distinction and for the real, terminal-taking-over kind Python also exposes.

Every block below is either read straight out of a file in that port’s own repository at build time, or quoted by hand from a doctest or README where no standalone file exists — the table at the end of this page says which, names the exact source, and says how that port’s own test suite checks it. None of it was rewritten to look alike: a whole tested file naturally carries more or fewer than exactly the three steps above — error handling, a second helper, a comment explaining a choice specific to that port, or no read-back at all — and that surrounding code is left in rather than trimmed to match. A difference in shape below is real fidelity to what each port actually ships, not inconsistency.

//! 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_or_empty().await.is_empty(),
"the scope cleaned up",
);
println!(
"sessions left behind: {}",
server.sessions_or_empty().await.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(())
}

Finding an existing session insteadLink to section

Every snippet above creates a fresh session. A script that runs more than once usually wants the opposite — attach if a session by that name already exists, create it otherwise. See Attaching to tmux for the verified call in each port, and Filtering and querying, in practice for what to do when the lookup might match more than one.

Where this comes fromLink to section

PortSourceIn this pageChecked by
Pythonsrc/libtmux/server.py, session.py, pane.py docstringshand-quoted, composed from three separate docstringspytest runs every >>> doctest (testpaths includes src/libtmux) against a real, isolated tmux session on every test run
TypeScriptexamples/quickstart/quickstart.tsread whole from the filerun against real tmux by bun test examples; its first half is also mirrored into README.md under a <!-- runs: ... --> marker, checked line-for-line by scripts/check-doc-runnable.ts
Rustcrates/libtmux/examples/scratch.rsread whole from the filerun to completion against a throwaway tmux by scripts/run-examples.sh (just examples), which CI runs and which also asserts the example leaves no session behind
Goexamples/quickstart/main.goread whole from the filethe whole file runs against a real tmux server as TestQuickstart; the docs:quickstart region inside it is additionally mirrored into README.md by go generate ./tmux, and CI fails if the two drift
Javaexamples/src/main/java/io/github/libtmux/examples/BuildAWorkspace.javaread whole from the filerun against real tmux by the examples module’s own ExamplesRunTest
.NETexamples/LibTmux.Examples/Snippets/OneShot.csread whole from the fileits ConnectAndBuild region is mirrored into README.md and checked by sync_snippets.py --check; the mirrored csharp run block is additionally compiled and run by ReadmeExampleTests
C++examples/05-readme.cpp, the connect and build regionshand-quoted — the file also carries the regions for five other sections of the READMEquoted verbatim into README.md, checked for drift by tools/docs/check_readme.py, and the whole file is built and run by CTest
SwiftExamples/Sources/ExampleCode/Changing.swiftread whole from the filematched against the README’s “Change what is there” section by Scripts/check_examples.py; compiled and run through the package’s public products by swift test --package-path Examples

“Read whole from the file” means the fence names the file with file="..." and the page is built by reading it, so the block above cannot say anything the file itself does not — there is no separate copy to fall out of sync. Nothing in any port’s repository uses the marker comments (region: name / endregion) this site’s own tooling looks for to slice a piece out of a longer file, so where only part of a file is relevant here, that part is quoted by hand instead, with the file and region named in the table above rather than pretended into a file= fence that would fail to build.

Esc

Type to search.