# rmux Implementation Plan

> **Status:** All 8 tasks completed. Project subsequently refactored — see `2026-03-01-rmux-design.md` for current module structure.
>
> This document preserves the original step-by-step build plan used to implement rmux from scratch. For current architecture, module layout, and issue tracking, refer to the design and issues documents.

**Goal:** Build a terminal multiplexer with native scrollback passthrough — scrolling works through the client terminal natively, no copy mode needed.

**Architecture:** Daemon+client over Unix sockets. Daemon holds PTY + VTE virtual grid per session. When lines scroll off the top of the grid, they're sent to the client as plain text (passthrough scrollback). On reattach, stored history is replayed first.

**Tech Stack:** Rust, vte, portable-pty, tokio, clap, serde+bincode, nix, thiserror, tracing

---

### Task 1: Project scaffold + CLI skeleton

**Files:**
- Create: `Cargo.toml`
- Create: `src/main.rs`
- Create: `src/cli.rs`

**Step 1: Create Cargo.toml**

```toml
[package]
name = "rmux"
version = "0.1.0"
edition = "2021"

[dependencies]
clap = { version = "4", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
bincode = "1"
vte = "0.13"
portable-pty = "0.8"
nix = { version = "0.29", features = ["term", "signal"] }
anyhow = "1"
```

**Step 2: Create CLI (src/cli.rs)**

```rust
use clap::{Parser, Subcommand};

#[derive(Parser)]
#[command(name = "rmux", version, about = "Terminal multiplexer with native scrollback")]
pub struct Cli {
    #[command(subcommand)]
    pub command: Option<Command>,

    /// Session name (shorthand for: attach if exists, create if not)
    pub name: Option<String>,
}

#[derive(Subcommand)]
pub enum Command {
    /// Create a new session
    New {
        /// Session name (auto-generated if omitted)
        name: Option<String>,
        /// Scrollback history size
        #[arg(long, default_value = "10000")]
        history: usize,
    },
    /// Attach to an existing session
    Attach {
        /// Session name
        name: String,
    },
    /// List active sessions
    List,
    /// Kill a session
    Kill {
        /// Session name
        name: String,
    },
}
```

**Step 3: Create main.rs**

```rust
mod cli;

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

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

    match cli.command {
        Some(Command::New { name, history }) => {
            let name = name.unwrap_or_else(generate_name);
            println!("new session: {} (history: {})", name, history);
        }
        Some(Command::Attach { name }) => {
            println!("attach: {}", name);
        }
        Some(Command::List) => {
            println!("list sessions");
        }
        Some(Command::Kill { name }) => {
            println!("kill: {}", name);
        }
        None => {
            if let Some(name) = cli.name {
                println!("auto: {}", name);
            } else {
                Cli::parse_from(["rmux", "--help"]);
            }
        }
    }

    Ok(())
}

fn generate_name() -> String {
    format!("s{}", std::process::id())
}
```

**Step 4: Build and verify CLI**

Run: `cd ~/Dev/Local/rmux && cargo build`
Expected: compiles without errors

Run: `cargo run -- --help`
Expected: shows usage with subcommands

Run: `cargo run -- new --history 5000 mywork`
Expected: prints "new session: mywork (history: 5000)"

Run: `cargo run -- mywork`
Expected: prints "auto: mywork"

---

### Task 2: Protocol messages

**Files:**
- Create: `src/protocol.rs`
- Modify: `src/main.rs` (add mod)

**Step 1: Define protocol**

```rust
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug)]
pub enum ClientMsg {
    /// Keyboard input from client
    Input(Vec<u8>),
    /// Terminal resized
    Resize { cols: u16, rows: u16 },
    /// Client wants to detach
    Detach,
    /// Request session list
    ListSessions,
    /// Create or attach to session
    Connect { name: String, history: usize },
    /// Kill a session
    KillSession { name: String },
}

#[derive(Serialize, Deserialize, Debug)]
pub enum ServerMsg {
    /// Scrollback line (passthrough to terminal)
    ScrollbackLine(Vec<u8>),
    /// Full screen redraw (ANSI bytes)
    ScreenUpdate(Vec<u8>),
    /// Scrollback history on reattach
    History(Vec<Vec<u8>>),
    /// Session list response
    SessionList(Vec<SessionInfo>),
    /// Session ended (shell exited)
    SessionEnded,
    /// Error
    Error(String),
    /// Connected successfully
    Connected { name: String },
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SessionInfo {
    pub name: String,
    pub pid: u32,
    pub cols: u16,
    pub rows: u16,
}

/// Length-prefixed message encoding
pub fn encode(msg: &impl Serialize) -> anyhow::Result<Vec<u8>> {
    let data = bincode::serialize(msg)?;
    let len = (data.len() as u32).to_be_bytes();
    let mut buf = Vec::with_capacity(4 + data.len());
    buf.extend_from_slice(&len);
    buf.extend_from_slice(&data);
    Ok(buf)
}

/// Read a length-prefixed message from a buffer
/// Returns (message_bytes, bytes_consumed) or None if incomplete
pub fn decode_frame(buf: &[u8]) -> Option<(&[u8], usize)> {
    if buf.len() < 4 {
        return None;
    }
    let len = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]) as usize;
    if buf.len() < 4 + len {
        return None;
    }
    Some((&buf[4..4 + len], 4 + len))
}
```

