Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add TypeVariable Command for Variable Typing Speed #580

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 119 additions & 50 deletions command.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"encoding/json"
"fmt"
"io"
"math/rand"
"os"
"os/exec"
"regexp"
Expand Down Expand Up @@ -49,35 +50,36 @@

// CommandFuncs maps command types to their executable functions.
var CommandFuncs = map[parser.CommandType]CommandFunc{
token.BACKSPACE: ExecuteKey(input.Backspace),
token.DELETE: ExecuteKey(input.Delete),
token.INSERT: ExecuteKey(input.Insert),
token.DOWN: ExecuteKey(input.ArrowDown),
token.ENTER: ExecuteKey(input.Enter),
token.LEFT: ExecuteKey(input.ArrowLeft),
token.RIGHT: ExecuteKey(input.ArrowRight),
token.SPACE: ExecuteKey(input.Space),
token.UP: ExecuteKey(input.ArrowUp),
token.TAB: ExecuteKey(input.Tab),
token.ESCAPE: ExecuteKey(input.Escape),
token.PAGEUP: ExecuteKey(input.PageUp),
token.PAGEDOWN: ExecuteKey(input.PageDown),
token.HIDE: ExecuteHide,
token.REQUIRE: ExecuteRequire,
token.SHOW: ExecuteShow,
token.SET: ExecuteSet,
token.OUTPUT: ExecuteOutput,
token.SLEEP: ExecuteSleep,
token.TYPE: ExecuteType,
token.CTRL: ExecuteCtrl,
token.ALT: ExecuteAlt,
token.SHIFT: ExecuteShift,
token.ILLEGAL: ExecuteNoop,
token.SCREENSHOT: ExecuteScreenshot,
token.COPY: ExecuteCopy,
token.PASTE: ExecutePaste,
token.ENV: ExecuteEnv,
token.WAIT: ExecuteWait,
token.BACKSPACE: ExecuteKey(input.Backspace),
token.DELETE: ExecuteKey(input.Delete),
token.INSERT: ExecuteKey(input.Insert),
token.DOWN: ExecuteKey(input.ArrowDown),
token.ENTER: ExecuteKey(input.Enter),
token.LEFT: ExecuteKey(input.ArrowLeft),
token.RIGHT: ExecuteKey(input.ArrowRight),
token.SPACE: ExecuteKey(input.Space),
token.UP: ExecuteKey(input.ArrowUp),
token.TAB: ExecuteKey(input.Tab),
token.ESCAPE: ExecuteKey(input.Escape),
token.PAGEUP: ExecuteKey(input.PageUp),
token.PAGEDOWN: ExecuteKey(input.PageDown),
token.HIDE: ExecuteHide,
token.REQUIRE: ExecuteRequire,
token.SHOW: ExecuteShow,
token.SET: ExecuteSet,
token.OUTPUT: ExecuteOutput,
token.SLEEP: ExecuteSleep,
token.TYPE: ExecuteType,
token.TYPE_VARIABLE: ExecuteTypeVariable,
token.CTRL: ExecuteCtrl,
token.ALT: ExecuteAlt,
token.SHIFT: ExecuteShift,
token.ILLEGAL: ExecuteNoop,
token.SCREENSHOT: ExecuteScreenshot,
token.COPY: ExecuteCopy,
token.PASTE: ExecutePaste,
token.ENV: ExecuteEnv,
token.WAIT: ExecuteWait,
}

// ExecuteNoop is a no-op command that does nothing.
Expand Down Expand Up @@ -365,6 +367,47 @@
return nil
}

// ExecuteTypeVariable types the argument string on the running instance of vhs, in a variable typing speed

Check failure on line 370 in command.go

View workflow job for this annotation

GitHub Actions / lint-soft

