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
12 changes: 8 additions & 4 deletions MiniSim.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@
76F2A914299050F9002D4EF6 /* UserDefaults+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76F2A913299050F9002D4EF6 /* UserDefaults+Configuration.swift */; };
76F2A9172991B7B6002D4EF6 /* ViewModifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76F2A9162991B7B6002D4EF6 /* ViewModifiers.swift */; };
76FCABAB29B390D5003BBF9A /* Collection+get.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76FCABAA29B390D5003BBF9A /* Collection+get.swift */; };
7D4142B4F3AF1D150D9222BD /* DeviceFamily.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F78DD26087D5448AAB2CC3B /* DeviceFamily.swift */; };
9B225A9C2C7E360D002620BA /* DeviceType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B225A9B2C7E360D002620BA /* DeviceType.swift */; };
/* End PBXBuildFile section */

Expand All @@ -129,6 +130,7 @@
52B363ED2AEC10B3006F515C /* ParametersTableFormViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParametersTableFormViewModel.swift; sourceTree = "<group>"; };
551B88292B1385E900B8D325 /* Terminal.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Terminal.swift; sourceTree = "<group>"; };
55CDB0772B1B6D24002418D7 /* TerminalApps.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalApps.swift; sourceTree = "<group>"; };
5F78DD26087D5448AAB2CC3B /* DeviceFamily.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DeviceFamily.swift; sourceTree = "<group>"; };
760554A22C085BEA001607FE /* Thread+Asserts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Thread+Asserts.swift"; sourceTree = "<group>"; };
76059BF42AD4361C0008D38B /* SetupPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupPreferences.swift; sourceTree = "<group>"; };
76059BF62AD449DC0008D38B /* OnboardingHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingHeader.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -304,6 +306,7 @@
7631218A2A12AFBC00EE7F48 /* Platform.swift */,
76F269862A2A39D100424BDA /* Variables.swift */,
9B225A9B2C7E360D002620BA /* DeviceType.swift */,
5F78DD26087D5448AAB2CC3B /* DeviceFamily.swift */,
);
path = Model;
sourceTree = "<group>";
Expand Down Expand Up @@ -721,6 +724,7 @@
76BF0AEE2C905C43003BE568 /* IOSDeviceService.swift in Sources */,
7645D4BE2982A1B100019227 /* DeviceService.swift in Sources */,
765ABF382A8BECD900A063CB /* ExecuteCommand.swift in Sources */,
7D4142B4F3AF1D150D9222BD /* DeviceFamily.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -745,7 +749,7 @@
/* Begin PBXTargetDependency section */
4A78928A2AF1A9A3004D3FC8 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
productRef = 4A7892892AF1A9A3004D3FC8 /* SwiftLintPlugin */;
productRef = 4A7892892AF1A9A3004D3FC8 /* plugin:SwiftLintPlugin */;
};
76B70F792B0D359D009D87A4 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
Expand All @@ -754,7 +758,7 @@
};
76B70F802B0D4F9D009D87A4 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
productRef = 76B70F7F2B0D4F9D009D87A4 /* SwiftLintPlugin */;
productRef = 76B70F7F2B0D4F9D009D87A4 /* plugin:SwiftLintPlugin */;
};
/* End PBXTargetDependency section */

Expand Down Expand Up @@ -1106,7 +1110,7 @@
/* End XCRemoteSwiftPackageReference section */