**Step 2: Add mod to main.rs**

Add `mod protocol;` to the top of main.rs.

**Step 3: Build**

Run: `cd ~/Dev/Local/rmux && cargo build`
Expected: compiles without errors

---

### Task 3: PTY wrapper

**Files:**
- Create: `src/pty.rs`
- Modify: `src/main.rs` (add mod)

**Step 1: Create PTY module**

```rust
use portable_pty::{native_pty_system, CommandBuilder, MasterPty, PtySize};
use std::io::{Read, Write};

pub struct Pty {
    master: Box<dyn MasterPty + Send>,
    reader: Box<dyn Read + Send>,
    child: Box<dyn portable_pty::Child + Send + Sync>,
}

impl Pty {
    pub fn spawn(cols: u16, rows: u16) -> anyhow::Result<Self> {
        let pty_system = native_pty_system();
        let pair = pty_system.openpty(PtySize {
            rows,
            cols,
            pixel_width: 0,
            pixel_height: 0,
        })?;

        let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/bash".to_string());
        let cmd = CommandBuilder::new(&shell);
        let child = pair.slave.spawn_command(cmd)?;
        let reader = pair.master.try_clone_reader()?;

        Ok(Self {
            master: pair.master,
            reader,
            child,
        })
    }

    pub fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
        self.reader.read(buf)
    }

    pub fn write(&mut self, data: &[u8]) -> std::io::Result<()> {
        self.master.write_all(data)?;
        self.master.flush()
    }

    pub fn resize(&self, cols: u16, rows: u16) -> anyhow::Result<()> {
        self.master.resize(PtySize {
            rows,
            cols,
            pixel_width: 0,
            pixel_height: 0,
        })?;
        Ok(())
    }

    pub fn is_alive(&mut self) -> bool {
        self.child.try_wait().ok().flatten().is_none()
    }
}
```

**Step 2: Add mod to main.rs**

Add `mod pty;` to the top of main.rs.

**Step 3: Build**

Run: `cd ~/Dev/Local/rmux && cargo build`
Expected: compiles without errors

---

### Task 4: VTE screen — virtual grid + scrollback passthrough

**Files:**
- Create: `src/screen.rs`
- Modify: `src/main.rs` (add mod)

This is the core component. It maintains a virtual terminal grid and detects when lines scroll off the top.

**Step 1: Create screen module**