Comment should end in a period (godot)
func ExecuteTypeVariable(c parser.Command, v *VHS) error {
typingSpeedVariable := v.Options.TypingSpeedVariable
var minTypingSpeed, maxTypingSpeed time.Duration
if c.Options != "" {
var err error
minTypingSpeed, err = time.ParseDuration(c.Options)
if err != nil {
return fmt.Errorf("failed to parse min typing speed: %w", err)
}
maxTypingSpeed, err = time.ParseDuration(c.Options)
if err != nil {
return fmt.Errorf("failed to parse max typing speed: %w", err)
}
} else {
minTypingSpeed = typingSpeedVariable.MinTypingSpeed
maxTypingSpeed = typingSpeedVariable.MaxTypingSpeed
}

for _, r := range c.Args {
k, ok := keymap[r]
if ok {
err := v.Page.Keyboard.Type(k)
if err != nil {
return fmt.Errorf("failed to type key %c: %w", r, err)
}
} else {
err := v.Page.MustElement("textarea").Input(string(r))
if err != nil {
return fmt.Errorf("failed to input text: %w", err)
}

v.Page.MustWaitIdle()
}
sleepDuration := minTypingSpeed + time.Duration(rand.Int63n(int64(maxTypingSpeed-minTypingSpeed)))

Check failure on line 404 in command.go

View workflow job for this annotation

GitHub Actions / lint

G404: Use of weak random number generator (math/rand or math/rand/v2 instead of crypto/rand) (gosec)
time.Sleep(sleepDuration)
}

return nil
}

