Getting Started
Choose the crate by the lifecycle you want tastty to own:
- Use
tastty-corewhen you already have bytes and need parsing, screen state, byte encoders, or host replies. - Use
tasttywhen you want a managed PTY session with spawning, input, resize, snapshots, and exit handling. - Use
tastty-driverwhen you want active session-control verbs such as spawn, send, wait, snapshot, inspect, signal, terminate, and kill.
Parser-only
tastty-core is the parser-only layer. It does not spawn a process, open a PTY,
or run a reader thread. Feed it bytes from your own transport, inspect the
virtual screen, and encode replies or input when needed.
use tastty_core::{Parser, TerminalSize};
# fn main() -> tastty_core::Result<()> {
let size = TerminalSize::new(24, 80)?;
let mut parser = Parser::new(size, 1000);
parser.process(b"hello\n");
let screen = parser.screen();
let cursor = screen.cursor();
assert_eq!(screen.size().cols, 80);
assert_eq!(cursor.row, 1);
# Ok(())
# }For interactive protocols, drain ScreenEvent values from the screen and send
encoded HostReply bytes through your own output path.
use tastty_core::{HostReply, Parser, ScreenEvent, TerminalSize};
# fn main() -> tastty_core::Result<()> {
let mut parser = Parser::new(TerminalSize::new(24, 80)?, 0);
parser.process(b"\x1b[6n");
for event in parser.screen_mut().drain_events() {
if matches!(event, ScreenEvent::DsrCursorPosition) {
let bytes = HostReply::DsrCursorPosition { row: 1, col: 1 }.encode();
// Write bytes back to the program that asked the query.
let _ = bytes;
}
}
# Ok(())
# }Managed PTY session
tastty owns the PTY lifecycle. A background reader thread feeds output into the
parser so calls such as screen, snapshot, and with_screen see current
terminal state. Use with_screen in render loops when cloning the full screen
would be unnecessary.
use tastty::{CommandBuilder, SessionOptions, Terminal, TerminalSize};
# fn main() -> tastty::Result<()> {
let mut cmd = CommandBuilder::new("sh");
cmd.arg("-c").arg("printf hello");
let opts = SessionOptions::default()
.size(TerminalSize::new(24, 80)?)
.scrollback(1000);
let terminal = Terminal::spawn(cmd, opts)?;
let snapshot = terminal.snapshot();
assert_eq!(snapshot.size.cols, 80);
# Ok(())
# }Send raw bytes when you already know the terminal protocol, or use typed input events when you want tastty to encode keys against the current screen mode.
use tastty::input::{KeyCode, KeyEvent, KeyModifiers};
# fn send_key(terminal: &tastty::Terminal) -> tastty::Result<()> {
terminal.send_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE))?;
# Ok(())
# }Driver-level session control
tastty-driver is the active-control layer. It is useful when your code wants
to describe intent instead of coordinating lower-level PTY calls directly.
use std::time::Duration;
use tastty_driver::{Builder, Session, TerminalSize, WaitCondition};
# fn main() -> Result<(), Box<dyn std::error::Error>> {
let session = Session::spawn(
Builder::shell_command("printf ready; sleep 1")
.size(TerminalSize::new(24, 80)?),
)?;
let outcome = session.wait(
WaitCondition::text("ready"),
Duration::from_secs(2),
)?;
let text = outcome.snapshot.text();
assert!(text.contains("ready"));
# Ok(())
# }The driver also accepts parsed mixed input, so text and named keys can be sent in one ordered sequence.
use tastty_driver::{parse_input, Session};
# fn send_line(session: &Session) -> tastty_driver::Result<()> {
let input = parse_input("echo ready<Enter>")?;
session.send(&input)?;
# Ok(())
# }