Interactive asciicast editor
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

220 lines
4.8 KiB

package main
import (
"flag"
"fmt"
"log"
"net"
"path"
"strings"
"time"
"github.com/gdamore/tcell/v2"
"gitlab.com/tslocum/cbind"
"gitlab.com/tslocum/cview"
)
var (
app *cview.Application
editorPaused bool
statusBuffer *cview.TextView
lengthBuffer *cview.TextView
slider *cview.Slider
castInfoBuffer *cview.TextView
editorCursor time.Duration
editorCursorTime time.Time
editorStatus string
)
func handleEditor() {
go handleRead()
go handleWrite()
for c := range commandsIn {
switch c.Type {
case commandStatus:
editorCursor = c.D
editorCursorTime = time.Now()
editorStatus = c.S
}
}
}
func connectToViewer(address string) error {
var err error
if strings.HasPrefix(address, "/") {
commandConn, err = net.Dial("unix", address)
} else {
commandConn, err = net.Dial("tcp", address)
}
if err != nil {
return err
}
go handleEditor()
return nil
}
func updateStatus() {
cursor := editorCursor
if !editorPaused && editorStatus == "playing" {
cursor += time.Since(editorCursorTime)
}
ms := ((cursor.Nanoseconds() / int64(time.Millisecond)) % 1000) / 100
statusBuffer.SetText(fmt.Sprintf("%d:%02d.%d", int(cursor.Minutes()), int(cursor.Seconds())%60, ms))
}
func updateSlider() {
lengthBuffer.SetText(fmt.Sprintf("%d:%02d", int(loadedCastLength.Minutes()), int(loadedCastLength.Seconds())%60))
slider.SetProgress(int(float64(editorCursor) / float64(loadedCastLength) * 100))
}
func handleUpdateUI() {
t := time.NewTicker(100 * time.Millisecond)
i := 0
for range t.C {
updateStatus()
if i == 0 {
updateSlider()
}
app.Draw()
i++
if i > 9 {
i = 0
}
}
}
func runEditor(controlAddress string, force bool) {
filePath := flag.Arg(0)
if filePath == "" {
log.Fatal("supply the path to an asciinema .cast file")
}
if !single {
err := connectToViewer(controlAddress)
if err != nil {
if !single {
log.Fatalf("failed to connect to viwer: %s", err)
}
err = nil
}
}
app = cview.NewApplication()
err := app.Init()
if err != nil {
log.Fatal(err)
}
if single {
app.QueueUpdate(func() {
w, h := app.GetScreenSize()
if (uint(w) < loadedCast.Header.Width || uint(h) < loadedCast.Header.Height) && !force {
log.Fatalf("recording dimensions (%dx%d) are larger than the current terminal (%dx%d). add --force to force playback", loadedCast.Header.Width, loadedCast.Header.Height, w, h)
}
})
}
app.EnableMouse(true)
statusBuffer = cview.NewTextView()
lengthBuffer = cview.NewTextView()
lengthBuffer.SetTextAlign(cview.AlignRight)
lengthBuffer.SetText("0:00")
sliderChanged := func(value int) {
c := time.Duration(float64(loadedCastLength) * (float64(value) / 100))
sendCommand(&command{commandPlay, c, ""})
}
slider = cview.NewSlider()
slider.SetChangedFunc(sliderChanged)
castInfoBuffer = cview.NewTextView()
castInfoBuffer.SetTextAlign(cview.AlignCenter)
grid := cview.NewGrid()
grid.SetRows(-1, 1, 3, -1)
grid.AddItem(cview.NewTextView(), 0, 0, 1, 2, 0, 0, true)
grid.AddItem(statusBuffer, 1, 0, 1, 1, 0, 0, false)
grid.AddItem(lengthBuffer, 1, 1, 1, 1, 0, 0, false)
grid.AddItem(slider, 2, 0, 1, 2, 0, 0, true)
grid.AddItem(castInfoBuffer, 3, 0, 1, 2, 0, 0, true)
grid.AddItem(cview.NewTextView(), 4, 0, 1, 2, 0, 0, true)
app.SetRoot(grid, true)
quit := func(ev *tcell.EventKey) *tcell.EventKey {
app.Stop()
return nil
}
doInterrupt := func(ev *tcell.EventKey) *tcell.EventKey {
interruptPlayback()
return nil
}
doPlay := func(ev *tcell.EventKey) *tcell.EventKey {
editorCursorTime = time.Now()
sendCommand(&command{commandPlay, 0, ""})
return nil
}
doPause := func(ev *tcell.EventKey) *tcell.EventKey {
editorPaused = !editorPaused
if editorPaused {
sendCommand(&command{commandPause, 0, ""})
} else {
editorCursorTime = time.Now()
sendCommand(&command{commandResume, 0, ""})
}
return nil
}
doFastForward := func(ev *tcell.EventKey) *tcell.EventKey {
c := editorCursor + 5*time.Second
editorCursor = c
sendCommand(&command{commandPlay, c, ""})
return nil
}
doRewind := func(ev *tcell.EventKey) *tcell.EventKey {
c := editorCursor - 5*time.Second
if c < 0 {
c = 0
}
editorCursor = c
sendCommand(&command{commandPlay, c, ""})
return nil
}
inputConfig := cbind.NewConfiguration()
inputConfig.Set("Escape", quit)
inputConfig.Set("Ctrl+c", quit)
inputConfig.Set("Ctrl+d", doInterrupt)
inputConfig.Set("Space", doPause)
inputConfig.Set("Enter", doPlay)
inputConfig.Set("Left", doRewind)
inputConfig.Set("Right", doFastForward)
app.SetInputCapture(inputConfig.Capture)
go handleUpdateUI()
err = loadCast(filePath)
if err != nil {
log.Fatalf("failed to load cast at %s: %s", filePath, err)
}
castInfoBuffer.SetText(path.Base(filePath))
castCommand <- &command{commandLoad, 0, filePath}
castCommand <- &command{commandPlay, 0, ""}
if err := app.Run(); err != nil {
log.Fatal(err)
}
}