mirror of
https://github.com/somegeekintn/SimDirs.git
synced 2026-04-26 14:47:41 +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
|
||||
|
||||
struct SimApp: Equatable {
|
||||
class SimApp: ObservableObject {
|
||||
@Published var state = State.unknown
|
||||
@Published var pid : Int?
|
||||
|
||||
weak var device : SimDevice?
|
||||
let identifier : String
|
||||
let bundleID : String
|
||||
let bundleName : String
|
||||
|
|
@ -18,12 +22,13 @@ struct SimApp: Equatable {
|
|||
let sandboxPath : String?
|
||||
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 bundleID = infoPList[kCFBundleIdentifierKey as String] as? String else { throw SimError.invalidApp }
|
||||
|
||||
self.bundlePath = bundlePath.path
|
||||
self.bundleID = bundleID
|
||||
self.device = device
|
||||
bundleName = (infoPList[kCFBundleNameKey as String] as? String) ?? "<missing>"
|
||||
displayName = (infoPList["CFBundleDisplayName"] as? String) ?? bundleName
|
||||
version = (infoPList["CFBundleShortVersionString"] as? String) ?? "<missing>"
|
||||
|
|
@ -69,6 +74,49 @@ struct SimApp: Equatable {
|
|||
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 {
|
||||
|
|
|
|||
|
|
@ -133,4 +133,46 @@ struct SimCtl {
|
|||
func saveVideo(_ device: SimDevice, url: URL) throws -> Process {
|
||||
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 {
|
||||
if NSWorkspace.shared.isFilePackage(atPath: testDir.path) {
|
||||
do {
|
||||
apps.append(try SimApp(bundlePath: testDir, sandboxPaths: sandboxPaths))
|
||||
apps.append(try SimApp(bundlePath: testDir, sandboxPaths: sandboxPaths, device: self))
|
||||
}
|
||||
catch {
|
||||
print("Failed to instantiate SimApp at \(testDir.path)")
|
||||
|
|
|
|||
|
|
@ -21,15 +21,17 @@ extension ToggleDescriptor {
|
|||
struct DescriptiveToggle<T: ToggleDescriptor>: View {
|
||||
@Binding var isOn : Bool
|
||||
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.descriptor = descriptor
|
||||
self.subtitled = subtitled
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Toggle(descriptor.titleKey, isOn: _isOn)
|
||||
.toggleStyle(DescriptiveToggleStyle(descriptor))
|
||||
.toggleStyle(DescriptiveToggleStyle(descriptor, subtitled: subtitled))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,25 +9,47 @@ import SwiftUI
|
|||
|
||||
extension SimApp {
|
||||
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 {
|
||||
var app : SimApp
|
||||
@ObservedObject var app : SimApp
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 0.0) {
|
||||
ContentHeader("Paths")
|
||||
Group {
|
||||
PathRow(title: "Bundle Path", path: app.bundlePath)
|
||||
if let sandboxPath = app.sandboxPath {
|
||||
PathRow(title: "Sandbox Path", path: sandboxPath)
|
||||
}
|
||||
else {
|
||||
Text("Sandbox Path: <unknown>")
|
||||
}
|
||||
}
|
||||
.font(.subheadline)
|
||||
.lineLimit(1)
|
||||
|
||||
PathRow(title: "Bundle Path", path: app.bundlePath)
|
||||
if let sandboxPath = app.sandboxPath {
|
||||
PathRow(title: "Sandbox Path", path: sandboxPath)
|
||||
}
|
||||
else {
|
||||
Text("Sandbox Path: <unknown>")
|
||||
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()
|
||||
}
|
||||
.font(.subheadline)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ struct AppHeader: View {
|
|||
Text("Minimum OS Version: \(app.minOSVersion)")
|
||||
}
|
||||
.font(.subheadline)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,10 +8,15 @@
|
|||
import SwiftUI
|
||||
|
||||
struct DescriptiveToggleStyle<T: ToggleDescriptor>: ToggleStyle {
|
||||
@Environment(\.isEnabled) var isEnabled
|
||||
@State var isFocused = false
|
||||
|
||||
var descriptor : T
|
||||
var subtitled : Bool
|
||||
|
||||
init(_ descriptor: T) {
|
||||
init(_ descriptor: T, subtitled: Bool = true) {
|
||||
self.descriptor = descriptor
|
||||
self.subtitled = subtitled
|
||||
}
|
||||
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
|
|
@ -19,7 +24,7 @@ struct DescriptiveToggleStyle<T: ToggleDescriptor>: ToggleStyle {
|
|||
VStack(spacing: 0) {
|
||||
ZStack {
|
||||
Circle()
|
||||
.foregroundColor(descriptor.circleColor)
|
||||
.foregroundColor(descriptor.circleColor.opacity(isFocused ? 1.0 : 0.9))
|
||||
descriptor.image
|
||||
.resizable()
|
||||
.foregroundColor(descriptor.isOn ? .white : Color("CircleSymbolOff"))
|
||||
|
|
@ -32,14 +37,18 @@ struct DescriptiveToggleStyle<T: ToggleDescriptor>: ToggleStyle {
|
|||
Group {
|
||||
Text(descriptor.titleKey)
|
||||
.fontWeight(.semibold)
|
||||
Text(descriptor.text)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
if subtitled {
|
||||
Text(descriptor.text)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.font(.system(size: 11))
|
||||
.allowsTightening(true)
|
||||
.minimumScaleFactor(0.5)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
.onHover { isFocused = $0 && isEnabled }
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ extension ButtonStyle where Self == SystemIconButtonStyle {
|
|||
}
|
||||
|
||||
struct SystemIconButtonStyle: ButtonStyle {
|
||||
@Environment(\.isEnabled) var isEnabled
|
||||
@State var isFocused = false
|
||||
let isActive : Bool
|
||||
let imageName : String
|
||||
|
|
@ -41,15 +42,15 @@ struct SystemIconButtonStyle: ButtonStyle {
|
|||
.foregroundColor(foregroundColor(pressed: configuration.isPressed))
|
||||
}
|
||||
.padding(1.0)
|
||||
.onHover { isFocused = $0 }
|
||||
.onHover { isFocused = $0 && isEnabled }
|
||||
}
|
||||
|
||||
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 {
|
||||
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