retach 0.10.0

Persistent terminal sessions with native scrollback passthrough
Documentation
//! retach — terminal multiplexer with native scrollback passthrough.
//!
//! Instead of intercepting scrollback (like tmux/screen), retach sends completed
//! lines directly to stdout, letting the client terminal handle scrolling natively.

mod cli;
mod client;
mod protocol;
mod pty;
mod server;
mod session;

use clap::Parser;
use cli::{Cli, Command};
use tracing_subscriber::EnvFilter;

fn init_tracing() {
    tracing_subscriber::fmt()
        .with_env_filter(
            EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("retach=info")),
        )
        .with_writer(std::io::stderr)
        .init();
}

fn main() -> anyhow::Result<()> {
    init_tracing();
    let cli = Cli::parse();

    let rt = tokio::runtime::Runtime::new()?;

    // `client::connect` returns the session child's exit code (when the process
    // exited while attached) so we can mirror it as our own process exit status.
    let mut child_exit_code: Option<i32> = None;
    let result = match cli.command {
        Command::Open { name, history } => rt
            .block_on(client::connect(
                &name,
                history,
                protocol::ConnectMode::CreateOrAttach,
            ))
            .map(|code| child_exit_code = code),
        Command::New { name, history } => {
            let name = name.unwrap_or_else(generate_name);
            rt.block_on(client::connect(
                &name,
                history,
                protocol::ConnectMode::CreateOnly,
            ))
            .map(|code| child_exit_code = code)
        }
        Command::Attach { name } => rt
            .block_on(client::connect(&name, 0, protocol::ConnectMode::AttachOnly))
            .map(|code| child_exit_code = code),
        Command::List => rt.block_on(client::list_sessions()),
        Command::Kill { name } => rt.block_on(client::kill_session(&name)),
        Command::Server => rt.block_on(server::run_server()),
    };

    // Shut down runtime with timeout to avoid hanging on blocked stdin thread
    rt.shutdown_timeout(std::time::Duration::from_millis(100));

    result?;

    // Propagate the session child's exit code so scripts checking $? see the
    // real status. RAII guards in connect() have already run by this point.
    if let Some(code) = child_exit_code {
        std::process::exit(code);
    }
    Ok(())
}

fn generate_name() -> String {
    use std::hash::{BuildHasher, Hasher};
    // RandomState::new() seeds from OS randomness, giving real uniqueness
    // without adding external dependencies.
    let hash = std::collections::hash_map::RandomState::new()
        .build_hasher()
        .finish();
    format!("s-{:016x}", hash)
}