```rust
use std::collections::VecDeque;
use vte::{Params, Perform, Parser};

#[derive(Clone, Debug)]
pub struct Cell {
    pub c: char,
    pub sgr: Vec<u8>, // raw SGR bytes for this cell's style
}

impl Default for Cell {
    fn default() -> Self {
        Self { c: ' ', sgr: Vec::new() }
    }
}

pub struct Screen {
    pub cols: u16,
    pub rows: u16,
    grid: Vec<Vec<Cell>>,
    cursor_x: u16,
    cursor_y: u16,
    current_sgr: Vec<u8>,
    in_alt_screen: bool,
    scrollback: VecDeque<Vec<u8>>,
    scrollback_limit: usize,
    /// Lines to send to client as passthrough scrollback
    pending_scrollback: Vec<Vec<u8>>,
    parser: Parser,
}

impl Screen {
    pub fn new(cols: u16, rows: u16, scrollback_limit: usize) -> Self {
        let grid = vec![vec![Cell::default(); cols as usize]; rows as usize];
        Self {
            cols,
            rows,
            grid,
            cursor_x: 0,
            cursor_y: 0,
            current_sgr: Vec::new(),
            in_alt_screen: false,
            scrollback: VecDeque::new(),
            scrollback_limit,
            pending_scrollback: Vec::new(),
            parser: Parser::new(),
        }
    }

    /// Process raw PTY output bytes
    pub fn process(&mut self, bytes: &[u8]) {
        for &byte in bytes {
            // We need to use a temporary to satisfy borrow checker
            // Parser::advance needs &mut self for Perform
            self.parser.advance(byte, &mut ScreenPerformer {
                cols: self.cols,
                rows: self.rows,
                grid: &mut self.grid,
                cursor_x: &mut self.cursor_x,
                cursor_y: &mut self.cursor_y,
                current_sgr: &mut self.current_sgr,
                in_alt_screen: &mut self.in_alt_screen,
                scrollback: &mut self.scrollback,
                scrollback_limit: self.scrollback_limit,
                pending_scrollback: &mut self.pending_scrollback,
            });
        }
    }

    /// Take pending scrollback lines (for sending to client)
    pub fn take_pending_scrollback(&mut self) -> Vec<Vec<u8>> {
        std::mem::take(&mut self.pending_scrollback)
    }

    /// Get stored scrollback history (for reattach)
    pub fn get_history(&self) -> Vec<Vec<u8>> {
        self.scrollback.iter().cloned().collect()
    }

    /// Render current screen as ANSI bytes
    pub fn render(&self) -> Vec<u8> {
        let mut out = Vec::new();
        // Clear screen + home cursor
        out.extend_from_slice(b"\x1b[2J\x1b[H");
        for (y, row) in self.grid.iter().enumerate() {
            // Move to row
            out.extend_from_slice(format!("\x1b[{};1H", y + 1).as_bytes());
            // Reset attributes
            out.extend_from_slice(b"\x1b[0m");
            let mut last_sgr: &[u8] = &[];
            for cell in row {
                if cell.sgr != last_sgr {
                    // Apply SGR
                    out.extend_from_slice(b"\x1b[0m");
                    if !cell.sgr.is_empty() {
                        out.extend_from_slice(&cell.sgr);
                    }
                    last_sgr = &cell.sgr;
                }
                let mut buf = [0u8; 4];
                out.extend_from_slice(cell.c.encode_utf8(&mut buf).as_bytes());
            }
        }
        // Reset + position cursor
        out.extend_from_slice(b"\x1b[0m");
        out.extend_from_slice(
            format!("\x1b[{};{}H", self.cursor_y + 1, self.cursor_x + 1).as_bytes(),
        );
        out
    }

    pub fn resize(&mut self, cols: u16, rows: u16) {
        self.cols = cols;
        self.rows = rows;
        self.grid.resize(rows as usize, vec![Cell::default(); cols as usize]);
        for row in &mut self.grid {
            row.resize(cols as usize, Cell::default());
        }
        if self.cursor_x >= cols {
            self.cursor_x = cols - 1;
        }
        if self.cursor_y >= rows {
            self.cursor_y = rows - 1;
        }
    }

    pub fn in_alt_screen(&self) -> bool {
        self.in_alt_screen
    }
}

/// Performer that borrows Screen fields (avoids self-borrow issues with Parser)
struct ScreenPerformer<'a> {
    cols: u16,
    rows: u16,
    grid: &'a mut Vec<Vec<Cell>>,
    cursor_x: &'a mut u16,
    cursor_y: &'a mut u16,
    current_sgr: &'a mut Vec<u8>,
    in_alt_screen: &'a mut bool,
    scrollback: &'a mut VecDeque<Vec<u8>>,
    scrollback_limit: usize,
    pending_scrollback: &'a mut Vec<Vec<u8>>,
}

impl<'a> ScreenPerformer<'a> {
    fn scroll_up(&mut self) {
        if !*self.in_alt_screen {
            // Render the top line as ANSI text for scrollback
            let line = self.render_line(0);
            if self.scrollback.len() >= self.scrollback_limit {
                self.scrollback.pop_front();
            }
            self.scrollback.push_back(line.clone());
            self.pending_scrollback.push(line);
        }
        self.grid.remove(0);
        self.grid.push(vec![Cell::default(); self.cols as usize]);
    }

    fn render_line(&self, y: usize) -> Vec<u8> {
        let row = &self.grid[y];
        let mut out = Vec::new();
        out.extend_from_slice(b"\x1b[0m");
        // Find last non-space character to avoid trailing spaces
        let last_non_space = row.iter().rposition(|c| c.c != ' ').unwrap_or(0);
        for (i, cell) in row.iter().enumerate() {
            if i > last_non_space && cell.c == ' ' {
                break;
            }
            if !cell.sgr.is_empty() {
                out.extend_from_slice(&cell.sgr);
            }
            let mut buf = [0u8; 4];
            out.extend_from_slice(cell.c.encode_utf8(&mut buf).as_bytes());
        }
        out.extend_from_slice(b"\x1b[0m");
        out
    }
}

impl<'a> Perform for ScreenPerformer<'a> {
    fn print(&mut self, c: char) {
        let x = *self.cursor_x as usize;
        let y = *self.cursor_y as usize;
        if x < self.cols as usize && y < self.rows as usize {
            self.grid[y][x] = Cell {
                c,
                sgr: self.current_sgr.clone(),
            };
            *self.cursor_x += 1;
            if *self.cursor_x >= self.cols {
                *self.cursor_x = 0;
                if *self.cursor_y >= self.rows - 1 {
                    self.scroll_up();
                } else {
                    *self.cursor_y += 1;
                }
            }
        }
    }

    fn execute(&mut self, byte: u8) {
        match byte {
            // Carriage return
            0x0D => *self.cursor_x = 0,
            // Line feed
            0x0A => {
                if *self.cursor_y >= self.rows - 1 {
                    self.scroll_up();
                } else {
                    *self.cursor_y += 1;
                }
            }
            // Backspace
            0x08 => {
                if *self.cursor_x > 0 {
                    *self.cursor_x -= 1;
                }
            }
            // Tab
            0x09 => {
                *self.cursor_x = (*self.cursor_x + 8) & !7;
                if *self.cursor_x >= self.cols {
                    *self.cursor_x = self.cols - 1;
                }
            }
            // Bell — ignore
            0x07 => {}
            _ => {}
        }
    }

    fn csi_dispatch(&mut self, params: &Params, intermediates: &[u8], _ignore: bool, action: char) {
        let ps: Vec<Vec<u16>> = params.iter().map(|p| p.to_vec()).collect();
        let p = |i: usize, default: u16| -> u16 {
            ps.get(i).and_then(|v| v.first().copied()).filter(|&v| v != 0).unwrap_or(default)
        };

        match action {
            // Cursor Up
            'A' => {
                let n = p(0, 1);
                *self.cursor_y = self.cursor_y.saturating_sub(n);
            }
            // Cursor Down
            'B' => {
                let n = p(0, 1);
                *self.cursor_y = (*self.cursor_y + n).min(self.rows - 1);
            }
            // Cursor Forward
            'C' => {
                let n = p(0, 1);
                *self.cursor_x = (*self.cursor_x + n).min(self.cols - 1);
            }
            // Cursor Back
            'D' => {
                let n = p(0, 1);
                *self.cursor_x = self.cursor_x.saturating_sub(n);
            }
            // Cursor Position (CUP)
            'H' | 'f' => {
                *self.cursor_y = p(0, 1).saturating_sub(1).min(self.rows - 1);
                *self.cursor_x = p(1, 1).saturating_sub(1).min(self.cols - 1);
            }
            // Erase in Display
            'J' => {
                let mode = p(0, 0);
                match mode {
                    0 => {
                        // Clear from cursor to end
                        let y = *self.cursor_y as usize;
                        let x = *self.cursor_x as usize;
                        for i in x..self.cols as usize {
                            self.grid[y][i] = Cell::default();
                        }
                        for row in self.grid.iter_mut().skip(y + 1) {
                            for cell in row.iter_mut() {
                                *cell = Cell::default();
                            }
                        }
                    }
                    1 => {
                        // Clear from start to cursor
                        let y = *self.cursor_y as usize;
                        let x = *self.cursor_x as usize;
                        for row in self.grid.iter_mut().take(y) {
                            for cell in row.iter_mut() {
                                *cell = Cell::default();
                            }
                        }
                        for i in 0..=x.min(self.cols as usize - 1) {
                            self.grid[y][i] = Cell::default();
                        }
                    }
                    2 | 3 => {
                        // Clear entire screen
                        for row in self.grid.iter_mut() {
                            for cell in row.iter_mut() {
                                *cell = Cell::default();
                            }
                        }
                    }
                    _ => {}
                }
            }
            // Erase in Line
            'K' => {
                let mode = p(0, 0);
                let y = *self.cursor_y as usize;
                let x = *self.cursor_x as usize;
                match mode {
                    0 => {
                        for i in x..self.cols as usize {
                            self.grid[y][i] = Cell::default();
                        }
                    }
                    1 => {
                        for i in 0..=x.min(self.cols as usize - 1) {
                            self.grid[y][i] = Cell::default();
                        }
                    }
                    2 => {
                        for cell in self.grid[y].iter_mut() {
                            *cell = Cell::default();
                        }
                    }
                    _ => {}
                }
            }
            // SGR (Select Graphic Rendition)
            'm' => {
                // Store raw SGR escape sequence
                let mut sgr = Vec::new();
                sgr.extend_from_slice(b"\x1b[");
                let param_strs: Vec<String> = ps.iter().map(|p| {
                    p.iter().map(|v| v.to_string()).collect::<Vec<_>>().join(":")
                }).collect();
                sgr.extend_from_slice(param_strs.join(";").as_bytes());
                sgr.push(b'm');
                if ps.is_empty() || (ps.len() == 1 && ps[0].first() == Some(&0)) {
                    self.current_sgr.clear();
                } else {
                    *self.current_sgr = sgr;
                }
            }
            // Scroll Up
            'S' => {
                let n = p(0, 1);
                for _ in 0..n {
                    self.scroll_up();
                }
            }
            // Delete lines
            'M' => {
                let n = p(0, 1) as usize;
                let y = *self.cursor_y as usize;
                for _ in 0..n {
                    if y < self.grid.len() {
                        self.grid.remove(y);
                        self.grid.push(vec![Cell::default(); self.cols as usize]);
                    }
                }
            }
            // Insert lines
            'L' => {
                let n = p(0, 1) as usize;
                let y = *self.cursor_y as usize;
                for _ in 0..n {
                    self.grid.pop();
                    self.grid.insert(y, vec![Cell::default(); self.cols as usize]);
                }
            }
            // Private modes (DECSET/DECRST)
            'h' | 'l' => {
                if intermediates == b"?" {
                    let enable = action == 'h';
                    for param in &ps {
                        if let Some(&mode) = param.first() {
                            match mode {
                                // Alt screen: 1049 (save cursor + alt), 1047/47 (alt screen)
                                1049 | 1047 | 47 => {
                                    *self.in_alt_screen = enable;
                                    if enable {
                                        // Clear screen on entering alt
                                        for row in self.grid.iter_mut() {
                                            for cell in row.iter_mut() {
                                                *cell = Cell::default();
                                            }
                                        }
                                        *self.cursor_x = 0;
                                        *self.cursor_y = 0;
                                    }
                                }
                                _ => {}
                            }
                        }
                    }
                }
            }
            _ => {}
        }
    }

    fn esc_dispatch(&mut self, intermediates: &[u8], _ignore: bool, byte: u8) {
        match (intermediates, byte) {
            // Reverse Index — scroll down (insert line at top)
            ([], b'M') => {
                if *self.cursor_y == 0 {
                    self.grid.pop();
                    self.grid.insert(0, vec![Cell::default(); self.cols as usize]);
                } else {
                    *self.cursor_y -= 1;
                }
            }
            _ => {}
        }
    }

    fn osc_dispatch(&mut self, _params: &[&[u8]], _bell_terminated: bool) {
        // Ignore OSC (title changes etc.) for now
    }

    fn hook(&mut self, _params: &Params, _intermediates: &[u8], _ignore: bool, _action: char) {}
    fn put(&mut self, _byte: u8) {}
    fn unhook(&mut self) {}
}
```

