fix: source nested tapes (#567)

Co-authored-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
This commit is contained in:
Ralf Steube
2025-03-26 22:46:05 +01:00
committed by GitHub
parent b41d47fa1d
commit fd38bed9b5
3 changed files with 55 additions and 86 deletions
+3 -56
View File
@@ -3,7 +3,6 @@ package main
import (
"encoding/json"
"fmt"
"io"
"os"
"os/exec"
"regexp"
@@ -12,25 +11,16 @@ import (
"time"
"github.com/atotto/clipboard"
"github.com/charmbracelet/vhs/lexer"
"github.com/charmbracelet/vhs/parser"
"github.com/charmbracelet/vhs/token"
"github.com/go-rod/rod/lib/input"
"github.com/mattn/go-runewidth"
)
// Execute executes a command on a running instance of vhs.
func Execute(c parser.Command, v *VHS) error {
if c.Type == token.SOURCE {
err := ExecuteSourceTape(c, v)
if err != nil {
return fmt.Errorf("failed to execute source tape: %w", err)
}
} else {
err := CommandFuncs[c.Type](c, v)
if err != nil {
return fmt.Errorf("failed to execute command: %w", err)
}
err := CommandFuncs[c.Type](c, v)
if err != nil {
return fmt.Errorf("failed to execute command: %w", err)
}
if v.recording && v.Options.Test.Output != "" {
@@ -710,49 +700,6 @@ func ExecuteSetCursorBlink(c parser.Command, v *VHS) error {
return nil
}
const sourceDisplayMaxLength = 10
// ExecuteSourceTape is a CommandFunc that executes all commands of source tape.
func ExecuteSourceTape(c parser.Command, v *VHS) error {
tapePath := c.Args
var out io.Writer = os.Stdout
if quietFlag {
out = io.Discard
}
// read tape file
tape, err := os.ReadFile(tapePath)
if err != nil {
return fmt.Errorf("failed to read tape %s: %w", tapePath, err)
}
l := lexer.New(string(tape))
p := parser.New(l)
cmds := p.Parse()
if len(p.Errors()) != 0 {
return InvalidSyntaxError{p.Errors()}
}
displayPath := runewidth.Truncate(strings.TrimSuffix(tapePath, extension), sourceDisplayMaxLength, "…")
// Run all commands from the sourced tape file.
for _, cmd := range cmds {
// Output have to be avoid in order to not overwrite output of the original tape.
if cmd.Type == token.SOURCE ||
cmd.Type == token.OUTPUT {
continue
}
_, _ = fmt.Fprintf(out, "%s %s\n", GrayStyle.Render(displayPath+":"), Highlight(cmd, false))
err := CommandFuncs[cmd.Type](cmd, v)
if err != nil {
return fmt.Errorf("failed to execute command %s: %w", cmd.Type.String(), err)
}
}
return nil
}
// ExecuteScreenshot is a CommandFunc that indicates a new screenshot must be taken.
func ExecuteScreenshot(c parser.Command, v *VHS) error {
v.ScreenshotNextFrame(c.Args)
+40 -28
View File
@@ -65,6 +65,7 @@ type Command struct {
Type CommandType
Options string
Args string
Source string
}
// String returns the string representation of the command.
@@ -123,7 +124,7 @@ func (p *Parser) Parse() []Command {
p.nextToken()
continue
}
cmds = append(cmds, p.parseCommand())
cmds = append(cmds, p.parseCommand()...)
p.nextToken()
}
@@ -131,7 +132,7 @@ func (p *Parser) Parse() []Command {
}
// parseCommand parses a command.
func (p *Parser) parseCommand() Command {
func (p *Parser) parseCommand() []Command {
switch p.cur.Type {
case token.SPACE,
token.BACKSPACE,
@@ -146,42 +147,42 @@ func (p *Parser) parseCommand() Command {
token.UP,
token.PAGE_UP,
token.PAGE_DOWN:
return p.parseKeypress(p.cur.Type)
return []Command{p.parseKeypress(p.cur.Type)}
case token.SET:
return p.parseSet()
return []Command{p.parseSet()}
case token.OUTPUT:
return p.parseOutput()
return []Command{p.parseOutput()}
case token.SLEEP:
return p.parseSleep()
return []Command{p.parseSleep()}
case token.TYPE:
return p.parseType()
return []Command{p.parseType()}
case token.CTRL:
return p.parseCtrl()
return []Command{p.parseCtrl()}
case token.ALT:
return p.parseAlt()
return []Command{p.parseAlt()}
case token.SHIFT:
return p.parseShift()
return []Command{p.parseShift()}
case token.HIDE:
return p.parseHide()
return []Command{p.parseHide()}
case token.REQUIRE:
return p.parseRequire()
return []Command{p.parseRequire()}
case token.SHOW:
return p.parseShow()
return []Command{p.parseShow()}
case token.WAIT:
return p.parseWait()
return []Command{p.parseWait()}
case token.SOURCE:
return p.parseSource()
case token.SCREENSHOT:
return p.parseScreenshot()
return []Command{p.parseScreenshot()}
case token.COPY:
return p.parseCopy()
return []Command{p.parseCopy()}
case token.PASTE:
return p.parsePaste()
return []Command{p.parsePaste()}
case token.ENV:
return p.parseEnv()
return []Command{p.parseEnv()}
default:
p.errors = append(p.errors, NewError(p.cur, "Invalid command: "+p.cur.Literal))
return Command{Type: token.ILLEGAL}
return []Command{{Type: token.ILLEGAL}}
}
}
@@ -659,13 +660,13 @@ func (p *Parser) parseEnv() Command {
// Source command takes a tape path to include in current tape.
//
// Source <path>
func (p *Parser) parseSource() Command {
func (p *Parser) parseSource() []Command {
cmd := Command{Type: token.SOURCE}
if p.peek.Type != token.STRING {
p.errors = append(p.errors, NewError(p.cur, "Expected path after Source"))
p.nextToken()
return cmd
return []Command{cmd}
}
srcPath := p.peek.Literal
@@ -675,7 +676,7 @@ func (p *Parser) parseSource() Command {
if ext != ".tape" {
p.errors = append(p.errors, NewError(p.peek, "Expected file with .tape extension"))
p.nextToken()
return cmd
return []Command{cmd}
}
// Check if tape exist
@@ -683,7 +684,7 @@ func (p *Parser) parseSource() Command {
notFoundErr := fmt.Sprintf("File %s not found", srcPath)
p.errors = append(p.errors, NewError(p.peek, notFoundErr))
p.nextToken()
return cmd
return []Command{cmd}
}
// Check if source tape contains nested Source command
@@ -692,7 +693,7 @@ func (p *Parser) parseSource() Command {
readErr := fmt.Sprintf("Unable to read file: %s", srcPath)
p.errors = append(p.errors, NewError(p.peek, readErr))
p.nextToken()
return cmd
return []Command{cmd}
}
srcTape := string(d)
@@ -701,7 +702,7 @@ func (p *Parser) parseSource() Command {
readErr := fmt.Sprintf("Source tape: %s is empty", srcPath)
p.errors = append(p.errors, NewError(p.peek, readErr))
p.nextToken()
return cmd
return []Command{cmd}
}
srcLexer := lexer.New(srcTape)
@@ -713,7 +714,7 @@ func (p *Parser) parseSource() Command {
if cmd.Type == token.SOURCE {
p.errors = append(p.errors, NewError(p.peek, "Nested Source detected"))
p.nextToken()
return cmd
return []Command{cmd}
}
}
@@ -722,12 +723,23 @@ func (p *Parser) parseSource() Command {
if len(srcErrors) > 0 {
p.errors = append(p.errors, NewError(p.peek, fmt.Sprintf("%s has %d errors", srcPath, len(srcErrors))))
p.nextToken()
return cmd
return []Command{cmd}
}
cmd.Args = p.peek.Literal
filtered := make([]Command, 0)
for _, srcCmd := range srcCmds {
// Output have to be avoid in order to not overwrite output of the original tape.
if srcCmd.Type == token.SOURCE ||
srcCmd.Type == token.OUTPUT {
continue
}
srcCmd.Source = cmd.Args
filtered = append(filtered, srcCmd)
}
p.nextToken()
return cmd
return filtered
}
// parseScreenshot parses screenshot command.
+12 -2
View File
@@ -7,8 +7,11 @@ import (
"github.com/charmbracelet/vhs/parser"
"github.com/charmbracelet/vhs/token"
"github.com/mattn/go-runewidth"
)
const sourceDisplayMaxLength = 10
// Highlight syntax highlights a command for prettier printing.
// It takes an argument whether or not to print the command in a faint style to
// represent hidden commands.
@@ -18,11 +21,17 @@ func Highlight(c parser.Command, faint bool) string {
argsStyle = NumberStyle
)
sourcePrefix := ""
if c.Source != "" {
displayPath := runewidth.Truncate(strings.TrimSuffix(c.Source, extension), sourceDisplayMaxLength, "…")
sourcePrefix = GrayStyle.Render(displayPath+":") + " "
}
if faint {
if c.Options != "" {
return FaintStyle.Render(fmt.Sprintf("%s %s %s", c.Type, c.Options, c.Args))
return sourcePrefix + FaintStyle.Render(fmt.Sprintf("%s %s %s", c.Type, c.Options, c.Args))
}
return FaintStyle.Render(fmt.Sprintf("%s %s", c.Type, c.Args))
return sourcePrefix + FaintStyle.Render(fmt.Sprintf("%s %s", c.Type, c.Args))
}
switch c.Type {
@@ -55,6 +64,7 @@ func Highlight(c parser.Command, faint bool) string {
}
var s strings.Builder
s.WriteString(sourcePrefix)
s.WriteString(CommandStyle.Render(c.Type.String()) + " ")
if c.Options != "" {
s.WriteString(optionsStyle.Render(c.Options))