Getting Started

Choose the crate by the lifecycle you want tastty to own:

  • Use tastty-core when you already have bytes and need parsing, screen state, byte encoders, or host replies.
  • Use tastty when you want a managed PTY session with spawning, input, resize, snapshots, and exit handling.
  • Use tastty-driver when 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(())
# }