**Step 2: Add mod to main.rs**

Add `mod screen;` to the top of main.rs.

**Step 3: Build**

Run: `cd ~/Dev/Local/rmux && cargo build`
Expected: compiles without errors

---

### Task 5: Session manager (daemon-side)

**Files:**
- Create: `src/session.rs`
- Modify: `src/main.rs` (add mod)

**Step 1: Create session module**

```rust
use crate::pty::Pty;
use crate::screen::Screen;
use std::collections::HashMap;

pub struct Session {
    pub name: String,
    pub pty: Pty,
    pub screen: Screen,
    pub cols: u16,
    pub rows: u16,
}

impl Session {
    pub fn new(name: String, cols: u16, rows: u16, history: usize) -> anyhow::Result<Self> {
        let pty = Pty::spawn(cols, rows)?;
        let screen = Screen::new(cols, rows, history);
        Ok(Self { name, pty, screen, cols, rows })
    }

    pub fn resize(&mut self, cols: u16, rows: u16) -> anyhow::Result<()> {
        self.cols = cols;
        self.rows = rows;
        self.pty.resize(cols, rows)?;
        self.screen.resize(cols, rows);
        Ok(())
    }
}

pub struct SessionManager {
    pub sessions: HashMap<String, Session>,
}

impl SessionManager {
    pub fn new() -> Self {
        Self {
            sessions: HashMap::new(),
        }
    }

    pub fn create(&mut self, name: String, cols: u16, rows: u16, history: usize) -> anyhow::Result<()> {
        if self.sessions.contains_key(&name) {
            anyhow::bail!("session '{}' already exists", name);
        }
        let session = Session::new(name.clone(), cols, rows, history)?;
        self.sessions.insert(name, session);
        Ok(())
    }

    pub fn get_mut(&mut self, name: &str) -> Option<&mut Session> {
        self.sessions.get_mut(name)
    }

    pub fn remove(&mut self, name: &str) -> Option<Session> {
        self.sessions.remove(name)
    }

    pub fn list(&self) -> Vec<crate::protocol::SessionInfo> {
        self.sessions.values().map(|s| crate::protocol::SessionInfo {
            name: s.name.clone(),
            pid: std::process::id(),
            cols: s.cols,
            rows: s.rows,
        }).collect()
    }
}
```

