# terminal-capture Automatizar una CLI/TUI interactiva y capturar su texto, de forma headless, a través de un pseudo-terminal (PTY). Cubre el ciclo completo: lanzar el proceso con un TTY real, inyectarle input scripteado, esperar a que el render se estabilice, y convertir el stream crudo de bytes a texto plano — bien reconstruyendo el layout 2D (TUIs con cursor absoluto), bien limpiando ANSI de output secuencial. Existe porque muchas CLIs (sobre todo la CLI `claude`) solo entran en su modo interactivo rico cuando detectan un TTY; un pipe normal las degrada. El PTY es virtual, en memoria: **nunca abre una ventana de terminal**. ## Funciones | ID | Firma | Qué hace | |---|---|---| | `pty_capture_idle_go_infra` | `func PTYCaptureIdle(ctx, name string, args []string, warmup time.Duration, inputs []string, stepDelay, idle, maxDur time.Duration) (string, error)` | Lanza `name args` en un PTY (40×120), espera `warmup`, escribe cada `inputs` separado por `stepDelay`, y captura todos los bytes hasta que pasa `idle` sin output nuevo o se alcanza `maxDur`. Devuelve el stream **crudo** (ANSI intacto). One-shot. | | `pty_capture_stream_go_infra` | `func PTYCaptureStream(ctx, name string, args []string, warmup time.Duration, inputs []string, stepDelay, snapshotInterval, idle, maxDur time.Duration) (<-chan string, error)` | Igual que `pty_capture_idle` pero emite **snapshots acumulativos** del buffer por un canal cada `snapshotInterval` — para hacer streaming de la TUI mientras renderiza. El consumidor renderiza/parsea cada snapshot. | | `text_prefix_delta_go_core` | `func PrefixDelta(prev, curr string) string` | Devuelve la parte de `curr` que sigue al prefijo común con `prev` (delta de streaming por snapshots). Pura, compara por runas. Heurística ante reflow. | | `vt_render_go_tui` | `func VTRender(raw string, rows, cols int) string` | Emula un terminal VT100 de `rows×cols`, alimenta `raw`, y devuelve el estado final de la pantalla como texto plano **con el layout reconstruido** (espacios reales donde el stream tenía movimientos de cursor). Pura. | | `strip_ansi_go_core` | `func StripANSI(s string) string` | Elimina secuencias ANSI/VT100 y caracteres de control de un stream **secuencial** (logs), preservando `\n`, `\t`, `\r`. Pura. NO reconstruye layout 2D. | | `parse_claude_tui_go_tui` | `func ParseClaudeTUI(screen string) ClaudeTUIParse` | Parsea la pantalla renderizada de la TUI de `claude` (salida de `vt_render`) y extrae los turnos (user/assistant/tool_use/tool_result) + la respuesta final (`Answer`), equivalente a lo que devolvería `claude -p`. Pura, heurística, específica de la TUI de claude. | ## Cuándo usar cada limpiador El corazón del grupo es `pty_capture_idle` (la captura). Lo que cambia es cómo conviertes el raw a texto: | Si la salida es… | Usa | Porque | |---|---|---| | Una TUI con posicionamiento absoluto (`claude`, `htop`, `dialog`) | `vt_render_go_tui` (modo screen) | Los "espacios" entre columnas eran movimientos de cursor; sin emular el grid las palabras se pegan (`2newMCPservers`). | | Output secuencial línea a línea (logs, builds) | `strip_ansi_go_core` (modo stream) | No hay layout 2D que reconstruir; basta quitar los escape codes. | | Quieres procesar los escape codes tú mismo | (ninguno — usa el raw) | El raw de `pty_capture_idle` ya los conserva. | ## Ejemplo canónico (end-to-end) Capturar la respuesta de la CLI `claude` como texto con layout, en Go: ```go import ( "context" "time" "fn-registry/functions/infra" "fn-registry/functions/tui" ) func main() { ctx := context.Background() // Teclear el prompt y pulsar Enter como pasos separados: un "\r" pegado al // texto lo trata claude como newline literal, no como submit. inputs := []string{"resume el README en 3 lineas", "\r"} raw, _ := infra.PTYCaptureIdle(ctx, "claude", nil, 4*time.Second, // warmup: deja cargar la TUI inputs, 600*time.Millisecond, 4*time.Second, // idle: corta tras 4s de silencio 60*time.Second) // maxDur: tope duro screen := tui.VTRender(raw, 40, 120) // reconstruye el layout 2D print(screen) } ``` La app `claude_extract` (`apps/claude_extract`) empaqueta exactamente este flujo como CLI, con modos `screen|stream|raw`, `--exec` para pipear a otro proceso, y `--cwd` para saltar el diálogo de arranque de claude. Es el consumidor de referencia del grupo. La app `claude_pipe` (`apps/claude_pipe`) va un paso más allá: añade `parse_claude_tui_go_tui` al final del pipeline para devolver la respuesta de claude **como dato** con el mismo shape que `claude -p --output-format json` (`--format json|text|turns`). Es la alternativa "parsea la TUI" a `claude -p`, para cuando se quiere expresamente ir a través de la TUI en vez del stream-json. ## Fronteras - **No es `claude -p`**: este grupo captura la TUI real (lo que se ve). Para interacción programática limpia con la CLI `claude`, usa `claude_stream_go_core` (`claude -p --output-format stream-json`). - **Linux/Unix only**: PTY POSIX (`creack/pty`). No Windows. - **Sin color**: `vt_render` reconstruye texto y layout, no atributos de color. - **Idle es heurístico**: TUIs con render periódico (spinners, relojes) no disparan el idle y caen al `maxDur`. Para `claude` el spinner se detiene al terminar la respuesta, así que corta bien. - **Dimensiones fijas 40×120**: el render debe usar el mismo tamaño que la captura o el wrapping no cuadra. ## Notas - Las dos funciones de limpieza son **puras**; solo `pty_capture_idle` es impura (lanza procesos). Puras en los bordes, impura en el centro de la captura. - `pty_capture_idle` no fija el cwd del hijo: para controlarlo, cambia el cwd del proceso que la invoca antes de llamarla (lo que hace `claude_extract --cwd`).