/* Begin XCSwiftPackageProductDependency section */
4A7892892AF1A9A3004D3FC8 /* SwiftLintPlugin */ = {
4A7892892AF1A9A3004D3FC8 /* plugin:SwiftLintPlugin */ = {
isa = XCSwiftPackageProductDependency;
package = 4A7892862AF1A767004D3FC8 /* XCRemoteSwiftPackageReference "SwiftLint" */;
productName = "plugin:SwiftLintPlugin";
Expand Down Expand Up @@ -1141,7 +1145,7 @@
package = 76AC9AF72A0EB50800864A8B /* XCRemoteSwiftPackageReference "SymbolPicker" */;
productName = SymbolPicker;
};
76B70F7F2B0D4F9D009D87A4 /* SwiftLintPlugin */ = {
76B70F7F2B0D4F9D009D87A4 /* plugin:SwiftLintPlugin */ = {
isa = XCSwiftPackageProductDependency;
package = 4A7892862AF1A767004D3FC8 /* XCRemoteSwiftPackageReference "SwiftLint" */;
productName = "plugin:SwiftLintPlugin";
Expand Down
21 changes: 0 additions & 21 deletions MiniSim/Assets.xcassets/vision_os.imageset/Contents.json

This file was deleted.

Binary file not shown.
18 changes: 9 additions & 9 deletions MiniSim/Extensions/NSMenuItem+ImageInit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,21 @@ extension NSMenuItem {
action: Selector?,
keyEquivalent: String,
type: DeviceMenuItem,
deviceFamily: DeviceFamily? = nil,
image: NSImage? = nil
) {
self.init(title: title, action: action, keyEquivalent: keyEquivalent)

if let image {
self.image = image
} else if let deviceFamily {
self.image = NSImage(
systemSymbolName: deviceFamily.iconName,
accessibilityDescription: title
)
} else {
if title.contains("Vision") {
self.image = NSImage(named: "vision_os")
self.image?.isTemplate = true
self.image?.size = NSSize(width: 15, height: 8.5)
} else {
let imageName = self.getSystemImageFromName(name: title)
self.image = NSImage(systemSymbolName: imageName, accessibilityDescription: title)
}
let imageName = self.getSystemImageFromName(name: title)
self.image = NSImage(systemSymbolName: imageName, accessibilityDescription: title)
}

self.tag = type.rawValue
Expand All @@ -42,7 +42,7 @@ extension NSMenuItem {
return "ipad.landscape"
}

if name.contains("Watch") {
if name.contains("Apple Watch") {
return "applewatch"
}

Expand Down
3 changes: 2 additions & 1 deletion MiniSim/Menu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import KeyboardShortcuts
import UserNotifications

class Menu: NSMenu {

Check warning on line 12 in MiniSim/Menu.swift

View workflow job for this annotation

GitHub Actions / lint

Type Body Length Violation: Class body should span 250 lines or less excluding comments and whitespace: currently spans 257 lines (type_body_length)
public let maxKeyEquivalent = 9
let actionExecutor = ActionExecutor()

Expand Down Expand Up @@ -225,7 +225,8 @@
title: device.displayName,
action: #selector(deviceItemClick),
keyEquivalent: "",
type: device.platform == .ios ? .launchIOS : .launchAndroid
type: device.platform == .ios ? .launchIOS : .launchAndroid,
deviceFamily: device.deviceFamily
)

menuItem.target = self
Expand Down
9 changes: 7 additions & 2 deletions MiniSim/Model/Device.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ struct Device: Hashable, Codable {
var booted: Bool
var platform: Platform
var type: DeviceType
var deviceFamily: DeviceFamily?

var displayName: String {
switch platform {
Expand All @@ -22,7 +23,7 @@ struct Device: Hashable, Codable {
}

enum CodingKeys: String, CodingKey {
case name, version, identifier, booted, platform, displayName, type
case name, version, identifier, booted, platform, displayName, type, deviceFamily
}

init(
Expand All @@ -31,14 +32,16 @@ struct Device: Hashable, Codable {
identifier: String?,
booted: Bool = false,
platform: Platform,
type: DeviceType
type: DeviceType,
deviceFamily: DeviceFamily? = nil
) {
self.name = name
self.version = version
self.identifier = identifier
self.booted = booted
self.platform = platform
self.type = type
self.deviceFamily = deviceFamily
}

init(from decoder: Decoder) throws {
Expand All @@ -49,6 +52,7 @@ struct Device: Hashable, Codable {
booted = try values.decode(Bool.self, forKey: .booted)
platform = try values.decode(Platform.self, forKey: .platform)
type = try values.decode(DeviceType.self, forKey: .type)
deviceFamily = try values.decodeIfPresent(DeviceFamily.self, forKey: .deviceFamily)
}

func encode(to encoder: Encoder) throws {
Expand All @@ -60,5 +64,6 @@ struct Device: Hashable, Codable {
try container.encode(platform, forKey: .platform)
try container.encode(displayName, forKey: .displayName)
try container.encode(type, forKey: .type)
try container.encodeIfPresent(deviceFamily, forKey: .deviceFamily)
}
}
53 changes: 53 additions & 0 deletions MiniSim/Model/DeviceFamily.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//
// DeviceFamily.swift
// MiniSim
//
// Created by Oskar Kwaśniewski on 17/01/2026.
//

import Foundation

enum DeviceFamily: String, Codable {
case iPhone
case iPad
case watch
// swiftlint:disable:next identifier_name
case tv
case vision
case unknown

var iconName: String {
switch self {
case .iPhone:
return "iphone"
case .iPad:
return "ipad.landscape"
case .watch:
return "applewatch"
case .tv:
return "appletv.fill"
case .vision:
return "visionpro"
case .unknown:
return "iphone"
}
}

/// Parses `deviceTypeIdentifier` from simctl JSON output.
/// Example: `com.apple.CoreSimulator.SimDeviceType.iPhone-14-Pro`
init(fromDeviceTypeIdentifier identifier: String) {
if identifier.contains("iPhone") {
self = .iPhone
} else if identifier.contains("iPad") {
self = .iPad
} else if identifier.contains("Apple-Watch") {
self = .watch
} else if identifier.contains("Apple-TV") {
self = .tv
} else if identifier.contains("Apple-Vision") {
self = .vision
} else {
self = .unknown
}
}
}
2 changes: 1 addition & 1 deletion MiniSim/Service/ActionExecutor.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Foundation
import AppKit
import Foundation

class ActionExecutor {
private let queue: DispatchQueue
Expand Down
1 change: 0 additions & 1 deletion MiniSim/Service/ActionFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
import Foundation

protocol ActionFactory {
static func createAction(for tag: SubMenuItems.Tags, device: Device, itemName: String, skipConfirmation: Bool) -> Action

Check warning on line 5 in MiniSim/Service/ActionFactory.swift

View workflow job for this annotation

GitHub Actions / lint

Line Length Violation: Line should be 120 characters or less; currently it has 122 characters (line_length)
}

class AndroidActionFactory: ActionFactory {
static func createAction(for tag: SubMenuItems.Tags, device: Device, itemName: String, skipConfirmation: Bool = false) -> any Action {

Check warning on line 9 in MiniSim/Service/ActionFactory.swift

View workflow job for this annotation

GitHub Actions / lint

Line Length Violation: Line should be 120 characters or less; currently it has 136 characters (line_length)
switch tag {
case .copyName:
return CopyNameAction(device: device)
Expand All @@ -31,7 +31,7 @@
}

class IOSActionFactory: ActionFactory {
static func createAction(for tag: SubMenuItems.Tags, device: Device, itemName: String, skipConfirmation: Bool = false) -> any Action {

Check warning on line 34 in MiniSim/Service/ActionFactory.swift

View workflow job for this annotation

GitHub Actions / lint

Line Length Violation: Line should be 120 characters or less; currently it has 136 characters (line_length)
switch tag {
case .copyName:
return CopyNameAction(device: device)
Expand All @@ -48,4 +48,3 @@
}
}
}

2 changes: 1 addition & 1 deletion MiniSim/Service/DeviceDiscoveryService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ class IOSDeviceDiscovery: DeviceDiscoveryService {
func getIOSSimulators() throws -> [Device] {
let output = try shell.execute(
command: DeviceConstants.ProcessPaths.xcrun.rawValue,
arguments: ["simctl", "list", "devices", "available"]
arguments: ["simctl", "list", "devices", "available", "-j"]
)
return DeviceParserFactory().getParser(.iosSimulator).parse(output)
}
Expand Down
59 changes: 43 additions & 16 deletions MiniSim/Service/DeviceParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,34 +27,61 @@ class DeviceParserFactory {
}

class IOSSimulatorParser: DeviceParser {
struct SimulatorDevice: Codable {
let name: String
let udid: String
let state: String
let deviceTypeIdentifier: String
let isAvailable: Bool
}

struct SimctlOutput: Codable {
let devices: [String: [SimulatorDevice]]
}

func parse(_ input: String) -> [Device] {
let lines = input.components(separatedBy: .newlines)
guard let jsonData = input.data(using: .utf8) else { return [] }
guard let simctlOutput = try? JSONDecoder().decode(SimctlOutput.self, from: jsonData) else {
return []
}

var devices: [Device] = []
let currentOSIdx = 1
let deviceNameIdx = 1
let identifierIdx = 4
let deviceStateIdx = 5
var osVersion = ""

lines.forEach { line in
if let currentOs = line.match("-- (.*?) --").first, !currentOs.isEmpty {
osVersion = currentOs[currentOSIdx]
}
if let device = line.match("(.*?) (\\(([0-9.]+)\\) )?\\(([0-9A-F-]+)\\) (\\(.*?)\\)").first {

for (runtimeIdentifier, simulatorDevices) in simctlOutput.devices {
let osVersion = parseOSVersion(from: runtimeIdentifier)

for simulator in simulatorDevices {
let deviceFamily = DeviceFamily(fromDeviceTypeIdentifier: simulator.deviceTypeIdentifier)

devices.append(
Device(
name: device[deviceNameIdx].trimmingCharacters(in: .whitespacesAndNewlines),
name: simulator.name,
version: osVersion,
identifier: device[identifierIdx],
booted: device[deviceStateIdx].contains("Booted"),
identifier: simulator.udid,
booted: simulator.state == "Booted",
platform: .ios,
type: .virtual
type: .virtual,
deviceFamily: deviceFamily
)
)
}
}

return devices
}

/// Parses OS version from runtime identifier.
/// Example: `com.apple.CoreSimulator.SimRuntime.iOS-18-5` -> `iOS 18.5`
private func parseOSVersion(from runtimeIdentifier: String) -> String {
let pattern = "com\\.apple\\.CoreSimulator\\.SimRuntime\\.(\\w+)-(\\d+)-(\\d+)"
guard let match = runtimeIdentifier.match(pattern).first, match.count >= 4 else {
return runtimeIdentifier
}
let platform = match[1]
let major = match[2]
let minor = match[3]
return "\(platform) \(major).\(minor)"
}
}

class IOSPhysicalDeviceParser: DeviceParser {
Expand Down
4 changes: 2 additions & 2 deletions MiniSim/Views/About.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ struct About: View {
Link("Created by Oskar Kwaśniewski", destination: URL(string: "https://github.com/okwasniewski")!)
.font(.caption)
}
.sheet(isPresented: $isAcknowledgementsListPresented, content: {
.sheet(isPresented: $isAcknowledgementsListPresented) {
NavigationView {
AcknowListSwiftUIView(acknowList: AcknowParser.defaultPackages()!)
.toolbar {
Expand All @@ -70,7 +70,7 @@ struct About: View {
}
}
.frame(minHeight: 450)
})
}
.frame(minWidth: minFrameWidth, minHeight: minFrameHeight)
}
}
8 changes: 5 additions & 3 deletions MiniSimTests/DeviceDiscoveryTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
if command.hasSuffix("adb") {
XCTAssertEqual(arguments, ["devices", "-l"])
return "mock adb output"
} else if command.hasSuffix("emulator") {
}
if command.hasSuffix("emulator") {
XCTAssertEqual(arguments, ["-list-avds"])
return "mock emulator output"
}
Expand All @@ -52,14 +53,15 @@
// iOS Tests
func testIOSDeviceDiscoveryCommands() throws {
throw XCTSkip("TODO: Test is failing on CI")

shellStub.mockedExecute = { command, arguments, _ in

Check warning on line 57 in MiniSimTests/DeviceDiscoveryTests.swift

View workflow job for this annotation

GitHub Actions / build

code after 'throw' will never be executed
XCTAssertEqual(command, DeviceConstants.ProcessPaths.xcrun.rawValue)
if arguments.contains("devicectl") {
XCTAssertTrue(arguments.contains("list"))
XCTAssertTrue(arguments.contains("devices"))
return ""
} else if arguments.contains("simctl") {
}
if arguments.contains("simctl") {
XCTAssertEqual(arguments, ["simctl", "list", "devices", "available"])
return "mock simctl output"
}
Expand Down
Loading
Loading