From bb16d08e221c62fba8154f6abd3baba543fc29ab Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Sat, 26 Nov 2022 10:24:24 -0600 Subject: [PATCH 01/18] add Match and MatchAny commands --- command.go | 95 ++++++++++++++++++++++++++++++++++++++++++++++----- man.go | 3 ++ parser.go | 36 +++++++++++++++++++ syntax.go | 2 ++ testing.go | 32 ++++++++++++++--- token.go | 20 +++++++++-- token_test.go | 17 +++++++++ vhs.go | 3 ++ 8 files changed, 192 insertions(+), 16 deletions(-) create mode 100644 token_test.go diff --git a/command.go b/command.go index 5e672a70..4ed8aec8 100644 --- a/command.go +++ b/command.go @@ -1,9 +1,11 @@ package main import ( + "context" "encoding/json" "fmt" "os/exec" + "regexp" "strconv" "strings" "time" @@ -34,21 +36,21 @@ var CommandTypes = []CommandType{ //nolint: deadcode TAB, TYPE, UP, + MATCH, + MATCH_ANY, } // String returns the string representation of the command. -func (c CommandType) String() string { - if len(c) < 1 { - return "" - } - s := string(c) - return string(s[0]) + strings.ToLower(s[1:]) -} +func (c CommandType) String() string { return toCamel(string(c)) } // CommandFunc is a function that executes a command on a running // instance of vhs. type CommandFunc func(c Command, v *VHS) +// ContextCommandFunc is an interuptable function that executes a command on a +// running instance of vhs. +type ContextCommandFunc func(ctx context.Context, c Command, v *VHS) + // CommandFuncs maps command types to their executable functions. var CommandFuncs = map[CommandType]CommandFunc{ BACKSPACE: ExecuteKey(input.Backspace), @@ -69,6 +71,8 @@ var CommandFuncs = map[CommandType]CommandFunc{ TYPE: ExecuteType, CTRL: ExecuteCtrl, ILLEGAL: ExecuteNoop, + MATCH: ExecuteMatch, + MATCH_ANY: ExecuteMatchAny, } // Command represents a command with options and arguments. @@ -123,6 +127,66 @@ func ExecuteKey(k input.Key) CommandFunc { } } +// ExecuteMatch is a CommandFunc that waits for the current +// line to match the given regex. +func ExecuteMatch(c Command, v *VHS) { + var rx *regexp.Regexp + if c.Args == "" { + rx = v.Options.Match + } else { + rx = regexp.MustCompile(c.Args) + } + + if rx == nil { + return + } + + t := time.NewTicker(10 * time.Millisecond) + defer t.Stop() + + for range t.C { + line, err := v.CurrentLine() + if err != nil { + return + } + + if rx.MatchString(line) { + return + } + } +} + +// ExecuteMatchAny is a CommandFunc that waits for the given regex +// to match any line on the screen. +func ExecuteMatchAny(c Command, v *VHS) { + var rx *regexp.Regexp + if c.Args == "" { + rx = v.Options.Match + } else { + rx = regexp.MustCompile(c.Args) + } + + if rx == nil { + return + } + + t := time.NewTicker(10 * time.Millisecond) + defer t.Stop() + + for range t.C { + lines, err := v.Buffer() + if err != nil { + return + } + + for _, line := range lines { + if rx.MatchString(line) { + return + } + } + } +} + // ExecuteCtrl is a CommandFunc that presses the argument key with the ctrl key // held down on the running instance of vhs. func ExecuteCtrl(c Command, v *VHS) { @@ -214,6 +278,7 @@ var Settings = map[string]CommandFunc{ "Width": ExecuteSetWidth, "Shell": ExecuteSetShell, "LoopOffset": ExecuteLoopOffset, + "Match": ExecuteSetMatch, } // ExecuteSet applies the settings on the running vhs specified by the @@ -261,8 +326,10 @@ func ExecuteSetShell(c Command, v *VHS) { } } -const bitSize = 64 -const base = 10 +const ( + bitSize = 64 + base = 10 +) // ExecuteSetLetterSpacing applies letter spacing (also known as tracking) on // the vhs. @@ -302,6 +369,16 @@ func ExecuteSetTypingSpeed(c Command, v *VHS) { v.Options.TypingSpeed = typingSpeed } +// ExecuteSetMatch applies the default prompt on the vhs. +func ExecuteSetMatch(c Command, v *VHS) { + rx, err := regexp.Compile(c.Args) + if err != nil { + return + } + + v.Options.Match = rx +} + // ExecuteSetPadding applies the padding on the vhs. func ExecuteSetPadding(c Command, v *VHS) { v.Options.Video.Padding, _ = strconv.Atoi(c.Args) diff --git a/man.go b/man.go index d4955c71..0d1a5154 100644 --- a/man.go +++ b/man.go @@ -39,6 +39,8 @@ The following is a list of all possible commands in VHS: * %Up% [repeat] * %Hide% * %Show% +* %Match% "[regexp]" +* %MatchAny% "[regexp]" ` manOutput = `The Output command instructs VHS where to save the output of the recording. @@ -61,6 +63,7 @@ The following is a list of all possible setting commands in VHS: * Set %Padding% * Set %Framerate% * Set %PlaybackSpeed% +* Set %Match% ` manBugs = "See GitHub Issues: " diff --git a/parser.go b/parser.go index 2118ef09..0e5aeafa 100644 --- a/parser.go +++ b/parser.go @@ -1,7 +1,9 @@ package main import ( + "fmt" "path/filepath" + "regexp" "strings" ) @@ -62,12 +64,46 @@ func (p *Parser) parseCommand() Command { return p.parseRequire() case SHOW: return p.parseShow() + case MATCH: + return p.parseMatch() + case MATCH_ANY: + return p.parseMatchAny() default: p.errors = append(p.errors, NewError(p.cur, "Invalid command: "+p.cur.Literal)) return Command{Type: ILLEGAL} } } +func (p *Parser) parseMatchAny() Command { + cmd := Command{Type: MATCH_ANY} + if p.peek.Type == STRING { + p.nextToken() + if _, err := regexp.Compile(p.cur.Literal); err != nil { + p.errors = append(p.errors, NewError(p.cur, fmt.Sprintf("Invalid regular expression '%s': %v", p.cur.Literal, err))) + return cmd + } + + cmd.Args = p.cur.Literal + } + + return cmd +} + +func (p *Parser) parseMatch() Command { + cmd := Command{Type: MATCH} + if p.peek.Type == STRING { + p.nextToken() + if _, err := regexp.Compile(p.cur.Literal); err != nil { + p.errors = append(p.errors, NewError(p.cur, fmt.Sprintf("Invalid regular expression '%s': %v", p.cur.Literal, err))) + return cmd + } + + cmd.Args = p.cur.Literal + } + + return cmd +} + // parseSpeed parses a typing speed indication. // // i.e. @