-
Notifications
You must be signed in to change notification settings - Fork 271
/
Copy pathcommand.go
357 lines (318 loc) · 9.51 KB
/
command.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
package main
import (
"encoding/json"
"fmt"
"os/exec"
"strconv"
"strings"
"time"
"github.com/go-rod/rod/lib/input"
)
// CommandType is a type that represents a command.
type CommandType TokenType
// CommandTypes is a list of the available commands that can be executed.
var CommandTypes = []CommandType{ //nolint: deadcode
BACKSPACE,
CTRL,
DOWN,
ENTER,
ESCAPE,
ILLEGAL,
LEFT,
RIGHT,
SET,
OUTPUT,
SLEEP,
SPACE,
HIDE,
REQUIRE,
SHOW,
TAB,
TYPE,
UP,
}
// 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:])
}
// CommandFunc is a function that executes a command on a running
// instance of vhs.
type CommandFunc func(c Command, v *VHS)
// CommandFuncs maps command types to their executable functions.
var CommandFuncs = map[CommandType]CommandFunc{
BACKSPACE: ExecuteKey(input.Backspace),
DOWN: ExecuteKey(input.ArrowDown),
ENTER: ExecuteKey(input.Enter),
LEFT: ExecuteKey(input.ArrowLeft),
RIGHT: ExecuteKey(input.ArrowRight),
SPACE: ExecuteKey(input.Space),
UP: ExecuteKey(input.ArrowUp),
TAB: ExecuteKey(input.Tab),
ESCAPE: ExecuteKey(input.Escape),
HIDE: ExecuteHide,
REQUIRE: ExecuteRequire,
SHOW: ExecuteShow,
SET: ExecuteSet,
OUTPUT: ExecuteOutput,
SLEEP: ExecuteSleep,
TYPE: ExecuteType,
CTRL: ExecuteCtrl,
ILLEGAL: ExecuteNoop,
}
// Command represents a command with options and arguments.
type Command struct {
Type CommandType
Options string
Args string
}
// String returns the string representation of the command.
// This includes the options and arguments of the command.
func (c Command) String() string {
if c.Options != "" {
return fmt.Sprintf("%s %s %s", c.Type, c.Options, c.Args)
}
return fmt.Sprintf("%s %s", c.Type, c.Options)
}
// Execute executes a command on a running instance of vhs.
func (c Command) Execute(v *VHS) {
CommandFuncs[c.Type](c, v)
if v.recording && v.Options.Test.Output != "" {
v.SaveOutput()
}
}
// ExecuteNoop is a no-op command that does nothing.
// Generally, this is used for Unknown commands when dealing with
// commands that are not recognized.
func ExecuteNoop(c Command, v *VHS) {}
// ExecuteKey is a higher-order function that returns a CommandFunc to execute
// a key press for a given key. This is so that the logic for key pressing
// (since they are repeatable and delayable) can be re-used.
//
// i.e. ExecuteKey(input.ArrowDown) would return a CommandFunc that executes
// the ArrowDown key press.
func ExecuteKey(k input.Key) CommandFunc {
return func(c Command, v *VHS) {
typingSpeed, err := time.ParseDuration(c.Options)
if err != nil {
typingSpeed = v.Options.TypingSpeed
}
repeat, err := strconv.Atoi(c.Args)
if err != nil {
repeat = 1
}
for i := 0; i < repeat; i++ {
_ = v.Page.Keyboard.Type(k)
time.Sleep(typingSpeed)
}
}
}
// 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) {
_ = v.Page.Keyboard.Press(input.ControlLeft)
for _, r := range c.Args {
if k, ok := keymap[r]; ok {
_ = v.Page.Keyboard.Type(k)
}
}
_ = v.Page.Keyboard.Release(input.ControlLeft)
}
// ExecuteHide is a CommandFunc that starts or stops the recording of the vhs.
func ExecuteHide(c Command, v *VHS) {
v.PauseRecording()
}
// ExecuteRequire is a CommandFunc that checks if all the binaries mentioned in the
// Require command are present. If not, it exits with a non-zero error.
func ExecuteRequire(c Command, v *VHS) {
_, err := exec.LookPath(c.Args)
if err != nil {
v.Errors = append(v.Errors, err)
}
}
// ExecuteShow is a CommandFunc that resumes the recording of the vhs.
func ExecuteShow(c Command, v *VHS) {
v.ResumeRecording()
}
// ExecuteSleep sleeps for the desired time specified through the argument of
// the Sleep command.
func ExecuteSleep(c Command, v *VHS) {
dur, err := time.ParseDuration(c.Args)
if err != nil {
return
}
time.Sleep(dur)
}
// ExecuteType types the argument string on the running instance of vhs.
func ExecuteType(c Command, v *VHS) {
typingSpeed, err := time.ParseDuration(c.Options)
if err != nil {
typingSpeed = v.Options.TypingSpeed
}
for _, r := range c.Args {
k, ok := keymap[r]
if ok {
_ = v.Page.Keyboard.Type(k)
} else {
_ = v.Page.MustElement("textarea").Input(string(r))
v.Page.MustWaitIdle()
}
time.Sleep(typingSpeed)
}
}
// ExecuteOutput applies the output on the vhs videos.
func ExecuteOutput(c Command, v *VHS) {
switch c.Options {
case ".mp4":
v.Options.Video.Output.MP4 = c.Args
case ".test", ".ascii", ".txt":
v.Options.Test.Output = c.Args
case ".png":
v.Options.Video.Input = c.Args
v.Options.Video.CleanupFrames = false
case ".webm":
v.Options.Video.Output.WebM = c.Args
default:
v.Options.Video.Output.GIF = c.Args
}
}
// 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,
"LoopOffset": ExecuteLoopOffset,
}
// ExecuteSet applies the settings on the running vhs specified by the
// option and argument pass to the command.
func ExecuteSet(c Command, v *VHS) {
Settings[c.Options](c, v)
}
// ExecuteSetFontSize applies the font size on the vhs.
func ExecuteSetFontSize(c Command, v *VHS) {
fontSize, _ := strconv.Atoi(c.Args)
v.Options.FontSize = fontSize
_, _ = v.Page.Eval(fmt.Sprintf("() => term.options.fontSize = %d", fontSize))
// When changing the font size only the canvas dimensions change which are
// scaled back during the render to fit the aspect ration and dimensions.
//
// We need to call term.fit to ensure that everything is resized properly.
_, _ = v.Page.Eval("term.fit")
}
// ExecuteSetFontFamily applies the font family on the vhs.
func ExecuteSetFontFamily(c Command, v *VHS) {
v.Options.FontFamily = c.Args
_, _ = v.Page.Eval(fmt.Sprintf("() => term.options.fontFamily = '%s'", c.Args))
}
// ExecuteSetHeight applies the height on the vhs.
func ExecuteSetHeight(c Command, v *VHS) {
v.Options.Video.Height, _ = strconv.Atoi(c.Args)
}
// ExecuteSetWidth applies the width on the vhs.
func ExecuteSetWidth(c Command, v *VHS) {
v.Options.Video.Width, _ = strconv.Atoi(c.Args)
}
const (
bitSize = 64
base = 10
)
// ExecuteSetLetterSpacing applies letter spacing (also known as tracking) on the
// vhs.
func ExecuteSetLetterSpacing(c Command, v *VHS) {
letterSpacing, _ := strconv.ParseFloat(c.Args, bitSize)
v.Options.LetterSpacing = letterSpacing
_, _ = v.Page.Eval(fmt.Sprintf("() => term.options.letterSpacing = %f", letterSpacing))
}
// ExecuteSetLineHeight applies the line height on the vhs.
func ExecuteSetLineHeight(c Command, v *VHS) {
lineHeight, _ := strconv.ParseFloat(c.Args, bitSize)
v.Options.LineHeight = lineHeight
_, _ = v.Page.Eval(fmt.Sprintf("() => term.options.lineHeight = %f", lineHeight))
}
// ExecuteSetTheme applies the theme on the vhs.
func ExecuteSetTheme(c Command, v *VHS) {
var err error
v.Options.Theme, err = getTheme(c.Args)
if err != nil {
v.Errors = append(v.Errors, err)
return
}
bts, _ := json.Marshal(v.Options.Theme)
_, _ = v.Page.Eval(fmt.Sprintf("() => term.options.theme = %s", string(bts)))
v.Options.Video.BackgroundColor = v.Options.Theme.Background
}
// ExecuteSetTypingSpeed applies the default typing speed on the vhs.
func ExecuteSetTypingSpeed(c Command, v *VHS) {
typingSpeed, err := time.ParseDuration(c.Args)
if err != nil {
return
}
v.Options.TypingSpeed = typingSpeed
}
// ExecuteSetPadding applies the padding on the vhs.
func ExecuteSetPadding(c Command, v *VHS) {
v.Options.Video.Padding, _ = strconv.Atoi(c.Args)
}
// ExecuteSetFramerate applies the framerate on the vhs.
func ExecuteSetFramerate(c Command, v *VHS) {
framerate, err := strconv.ParseInt(c.Args, base, 0)
if err != nil {
return
}
v.Options.Video.Framerate = int(framerate)
}
// ExecuteSetPlaybackSpeed applies the playback speed option on the vhs.
func ExecuteSetPlaybackSpeed(c Command, v *VHS) {
playbackSpeed, err := strconv.ParseFloat(c.Args, bitSize)
if err != nil {
return
}
v.Options.Video.PlaybackSpeed = playbackSpeed
}
// ExecuteLoopOffset applies the loop offset option on the vhs.
func ExecuteLoopOffset(c Command, v *VHS) {
loopOffset, err := strconv.ParseFloat(strings.TrimRight(c.Args, "%"), bitSize)
if err != nil {
return
}
v.Options.LoopOffset = loopOffset
}
func getTheme(s string) (Theme, error) {
if strings.TrimSpace(s) == "" {
return DefaultTheme, nil
}
switch s[0] {
case '{':
return getJSONTheme(s)
default:
return getNamedTheme(s)
}
}
func getNamedTheme(s string) (Theme, error) {
theme, suggestions, ok := findTheme(s)
if !ok && len(suggestions) > 0 {
return DefaultTheme, fmt.Errorf("invalid `Set Theme %q`: did you mean %q", s, strings.Join(suggestions, ", "))
}
if !ok {
return DefaultTheme, fmt.Errorf("invalid `Set Theme %q`: theme does not exist", s)
}
return theme, nil
}
func getJSONTheme(s string) (Theme, error) {
var t Theme
if err := json.Unmarshal([]byte(s), &t); err != nil {
return DefaultTheme, fmt.Errorf("invalid `Set Theme %q: %w`", s, err)
}
return t, nil
}