diff --git a/command.go b/command.go index 4e5b0d06..d7d9107f 100644 --- a/command.go +++ b/command.go @@ -39,37 +39,38 @@ type CommandFunc func(c parser.Command, v *VHS) error // 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.PAGE_UP: ExecuteKey(input.PageUp), - token.PAGE_DOWN: ExecuteKey(input.PageDown), - token.SCROLL_UP: ExecuteScroll(-1), - token.SCROLL_DOWN: ExecuteScroll(1), - 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.PAGE_UP: ExecuteKey(input.PageUp), + token.PAGE_DOWN: ExecuteKey(input.PageDown), + token.SCROLL_UP: ExecuteScroll(-1), + token.SCROLL_DOWN: ExecuteScroll(1), + 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.AWAIT_PROMPT: ExecuteAwaitPrompt, + token.WAIT: ExecuteWait, } // ExecuteNoop is a no-op command that does nothing. @@ -208,6 +209,47 @@ func ExecuteWait(c parser.Command, v *VHS) error { } } +// ExecuteAwaitPrompt waits for the shell to emit a new prompt marker. +// It detects prompt markers (OSC 133;A) that are embedded in each shell's prompt +// configuration. Unlike Wait (which matches terminal content), AwaitPrompt +// detects when the shell has finished executing a command and is ready for input. +func ExecuteAwaitPrompt(c parser.Command, v *VHS) error { + timeout := v.Options.WaitTimeout + if c.Options != "" { + t, err := time.ParseDuration(c.Options) + if err != nil { + return fmt.Errorf("failed to parse duration: %w", err) + } + timeout = t + } + + // Record the current prompt count so we can detect the next one. + baseline, err := v.PromptCount() + if err != nil { + return fmt.Errorf("failed to read prompt count: %w", err) + } + + checkT := time.NewTicker(WaitTick) + defer checkT.Stop() + timeoutT := time.NewTimer(timeout) + defer timeoutT.Stop() + + for { + select { + case <-checkT.C: + current, err := v.PromptCount() + if err != nil { + return fmt.Errorf("failed to read prompt count: %w", err) + } + if current > baseline { + return nil + } + case <-timeoutT.C: + return fmt.Errorf("timeout waiting for shell prompt (waited %s)", timeout) + } + } +} + // 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 { diff --git a/command_test.go b/command_test.go index 19ddc98a..0c811afd 100644 --- a/command_test.go +++ b/command_test.go @@ -8,12 +8,12 @@ import ( ) func TestCommand(t *testing.T) { - const numberOfCommands = 31 + const numberOfCommands = 32 if len(parser.CommandTypes) != numberOfCommands { t.Errorf("Expected %d commands, got %d", numberOfCommands, len(parser.CommandTypes)) } - const numberOfCommandFuncs = 31 + const numberOfCommandFuncs = 32 if len(CommandFuncs) != numberOfCommandFuncs { t.Errorf("Expected %d commands, got %d", numberOfCommandFuncs, len(CommandFuncs)) } diff --git a/parser/parser.go b/parser/parser.go index 39a4dcb4..3230c701 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -52,6 +52,7 @@ var CommandTypes = []CommandType{ token.TAB, token.TYPE, token.UP, + token.AWAIT_PROMPT, token.WAIT, token.SOURCE, token.SCREENSHOT, @@ -173,6 +174,8 @@ func (p *Parser) parseCommand() []Command { return []Command{p.parseRequire()} case token.SHOW: return []Command{p.parseShow()} + case token.AWAIT_PROMPT: + return []Command{p.parseAwaitPrompt()} case token.WAIT: return []Command{p.parseWait()} case token.SOURCE: @@ -230,6 +233,12 @@ func (p *Parser) parseWait() Command { return cmd } +func (p *Parser) parseAwaitPrompt() Command { + cmd := Command{Type: token.AWAIT_PROMPT} + cmd.Options = p.parseSpeed() + return cmd +} + // parseSpeed parses a typing speed indication. // // i.e. @