package terminal import ( "io" "os" "os/exec" "os/signal" "syscall" "time" "github.com/asciinema/asciinema/Godeps/_workspace/src/code.google.com/p/go.crypto/ssh/terminal" "github.com/asciinema/asciinema/Godeps/_workspace/src/github.com/creack/termios/raw" "github.com/asciinema/asciinema/Godeps/_workspace/src/github.com/kr/pty" "github.com/asciinema/asciinema/Godeps/_workspace/src/golang.org/x/text/encoding/unicode" "github.com/asciinema/asciinema/Godeps/_workspace/src/golang.org/x/text/transform" "github.com/asciinema/asciinema/ptyx" "github.com/asciinema/asciinema/util" ) type Terminal interface { Size() (int, int, error) Record(string, io.Writer) error Write([]byte) error } type Pty struct { Stdin *os.File Stdout *os.File } func NewTerminal() Terminal { return &Pty{Stdin: os.Stdin, Stdout: os.Stdout} } func (p *Pty) Size() (int, int, error) { return pty.Getsize(p.Stdout) } func (p *Pty) Record(command string, w io.Writer) error { // start command in pty cmd := exec.Command("sh", "-c", command) cmd.Env = append(os.Environ(), "ASCIINEMA_REC=1") master, err := pty.Start(cmd) if err != nil { return err } defer master.Close() // install WINCH signal handler signals := make(chan os.Signal, 1) signal.Notify(signals, syscall.SIGWINCH) defer signal.Stop(signals) go func() { for _ = range signals { p.resize(master) } }() defer close(signals) // put stdin in raw mode (if it's a tty) fd := p.Stdin.Fd() if terminal.IsTerminal(int(fd)) { oldState, err := raw.MakeRaw(fd) if err != nil { return err } defer raw.TcSetAttr(fd, oldState) } // do initial resize p.resize(master) // start stdin -> master copying stop := util.Copy(master, p.Stdin) // copy pty master -> p.stdout & w stdout := transform.NewWriter(w, unicode.UTF8.NewEncoder()) defer stdout.Close() stdoutWaitChan := make(chan struct{}) go func() { io.Copy(io.MultiWriter(p.Stdout, stdout), master) stdoutWaitChan <- struct{}{} }() // wait for the process to exit and reap it cmd.Wait() // wait for master -> stdout copying to finish // // sometimes after process exits reading from master blocks forever (race condition?) // we're using timeout here to overcome this problem select { case <-stdoutWaitChan: case <-time.After(200 * time.Millisecond): } // stop stdin -> master copying stop() return nil } func (p *Pty) Write(data []byte) error { _, err := p.Stdout.Write(data) if err != nil { return err } err = p.Stdout.Sync() if err != nil { return err } return nil } func (p *Pty) resize(f *os.File) { var rows, cols int if terminal.IsTerminal(int(p.Stdout.Fd())) { rows, cols, _ = p.Size() } else { rows = 24 cols = 80 } ptyx.Setsize(f, rows, cols) }