mirror of
https://github.com/somegeekintn/SimDirs.git
synced 2026-04-27 14:57:40 +00:00
Now we can launch / terminate apps.
This commit is contained in:
parent
762f0b7bc0
commit
018ed0eecc
8 changed files with 146 additions and 21 deletions
|
|
@ -7,7 +7,11 @@
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct SimApp: Equatable {
|
class SimApp: ObservableObject {
|
||||||
|
@Published var state = State.unknown
|
||||||
|
@Published var pid : Int?
|
||||||
|
|
||||||
|
weak var device : SimDevice?
|
||||||
let identifier : String
|
let identifier : String
|
||||||
let bundleID : String
|
let bundleID : String
|
||||||
let bundleName : String
|
let bundleName : String
|
||||||
|
|
@ -18,12 +22,13 @@ struct SimApp: Equatable {
|
||||||
let sandboxPath : String?
|
let sandboxPath : String?
|
||||||
let nsIcon : NSImage?
|
let nsIcon : NSImage?
|
||||||
|
|
||||||
init(bundlePath: URL, sandboxPaths: [String : URL]) throws {
|
init(bundlePath: URL, sandboxPaths: [String : URL], device: SimDevice) throws {
|
||||||
guard let infoPList = PropertyListSerialization.propertyList(from: bundlePath.appendingPathComponent("Info.plist")) else { throw SimError.invalidApp }
|
guard let infoPList = PropertyListSerialization.propertyList(from: bundlePath.appendingPathComponent("Info.plist")) else { throw SimError.invalidApp }
|
||||||
guard let bundleID = infoPList[kCFBundleIdentifierKey as String] as? String else { throw SimError.invalidApp }
|
guard let bundleID = infoPList[kCFBundleIdentifierKey as String] as? String else { throw SimError.invalidApp }
|
||||||
|
|
||||||
self.bundlePath = bundlePath.path
|
self.bundlePath = bundlePath.path
|
||||||
self.bundleID = bundleID
|
self.bundleID = bundleID
|
||||||
|
self.device = device
|
||||||
bundleName = (infoPList[kCFBundleNameKey as String] as? String) ?? "<missing>"
|
bundleName = (infoPList[kCFBundleNameKey as String] as? String) ?? "<missing>"
|
||||||
displayName = (infoPList["CFBundleDisplayName"] as? String) ?? bundleName
|
displayName = (infoPList["CFBundleDisplayName"] as? String) ?? bundleName
|
||||||
version = (infoPList["CFBundleShortVersionString"] as? String) ?? "<missing>"
|
version = (infoPList["CFBundleShortVersionString"] as? String) ?? "<missing>"
|
||||||
|
|
@ -69,6 +74,49 @@ struct SimApp: Equatable {
|
||||||
return validIcon ? icon : nil
|
return validIcon ? icon : nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func discoverState() {
|
||||||
|
Task {
|
||||||
|
let result = try await SimCtl().getAppPID(self)
|
||||||
|
|
||||||
|
await MainActor.run {
|
||||||
|
pid = result
|
||||||
|
state = pid != nil ? .launched : .terminated
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toggleLaunchState() {
|
||||||
|
Task {
|
||||||
|
switch state {
|
||||||
|
case .launched:
|
||||||
|
try SimCtl().terminate(self)
|
||||||
|
|
||||||
|
await MainActor.run {
|
||||||
|
pid = nil
|
||||||
|
state = .terminated
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
let result = try await SimCtl().launch(self)
|
||||||
|
|
||||||
|
await MainActor.run {
|
||||||
|
pid = result
|
||||||
|
state = pid != nil ? .launched : .terminated
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SimApp {
|
||||||
|
enum State: String {
|
||||||
|
case terminated = "terminated"
|
||||||
|
case launched = "launched"
|
||||||
|
case unknown = "unknown"
|
||||||
|
|
||||||
|
var isOn : Bool { self == .launched }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SimApp: SourceItemData {
|
extension SimApp: SourceItemData {
|
||||||
|
|
|
||||||
|
|
@ -133,4 +133,46 @@ struct SimCtl {
|
||||||
func saveVideo(_ device: SimDevice, url: URL) throws -> Process {
|
func saveVideo(_ device: SimDevice, url: URL) throws -> Process {
|
||||||
return try run(args: ["io", device.udid, "recordVideo", "--force", url.path])
|
return try run(args: ["io", device.udid, "recordVideo", "--force", url.path])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getAppPID(_ app: SimApp) async throws -> Int? {
|
||||||
|
guard let device = app.device else { return nil }
|
||||||
|
let list : String = try await runAsync(args: ["spawn", device.udid, "launchctl", "list"])
|
||||||
|
let regex = try NSRegularExpression(pattern: "(?<PID>[0-9]+).*\(app.bundleID)")
|
||||||
|
let nsRange = NSRange(location: 0, length: (list as NSString).length)
|
||||||
|
var pid : Int? = nil
|
||||||
|
|
||||||
|
if let match = regex.firstMatch(in: list, range: nsRange) {
|
||||||
|
let range = match.range(withName: "PID")
|
||||||
|
|
||||||
|
if range.location != NSNotFound {
|
||||||
|
pid = Int((list as NSString).substring(with: range))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pid
|
||||||
|
}
|
||||||
|
|
||||||
|
func launch(_ app: SimApp) async throws -> Int? {
|
||||||
|
guard let device = app.device else { return nil }
|
||||||
|
let output : String = try await runAsync(args: ["launch", device.udid, app.bundleID])
|
||||||
|
let regex = try NSRegularExpression(pattern: ".*: (?<PID>[0-9]+)")
|
||||||
|
let nsRange = NSRange(location: 0, length: (output as NSString).length)
|
||||||
|
var pid : Int? = nil
|
||||||
|
|
||||||
|
if let match = regex.firstMatch(in: output, range: nsRange) {
|
||||||
|
let range = match.range(withName: "PID")
|
||||||
|
|
||||||
|
if range.location != NSNotFound {
|
||||||
|
pid = Int((output as NSString).substring(with: range))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pid
|
||||||
|
}
|
||||||
|
|
||||||
|
func terminate(_ app: SimApp) throws {
|
||||||
|
guard let device = app.device else { return }
|
||||||
|
|
||||||
|
try runAsync(args: ["terminate", device.udid, app.bundleID])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ class SimDevice: ObservableObject, Decodable {
|
||||||
for testDir in testDirs {
|
for testDir in testDirs {
|
||||||
if NSWorkspace.shared.isFilePackage(atPath: testDir.path) {
|
if NSWorkspace.shared.isFilePackage(atPath: testDir.path) {
|
||||||
do {
|
do {
|
||||||
apps.append(try SimApp(bundlePath: testDir, sandboxPaths: sandboxPaths))
|
apps.append(try SimApp(bundlePath: testDir, sandboxPaths: sandboxPaths, device: self))
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
print("Failed to instantiate SimApp at \(testDir.path)")
|
print("Failed to instantiate SimApp at \(testDir.path)")
|
||||||
|
|
|
||||||
|
|
@ -21,15 +21,17 @@ extension ToggleDescriptor {
|
||||||
struct DescriptiveToggle<T: ToggleDescriptor>: View {
|
struct DescriptiveToggle<T: ToggleDescriptor>: View {
|
||||||
@Binding var isOn : Bool
|
@Binding var isOn : Bool
|
||||||
var descriptor : T
|
var descriptor : T
|
||||||
|
var subtitled : Bool
|
||||||
|
|
||||||
init(_ descriptor: T, isOn: Binding<Bool>) {
|
init(_ descriptor: T, isOn: Binding<Bool>, subtitled: Bool = true) {
|
||||||
self._isOn = isOn
|
self._isOn = isOn
|
||||||
self.descriptor = descriptor
|
self.descriptor = descriptor
|
||||||
|
self.subtitled = subtitled
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Toggle(descriptor.titleKey, isOn: _isOn)
|
Toggle(descriptor.titleKey, isOn: _isOn)
|
||||||
.toggleStyle(DescriptiveToggleStyle(descriptor))
|
.toggleStyle(DescriptiveToggleStyle(descriptor, subtitled: subtitled))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,15 +9,26 @@ import SwiftUI
|
||||||
|
|
||||||
extension SimApp {
|
extension SimApp {
|
||||||
public var content : some View { AppContent(app: self) }
|
public var content : some View { AppContent(app: self) }
|
||||||
|
|
||||||
|
var isLaunched : Bool {
|
||||||
|
get { state.isOn }
|
||||||
|
set { toggleLaunchState() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SimApp.State: ToggleDescriptor {
|
||||||
|
var titleKey : LocalizedStringKey { isOn ? "Terminate" : "Launch" }
|
||||||
|
var text : String { isOn ? "Launched" : "Terminated" }
|
||||||
|
var image : Image { Image(systemName: "power.circle") }
|
||||||
}
|
}
|
||||||
|
|
||||||
struct AppContent: View {
|
struct AppContent: View {
|
||||||
var app : SimApp
|
@ObservedObject var app : SimApp
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 0.0) {
|
VStack(alignment: .leading, spacing: 0.0) {
|
||||||
ContentHeader("Paths")
|
ContentHeader("Paths")
|
||||||
|
Group {
|
||||||
PathRow(title: "Bundle Path", path: app.bundlePath)
|
PathRow(title: "Bundle Path", path: app.bundlePath)
|
||||||
if let sandboxPath = app.sandboxPath {
|
if let sandboxPath = app.sandboxPath {
|
||||||
PathRow(title: "Sandbox Path", path: sandboxPath)
|
PathRow(title: "Sandbox Path", path: sandboxPath)
|
||||||
|
|
@ -28,6 +39,17 @@ struct AppContent: View {
|
||||||
}
|
}
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
|
|
||||||
|
ContentHeader("Actions")
|
||||||
|
HStack(spacing: 16) {
|
||||||
|
DescriptiveToggle(app.state, isOn: $app.isLaunched, subtitled: false)
|
||||||
|
.frame(width: 58)
|
||||||
|
}
|
||||||
|
.environment(\.isEnabled, app.device?.isBooted == true)
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
app.discoverState()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ struct AppHeader: View {
|
||||||
Text("Minimum OS Version: \(app.minOSVersion)")
|
Text("Minimum OS Version: \(app.minOSVersion)")
|
||||||
}
|
}
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
|
.textSelection(.enabled)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,15 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct DescriptiveToggleStyle<T: ToggleDescriptor>: ToggleStyle {
|
struct DescriptiveToggleStyle<T: ToggleDescriptor>: ToggleStyle {
|
||||||
var descriptor : T
|
@Environment(\.isEnabled) var isEnabled
|
||||||
|
@State var isFocused = false
|
||||||
|
|
||||||
init(_ descriptor: T) {
|
var descriptor : T
|
||||||
|
var subtitled : Bool
|
||||||
|
|
||||||
|
init(_ descriptor: T, subtitled: Bool = true) {
|
||||||
self.descriptor = descriptor
|
self.descriptor = descriptor
|
||||||
|
self.subtitled = subtitled
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeBody(configuration: Configuration) -> some View {
|
func makeBody(configuration: Configuration) -> some View {
|
||||||
|
|
@ -19,7 +24,7 @@ struct DescriptiveToggleStyle<T: ToggleDescriptor>: ToggleStyle {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
ZStack {
|
ZStack {
|
||||||
Circle()
|
Circle()
|
||||||
.foregroundColor(descriptor.circleColor)
|
.foregroundColor(descriptor.circleColor.opacity(isFocused ? 1.0 : 0.9))
|
||||||
descriptor.image
|
descriptor.image
|
||||||
.resizable()
|
.resizable()
|
||||||
.foregroundColor(descriptor.isOn ? .white : Color("CircleSymbolOff"))
|
.foregroundColor(descriptor.isOn ? .white : Color("CircleSymbolOff"))
|
||||||
|
|
@ -32,14 +37,18 @@ struct DescriptiveToggleStyle<T: ToggleDescriptor>: ToggleStyle {
|
||||||
Group {
|
Group {
|
||||||
Text(descriptor.titleKey)
|
Text(descriptor.titleKey)
|
||||||
.fontWeight(.semibold)
|
.fontWeight(.semibold)
|
||||||
|
|
||||||
|
if subtitled {
|
||||||
Text(descriptor.text)
|
Text(descriptor.text)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.font(.system(size: 11))
|
.font(.system(size: 11))
|
||||||
.allowsTightening(true)
|
.allowsTightening(true)
|
||||||
.minimumScaleFactor(0.5)
|
.minimumScaleFactor(0.5)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
}
|
}
|
||||||
|
.onHover { isFocused = $0 && isEnabled }
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ extension ButtonStyle where Self == SystemIconButtonStyle {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SystemIconButtonStyle: ButtonStyle {
|
struct SystemIconButtonStyle: ButtonStyle {
|
||||||
|
@Environment(\.isEnabled) var isEnabled
|
||||||
@State var isFocused = false
|
@State var isFocused = false
|
||||||
let isActive : Bool
|
let isActive : Bool
|
||||||
let imageName : String
|
let imageName : String
|
||||||
|
|
@ -41,15 +42,15 @@ struct SystemIconButtonStyle: ButtonStyle {
|
||||||
.foregroundColor(foregroundColor(pressed: configuration.isPressed))
|
.foregroundColor(foregroundColor(pressed: configuration.isPressed))
|
||||||
}
|
}
|
||||||
.padding(1.0)
|
.padding(1.0)
|
||||||
.onHover { isFocused = $0 }
|
.onHover { isFocused = $0 && isEnabled }
|
||||||
}
|
}
|
||||||
|
|
||||||
func foregroundColor(pressed: Bool) -> Color {
|
func foregroundColor(pressed: Bool) -> Color {
|
||||||
return .primary.opacity(pressed ? 1.0 : 0.6)
|
return .primary.opacity(pressed ? 1.0 : (isEnabled ? 0.7 : 0.6))
|
||||||
}
|
}
|
||||||
|
|
||||||
func backgroundColor(pressed: Bool) -> Color {
|
func backgroundColor(pressed: Bool) -> Color {
|
||||||
return .primary.opacity(0.05 + (isFocused ? 0.1 : 0.0) + (pressed ? 0.1 : 0.0))
|
return .primary.opacity((isEnabled ? 0.1 : 0.05) + (isFocused ? 0.1 : 0.0) + (pressed ? 0.1 : 0.0))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue