Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ go.work
# Screenshots and recordings
screenshot_*.png
recording_*.mp4
*.mp4
*.gif
*.png

# Temporary files
/tmp/
23 changes: 23 additions & 0 deletions cmd/constants.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package cmd

const (
DarwinOS = "darwin"
StateBooted = "Booted"
StateShutdown = "Shutdown"
TypeIOSSimulator = "iOS Simulator"
TypeAndroidEmulator = "Android Emulator"
PlatformIOS = "ios"
PlatformAndroid = "android"
ExtPNG = ".png"
ExtMP4 = ".mp4"
ExtGIF = ".gif"
CmdXCrun = "xcrun"
CmdSimctl = "simctl"
CmdAdb = "adb"
CmdEmulator = "emulator"
CmdAvdManager = "avdmanager"
CmdFFmpeg = "ffmpeg"
CmdOsaScript = "osascript"
PrefixScreenshot = "screenshot"
PrefixRecording = "recording"
)
150 changes: 76 additions & 74 deletions cmd/device.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,6 @@ import (
"github.com/spf13/cobra"
)

const (
darwinOS = "darwin"
bootedState = "Booted"
)

var startCmd = &cobra.Command{
Use: "start [device-name-or-udid|lts]",
Aliases: []string{"s"},
Expand All @@ -24,7 +19,6 @@ Use 'lts' to start the last started device.`,
Run: func(cmd *cobra.Command, args []string) {
deviceID := args[0]

// Handle "lts" (last started) case
if deviceID == "lts" {
lastDevice, err := getLastStartedDevice()
if err != nil || lastDevice == nil {
Expand All @@ -36,7 +30,7 @@ Use 'lts' to start the last started device.`,
deviceID = lastDevice.Name
}

if runtime.GOOS == darwinOS {
if runtime.GOOS == DarwinOS {
if startIOSSimulator(deviceID) {
return
}
Expand All @@ -59,7 +53,7 @@ var stopCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
deviceID := args[0]

if runtime.GOOS == darwinOS {
if runtime.GOOS == DarwinOS {
if stopIOSSimulator(deviceID) {
return
}
Expand All @@ -82,13 +76,13 @@ var shutdownCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
deviceID := args[0]

if runtime.GOOS == "darwin" {
if runtime.GOOS == DarwinOS {
if shutdownIOSSimulator(deviceID) {
return
}
}

if stopAndroidEmulator(deviceID) { // Android doesn't distinguish between stop and shutdown
if stopAndroidEmulator(deviceID) {
return
}

Expand All @@ -105,7 +99,7 @@ var restartCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
deviceID := args[0]

if runtime.GOOS == "darwin" {
if runtime.GOOS == DarwinOS {
if restartIOSSimulator(deviceID) {
return
}
Expand All @@ -129,7 +123,7 @@ var deleteCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
deviceID := args[0]

if runtime.GOOS == "darwin" {
if runtime.GOOS == DarwinOS {
if deleteIOSSimulator(deviceID) {
return
}
Expand Down Expand Up @@ -183,7 +177,7 @@ var ltsCmd = &cobra.Command{
fmt.Printf("Starting last device: %s (%s)\n", lastDevice.Name, lastDevice.Type)
deviceID := lastDevice.Name

if runtime.GOOS == "darwin" {
if runtime.GOOS == DarwinOS {
if startIOSSimulator(deviceID) {
return
}
Expand All @@ -204,7 +198,7 @@ func startIOSSimulator(deviceID string) bool {
}

fmt.Printf("Starting iOS simulator '%s'...\n", deviceID)
cmd := exec.Command("xcrun", "simctl", "boot", device.UDID)
cmd := exec.Command(CmdXCrun, CmdSimctl, "boot", device.UDID)
if err := cmd.Run(); err != nil {
fmt.Printf("Error starting iOS simulator: %v\n", err)
return false
Expand All @@ -215,8 +209,7 @@ func startIOSSimulator(deviceID string) bool {
fmt.Printf("Warning: Could not open Simulator app: %v\n", err)
}

// Save as last started device with complete information
device.State = "Booted"
device.State = StateBooted
if err := saveLastStartedDevice(device); err != nil {
fmt.Printf("Warning: Could not save last started device: %v\n", err)
}
Expand All @@ -227,13 +220,13 @@ func startIOSSimulator(deviceID string) bool {
}

func stopIOSSimulator(deviceID string) bool {
udid := findIOSSimulatorUDID(deviceID)
udid, _ := findIOSSimulator(deviceID)
if udid == "" {
return false
}

fmt.Printf("Stopping iOS simulator '%s'...\n", deviceID)
cmd := exec.Command("xcrun", "simctl", "shutdown", udid)
cmd := exec.Command(CmdXCrun, CmdSimctl, "shutdown", udid)
if err := cmd.Run(); err != nil {
fmt.Printf("Error stopping iOS simulator: %v\n", err)
return false
Expand All @@ -256,10 +249,10 @@ func restartIOSSimulator(deviceID string) bool {

fmt.Printf("Restarting iOS simulator '%s'...\n", deviceID)

shutdownCmd := exec.Command("xcrun", "simctl", "shutdown", device.UDID)
shutdownCmd := exec.Command(CmdXCrun, CmdSimctl, "shutdown", device.UDID)
_ = shutdownCmd.Run() // Ignore error if already shutdown

bootCmd := exec.Command("xcrun", "simctl", "boot", device.UDID)
bootCmd := exec.Command(CmdXCrun, CmdSimctl, "boot", device.UDID)
if err := bootCmd.Run(); err != nil {
fmt.Printf("Error restarting iOS simulator: %v\n", err)
return false
Expand All @@ -270,8 +263,7 @@ func restartIOSSimulator(deviceID string) bool {
fmt.Printf("Warning: Could not open Simulator app: %v\n", err)
}

// Save as last started device with complete information
device.State = "Booted"
device.State = StateBooted
if err := saveLastStartedDevice(device); err != nil {
fmt.Printf("Warning: Could not save last started device: %v\n", err)
}
Expand All @@ -285,12 +277,12 @@ func startAndroidEmulator(deviceID string) bool {
if isAndroidEmulatorRunning(deviceID) {
fmt.Printf("Android emulator '%s' is already running\n", deviceID)

// Save as last started device even if already running
udid, name := findRunningAndroidEmulator(deviceID)
Copy link

Copilot AI Jul 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The function findRunningAndroidEmulator is called to get both udid and name, but the existing logic above already has deviceID. Consider whether the name retrieval is necessary or if the function signature could be simplified.

Copilot uses AI. Check for mistakes.
device := &Device{
Name: deviceID,
UDID: findRunningAndroidEmulator(deviceID),
Type: "Android Emulator",
State: "Booted",
Name: name,
UDID: udid,
Type: TypeAndroidEmulator,
State: StateBooted,
}
if err := saveLastStartedDevice(device); err != nil {
fmt.Printf("Warning: Could not save last started device: %v\n", err)
Expand All @@ -304,18 +296,17 @@ func startAndroidEmulator(deviceID string) bool {
}

fmt.Printf("Starting Android emulator '%s'...\n", deviceID)
cmd := exec.Command("emulator", "-avd", deviceID)
cmd := exec.Command(CmdEmulator, "-avd", deviceID)
if err := cmd.Start(); err != nil {
fmt.Printf("Error starting Android emulator: %v\n", err)
return false
}

// Save as last started device
device := &Device{
Name: deviceID,
UDID: "starting", // Will be updated when emulator is fully running
Type: "Android Emulator",
State: "Booted",
UDID: "starting",
Type: TypeAndroidEmulator,
State: StateBooted,
}
if err := saveLastStartedDevice(device); err != nil {
fmt.Printf("Warning: Could not save last started device: %v\n", err)
Expand All @@ -327,13 +318,13 @@ func startAndroidEmulator(deviceID string) bool {
}

func stopAndroidEmulator(deviceID string) bool {
runningUDID := findRunningAndroidEmulator(deviceID)
if runningUDID == "" {
udid, _ := findRunningAndroidEmulator(deviceID)
if udid == "" {
return false
}

fmt.Printf("Stopping Android emulator '%s'...\n", deviceID)
cmd := exec.Command("adb", "-s", runningUDID, "emu", "kill")
cmd := exec.Command(CmdAdb, "-s", udid, "emu", "kill")
if err := cmd.Run(); err != nil {
fmt.Printf("Error stopping Android emulator: %v\n", err)
return false
Expand All @@ -350,12 +341,11 @@ func restartAndroidEmulator(deviceID string) bool {
stopAndroidEmulator(deviceID)

if startAndroidEmulator(deviceID) {
// Save as last started device
device := &Device{
Name: deviceID,
UDID: "restarting",
Type: "Android Emulator",
State: "Booted",
Type: TypeAndroidEmulator,
State: StateBooted,
}
if err := saveLastStartedDevice(device); err != nil {
fmt.Printf("Warning: Could not save last started device: %v\n", err)
Expand All @@ -368,19 +358,17 @@ func restartAndroidEmulator(deviceID string) bool {
}

func deleteIOSSimulator(deviceID string) bool {
udid := findIOSSimulatorUDID(deviceID)
udid, _ := findIOSSimulator(deviceID)
if udid == "" {
return false
}

fmt.Printf("Deleting iOS simulator '%s'...\n", deviceID)

// Shutdown the simulator if it's running
shutdownCmd := exec.Command("xcrun", "simctl", "shutdown", udid)
_ = shutdownCmd.Run() // Ignore error if already shutdown
shutdownCmd := exec.Command(CmdXCrun, CmdSimctl, "shutdown", udid)
_ = shutdownCmd.Run()

// Delete the simulator
cmd := exec.Command("xcrun", "simctl", "delete", udid)
cmd := exec.Command(CmdXCrun, CmdSimctl, "delete", udid)
if err := cmd.Run(); err != nil {
fmt.Printf("Error deleting iOS simulator: %v\n", err)
return false
Expand All @@ -398,11 +386,9 @@ func deleteAndroidEmulator(deviceID string) bool {

fmt.Printf("Deleting Android emulator '%s'...\n", deviceID)

// Stop the emulator if it's running
stopAndroidEmulator(deviceID)

// Delete the AVD
cmd := exec.Command("avdmanager", "delete", "avd", "-n", deviceID)
cmd := exec.Command(CmdAvdManager, "delete", "avd", "-n", deviceID)
if err := cmd.Run(); err != nil {
fmt.Printf("Error deleting Android emulator: %v\n", err)
return false
Expand All @@ -413,19 +399,24 @@ func deleteAndroidEmulator(deviceID string) bool {
return true
}

func findIOSSimulatorUDID(deviceID string) string {
func findIOSSimulator(deviceID string) (string, string) {
if len(deviceID) == 36 && strings.Count(deviceID, "-") == 4 {
return deviceID
sims := getIOSSimulators()
for _, sim := range sims {
if sim.UDID == deviceID {
return sim.UDID, sim.Name
}
}
}

simulators := getIOSSimulators()
for _, sim := range simulators {
if strings.EqualFold(sim.Name, deviceID) || sim.UDID == deviceID {
return sim.UDID
sims := getIOSSimulators()
for _, sim := range sims {
if strings.EqualFold(sim.Name, deviceID) {
return sim.UDID, sim.Name
}
}

return ""
return "", ""
}

func findIOSSimulatorByID(deviceID string) *Device {
Expand All @@ -439,8 +430,19 @@ func findIOSSimulatorByID(deviceID string) *Device {
return nil
}

func getRunningIOSSimulator() (*iOSSimulator, error) {
sims := getIOSSimulators()
for _, sim := range sims {
if sim.State == StateBooted {
return &iOSSimulator{udid: sim.UDID, name: sim.Name}, nil
}
}

return nil, ErrNoRunningIOSSimulator
}

func doesAndroidAVDExist(avdName string) bool {
cmd := exec.Command("emulator", "-list-avds")
cmd := exec.Command(CmdEmulator, "-list-avds")
output, err := cmd.Output()
if err != nil {
return false
Expand All @@ -457,43 +459,43 @@ func doesAndroidAVDExist(avdName string) bool {
}

func isAndroidEmulatorRunning(avdName string) bool {
return findRunningAndroidEmulator(avdName) != ""
udid, _ := findRunningAndroidEmulator(avdName)
return udid != ""
}

func findRunningAndroidEmulator(avdName string) string {
cmd := exec.Command("adb", "devices")
func findRunningAndroidEmulator(avdName string) (string, string) {
cmd := exec.Command(CmdAdb, "devices")
output, err := cmd.Output()
if err != nil {
return ""
return "", ""
}

lines := strings.Split(string(output), "\n")
for _, line := range lines {
lines := strings.SplitSeq(string(output), "\n")
for line := range lines {
if strings.Contains(line, "emulator-") && strings.Contains(line, "device") {
parts := strings.Fields(line)
if len(parts) >= 2 {
if len(parts) > 0 {
emulatorID := parts[0]
nameCmd := exec.Command("adb", "-s", emulatorID, "emu", "avd", "name")
nameCmd := exec.Command(CmdAdb, "-s", emulatorID, "emu", "avd", "name")
nameOutput, err := nameCmd.Output()
if err == nil {
actualName := strings.TrimSpace(string(nameOutput))
if actualName == avdName {
return emulatorID
if avdName == "" || actualName == avdName {
return emulatorID, actualName
}
}
}
}
}

return ""
return "", ""
}

func init() {
rootCmd.AddCommand(startCmd)
rootCmd.AddCommand(stopCmd)
rootCmd.AddCommand(shutdownCmd)
rootCmd.AddCommand(restartCmd)
rootCmd.AddCommand(deleteCmd)
rootCmd.AddCommand(lastCmd)
rootCmd.AddCommand(ltsCmd)
func getRunningAndroidEmulator() (*androidEmulator, error) {
udid, name := findRunningAndroidEmulator("") // Find any running emulator
if udid != "" {
return &androidEmulator{udid: udid, name: name}, nil
}

return nil, ErrNoRunningAndroidEmulator
}
Loading
Loading