// ExecuteOutput applies the output on the vhs videos.
func ExecuteOutput(c parser.Command, v *VHS) error {
switch c.Options {
Expand Down Expand Up @@ -420,27 +463,28 @@

// Settings maps the Set commands to their respective functions.
var Settings = map[string]CommandFunc{
"FontFamily": ExecuteSetFontFamily,
"FontSize": ExecuteSetFontSize,
"Framerate": ExecuteSetFramerate,
"Height": ExecuteSetHeight,
"LetterSpacing": ExecuteSetLetterSpacing,
"LineHeight": ExecuteSetLineHeight,
"PlaybackSpeed": ExecuteSetPlaybackSpeed,
"Padding": ExecuteSetPadding,
"Theme": ExecuteSetTheme,
"TypingSpeed": ExecuteSetTypingSpeed,
"Width": ExecuteSetWidth,
"Shell": ExecuteSetShell,
"LoopOffset": ExecuteLoopOffset,
"MarginFill": ExecuteSetMarginFill,
"Margin": ExecuteSetMargin,
"WindowBar": ExecuteSetWindowBar,
"WindowBarSize": ExecuteSetWindowBarSize,
"BorderRadius": ExecuteSetBorderRadius,
"WaitPattern": ExecuteSetWaitPattern,
"WaitTimeout": ExecuteSetWaitTimeout,
"CursorBlink": ExecuteSetCursorBlink,
"FontFamily": ExecuteSetFontFamily,
"FontSize": ExecuteSetFontSize,
"Framerate": ExecuteSetFramerate,
"Height": ExecuteSetHeight,
"LetterSpacing": ExecuteSetLetterSpacing,
"LineHeight": ExecuteSetLineHeight,
"PlaybackSpeed": ExecuteSetPlaybackSpeed,
"Padding": ExecuteSetPadding,
"Theme": ExecuteSetTheme,
"TypingSpeed": ExecuteSetTypingSpeed,
"TypingSpeedVariable": ExecuteSetTypingSpeedVariable,
"Width": ExecuteSetWidth,
"Shell": ExecuteSetShell,
"LoopOffset": ExecuteLoopOffset,
"MarginFill": ExecuteSetMarginFill,
"Margin": ExecuteSetMargin,
"WindowBar": ExecuteSetWindowBar,
"WindowBarSize": ExecuteSetWindowBarSize,
"BorderRadius": ExecuteSetBorderRadius,
"WaitPattern": ExecuteSetWaitPattern,
"WaitTimeout": ExecuteSetWaitTimeout,
"CursorBlink": ExecuteSetCursorBlink,
}

// ExecuteSet applies the settings on the running vhs specified by the
Expand Down Expand Up @@ -590,6 +634,31 @@
return nil
}

// ExecuteSetTypingSpeedVariable applies the default typing speed variable on the vhs.
func ExecuteSetTypingSpeedVariable(c parser.Command, v *VHS) error {
typingSpeedVariable := strings.Split(c.Args, " ")
if len(typingSpeedVariable) != 2 {

Check failure on line 640 in command.go

View workflow job for this annotation

GitHub Actions / lint-soft

Magic number: 2, in <condition> detected (mnd)
return fmt.Errorf("failed to parse typing speed variable args, expected 2 values")
}
minTypingSpeed, err := time.ParseDuration(typingSpeedVariable[0])
if err != nil {
return fmt.Errorf("failed to parse min typing speed: %w", err)
}
maxTypingSpeed, err := time.ParseDuration(typingSpeedVariable[1])
if err != nil {
return fmt.Errorf("failed to parse max typing speed: %w", err)
}

if minTypingSpeed > maxTypingSpeed {
return fmt.Errorf("invalid typing speed variable range: min (%s) should be less than or equal to max (%s)", minTypingSpeed, maxTypingSpeed)
}

v.Options.TypingSpeedVariable.MinTypingSpeed = minTypingSpeed
v.Options.TypingSpeedVariable.MaxTypingSpeed = maxTypingSpeed

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)
Expand Down
4 changes: 2 additions & 2 deletions command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ import (
)

func TestCommand(t *testing.T) {
const numberOfCommands = 29
const numberOfCommands = 30
if len(parser.CommandTypes) != numberOfCommands {
t.Errorf("Expected %d commands, got %d", numberOfCommands, len(parser.CommandTypes))
}

const numberOfCommandFuncs = 29
const numberOfCommandFuncs = 30
if len(CommandFuncs) != numberOfCommandFuncs {
t.Errorf("Expected %d commands, got %d", numberOfCommandFuncs, len(CommandFuncs))
}
Expand Down
1 change: 1 addition & 0 deletions examples/fixtures/all.tape
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Set TypingSpeed .1
Set LoopOffset 60.4
Set LoopOffset 20.99%
Set CursorBlink false
Set TypingSpeedVariable 0.1 1s

# Sleep:
Sleep 1
Expand Down
8 changes: 8 additions & 0 deletions lexer/lexer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Set Padding 5
Set CursorBlink false
Type "echo 'Hello, world!'"
Enter
TypeVariable "echo 'Hello, world!'"
[email protected] "echo 'Hello, world!'"
Left 3
Sleep 1
Expand Down Expand Up @@ -45,6 +46,8 @@ Wait+Screen@1m /foobar/`
{token.TYPE, "Type"},
{token.STRING, "echo 'Hello, world!'"},
{token.ENTER, "Enter"},
{token.TYPE_VARIABLE, "TypeVariable"},
{token.STRING, "echo 'Hello, world!'"},
{token.TYPE, "Type"},
{token.AT, "@"},
{token.NUMBER, ".1"},
Expand Down Expand Up @@ -164,6 +167,11 @@ func TestLexTapeFile(t *testing.T) {
{token.SET, "Set"},
{token.CURSOR_BLINK, "CursorBlink"},
{token.BOOLEAN, "false"},
{token.SET, "Set"},
{token.TYPING_SPEED_VARIABLE, "TypingSpeedVariable"},
{token.NUMBER, "0.1"},
{token.NUMBER, "1"},
{token.SECONDS, "s"},
{token.COMMENT, " Sleep:"},
{token.SLEEP, "Sleep"},
{token.NUMBER, "1"},
Expand Down
59 changes: 59 additions & 0 deletions parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
token.SHOW,
token.TAB,
token.TYPE,
token.TYPE_VARIABLE,
token.UP,
token.WAIT,
token.SOURCE,
Expand Down Expand Up @@ -155,6 +156,8 @@
return p.parseSleep()
case token.TYPE:
return p.parseType()
case token.TYPE_VARIABLE:
return p.parseTypeVariable()
case token.CTRL:
return p.parseCtrl()
case token.ALT:
Expand Down Expand Up @@ -462,6 +465,29 @@
} else if cmd.Options == "TypingSpeed" {
cmd.Args += "s"
}
case token.TYPING_SPEED_VARIABLE:
firstArg := p.peek.Literal
p.nextToken()
if p.peek.Type == token.MILLISECONDS || p.peek.Type == token.SECONDS {
firstArg += p.peek.Literal
p.nextToken()
} else {
firstArg += "s"
}

var secondArg string
if p.peek.Type == token.NUMBER {
secondArg = p.peek.Literal
p.nextToken()
if p.peek.Type == token.MILLISECONDS || p.peek.Type == token.SECONDS {
secondArg += p.peek.Literal
p.nextToken()
} else {
secondArg += "s"
}
}

cmd.Args = firstArg + " " + secondArg
case token.WINDOW_BAR:
cmd.Args = p.peek.Literal
p.nextToken()
Expand Down Expand Up @@ -590,6 +616,39 @@
return cmd
}

// parseTypeVariable parses a type variable command.

Check failure on line 619 in parser/parser.go

View workflow job for this annotation

GitHub Actions / lint-soft

Comment should end in a period (godot)
// A type variable command takes a string to type.
//
// TypeVariable "string"
func (p *Parser) parseTypeVariable() Command {
cmd := Command{Type: token.TYPE_VARIABLE}

cmd.Options = p.parseSpeed()

if p.peek.Type != token.STRING {
p.errors = append(p.errors, NewError(p.peek, p.cur.Literal+" expects string"))
}

for p.peek.Type == token.STRING {
p.nextToken()
cmd.Args += p.cur.Literal

// If the next token is a string, add a space between them.
// Since tokens must be separated by a whitespace, this is most likely
// what the user intended.
//
// Although it is possible that there may be multiple spaces / tabs between
// the tokens, however if the user was intending to type multiple spaces
// they would need to use a string literal.

if p.peek.Type == token.STRING {
cmd.Args += " "
}
}

return cmd
}

// parseCopy parses a copy command
// A copy command takes a string to the clipboard
//
Expand Down
7 changes: 7 additions & 0 deletions parser/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import (
func TestParser(t *testing.T) {
input := `
Set TypingSpeed 100ms
Set TypingSpeedVariable 1s 5s
Set TypingSpeedVariable 0.1 1s
Set TypingSpeedVariable 1s 2
Set WaitTimeout 1m
Set WaitPattern /foo/
Type "echo 'Hello, World!'"
Expand All @@ -37,6 +40,9 @@ Wait@100ms /foobar/`

expected := []Command{
{Type: token.SET, Options: "TypingSpeed", Args: "100ms"},
{Type: token.SET, Options: "TypingSpeedVariable", Args: "1s 5s"},
{Type: token.SET, Options: "TypingSpeedVariable", Args: "0.1s 1s"},
{Type: token.SET, Options: "TypingSpeedVariable", Args: "1s 2s"},
{Type: token.SET, Options: "WaitTimeout", Args: "1m"},
{Type: token.SET, Options: "WaitPattern", Args: "foo"},
{Type: token.TYPE, Options: "", Args: "echo 'Hello, World!'"},
Expand Down Expand Up @@ -139,6 +145,7 @@ func TestParseTapeFile(t *testing.T) {
{Type: token.SET, Options: "LoopOffset", Args: "60.4%"},
{Type: token.SET, Options: "LoopOffset", Args: "20.99%"},
{Type: token.SET, Options: "CursorBlink", Args: "false"},
{Type: token.SET, Options: "TypingSpeedVariable", Args: "0.1s 1s"},
{Type: token.SLEEP, Options: "", Args: "1s"},
{Type: token.SLEEP, Options: "", Args: "500ms"},
{Type: token.SLEEP, Options: "", Args: ".5s"},
Expand Down
Loading
Loading