Skip to content

Commit e481565

Browse files
feat: add --smooth and --duration-ms flags to move-mouse and drag-mouse
Support human-like Bezier curve movement from the CLI for both move-mouse and drag-mouse commands. Smooth is enabled by default; pass --smooth=false for instant teleport (move) or linear interpolation (drag). Use --duration-ms to control movement timing. Made-with: Cursor
1 parent 2d940f3 commit e481565

2 files changed

Lines changed: 151 additions & 2 deletions

File tree

cmd/browsers.go

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -674,6 +674,8 @@ type BrowsersComputerMoveMouseInput struct {
674674
X int64
675675
Y int64
676676
HoldKeys []string
677+
Smooth *bool
678+
DurationMs *int64
677679
}
678680

679681
type BrowsersComputerScreenshotInput struct {
@@ -718,6 +720,8 @@ type BrowsersComputerDragMouseInput struct {
718720
StepsPerSegment int64
719721
Button string
720722
HoldKeys []string
723+
Smooth *bool
724+
DurationMs *int64
721725
}
722726

723727
type BrowsersComputerSetCursorInput struct {
@@ -767,6 +771,16 @@ func (b BrowsersCmd) ComputerMoveMouse(ctx context.Context, in BrowsersComputerM
767771
if len(in.HoldKeys) > 0 {
768772
body.HoldKeys = in.HoldKeys
769773
}
774+
extras := map[string]any{}
775+
if in.Smooth != nil {
776+
extras["smooth"] = *in.Smooth
777+
}
778+
if in.DurationMs != nil {
779+
extras["duration_ms"] = *in.DurationMs
780+
}
781+
if len(extras) > 0 {
782+
body.SetExtraFields(extras)
783+
}
770784
if err := b.computer.MoveMouse(ctx, br.SessionID, body); err != nil {
771785
return util.CleanedUpSdkError{Err: err}
772786
}
@@ -912,6 +926,16 @@ func (b BrowsersCmd) ComputerDragMouse(ctx context.Context, in BrowsersComputerD
912926
if len(in.HoldKeys) > 0 {
913927
body.HoldKeys = in.HoldKeys
914928
}
929+
extras := map[string]any{}
930+
if in.Smooth != nil {
931+
extras["smooth"] = *in.Smooth
932+
}
933+
if in.DurationMs != nil {
934+
extras["duration_ms"] = *in.DurationMs
935+
}
936+
if len(extras) > 0 {
937+
body.SetExtraFields(extras)
938+
}
915939
if err := b.computer.DragMouse(ctx, br.SessionID, body); err != nil {
916940
return util.CleanedUpSdkError{Err: err}
917941
}
@@ -2239,6 +2263,8 @@ func init() {
22392263
_ = computerMove.MarkFlagRequired("x")
22402264
_ = computerMove.MarkFlagRequired("y")
22412265
computerMove.Flags().StringSlice("hold-key", []string{}, "Modifier keys to hold (repeatable)")
2266+
computerMove.Flags().Bool("smooth", true, "Use human-like Bezier curve path instead of instant teleport")
2267+
computerMove.Flags().Int64("duration-ms", 0, "Target duration in ms for smooth movement (50-5000, 0 for auto)")
22422268

22432269
computerScreenshot := &cobra.Command{Use: "screenshot <id>", Short: "Capture a screenshot (optionally of a region)", Args: cobra.ExactArgs(1), RunE: runBrowsersComputerScreenshot}
22442270
computerScreenshot.Flags().Int64("x", 0, "Top-left X")
@@ -2278,6 +2304,8 @@ func init() {
22782304
computerDrag.Flags().Int64("steps-per-segment", 0, "Number of move steps per path segment")
22792305
computerDrag.Flags().String("button", "left", "Mouse button: left,middle,right")
22802306
computerDrag.Flags().StringSlice("hold-key", []string{}, "Modifier keys to hold (repeatable)")
2307+
computerDrag.Flags().Bool("smooth", true, "Use human-like Bezier curves between waypoints")
2308+
computerDrag.Flags().Int64("duration-ms", 0, "Target duration in ms for smooth drag (50-10000, 0 for auto)")
22812309

22822310
// computer set-cursor
22832311
computerSetCursor := &cobra.Command{Use: "set-cursor <id>", Short: "Hide or show the cursor", Args: cobra.ExactArgs(1), RunE: runBrowsersComputerSetCursor}
@@ -2864,8 +2892,17 @@ func runBrowsersComputerMoveMouse(cmd *cobra.Command, args []string) error {
28642892
x, _ := cmd.Flags().GetInt64("x")
28652893
y, _ := cmd.Flags().GetInt64("y")
28662894
holdKeys, _ := cmd.Flags().GetStringSlice("hold-key")
2895+
in := BrowsersComputerMoveMouseInput{Identifier: args[0], X: x, Y: y, HoldKeys: holdKeys}
2896+
if cmd.Flags().Changed("smooth") {
2897+
v, _ := cmd.Flags().GetBool("smooth")
2898+
in.Smooth = &v
2899+
}
2900+
if cmd.Flags().Changed("duration-ms") {
2901+
v, _ := cmd.Flags().GetInt64("duration-ms")
2902+
in.DurationMs = &v
2903+
}
28672904
b := BrowsersCmd{browsers: &svc, computer: &svc.Computer}
2868-
return b.ComputerMoveMouse(cmd.Context(), BrowsersComputerMoveMouseInput{Identifier: args[0], X: x, Y: y, HoldKeys: holdKeys})
2905+
return b.ComputerMoveMouse(cmd.Context(), in)
28692906
}
28702907

28712908
func runBrowsersComputerScreenshot(cmd *cobra.Command, args []string) error {
@@ -2959,8 +2996,17 @@ func runBrowsersComputerDragMouse(cmd *cobra.Command, args []string) error {
29592996
path = append(path, []int64{x, y})
29602997
}
29612998

2999+
in := BrowsersComputerDragMouseInput{Identifier: args[0], Path: path, Delay: delay, StepDelayMs: stepDelayMs, StepsPerSegment: stepsPerSegment, Button: button, HoldKeys: holdKeys}
3000+
if cmd.Flags().Changed("smooth") {
3001+
v, _ := cmd.Flags().GetBool("smooth")
3002+
in.Smooth = &v
3003+
}
3004+
if cmd.Flags().Changed("duration-ms") {
3005+
v, _ := cmd.Flags().GetInt64("duration-ms")
3006+
in.DurationMs = &v
3007+
}
29623008
b := BrowsersCmd{browsers: &svc, computer: &svc.Computer}
2963-
return b.ComputerDragMouse(cmd.Context(), BrowsersComputerDragMouseInput{Identifier: args[0], Path: path, Delay: delay, StepDelayMs: stepDelayMs, StepsPerSegment: stepsPerSegment, Button: button, HoldKeys: holdKeys})
3009+
return b.ComputerDragMouse(cmd.Context(), in)
29643010
}
29653011

29663012
func runBrowsersComputerSetCursor(cmd *cobra.Command, args []string) error {

cmd/browsers_test.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1053,6 +1053,109 @@ func TestBrowsersComputerMoveMouse_PrintsSuccess(t *testing.T) {
10531053
assert.Contains(t, out, "Moved mouse to (5,6)")
10541054
}
10551055

1056+
func TestBrowsersComputerMoveMouse_SmoothFalse(t *testing.T) {
1057+
setupStdoutCapture(t)
1058+
fakeBrowsers := newFakeBrowsersServiceWithSimpleGet()
1059+
var capturedBody kernel.BrowserComputerMoveMouseParams
1060+
fakeComp := &FakeComputerService{
1061+
MoveMouseFunc: func(ctx context.Context, id string, body kernel.BrowserComputerMoveMouseParams, opts ...option.RequestOption) error {
1062+
capturedBody = body
1063+
return nil
1064+
},
1065+
}
1066+
b := BrowsersCmd{browsers: fakeBrowsers, computer: fakeComp}
1067+
smooth := false
1068+
_ = b.ComputerMoveMouse(context.Background(), BrowsersComputerMoveMouseInput{Identifier: "id", X: 100, Y: 200, Smooth: &smooth})
1069+
extras := capturedBody.ExtraFields()
1070+
assert.Contains(t, extras, "smooth")
1071+
assert.Equal(t, false, extras["smooth"])
1072+
}
1073+
1074+
func TestBrowsersComputerMoveMouse_DurationMs(t *testing.T) {
1075+
setupStdoutCapture(t)
1076+
fakeBrowsers := newFakeBrowsersServiceWithSimpleGet()
1077+
var capturedBody kernel.BrowserComputerMoveMouseParams
1078+
fakeComp := &FakeComputerService{
1079+
MoveMouseFunc: func(ctx context.Context, id string, body kernel.BrowserComputerMoveMouseParams, opts ...option.RequestOption) error {
1080+
capturedBody = body
1081+
return nil
1082+
},
1083+
}
1084+
b := BrowsersCmd{browsers: fakeBrowsers, computer: fakeComp}
1085+
smooth := true
1086+
dur := int64(1500)
1087+
_ = b.ComputerMoveMouse(context.Background(), BrowsersComputerMoveMouseInput{Identifier: "id", X: 100, Y: 200, Smooth: &smooth, DurationMs: &dur})
1088+
extras := capturedBody.ExtraFields()
1089+
assert.Contains(t, extras, "smooth")
1090+
assert.Equal(t, true, extras["smooth"])
1091+
assert.Contains(t, extras, "duration_ms")
1092+
assert.Equal(t, int64(1500), extras["duration_ms"])
1093+
}
1094+
1095+
func TestBrowsersComputerMoveMouse_NoSmoothFlag(t *testing.T) {
1096+
setupStdoutCapture(t)
1097+
fakeBrowsers := newFakeBrowsersServiceWithSimpleGet()
1098+
var capturedBody kernel.BrowserComputerMoveMouseParams
1099+
fakeComp := &FakeComputerService{
1100+
MoveMouseFunc: func(ctx context.Context, id string, body kernel.BrowserComputerMoveMouseParams, opts ...option.RequestOption) error {
1101+
capturedBody = body
1102+
return nil
1103+
},
1104+
}
1105+
b := BrowsersCmd{browsers: fakeBrowsers, computer: fakeComp}
1106+
_ = b.ComputerMoveMouse(context.Background(), BrowsersComputerMoveMouseInput{Identifier: "id", X: 100, Y: 200})
1107+
extras := capturedBody.ExtraFields()
1108+
assert.Empty(t, extras)
1109+
}
1110+
1111+
func TestBrowsersComputerDragMouse_SmoothFalse(t *testing.T) {
1112+
setupStdoutCapture(t)
1113+
fakeBrowsers := newFakeBrowsersServiceWithSimpleGet()
1114+
var capturedBody kernel.BrowserComputerDragMouseParams
1115+
fakeComp := &FakeComputerService{
1116+
DragMouseFunc: func(ctx context.Context, id string, body kernel.BrowserComputerDragMouseParams, opts ...option.RequestOption) error {
1117+
capturedBody = body
1118+
return nil
1119+
},
1120+
}
1121+
b := BrowsersCmd{browsers: fakeBrowsers, computer: fakeComp}
1122+
smooth := false
1123+
_ = b.ComputerDragMouse(context.Background(), BrowsersComputerDragMouseInput{
1124+
Identifier: "id",
1125+
Path: [][]int64{{100, 200}, {300, 400}},
1126+
Smooth: &smooth,
1127+
})
1128+
extras := capturedBody.ExtraFields()
1129+
assert.Contains(t, extras, "smooth")
1130+
assert.Equal(t, false, extras["smooth"])
1131+
}
1132+
1133+
func TestBrowsersComputerDragMouse_DurationMs(t *testing.T) {
1134+
setupStdoutCapture(t)
1135+
fakeBrowsers := newFakeBrowsersServiceWithSimpleGet()
1136+
var capturedBody kernel.BrowserComputerDragMouseParams
1137+
fakeComp := &FakeComputerService{
1138+
DragMouseFunc: func(ctx context.Context, id string, body kernel.BrowserComputerDragMouseParams, opts ...option.RequestOption) error {
1139+
capturedBody = body
1140+
return nil
1141+
},
1142+
}
1143+
b := BrowsersCmd{browsers: fakeBrowsers, computer: fakeComp}
1144+
smooth := true
1145+
dur := int64(3000)
1146+
_ = b.ComputerDragMouse(context.Background(), BrowsersComputerDragMouseInput{
1147+
Identifier: "id",
1148+
Path: [][]int64{{100, 200}, {300, 400}},
1149+
Smooth: &smooth,
1150+
DurationMs: &dur,
1151+
})
1152+
extras := capturedBody.ExtraFields()
1153+
assert.Contains(t, extras, "smooth")
1154+
assert.Equal(t, true, extras["smooth"])
1155+
assert.Contains(t, extras, "duration_ms")
1156+
assert.Equal(t, int64(3000), extras["duration_ms"])
1157+
}
1158+
10561159
func TestBrowsersComputerScreenshot_SavesFile(t *testing.T) {
10571160
setupStdoutCapture(t)
10581161
dir := t.TempDir()

0 commit comments

Comments
 (0)