diff --git a/command.go b/command.go index fa391623..d60f7852 100644 --- a/command.go +++ b/command.go @@ -6,6 +6,7 @@ import ( "io" "os" "os/exec" + "regexp" "strconv" "strings" "time" @@ -76,6 +77,7 @@ var CommandFuncs = map[parser.CommandType]CommandFunc{ token.COPY: ExecuteCopy, token.PASTE: ExecutePaste, token.ENV: ExecuteEnv, + token.WAIT: ExecuteWait, } // ExecuteNoop is a no-op command that does nothing. @@ -111,6 +113,71 @@ func ExecuteKey(k input.Key) CommandFunc { } } +// WaitTick is the amount of time to wait between checking for a match. +const WaitTick = 10 * time.Millisecond + +// ExecuteWait is a CommandFunc that waits for a regex match for the given amount of time. +func ExecuteWait(c parser.Command, v *VHS) error { + scope, rxStr, ok := strings.Cut(c.Args, " ") + rx := v.Options.WaitPattern + if ok { + // This is validated on parse so using MustCompile reduces noise. + rx = regexp.MustCompile(rxStr) + } + + timeout := v.Options.WaitTimeout + if c.Options != "" { + t, err := time.ParseDuration(c.Options) + if err != nil { + // Shouldn't be possible due to parse validation. + return fmt.Errorf("failed to parse duration: %w", err) + } + timeout = t + } + + checkT := time.NewTicker(WaitTick) + defer checkT.Stop() + timeoutT := time.NewTimer(timeout) + defer timeoutT.Stop() + + for { + var last string + switch scope { + case "Line": + line, err := v.CurrentLine() + if err != nil { + return fmt.Errorf("failed to get current line: %w", err) + } + last = line + + if rx.MatchString(line) { + return nil + } + case "Screen": + lines, err := v.Buffer() + if err != nil { + return fmt.Errorf("failed to get buffer: %w", err) + } + last = strings.Join(lines, "\n") + + if rx.MatchString(last) { + return nil + } + default: + // Should be impossible due to parse validation, but we don't want to + // hang if it does happen due to a bug. + return fmt.Errorf("invalid scope %q", scope) + } + + select { + case <-checkT.C: + continue + case <-timeoutT.C: + return fmt.Errorf("timeout waiting for %q to match %s; last value was: %s", c.Args, rx.String(), last) + } + } +} + // ExecuteCtrl is a CommandFunc that presses the argument keys and/or modifiers // with the ctrl key held down on the running instance of vhs. func ExecuteCtrl(c parser.Command, v *VHS) error { @@ -371,6 +438,8 @@ var Settings = map[string]CommandFunc{ "WindowBar": ExecuteSetWindowBar, "WindowBarSize": ExecuteSetWindowBarSize, "BorderRadius": ExecuteSetBorderRadius, + "WaitPattern": ExecuteSetWaitPattern, + "WaitTimeout": ExecuteSetWaitTimeout, "CursorBlink": ExecuteSetCursorBlink, } @@ -521,6 +590,26 @@ func ExecuteSetTypingSpeed(c parser.Command, v *VHS) error { return nil } +// ExecuteSetWaitTimeout applies the default wait timeout on the vhs. +func ExecuteSetWaitTimeout(c parser.Command, v *VHS) error { + waitTimeout, err := time.ParseDuration(c.Args) + if err != nil { + return fmt.Errorf("failed to parse wait timeout: %w", err) + } + v.Options.WaitTimeout = waitTimeout + return nil +} + +// ExecuteSetWaitPattern applies the default wait pattern on the vhs. +func ExecuteSetWaitPattern(c parser.Command, v *VHS) error { + rx, err := regexp.Compile(c.Args) + if err != nil { + return fmt.Errorf("failed to compile regexp: %w", err) + } + v.Options.WaitPattern = rx + return nil +} + // ExecuteSetPadding applies the padding on the vhs. func ExecuteSetPadding(c parser.Command, v *VHS) error { padding, err := strconv.Atoi(c.Args) diff --git a/command_test.go b/command_test.go index c8629cde..eb89fd10 100644 --- a/command_test.go +++ b/command_test.go @@ -8,12 +8,12 @@ import ( ) func TestCommand(t *testing.T) { - const numberOfCommands = 28 + const numberOfCommands = 29 if len(parser.CommandTypes) != numberOfCommands { t.Errorf("Expected %d commands, got %d", numberOfCommands, len(parser.CommandTypes)) } - const numberOfCommandFuncs = 28 + const numberOfCommandFuncs = 29 if len(CommandFuncs) != numberOfCommandFuncs { t.Errorf("Expected %d commands, got %d", numberOfCommandFuncs, len(CommandFuncs)) } diff --git a/lexer/lexer.go b/lexer/lexer.go index d8fc434b..1e8301b1 100644 --- a/lexer/lexer.go +++ b/lexer/lexer.go @@ -67,6 +67,10 @@ func (l *Lexer) NextToken() token.Token { tok.Type = token.STRING tok.Literal = l.readString('"') l.readChar() + case '/': + tok.Type = token.REGEX + tok.Literal = l.readString('/') + l.readChar() default: if isDigit(l.ch) || (isDot(l.ch) && isDigit(l.peekChar())) { tok.Literal = l.readNumber() diff --git a/lexer/lexer_test.go b/lexer/lexer_test.go index 1383dfe7..f981d0e9 100644 --- a/lexer/lexer_test.go +++ b/lexer/lexer_test.go @@ -24,7 +24,8 @@ Ctrl+C Enter Sleep .1 Sleep 100ms -Sleep 2` +Sleep 2 +Wait+Screen@1m /foobar/` tests := []struct { expectedType token.Type @@ -71,6 +72,13 @@ Sleep 2` {token.MILLISECONDS, "ms"}, {token.SLEEP, "Sleep"}, {token.NUMBER, "2"}, + {token.WAIT, "Wait"}, + {token.PLUS, "+"}, + {token.STRING, "Screen"}, + {token.AT, "@"}, + {token.NUMBER, "1"}, + {token.MINUTES, "m"}, + {token.REGEX, "foobar"}, } l := New(input) diff --git a/man.go b/man.go index 8885e0b8..546a7c0a 100644 --- a/man.go +++ b/man.go @@ -43,6 +43,7 @@ The following is a list of all possible commands in VHS: * %PageDown% [repeat] * %Hide% * %Show% +* %Wait%[+Screen][@] // * %Escape% * %Alt%+ * %Space% [repeat] @@ -72,6 +73,8 @@ The following is a list of all possible setting commands in VHS: * Set %Padding% * Set %Framerate% * Set %PlaybackSpeed% +* Set %WaitTimeout%