|
| 1 | +# MiniSim - Agent Guidelines |
| 2 | + |
| 3 | +MiniSim is a macOS menu bar utility for launching iOS simulators and Android emulators. Written in Swift and AppKit. |
| 4 | + |
| 5 | +## Build Commands |
| 6 | + |
| 7 | +```bash |
| 8 | +# Build the project |
| 9 | +xcodebuild -scheme MiniSim -configuration Debug build |
| 10 | + |
| 11 | +# Build for release |
| 12 | +xcodebuild -scheme MiniSim -configuration Release build |
| 13 | + |
| 14 | +# Clean build |
| 15 | +xcodebuild -scheme MiniSim clean build |
| 16 | +``` |
| 17 | + |
| 18 | +## Testing |
| 19 | + |
| 20 | +```bash |
| 21 | +# Run all tests |
| 22 | +xcodebuild test -scheme MiniSim -destination 'platform=macOS' |
| 23 | + |
| 24 | +# Run a single test file |
| 25 | +xcodebuild test -scheme MiniSim -destination 'platform=macOS' \ |
| 26 | + -only-testing:MiniSimTests/DeviceParserTests |
| 27 | + |
| 28 | +# Run a single test method |
| 29 | +xcodebuild test -scheme MiniSim -destination 'platform=macOS' \ |
| 30 | + -only-testing:MiniSimTests/DeviceParserTests/testIOSSimulatorParser |
| 31 | +``` |
| 32 | + |
| 33 | +## Linting |
| 34 | + |
| 35 | +SwiftLint is integrated via SPM. Config in `.swiftlint.yml`. |
| 36 | + |
| 37 | +```bash |
| 38 | +# Run SwiftLint |
| 39 | +swiftlint |
| 40 | + |
| 41 | +# Auto-fix issues |
| 42 | +swiftlint --fix |
| 43 | +``` |
| 44 | + |
| 45 | +## Project Structure |
| 46 | + |
| 47 | +``` |
| 48 | +MiniSim/ |
| 49 | +├── Model/ # Data models (Device, Command, Platform, etc.) |
| 50 | +├── Service/ # Business logic and services |
| 51 | +│ ├── CustomErrors/ # Custom error types |
| 52 | +│ └── Terminal/ # Terminal integration |
| 53 | +├── Views/ # SwiftUI views |
| 54 | +│ ├── Onboarding/ # Onboarding flow |
| 55 | +│ ├── CustomCommands/ # Custom commands UI |
| 56 | +│ └── ParametersTable/ # Parameters management |
| 57 | +├── Extensions/ # Swift extensions |
| 58 | +├── MenuItems/ # NSMenu item implementations |
| 59 | +├── Components/ # Reusable UI components |
| 60 | +└── AppleScript Commands/ # AppleScript integration |
| 61 | +``` |
| 62 | + |
| 63 | +## Code Style Guidelines |
| 64 | + |
| 65 | +### Imports |
| 66 | +- Sort imports alphabetically (enforced by SwiftLint `sorted_imports`) |
| 67 | +- Group Foundation/AppKit imports first, then third-party |
| 68 | + |
| 69 | +```swift |
| 70 | +import AppKit |
| 71 | +import Foundation |
| 72 | +import UserNotifications |
| 73 | +``` |
| 74 | + |
| 75 | +### Naming Conventions |
| 76 | +- Types: `PascalCase` (e.g., `DeviceService`, `ActionFactory`) |
| 77 | +- Variables/functions: `camelCase` |
| 78 | +- Enums: `PascalCase` for type, `camelCase` for cases |
| 79 | +- Protocols: Suffix with `Protocol` for dependency injection (e.g., `ShellProtocol`, `ADBProtocol`) |
| 80 | +- `id` and `ID` are allowed as identifier names |
| 81 | + |
| 82 | +### Types and Protocols |
| 83 | +- Use protocols for dependency injection and testability |
| 84 | +- Prefer `struct` for data models, `class` for services with state |
| 85 | +- Use `final class` when inheritance is not needed |
| 86 | + |
| 87 | +```swift |
| 88 | +protocol DeviceServiceCommon { |
| 89 | + var shell: ShellProtocol { get set } |
| 90 | + var device: Device { get } |
| 91 | + func deleteDevice() throws |
| 92 | +} |
| 93 | + |
| 94 | +struct Device: Hashable, Codable { |
| 95 | + var name: String |
| 96 | + var identifier: String? |
| 97 | + var booted: Bool |
| 98 | +} |
| 99 | +``` |
| 100 | + |
| 101 | +### Error Handling |
| 102 | +- Define custom errors as enums conforming to `Error` and `LocalizedError` |
| 103 | +- Group related errors in `Service/CustomErrors/` |
| 104 | +- Provide meaningful `errorDescription` for user-facing messages |
| 105 | + |
| 106 | +```swift |
| 107 | +enum DeviceError: Error, Equatable { |
| 108 | + case deviceNotFound |
| 109 | + case xcodeError |
| 110 | + case unexpected(code: Int) |
| 111 | +} |
| 112 | + |
| 113 | +extension DeviceError: LocalizedError { |
| 114 | + public var errorDescription: String? { |
| 115 | + switch self { |
| 116 | + case .deviceNotFound: |
| 117 | + return NSLocalizedString("Selected device was not found...", comment: "") |
| 118 | + } |
| 119 | + } |
| 120 | +} |
| 121 | +``` |
| 122 | + |
| 123 | +### SwiftUI Views |
| 124 | +- Use MVVM pattern with nested `ViewModel` class |
| 125 | +- ViewModels extend from view type (e.g., `CustomCommands.ViewModel`) |
| 126 | +- Use `@Published` for observable state |
| 127 | + |
| 128 | +```swift |
| 129 | +extension CustomCommands { |
| 130 | + class ViewModel: ObservableObject { |
| 131 | + @Published var commands: [Command] = [] |
| 132 | + @Published var showForm = false |
| 133 | + } |
| 134 | +} |
| 135 | +``` |
| 136 | + |
| 137 | +### Extensions |
| 138 | +- File naming: `TypeName+Feature.swift` (e.g., `UserDefaults+Configuration.swift`) |
| 139 | +- One extension purpose per file |
| 140 | + |
| 141 | +### Factory Pattern |
| 142 | +- Use factory classes for creating platform-specific implementations |
| 143 | +- Follow `ActionFactory` pattern for iOS/Android differentiation |
| 144 | + |
| 145 | +```swift |
| 146 | +class AndroidActionFactory: ActionFactory { |
| 147 | + static func createAction(for tag: SubMenuItems.Tags, ...) -> any Action { |
| 148 | + switch tag { |
| 149 | + case .copyName: return CopyNameAction(device: device) |
| 150 | + } |
| 151 | + } |
| 152 | +} |
| 153 | +``` |
| 154 | + |
| 155 | +### Testing |
| 156 | +- Test files mirror source structure in `MiniSimTests/` |
| 157 | +- Use `@testable import MiniSim` |
| 158 | +- Create stub/mock classes for dependencies (see `Mocks/ShellStub.swift`) |
| 159 | +- Override class methods in nested test classes for mocking |
| 160 | + |
| 161 | +```swift |
| 162 | +class ADBTests: XCTestCase { |
| 163 | + var shellStub: ShellStub! |
| 164 | + |
| 165 | + override func setUp() { |
| 166 | + shellStub = ShellStub() |
| 167 | + ADB.shell = shellStub |
| 168 | + } |
| 169 | +} |
| 170 | +``` |
| 171 | + |
| 172 | +### SwiftLint Rules (Key Enabled) |
| 173 | +- `sorted_imports` - alphabetical import ordering |
| 174 | +- `implicit_return` - omit return in single-expression closures |
| 175 | +- `trailing_closure` - use trailing closure syntax |
| 176 | +- `vertical_whitespace_closing_braces` - no blank lines before closing braces |
| 177 | +- `shorthand_optional_binding` - use `if let x` instead of `if let x = x` |
| 178 | +- `weak_delegate` - delegates should be weak |
| 179 | + |
| 180 | +### Disabled Rules |
| 181 | +- `nesting` - nested types are allowed |
| 182 | + |
| 183 | +### Documentation |
| 184 | +- Add JSDoc-style comments for new public functions |
| 185 | +- Focus on "why" not "what" for complex logic |
| 186 | +- No comments in JSX/SwiftUI describing what renders |
| 187 | + |
| 188 | +### Threading |
| 189 | +- Use `Thread.assertBackgroundThread()` for background-only operations |
| 190 | +- Main thread assertions available via `Thread+Asserts.swift` |
| 191 | + |
| 192 | +## Dependencies (SPM) |
| 193 | +- `KeyboardShortcuts` - Global keyboard shortcuts |
| 194 | +- `LaunchAtLogin` - Login item management |
| 195 | +- `Sparkle` - Auto-updates |
| 196 | +- `ShellOut` - Shell command execution |
| 197 | +- `CodeEditor` - Code editing in preferences |
| 198 | +- `SymbolPicker` - SF Symbol picker |
| 199 | +- `Settings` - Preferences window |
| 200 | +- `AcknowList` - Acknowledgements |
| 201 | +- `SwiftLint` - Linting (build plugin) |
0 commit comments