Initial import

This commit is contained in:
2025-10-10 10:05:13 +02:00
parent 3effc1597b
commit b96b6e7f8f
164 changed files with 5473 additions and 0 deletions

132
recorder/asciicast.go Normal file
View 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
View 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
View 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
View 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}
}