package server import ( "io" "os" "os/exec" "regexp" "github.com/creack/pty" ) // ansiRegex matches ANSI escape sequences for stripping terminal formatting. var ansiRegex = regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]|\x1b\].*?\x07|\x1b\[[\?0-9;]*[a-zA-Z]`) // stripAnsi removes ANSI escape codes from a string. func stripAnsi(s string) string { return ansiRegex.ReplaceAllString(s, "") } // startPTY starts a command in a pseudo-terminal and returns the PTY master fd. // The caller is responsible for closing the returned *os.File. func startPTY(cmd *exec.Cmd) (*os.File, error) { ptmx, err := pty.StartWithSize(cmd, &pty.Winsize{Rows: 40, Cols: 120}) if err != nil { return nil, err } return ptmx, nil } // readPTY reads from a PTY master and calls sendLine for each chunk of text. // It splits on newlines so each SSE event is one line. func readPTY(ptmx io.Reader, sendLine func(string)) { buf := make([]byte, 4096) var partial string for { n, err := ptmx.Read(buf) if n > 0 { text := partial + stripAnsi(string(buf[:n])) partial = "" // Split into lines, keep partial for next read for { idx := -1 for i, b := range []byte(text) { if b == '\n' || b == '\r' { idx = i break } } if idx < 0 { partial = text break } line := text[:idx] // Skip empty lines from \r\n sequences if line != "" { sendLine(line) } // Skip past the newline character(s) text = text[idx+1:] } } if err != nil { // Send remaining partial text if partial != "" { sendLine(partial) } break } } }