**Step 2: Add mod to main.rs**

Add `mod session;` to main.rs.

**Step 3: Build**

Run: `cd ~/Dev/Local/rmux && cargo build`
Expected: compiles without errors

---

### Task 6: Server — Unix socket daemon with PTY I/O loop

**Files:**
- Create: `src/server.rs`
- Modify: `src/main.rs` (add mod + server launch)

**Step 1: Create server module**

```rust
use crate::protocol::{self, ClientMsg, ServerMsg};
use crate::session::SessionManager;
use std::sync::Arc;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::UnixListener;
use tokio::sync::Mutex;

fn socket_dir() -> std::path::PathBuf {
    let uid = nix::unistd::getuid();
    let dir = std::path::PathBuf::from(format!("/tmp/rmux-{}", uid));
    let _ = std::fs::create_dir_all(&dir);
    dir
}

pub fn socket_path() -> std::path::PathBuf {
    socket_dir().join("rmux.sock")
}

pub fn cleanup_socket() {
    let _ = std::fs::remove_file(socket_path());
}

pub async fn run_server() -> anyhow::Result<()> {
    let path = socket_path();
    let _ = std::fs::remove_file(&path);

    let listener = UnixListener::bind(&path)?;
    eprintln!("rmux server listening on {:?}", path);

    let manager = Arc::new(Mutex::new(SessionManager::new()));

    loop {
        let (stream, _) = listener.accept().await?;
        let manager = manager.clone();
        tokio::spawn(async move {
            if let Err(e) = handle_client(stream, manager).await {
                eprintln!("client error: {}", e);
            }
        });
    }
}

async fn handle_client(
    mut stream: tokio::net::UnixStream,
    manager: Arc<Mutex<SessionManager>>,
) -> anyhow::Result<()> {
    let mut buf = vec![0u8; 65536];
    let mut read_buf = Vec::new();

    // Read first message — must be Connect or ListSessions or KillSession
    loop {
        let n = stream.read(&mut buf).await?;
        if n == 0 {
            return Ok(());
        }
        read_buf.extend_from_slice(&buf[..n]);

        if let Some((data, consumed)) = protocol::decode_frame(&read_buf) {
            let msg: ClientMsg = bincode::deserialize(data)?;
            read_buf.drain(..consumed);

            match msg {
                ClientMsg::Connect { name, history } => {
                    return handle_session(stream, manager, name, history, read_buf).await;
                }
                ClientMsg::ListSessions => {
                    let list = manager.lock().await.list();
                    let resp = protocol::encode(&ServerMsg::SessionList(list))?;
                    stream.write_all(&resp).await?;
                    return Ok(());
                }
                ClientMsg::KillSession { name } => {
                    let mut mgr = manager.lock().await;
                    if mgr.remove(&name).is_some() {
                        let resp = protocol::encode(&ServerMsg::Connected { name })?;
                        stream.write_all(&resp).await?;
                    } else {
                        let resp = protocol::encode(&ServerMsg::Error(
                            format!("session '{}' not found", name),
                        ))?;
                        stream.write_all(&resp).await?;
                    }
                    return Ok(());
                }
                _ => {
                    let resp = protocol::encode(&ServerMsg::Error(
                        "expected Connect, ListSessions, or KillSession".into(),
                    ))?;
                    stream.write_all(&resp).await?;
                    return Ok(());
                }
            }
        }
    }
}

async fn handle_session(
    stream: tokio::net::UnixStream,
    manager: Arc<Mutex<SessionManager>>,
    name: String,
    history: usize,
    leftover: Vec<u8>,
) -> anyhow::Result<()> {
    // Create session if it doesn't exist
    {
        let mut mgr = manager.lock().await;
        if mgr.get_mut(&name).is_none() {
            mgr.create(name.clone(), 80, 24, history)?;
        }
    }

    // Send Connected
    let (mut reader, mut writer) = stream.into_split();
    let connected = protocol::encode(&ServerMsg::Connected { name: name.clone() })?;
    writer.write_all(&connected).await?;

    // Send history + current screen
    {
        let mut mgr = manager.lock().await;
        let session = mgr.get_mut(&name).unwrap();
        let hist = session.screen.get_history();
        if !hist.is_empty() {
            let msg = protocol::encode(&ServerMsg::History(hist))?;
            writer.write_all(&msg).await?;
        }
        let screen = session.screen.render();
        let msg = protocol::encode(&ServerMsg::ScreenUpdate(screen))?;
        writer.write_all(&msg).await?;
    }

    let name_clone = name.clone();
    let manager_clone = manager.clone();

    // Task: read from PTY → send to client
    let pty_to_client = tokio::spawn(async move {
        let mut buf = [0u8; 4096];
        loop {
            // Read from PTY (blocking — run in spawn_blocking)
            let name = name_clone.clone();
            let mgr = manager_clone.clone();
            let data = tokio::task::spawn_blocking(move || {
                let mut mgr = mgr.blocking_lock();
                if let Some(session) = mgr.get_mut(&name) {
                    let n = session.pty.read(&mut buf)?;
                    if n == 0 {
                        return Ok::<_, anyhow::Error>(None);
                    }
                    session.screen.process(&buf[..n]);

                    // Collect passthrough scrollback lines
                    let scrollback_lines = session.screen.take_pending_scrollback();
                    let screen_update = session.screen.render();
                    let alive = session.pty.is_alive();

                    Ok(Some((scrollback_lines, screen_update, alive)))
                } else {
                    Ok(None)
                }
            })
            .await??;

            match data {
                Some((scrollback_lines, screen_update, alive)) => {
                    // Send scrollback lines
                    for line in scrollback_lines {
                        let msg = protocol::encode(&ServerMsg::ScrollbackLine(line))?;
                        writer.write_all(&msg).await?;
                    }
                    // Send screen update
                    let msg = protocol::encode(&ServerMsg::ScreenUpdate(screen_update))?;
                    writer.write_all(&msg).await?;

                    if !alive {
                        let msg = protocol::encode(&ServerMsg::SessionEnded)?;
                        writer.write_all(&msg).await?;
                        break;
                    }
                }
                None => break,
            }
        }
        Ok::<_, anyhow::Error>(())
    });

    let name_clone2 = name.clone();
    let manager_clone2 = manager.clone();

    // Task: read from client → write to PTY
    let client_to_pty = tokio::spawn(async move {
        let mut buf = vec![0u8; 65536];
        let mut read_buf = leftover;

        loop {
            let n = reader.read(&mut buf).await?;
            if n == 0 {
                break; // Client disconnected (detach)
            }
            read_buf.extend_from_slice(&buf[..n]);

            while let Some((data, consumed)) = protocol::decode_frame(&read_buf) {
                let msg: ClientMsg = bincode::deserialize(data)?;
                read_buf.drain(..consumed);

                match msg {
                    ClientMsg::Input(input) => {
                        let mut mgr = manager_clone2.lock().await;
                        if let Some(session) = mgr.get_mut(&name_clone2) {
                            session.pty.write(&input)?;
                        }
                    }
                    ClientMsg::Resize { cols, rows } => {
                        let mut mgr = manager_clone2.lock().await;
                        if let Some(session) = mgr.get_mut(&name_clone2) {
                            session.resize(cols, rows)?;
                        }
                    }
                    ClientMsg::Detach => {
                        return Ok::<_, anyhow::Error>(());
                    }
                    _ => {}
                }
            }
        }

        Ok(())
    });

    // Wait for either task to finish
    tokio::select! {
        r = pty_to_client => { r??; }
        r = client_to_pty => { r??; }
    }

    Ok(())
}
```

