Initial import
This commit is contained in:
132
recorder/asciicast.go
Normal file
132
recorder/asciicast.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package recorder
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type asciiCastRecorder struct {
|
||||
wc io.WriteCloser
|
||||
mu sync.Mutex
|
||||
header asciiCastHeader
|
||||
last time.Time
|
||||
closed bool
|
||||
}
|
||||
|
||||
type asciiCastHeader struct {
|
||||
Version int `json:"version"`
|
||||
Term asciiCastTerm `json:"term"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Env map[string]string `json:"env,omitempty"`
|
||||
}
|
||||
|
||||
type asciiCastTerm struct {
|
||||
Columns int `json:"cols"`
|
||||
Rows int `json:"rows"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type asciiCastDuration float64
|
||||
|
||||
func (d asciiCastDuration) MarshalJSON() ([]byte, error) {
|
||||
return []byte(strconv.FormatFloat(float64(d), 'f', 6, 64)), nil
|
||||
}
|
||||
|
||||
type asciiCastFrame struct {
|
||||
Delay float64
|
||||
Data []byte
|
||||
}
|
||||
|
||||
func (f asciiCastFrame) MarshalJSON() ([]byte, error) {
|
||||
s, _ := json.Marshal(string(f.Data))
|
||||
return []byte(fmt.Sprintf(`[%.6f, %s]`, f.Delay, s)), nil
|
||||
}
|
||||
|
||||
func newAsciiCastRecorder(wc io.WriteCloser, info Info) (*asciiCastRecorder, error) {
|
||||
now := time.Now()
|
||||
if err := json.NewEncoder(wc).Encode(asciiCastHeader{
|
||||
Version: 3,
|
||||
Term: asciiCastTerm{
|
||||
Columns: info.Columns,
|
||||
Rows: info.Rows,
|
||||
Type: info.TerminalType,
|
||||
},
|
||||
Timestamp: now.Unix(),
|
||||
Title: info.Title,
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := io.WriteString(wc, "\n"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &asciiCastRecorder{
|
||||
wc: wc,
|
||||
last: now,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *asciiCastRecorder) Close() error {
|
||||
return r.wc.Close()
|
||||
}
|
||||
|
||||
func (r *asciiCastRecorder) writeFrame(kind rune, p []byte) (int, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if r.closed {
|
||||
return 0, io.ErrClosedPipe
|
||||
}
|
||||
|
||||
var (
|
||||
now = time.Now()
|
||||
delay = now.Sub(r.last)
|
||||
)
|
||||
|
||||
v, _ := json.Marshal(string(p))
|
||||
_, err := io.WriteString(r.wc, "["+strconv.FormatFloat(delay.Seconds(), 'f', 6, 64)+", \""+string(kind)+"\", "+string(v)+"]\n")
|
||||
|
||||
r.last = now
|
||||
return len(p), err
|
||||
}
|
||||
|
||||
type asciiCastWriter struct {
|
||||
r *asciiCastRecorder
|
||||
kind rune
|
||||
}
|
||||
|
||||
func (w *asciiCastWriter) Close() error {
|
||||
return w.r.Close()
|
||||
}
|
||||
|
||||
func (w *asciiCastWriter) Write(p []byte) (int, error) {
|
||||
return w.r.writeFrame(w.kind, p)
|
||||
}
|
||||
|
||||
func (r *asciiCastRecorder) Reads() io.WriteCloser {
|
||||
return &asciiCastWriter{r, 'o'}
|
||||
}
|
||||
|
||||
func (r *asciiCastRecorder) Writes() io.WriteCloser {
|
||||
return &asciiCastWriter{r, 'i'}
|
||||
}
|
||||
|
||||
func (r *asciiCastRecorder) Resize(columns, rows int) {
|
||||
var (
|
||||
now = time.Now()
|
||||
delay = now.Sub(r.last)
|
||||
)
|
||||
r.mu.Lock()
|
||||
_, _ = fmt.Fprintf(r.wc, "[%.6f, \"r\", \"%dx%d\"]\n", delay.Seconds(), columns, rows)
|
||||
r.last = now
|
||||
r.mu.Unlock()
|
||||
}
|
||||
|
||||
var (
|
||||
_ Recorder = (*asciiCastRecorder)(nil)
|
||||
_ Resizer = (*asciiCastRecorder)(nil)
|
||||
)
|
68
recorder/recorder.go
Normal file
68
recorder/recorder.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package recorder
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Format int
|
||||
|
||||
const (
|
||||
Text Format = iota
|
||||
TTYRec
|
||||
ASCIICastv3
|
||||
)
|
||||
|
||||
var (
|
||||
ErrFormat = errors.New("recorder: unknown format")
|
||||
)
|
||||
|
||||
type Recorder interface {
|
||||
Reads() io.WriteCloser
|
||||
Writes() io.WriteCloser
|
||||
}
|
||||
|
||||
type Resizer interface {
|
||||
Resize(columns, rows int)
|
||||
}
|
||||
|
||||
type Info struct {
|
||||
Columns int
|
||||
Rows int
|
||||
TerminalType string
|
||||
Title string
|
||||
Env map[string]string
|
||||
}
|
||||
|
||||
func New(name string, format Format, info Info) (Recorder, error) {
|
||||
f, err := os.OpenFile(name, os.O_CREATE|os.O_WRONLY, 0o640)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewWriter(f, format, info)
|
||||
}
|
||||
|
||||
func NewWriter(wc io.WriteCloser, format Format, info Info) (Recorder, error) {
|
||||
switch format {
|
||||
case Text:
|
||||
return textRecorder{wc}, nil
|
||||
|
||||
case ASCIICastv3:
|
||||
return newAsciiCastRecorder(wc, info)
|
||||
|
||||
case TTYRec:
|
||||
return newTTYRecRecorder(wc), nil
|
||||
|
||||
default:
|
||||
return nil, ErrFormat
|
||||
}
|
||||
}
|
||||
|
||||
type nopCloser struct {
|
||||
io.Writer
|
||||
}
|
||||
|
||||
func (nopCloser) Close() error {
|
||||
return nil
|
||||
}
|
15
recorder/text.go
Normal file
15
recorder/text.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package recorder
|
||||
|
||||
import "io"
|
||||
|
||||
type textRecorder struct {
|
||||
io.WriteCloser
|
||||
}
|
||||
|
||||
func (r textRecorder) Reads() io.WriteCloser {
|
||||
return r.WriteCloser
|
||||
}
|
||||
|
||||
func (r textRecorder) Writes() io.WriteCloser {
|
||||
return r.WriteCloser
|
||||
}
|
72
recorder/ttyrec.go
Normal file
72
recorder/ttyrec.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package recorder
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ttyRecordHeader struct {
|
||||
Sec int32
|
||||
USec int32
|
||||
Len int32
|
||||
}
|
||||
|
||||
func (h *ttyRecordHeader) Now() {
|
||||
var (
|
||||
now = time.Now()
|
||||
tv = syscall.NsecToTimeval(now.UnixNano())
|
||||
)
|
||||
h.Sec = int32(tv.Sec)
|
||||
h.USec = tv.Usec
|
||||
}
|
||||
|
||||
func (h *ttyRecordHeader) WriteTo(w io.Writer) (int64, error) {
|
||||
var b [6]byte
|
||||
binary.LittleEndian.PutUint32(b[0:], uint32(h.Sec))
|
||||
binary.LittleEndian.PutUint32(b[2:], uint32(h.USec))
|
||||
binary.LittleEndian.PutUint32(b[4:], uint32(h.Len))
|
||||
n, err := w.Write(b[:])
|
||||
return int64(n), err
|
||||
}
|
||||
|
||||
type ttyRecRecorder struct {
|
||||
mu sync.Mutex
|
||||
wc io.WriteCloser
|
||||
header *ttyRecordHeader
|
||||
}
|
||||
|
||||
func newTTYRecRecorder(wc io.WriteCloser) *ttyRecRecorder {
|
||||
return &ttyRecRecorder{
|
||||
wc: wc,
|
||||
header: new(ttyRecordHeader),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ttyRecRecorder) Close() error {
|
||||
return r.wc.Close()
|
||||
}
|
||||
|
||||
func (w *ttyRecRecorder) Write(p []byte) (n int, err error) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
w.header.Now()
|
||||
if _, err = w.header.WriteTo(w.wc); err != nil {
|
||||
return
|
||||
}
|
||||
if _, err = w.wc.Write(p); err != nil {
|
||||
return
|
||||
}
|
||||
return len(p) + 9, nil
|
||||
}
|
||||
|
||||
func (r *ttyRecRecorder) Reads() io.WriteCloser {
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *ttyRecRecorder) Writes() io.WriteCloser {
|
||||
return nopCloser{io.Discard}
|
||||
}
|
Reference in New Issue
Block a user