fix: source nested tapes (#567)
Co-authored-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
This commit is contained in:
+3
-56
@@ -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
@@ -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.
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user