**Step 2: Add server launch to main.rs**

Update main.rs to launch server when `rmux server` or internally:

Add to the `match` in main:
```rust
// In the imports, add:
mod server;

// Add a hidden Server subcommand to cli.rs:
// In Command enum:
    /// Start the server (internal)
    #[command(hide = true)]
    Server,

// In the match in main.rs, add:
    Some(Command::Server) => {
        let rt = tokio::runtime::Runtime::new()?;
        rt.block_on(server::run_server())?;
    }
```

**Step 3: Build**

Run: `cd ~/Dev/Local/rmux && cargo build`
Expected: compiles without errors

---

### Task 7: Client — raw terminal + socket connection

**Files:**
- Create: `src/client.rs`
- Modify: `src/main.rs` (wire up all commands)

**Step 1: Create client module**

```rust
use crate::protocol::{self, ClientMsg, ServerMsg};
use nix::sys::termios;
use std::io::{self, Read, Write};
use std::os::unix::io::AsRawFd;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::UnixStream;

struct RawMode {
    original: termios::Termios,
    fd: i32,
}

impl RawMode {
    fn enter() -> anyhow::Result<Self> {
        let fd = io::stdin().as_raw_fd();
        let original = termios::tcgetattr(fd)?;
        let mut raw = original.clone();
        termios::cfmakeraw(&mut raw);
        termios::tcsetattr(fd, termios::SetArg::TCSANOW, &raw)?;
        Ok(Self { original, fd })
    }
}

impl Drop for RawMode {
    fn drop(&mut self) {
        let _ = termios::tcsetattr(self.fd, termios::SetArg::TCSANOW, &self.original);
    }
}

fn get_terminal_size() -> (u16, u16) {
    if let Some((w, h)) = term_size::dimensions() {
        (w as u16, h as u16)
    } else {
        (80, 24)
    }
}

pub async fn connect(name: &str, history: usize) -> anyhow::Result<()> {
    ensure_server_running().await?;

    let mut stream = UnixStream::connect(crate::server::socket_path()).await?;

    // Send Connect
    let msg = protocol::encode(&ClientMsg::Connect {
        name: name.to_string(),
        history,
    })?;
    stream.write_all(&msg).await?;

    // Enter raw mode
    let _raw = RawMode::enter()?;

    let (mut sock_reader, mut sock_writer) = stream.into_split();

    // Send initial resize
    let (cols, rows) = get_terminal_size();
    let resize_msg = protocol::encode(&ClientMsg::Resize { cols, rows })?;
    sock_writer.write_all(&resize_msg).await?;

    // SIGWINCH handler for resize
    let mut sigwinch = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::window_change())?;
    let sock_writer_clone = std::sync::Arc::new(tokio::sync::Mutex::new(sock_writer));
    let sw = sock_writer_clone.clone();
    tokio::spawn(async move {
        while sigwinch.recv().await.is_some() {
            let (cols, rows) = get_terminal_size();
            if let Ok(msg) = protocol::encode(&ClientMsg::Resize { cols, rows }) {
                let mut w = sw.lock().await;
                let _ = w.write_all(&msg).await;
            }
        }
    });

    // Stdin → socket (in blocking thread)
    let sw2 = sock_writer_clone.clone();
    tokio::spawn(async move {
        let mut buf = [0u8; 1024];
        loop {
            let n = tokio::task::spawn_blocking(move || {
                io::stdin().read(&mut buf)
            })
            .await;

            match n {
                Ok(Ok(0)) => break,
                Ok(Ok(n)) => {
                    // Check for detach key (Ctrl+\, byte 0x1c)
                    if buf[..n].contains(&0x1c) {
                        let msg = protocol::encode(&ClientMsg::Detach).unwrap();
                        let mut w = sw2.lock().await;
                        let _ = w.write_all(&msg).await;
                        break;
                    }
                    let msg = protocol::encode(&ClientMsg::Input(buf[..n].to_vec())).unwrap();
                    let mut w = sw2.lock().await;
                    let _ = w.write_all(&msg).await;
                }
                _ => break,
            }
        }
    });

    // Socket → stdout
    let mut buf = vec![0u8; 65536];
    let mut read_buf = Vec::new();
    let mut stdout = io::stdout();

    loop {
        let n = sock_reader.read(&mut buf).await?;
        if n == 0 {
            break;
        }
        read_buf.extend_from_slice(&buf[..n]);

        while let Some((data, consumed)) = protocol::decode_frame(&read_buf) {
            let msg: ServerMsg = bincode::deserialize(data)?;
            read_buf.drain(..consumed);

            match msg {
                ServerMsg::ScrollbackLine(line) => {
                    // Write to stdout as native scrollback
                    stdout.write_all(&line)?;
                    stdout.write_all(b"\r\n")?;
                    stdout.flush()?;
                }
                ServerMsg::ScreenUpdate(data) => {
                    stdout.write_all(&data)?;
                    stdout.flush()?;
                }
                ServerMsg::History(lines) => {
                    for line in lines {
                        stdout.write_all(&line)?;
                        stdout.write_all(b"\r\n")?;
                    }
                    stdout.flush()?;
                }
                ServerMsg::Connected { name } => {
                    // Connected OK
                    eprintln!("[rmux: connected to '{}']", name);
                }
                ServerMsg::SessionEnded => {
                    eprintln!("[rmux: session ended]");
                    return Ok(());
                }
                ServerMsg::Error(e) => {
                    eprintln!("[rmux error: {}]", e);
                    return Ok(());
                }
                ServerMsg::SessionList(_) => {}
            }
        }
    }

    eprintln!("[rmux: detached]");
    Ok(())
}

pub async fn list_sessions() -> anyhow::Result<()> {
    let path = crate::server::socket_path();
    if !path.exists() {
        println!("No active sessions");
        return Ok(());
    }

    let mut stream = UnixStream::connect(&path).await?;
    let msg = protocol::encode(&ClientMsg::ListSessions)?;
    stream.write_all(&msg).await?;

    let mut buf = vec![0u8; 65536];
    let mut read_buf = Vec::new();
    loop {
        let n = stream.read(&mut buf).await?;
        if n == 0 {
            break;
        }
        read_buf.extend_from_slice(&buf[..n]);
        if let Some((data, _)) = protocol::decode_frame(&read_buf) {
            let msg: ServerMsg = bincode::deserialize(data)?;
            match msg {
                ServerMsg::SessionList(sessions) => {
                    if sessions.is_empty() {
                        println!("No active sessions");
                    } else {
                        for s in sessions {
                            println!("{} ({}x{})", s.name, s.cols, s.rows);
                        }
                    }
                }
                _ => {}
            }
            break;
        }
    }

    Ok(())
}

pub async fn kill_session(name: &str) -> anyhow::Result<()> {
    let path = crate::server::socket_path();
    if !path.exists() {
        anyhow::bail!("server not running");
    }

    let mut stream = UnixStream::connect(&path).await?;
    let msg = protocol::encode(&ClientMsg::KillSession {
        name: name.to_string(),
    })?;
    stream.write_all(&msg).await?;

    let mut buf = vec![0u8; 4096];
    let mut read_buf = Vec::new();
    loop {
        let n = stream.read(&mut buf).await?;
        if n == 0 {
            break;
        }
        read_buf.extend_from_slice(&buf[..n]);
        if let Some((data, _)) = protocol::decode_frame(&read_buf) {
            let msg: ServerMsg = bincode::deserialize(data)?;
            match msg {
                ServerMsg::Connected { name } => println!("killed session '{}'", name),
                ServerMsg::Error(e) => println!("error: {}", e),
                _ => {}
            }
            break;
        }
    }

    Ok(())
}

async fn ensure_server_running() -> anyhow::Result<()> {
    let path = crate::server::socket_path();
    if UnixStream::connect(&path).await.is_ok() {
        return Ok(());
    }

    // Start server as background process
    let exe = std::env::current_exe()?;
    std::process::Command::new(exe)
        .arg("server")
        .stdin(std::process::Stdio::null())
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::null())
        .spawn()?;

    // Wait for server to start
    for _ in 0..50 {
        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
        if UnixStream::connect(&path).await.is_ok() {
            return Ok(());
        }
    }

    anyhow::bail!("failed to start server");
}
```

