vhs/evaluator.go
Carlos Alexandro Becker 710bb769af
fix: lint (#637)
2025-06-17 20:49:26 +02:00

190 lines
4.9 KiB
Go

package main
import (
"context"
"fmt"
"io"
"log"
"os"
"github.com/charmbracelet/vhs/lexer"
"github.com/charmbracelet/vhs/parser"
"github.com/charmbracelet/vhs/token"
"github.com/go-rod/rod"
)
// EvaluatorOption is a function that can be used to modify the VHS instance.
type EvaluatorOption func(*VHS)
// Evaluate takes as input a tape string, an output writer, and an output file
// and evaluates all the commands within the tape string and produces a GIF.
func Evaluate(ctx context.Context, tape string, out io.Writer, opts ...EvaluatorOption) []error {
l := lexer.New(tape)
p := parser.New(l)
cmds := p.Parse()
errs := p.Errors()
if len(errs) != 0 || len(cmds) == 0 {
return []error{InvalidSyntaxError{errs}}
}
v := New()
for _, cmd := range cmds {
if cmd.Type == token.SET && cmd.Options == "Shell" || cmd.Type == token.ENV {
err := Execute(cmd, &v)
if err != nil {
return []error{err}
}
}
}
// Start things up
if err := v.Start(); err != nil {
return []error{err}
}
defer func() { _ = v.close() }()
// Let's wait until we can access the window.term variable.
//
// This is necessary because some SET commands modify the terminal.
err := v.Page.Wait(rod.Eval("() => window.term != undefined"))
if err != nil {
return []error{err}
}
var offset int
for i, cmd := range cmds {
if cmd.Type == token.SET || cmd.Type == token.OUTPUT || cmd.Type == token.REQUIRE {
_, _ = fmt.Fprintln(out, Highlight(cmd, false))
if cmd.Options != "Shell" {
err := Execute(cmd, &v)
if err != nil {
return []error{err}
}
}
} else {
offset = i
break
}
}
// Make sure image is big enough to fit padding, bar, and margins
video := v.Options.Video
minWidth := double(video.Style.Padding) + double(video.Style.Margin)
minHeight := double(video.Style.Padding) + double(video.Style.Margin)
if video.Style.WindowBar != "" {
minHeight += video.Style.WindowBarSize
}
if video.Style.Height < minHeight || video.Style.Width < minWidth {
//nolint:staticcheck
v.Errors = append(
v.Errors,
fmt.Errorf(
"Dimensions must be at least %d x %d",
minWidth, minHeight,
),
)
}
if len(v.Errors) > 0 {
return v.Errors
}
// Setup the terminal session so we can start executing commands.
v.Setup()
// If the first command (after Settings and Outputs) is a Hide command, we can
// begin executing the commands before we start recording to avoid capturing
// any unwanted frames.
if cmds[offset].Type == token.HIDE {
for i, cmd := range cmds[offset:] {
if cmd.Type == token.SHOW {
offset += i
break
}
_, _ = fmt.Fprintln(out, Highlight(cmd, true))
err := Execute(cmd, &v)
if err != nil {
return []error{err}
}
}
}
// Begin recording frames as we are now in a recording state.
ctx, cancel := context.WithCancel(ctx)
ch := v.Record(ctx)
// Clean up temporary files at the end.
defer func() {
if v.Options.Video.Output.Frames != "" {
// Move the frames to the output directory.
_ = os.Rename(v.Options.Video.Input, v.Options.Video.Output.Frames)
}
_ = v.Cleanup()
}()
teardown := func() {
// Stop recording frames.
cancel()
// Read from channel to ensure recorder is done.
<-ch
}
// Log errors from the recording process.
go func() {
for err := range ch {
log.Print(err.Error())
}
}()
for _, cmd := range cmds[offset:] {
if ctx.Err() != nil {
teardown()
return []error{ctx.Err()}
}
// When changing the FontFamily, FontSize, LineHeight, Padding
// The xterm.js canvas changes dimensions and causes FFMPEG to not work
// correctly (specifically) with palettegen.
// It will be possible to change settings on the fly in the future, but
// it is currently not as it does not result in a proper render of the
// GIF as the frame sequence will change dimensions. This is fixable.
//
// We should remove if isSetting statement.
isSetting := cmd.Type == token.SET && cmd.Options != "TypingSpeed"
if isSetting {
fmt.Println(ErrorStyle.Render(fmt.Sprintf("WARN: 'Set %s %s' has been ignored. Move the directive to the top of the file.\nLearn more: https://github.com/charmbracelet/vhs#settings", cmd.Options, cmd.Args)))
}
if isSetting || cmd.Type == token.REQUIRE {
_, _ = fmt.Fprintln(out, Highlight(cmd, true))
continue
}
_, _ = fmt.Fprintln(out, Highlight(cmd, !v.recording || cmd.Type == token.SHOW || cmd.Type == token.HIDE || isSetting))
err := Execute(cmd, &v)
if err != nil {
teardown()
return []error{err}
}
}
// If running as an SSH server, the output file is a temporary file
// to use for the output.
//
// We need to set the GIF file path before it is created but after all of
// the settings and commands are executed. This is done in `serve.go`.
//
// Since the GIF creation is deferred, setting the output file here will
// achieve what we want.
for _, opt := range opts {
opt(&v)
}
teardown()
if err := v.Render(); err != nil {
return []error{err}
}
return nil
}