From ace47c981abd48d9bb61c0ed5018cb8e513587b7 Mon Sep 17 00:00:00 2001 From: Le Trung Son Date: Thu, 30 Apr 2026 22:55:01 +0700 Subject: [PATCH 1/6] feat: Add Start iOS Simulator option --- lib/src/services/device_service.dart | 71 ++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/lib/src/services/device_service.dart b/lib/src/services/device_service.dart index 4710b07..bba6ec3 100644 --- a/lib/src/services/device_service.dart +++ b/lib/src/services/device_service.dart @@ -21,6 +21,8 @@ class DeviceService { DeviceService(this.context, this.statusBar, this.daemonService); + bool get _isMacOS => io.Platform.isMacOS; + Future init() async { _deviceAddedSub = daemonService.onDeviceAdded.listen((device) { if (!_devices.any((d) => d['id'] == device['id'])) { @@ -74,6 +76,8 @@ class DeviceService { } Future selectDevice([Map? position]) async { + final hasIosSimulatorDevice = _devices.any(_isIosSimulatorDevice); + // Show cached devices immediately + Refresh option final items = _devices.map((d) { final icon = _getDeviceIcon(d); @@ -90,6 +94,15 @@ class DeviceService { // Add divider/refresh option items.add(const QuickPickItem(label: '', isSeparator: true)); + if (_isMacOS && !hasIosSimulatorDevice) { + items.add(const QuickPickItem( + label: 'Start iOS Simulator', + detail: 'Launch Apple Simulator app', + payload: 'start-ios-simulator', + icon: iconPlay, + )); + } + items.add(const QuickPickItem( label: 'Refresh Devices...', detail: 'Scan for connected devices', @@ -109,6 +122,8 @@ class DeviceService { await context.window.showMessage('Scanning for connected devices'); await refreshDevices(); await selectDevice(position); // Re-open picker + } else if (payload == 'start-ios-simulator') { + await _startIosSimulator(); } else { _selectedDeviceId = payload; await _updateToolbar(); @@ -116,6 +131,62 @@ class DeviceService { } } + Future _startIosSimulator() async { + if (!_isMacOS) { + return; + } + + await context.window.showMessage('Starting iOS Simulator...'); + + try { + final result = await context.shell.run( + 'sh', + ['-lc', 'open -a Simulator'], + ); + if (result.exitCode != 0) { + final stderr = result.stderr.toString().trim(); + await context.window.showMessage( + stderr.isNotEmpty + ? 'Failed to start iOS Simulator: $stderr' + : 'Failed to start iOS Simulator.', + type: MessageType.error, + ); + return; + } + // Give Simulator a moment so daemon refresh can discover it. + await Future.delayed(const Duration(seconds: 2)); + await refreshDevices(); + + // Auto-select simulator so toolbar switches away from macOS host. + for (final device in _devices) { + if (_isIosSimulatorDevice(device) && device['id'] != null) { + _selectedDeviceId = device['id'] as String; + break; + } + } + await _updateToolbar(); + } catch (e) { + await context.window.showMessage( + 'Failed to start iOS Simulator: $e', + type: MessageType.error, + ); + } + } + + bool _isIosSimulatorDevice(Map device) { + final category = device['category']?.toString().toLowerCase() ?? ''; + final platformType = device['platformType']?.toString().toLowerCase() ?? ''; + final platform = device['platform']?.toString().toLowerCase() ?? ''; + final targetPlatform = + device['targetPlatform']?.toString().toLowerCase() ?? ''; + + final iosLikePlatform = + platform == 'ios' || targetPlatform.startsWith('ios'); + + return (category == 'mobile' || platformType == 'mobile') && + iosLikePlatform; + } + Future _updateToolbar() async { if (_isLoading) { await context.toolbar.registerItem( From 44eda0ef73656ea137557a43972208c3d6ef4e7f Mon Sep 17 00:00:00 2001 From: Le Trung Son Date: Thu, 30 Apr 2026 23:01:41 +0700 Subject: [PATCH 2/6] feat: Enhance device sorting --- lib/src/services/device_service.dart | 41 +++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/lib/src/services/device_service.dart b/lib/src/services/device_service.dart index bba6ec3..7875289 100644 --- a/lib/src/services/device_service.dart +++ b/lib/src/services/device_service.dart @@ -55,11 +55,16 @@ class DeviceService { await _updateToolbar(); try { - final devices = await daemonService.getDevices(); + final devices = + List>.from(await daemonService.getDevices()) + ..sort((a, b) => _deviceSortRank(a).compareTo(_deviceSortRank(b))); _devices = List>.from(devices); if (_devices.isNotEmpty) { - if (_selectedDeviceId == null || + if (!_isInitialized) { + // During startup, prefer the best-ranked device from the refreshed list. + _selectedDeviceId = _devices.first['id'] as String?; + } else if (_selectedDeviceId == null || !_devices.any((d) => d['id'] == _selectedDeviceId)) { _selectedDeviceId = _devices.first['id']; } @@ -76,10 +81,13 @@ class DeviceService { } Future selectDevice([Map? position]) async { - final hasIosSimulatorDevice = _devices.any(_isIosSimulatorDevice); + final devices = List>.from(_devices) + ..sort((a, b) => _deviceSortRank(a).compareTo(_deviceSortRank(b))); + + final hasIosSimulatorDevice = devices.any(_isIosSimulatorDevice); // Show cached devices immediately + Refresh option - final items = _devices.map((d) { + final items = devices.map((d) { final icon = _getDeviceIcon(d); return QuickPickItem( @@ -173,6 +181,16 @@ class DeviceService { } } + int _deviceSortRank(Map device) { + if (_isMacOS && _isIosSimulatorDevice(device)) { + return 0; + } + if (_isMacOS && _isMacOsDesktopDevice(device)) { + return 1; + } + return 2; + } + bool _isIosSimulatorDevice(Map device) { final category = device['category']?.toString().toLowerCase() ?? ''; final platformType = device['platformType']?.toString().toLowerCase() ?? ''; @@ -187,6 +205,21 @@ class DeviceService { iosLikePlatform; } + bool _isMacOsDesktopDevice(Map device) { + final category = device['category']?.toString().toLowerCase() ?? ''; + final platformType = device['platformType']?.toString().toLowerCase() ?? ''; + final platform = device['platform']?.toString().toLowerCase() ?? ''; + final targetPlatform = + device['targetPlatform']?.toString().toLowerCase() ?? ''; + + return category == 'desktop' && + (platformType == 'desktop' || + platform == 'darwin' || + platform == 'macos' || + targetPlatform.startsWith('darwin') || + targetPlatform.startsWith('macos')); + } + Future _updateToolbar() async { if (_isLoading) { await context.toolbar.registerItem( From bbf36e640aed171b26df79ff091e260846b957b9 Mon Sep 17 00:00:00 2001 From: Le Trung Son Date: Fri, 1 May 2026 15:22:30 +0700 Subject: [PATCH 3/6] fix: not auto select iOS Simulator --- lib/src/services/device_service.dart | 77 ++++++++++++++++++++++++---- 1 file changed, 66 insertions(+), 11 deletions(-) diff --git a/lib/src/services/device_service.dart b/lib/src/services/device_service.dart index 7875289..ac6c10e 100644 --- a/lib/src/services/device_service.dart +++ b/lib/src/services/device_service.dart @@ -161,18 +161,13 @@ class DeviceService { ); return; } - // Give Simulator a moment so daemon refresh can discover it. - await Future.delayed(const Duration(seconds: 2)); - await refreshDevices(); - - // Auto-select simulator so toolbar switches away from macOS host. - for (final device in _devices) { - if (_isIosSimulatorDevice(device) && device['id'] != null) { - _selectedDeviceId = device['id'] as String; - break; - } + final selected = await _waitAndSelectIosSimulator(); + if (!selected) { + await context.window.showMessage( + 'Simulator started, but no iOS simulator device was detected yet. Try Refresh Devices.', + type: MessageType.warning, + ); } - await _updateToolbar(); } catch (e) { await context.window.showMessage( 'Failed to start iOS Simulator: $e', @@ -181,6 +176,66 @@ class DeviceService { } } + Future _waitAndSelectIosSimulator() async { + await refreshDevices(); + + final existing = _findFirstIosSimulatorDevice(_devices); + if (_trySelectDevice(existing)) { + await _updateToolbar(); + return true; + } + + final completer = Completer?>(); + late final StreamSubscription> addedSub; + addedSub = daemonService.onDeviceAdded.listen((device) { + if (!completer.isCompleted && _isIosSimulatorDevice(device)) { + completer.complete(device); + } + }); + + try { + final addedDevice = await completer.future.timeout( + const Duration(seconds: 20), + onTimeout: () => null, + ); + + await refreshDevices(); + + if (_trySelectDevice(addedDevice)) { + await _updateToolbar(); + return true; + } + + final detected = _findFirstIosSimulatorDevice(_devices); + if (_trySelectDevice(detected)) { + await _updateToolbar(); + return true; + } + } finally { + await addedSub.cancel(); + } + + return false; + } + + Map? _findFirstIosSimulatorDevice( + List> devices) { + for (final device in devices) { + if (_isIosSimulatorDevice(device)) { + return device; + } + } + return null; + } + + bool _trySelectDevice(Map? device) { + if (device == null) return false; + final id = device['id']; + if (id is! String || id.isEmpty) return false; + _selectedDeviceId = id; + return true; + } + int _deviceSortRank(Map device) { if (_isMacOS && _isIosSimulatorDevice(device)) { return 0; From 445fdccc4e76a51e2c85d52f4a8cf2c4af1fdbfc Mon Sep 17 00:00:00 2001 From: Simon Pham Date: Sat, 2 May 2026 12:10:20 +0700 Subject: [PATCH 4/6] =?UTF-8?q?=F0=9F=A9=B9=20Fix=20real=20devices=20mista?= =?UTF-8?q?kenly=20treated=20as=20simulator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/src/services/device_service.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/src/services/device_service.dart b/lib/src/services/device_service.dart index ac6c10e..5ea4e7e 100644 --- a/lib/src/services/device_service.dart +++ b/lib/src/services/device_service.dart @@ -247,6 +247,7 @@ class DeviceService { } bool _isIosSimulatorDevice(Map device) { + final isEmulator = device['emulator'] == true; final category = device['category']?.toString().toLowerCase() ?? ''; final platformType = device['platformType']?.toString().toLowerCase() ?? ''; final platform = device['platform']?.toString().toLowerCase() ?? ''; @@ -256,7 +257,8 @@ class DeviceService { final iosLikePlatform = platform == 'ios' || targetPlatform.startsWith('ios'); - return (category == 'mobile' || platformType == 'mobile') && + return isEmulator && + (category == 'mobile' || platformType == 'mobile') && iosLikePlatform; } From 0322c301a86d97fd962d235a267d1d9ba99d2379 Mon Sep 17 00:00:00 2001 From: Simon Pham Date: Sat, 2 May 2026 12:37:30 +0700 Subject: [PATCH 5/6] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Remove=20redundant=20`?= =?UTF-8?q?sh`=20and=20`bash`=20from=20plugin's=20shell=20permissions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/src/services/device_service.dart | 4 ++-- plugin.yaml | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/src/services/device_service.dart b/lib/src/services/device_service.dart index 5ea4e7e..1fd4878 100644 --- a/lib/src/services/device_service.dart +++ b/lib/src/services/device_service.dart @@ -148,8 +148,8 @@ class DeviceService { try { final result = await context.shell.run( - 'sh', - ['-lc', 'open -a Simulator'], + 'open', + ['-a', 'Simulator'], ); if (result.exitCode != 0) { final stderr = result.stderr.toString().trim(); diff --git a/plugin.yaml b/plugin.yaml index 2c81551..9fd577a 100644 --- a/plugin.yaml +++ b/plugin.yaml @@ -13,8 +13,7 @@ permissions: - flutter - dart - fvm - - sh - - bash + - open configuration: - key: flutter.hotReloadOnSave From 7dc45d7ae94d0895459e7faca4e6cc7f36147bed Mon Sep 17 00:00:00 2001 From: Simon Pham Date: Sat, 2 May 2026 12:42:57 +0700 Subject: [PATCH 6/6] =?UTF-8?q?=F0=9F=93=9D=20Add=20changelog=20for=20vers?= =?UTF-8?q?ion=201.6.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a68a29..15a5ff8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 1.6.0 (2026-05-02) + +### 🚀 New Features +- Add an option to Start iOS Simulator in Device Picker UI (many thanks to [@chungxon](https://github.com/chungxon) for your contribution). + ## 1.5.0 (2026-04-22) _This release requires Lumide version >= 0.2.0._