**Step 2: Update Cargo.toml — add term_size dependency**

Add to `[dependencies]`:
```toml
term_size = "0.3"
```

**Step 3: Wire up main.rs**

```rust
mod cli;
mod client;
mod protocol;
mod pty;
mod screen;
mod server;
mod session;

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

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

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

    match cli.command {
        Some(Command::New { name, history }) => {
            let name = name.unwrap_or_else(generate_name);
            rt.block_on(client::connect(&name, history))?;
        }
        Some(Command::Attach { name }) => {
            rt.block_on(client::connect(&name, 10000))?;
        }
        Some(Command::List) => {
            rt.block_on(client::list_sessions())?;
        }
        Some(Command::Kill { name }) => {
            rt.block_on(client::kill_session(&name))?;
        }
        Some(Command::Server) => {
            rt.block_on(server::run_server())?;
        }
        None => {
            if let Some(name) = cli.name {
                rt.block_on(client::connect(&name, 10000))?;
            } else {
                Cli::parse_from(["rmux", "--help"]);
            }
        }
    }

    Ok(())
}

fn generate_name() -> String {
    format!("s{}", std::process::id())
}
```

**Step 4: Build and test**

Run: `cd ~/Dev/Local/rmux && cargo build`
Expected: compiles

Run: `cargo run -- work`
Expected: launches shell, typing works, Ctrl+\ detaches

Run: `cargo run -- attach work`
Expected: reattaches, shows screen + history

Run: `cargo run -- list`
Expected: shows "work (80x24)"

---

### Task 8: Integration testing

**Step 1: Manual smoke tests**

```bash
# Build
cd ~/Dev/Local/rmux && cargo build --release

# Test 1: Create session, type stuff, detach
./target/release/rmux work
# Type: echo hello && ls -la
# Scroll should work natively
# Press Ctrl+\ to detach

# Test 2: Reattach — should see history
./target/release/rmux work

# Test 3: List sessions
./target/release/rmux list

# Test 4: Kill session
./target/release/rmux kill work

# Test 5: Auto-create
./target/release/rmux test
# Should create new session "test"

# Test 6: Vim (alt screen)
./target/release/rmux vimtest
# Type: vim
# Edit something, :q
# Scrollback should NOT have vim garbage

# Test 7: Resize
# Resize terminal window while connected
# Text should reflow
```

**Step 2: Fix any issues found during testing**

---
