From 72f4faa4fa785f937716307c9f806c42e2026562 Mon Sep 17 00:00:00 2001 From: gmegidish Date: Wed, 2 Jul 2025 13:19:54 +0200 Subject: [PATCH 1/3] feat: screencapture through cli --- main.go | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 62 insertions(+), 5 deletions(-) diff --git a/main.go b/main.go index 7d1388b..4950d81 100644 --- a/main.go +++ b/main.go @@ -35,6 +35,9 @@ var ( screenshotFormat string screenshotJpegQuality int + // for screencapture command + screencaptureFormat string + // for devices command platform string deviceType string @@ -48,7 +51,9 @@ var rootCmd = &cobra.Command{ CompletionOptions: cobra.CompletionOptions{ HiddenDefaultCmd: true, }, - Version: version, + Version: version, + SilenceUsage: true, + SilenceErrors: true, } var devicesCmd = &cobra.Command{ @@ -104,6 +109,54 @@ var screenshotCmd = &cobra.Command{ }, } +var screencaptureCmd = &cobra.Command{ + Use: "screencapture", + Short: "Stream screen capture from a connected device", + Long: `Streams MJPEG screen capture from a specified device to stdout. Only supports MJPEG format.`, + RunE: func(cmd *cobra.Command, args []string) error { + // Validate format + if screencaptureFormat != "mjpeg" { + response := commands.NewErrorResponse(fmt.Errorf("format must be 'mjpeg' for screen capture")) + printJson(response) + return fmt.Errorf(response.Error) + } + + // Find the target device + targetDevice, err := commands.FindDeviceOrAutoSelect(deviceId) + if err != nil { + response := commands.NewErrorResponse(fmt.Errorf("error finding device: %v", err)) + printJson(response) + return fmt.Errorf(response.Error) + } + + // Start agent + err = targetDevice.StartAgent() + if err != nil { + response := commands.NewErrorResponse(fmt.Errorf("error starting agent: %v", err)) + printJson(response) + return fmt.Errorf(response.Error) + } + + // Start screen capture and stream to stdout + err = targetDevice.StartScreenCapture(screencaptureFormat, func(data []byte) bool { + _, writeErr := os.Stdout.Write(data) + if writeErr != nil { + fmt.Fprintf(os.Stderr, "Error writing data: %v\n", writeErr) + return false + } + return true + }) + + if err != nil { + response := commands.NewErrorResponse(fmt.Errorf("error starting screen capture: %v", err)) + printJson(response) + return fmt.Errorf(response.Error) + } + + return nil + }, +} + var rebootCmd = &cobra.Command{ Use: "reboot", Short: "Reboot a connected device or simulator", @@ -348,6 +401,7 @@ func init() { // add main commands rootCmd.AddCommand(devicesCmd) rootCmd.AddCommand(screenshotCmd) + rootCmd.AddCommand(screencaptureCmd) rootCmd.AddCommand(rebootCmd) rootCmd.AddCommand(infoCmd) rootCmd.AddCommand(ioCmd) @@ -373,14 +427,17 @@ func init() { devicesCmd.Flags().StringVar(&platform, "platform", "", "target platform (ios or android)") devicesCmd.Flags().StringVar(&deviceType, "type", "", "filter by device type (real or simulator/emulator)") - screenshotCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to take screenshot from (optional if only one device connected)") + screenshotCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to take screenshot from") screenshotCmd.Flags().StringVarP(&screenshotOutputPath, "output", "o", "", "Output file path for screenshot (e.g., screen.png, or '-' for stdout)") screenshotCmd.Flags().StringVarP(&screenshotFormat, "format", "f", "png", "Output format for screenshot (png or jpeg)") screenshotCmd.Flags().IntVarP(&screenshotJpegQuality, "quality", "q", 90, "JPEG quality (1-100, only applies if format is jpeg)") + screencaptureCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to capture from") + screencaptureCmd.Flags().StringVarP(&screencaptureFormat, "format", "f", "mjpeg", "Output format for screen capture") + rebootCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to reboot") - infoCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to get info from (optional if only one device connected)") + infoCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to get info from") // io command flags ioTapCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to tap on") @@ -392,9 +449,9 @@ func init() { // apps command flags appsLaunchCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to launch app on") - appsTerminateCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to terminate app on (optional if only one device connected)") + appsTerminateCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to terminate app on") - appsListCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to list apps from (optional if only one device connected)") + appsListCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to list apps from") // url command flags urlCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to open URL on") From b6580b38b0461a6fc37cd5c278493c6ebcc24bf7 Mon Sep 17 00:00:00 2001 From: gmegidish Date: Wed, 2 Jul 2025 13:25:22 +0200 Subject: [PATCH 2/3] fix: info should return an object called 'device' --- commands/info.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/commands/info.go b/commands/info.go index 26970c3..a7d4997 100644 --- a/commands/info.go +++ b/commands/info.go @@ -15,5 +15,9 @@ func InfoCommand(deviceID string) *CommandResponse { return NewErrorResponse(fmt.Errorf("error getting device info: %v", err)) } - return NewSuccessResponse(info) + response := map[string]interface{}{ + "device": info, + } + + return NewSuccessResponse(response) } From 7a3287b89f5dbd9e3f1ea40f227be013e0cd2dd7 Mon Sep 17 00:00:00 2001 From: gmegidish Date: Wed, 2 Jul 2025 13:35:10 +0200 Subject: [PATCH 3/3] refactor: move info and reboot under device --- commands/info.go | 11 +++++++-- devices/wda/requests.go | 2 +- devices/wda/windowsize.go | 4 ++-- main.go | 50 +++++++++++++++------------------------ 4 files changed, 31 insertions(+), 36 deletions(-) diff --git a/commands/info.go b/commands/info.go index a7d4997..452fae0 100644 --- a/commands/info.go +++ b/commands/info.go @@ -2,8 +2,15 @@ package commands import ( "fmt" + + "github.com/mobile-next/mobilecli/devices" ) +// DeviceInfoResponse represents the response for a device info command +type DeviceInfoResponse struct { + Device *devices.FullDeviceInfo `json:"device"` +} + func InfoCommand(deviceID string) *CommandResponse { targetDevice, err := FindDeviceOrAutoSelect(deviceID) if err != nil { @@ -15,8 +22,8 @@ func InfoCommand(deviceID string) *CommandResponse { return NewErrorResponse(fmt.Errorf("error getting device info: %v", err)) } - response := map[string]interface{}{ - "device": info, + response := DeviceInfoResponse{ + Device: info, } return NewSuccessResponse(response) diff --git a/devices/wda/requests.go b/devices/wda/requests.go index 8e320c1..c2c3765 100644 --- a/devices/wda/requests.go +++ b/devices/wda/requests.go @@ -150,7 +150,7 @@ func CreateSession() (string, error) { return "", fmt.Errorf("failed to create session: %v", err) } - log.Printf("createSession response: %v", response) + // log.Printf("createSession response: %v", response) sessionId := response["sessionId"].(string) return sessionId, nil diff --git a/devices/wda/windowsize.go b/devices/wda/windowsize.go index 2ad63ec..d049cbf 100644 --- a/devices/wda/windowsize.go +++ b/devices/wda/windowsize.go @@ -2,7 +2,6 @@ package wda import ( "fmt" - "log" ) type Size struct { @@ -31,7 +30,8 @@ func GetWindowSize() (*WindowSize, error) { if err != nil { return nil, err } - log.Printf("response: %v", response["value"]) + + // log.Printf("response: %v", response["value"]) windowSize := response["value"].(map[string]interface{}) screenSize := windowSize["screenSize"].(map[string]interface{}) diff --git a/main.go b/main.go index 4950d81..f425f6b 100644 --- a/main.go +++ b/main.go @@ -10,7 +10,6 @@ import ( "strings" "github.com/mobile-next/mobilecli/commands" - "github.com/mobile-next/mobilecli/devices" "github.com/mobile-next/mobilecli/server" "github.com/mobile-next/mobilecli/utils" "github.com/spf13/cobra" @@ -157,7 +156,13 @@ var screencaptureCmd = &cobra.Command{ }, } -var rebootCmd = &cobra.Command{ +var deviceCmd = &cobra.Command{ + Use: "device", + Short: "Device management commands", + Long: `Commands for managing individual devices including rebooting and getting device information.`, +} + +var deviceRebootCmd = &cobra.Command{ Use: "reboot", Short: "Reboot a connected device or simulator", Long: `Reboots a specified device (using its ID). Supports iOS (real/simulator) and Android (real/emulator).`, @@ -176,7 +181,7 @@ var rebootCmd = &cobra.Command{ }, } -var infoCmd = &cobra.Command{ +var deviceInfoCmd = &cobra.Command{ Use: "info", Short: "Get device info", Long: `Get detailed information about a connected device, such as OS, version, and screen size.`, @@ -400,15 +405,18 @@ func init() { // add main commands rootCmd.AddCommand(devicesCmd) + rootCmd.AddCommand(deviceCmd) rootCmd.AddCommand(screenshotCmd) rootCmd.AddCommand(screencaptureCmd) - rootCmd.AddCommand(rebootCmd) - rootCmd.AddCommand(infoCmd) rootCmd.AddCommand(ioCmd) rootCmd.AddCommand(appsCmd) rootCmd.AddCommand(serverCmd) rootCmd.AddCommand(urlCmd) + // add device subcommands + deviceCmd.AddCommand(deviceRebootCmd) + deviceCmd.AddCommand(deviceInfoCmd) + // add io subcommands ioCmd.AddCommand(ioTapCmd) ioCmd.AddCommand(ioButtonCmd) @@ -424,33 +432,32 @@ func init() { serverStartCmd.Flags().String("listen", "", "Address to listen on (e.g., 'localhost:12000' or '0.0.0.0:13000')") serverStartCmd.Flags().Bool("cors", false, "Enable CORS support") + // devices command flags devicesCmd.Flags().StringVar(&platform, "platform", "", "target platform (ios or android)") devicesCmd.Flags().StringVar(&deviceType, "type", "", "filter by device type (real or simulator/emulator)") + // screenshot command flags screenshotCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to take screenshot from") screenshotCmd.Flags().StringVarP(&screenshotOutputPath, "output", "o", "", "Output file path for screenshot (e.g., screen.png, or '-' for stdout)") screenshotCmd.Flags().StringVarP(&screenshotFormat, "format", "f", "png", "Output format for screenshot (png or jpeg)") screenshotCmd.Flags().IntVarP(&screenshotJpegQuality, "quality", "q", 90, "JPEG quality (1-100, only applies if format is jpeg)") + // screencapture command flags screencaptureCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to capture from") screencaptureCmd.Flags().StringVarP(&screencaptureFormat, "format", "f", "mjpeg", "Output format for screen capture") - rebootCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to reboot") - - infoCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to get info from") + // device command flags + deviceRebootCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to reboot") + deviceInfoCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to get info from") // io command flags ioTapCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to tap on") - ioButtonCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to press button on") - ioTextCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to send keys to") // apps command flags appsLaunchCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to launch app on") - appsTerminateCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to terminate app on") - appsListCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to list apps from") // url command flags @@ -467,22 +474,3 @@ func main() { os.Exit(1) } } - -func findTargetDevice(deviceID string) (devices.ControllableDevice, error) { - if deviceID == "" { - return nil, fmt.Errorf("--device flag is required") - } - - allDevices, err := devices.GetAllControllableDevices() - if err != nil { - return nil, fmt.Errorf("failed to list devices: %w", err) - } - - for _, d := range allDevices { - if d.ID() == deviceID { - return d, nil - } - } - - return nil, fmt.Errorf("device with ID '%s' not found", deviceID) -}