Skip to content

Commit

Permalink
added power action and open in Finder to context menu
Browse files Browse the repository at this point in the history
  • Loading branch information
ikorich committed Apr 26, 2024
1 parent 79ec837 commit ccfb2a2
Show file tree
Hide file tree
Showing 5 changed files with 157 additions and 99 deletions.
4 changes: 2 additions & 2 deletions ControlRoom.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,8 @@
511BA58F23F4030D00E3E660 /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = "<group>"; };
511BA59123F4031F00E3E660 /* ControlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlView.swift; sourceTree = "<group>"; };
511BA59523F408F800E3E660 /* LoadingFailedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingFailedView.swift; sourceTree = "<group>"; };
511BA59723F4096800E3E660 /* Simulator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Simulator.swift; sourceTree = "<group>"; };
511BA59B23F4172400E3E660 /* SystemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemView.swift; sourceTree = "<group>"; };
511BA59723F4096800E3E660 /* Simulator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Simulator.swift; sourceTree = "<group>"; wrapsLines = 1; };
511BA59B23F4172400E3E660 /* SystemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemView.swift; sourceTree = "<group>"; usesTabs = 1; };
511BA59F23F4197200E3E660 /* StatusBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusBarView.swift; sourceTree = "<group>"; };
511BA5A123F41A5900E3E660 /* Binding-OnChange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Binding-OnChange.swift"; sourceTree = "<group>"; };
511BA5A723F42EE400E3E660 /* AppView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppView.swift; sourceTree = "<group>"; };
Expand Down
187 changes: 111 additions & 76 deletions ControlRoom/Controllers/Simulator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ typealias Runtime = SimCtl.Runtime
typealias DeviceType = SimCtl.DeviceType

/// Stores one simulator and its identifier.
struct Simulator: Identifiable, Comparable, Hashable {
struct Simulator: Identifiable, Comparable {
enum State {
case unknown
case creating
Expand All @@ -37,6 +37,28 @@ struct Simulator: Identifiable, Comparable, Hashable {
self = .unknown
}
}

var menuActionName: String {
switch self {
case .unknown: ""
case .creating: "Creating..."
case .booting: "Booting..."
case .booted: "Shutdown"
case .shuttingDown: "Shutting Down..."
case .shutdown: "Boot"
}
}

var isActionAllowed: Bool {
switch self {
case .unknown: false
case .creating: false
case .booting: false
case .booted: true
case .shuttingDown: false
case .shutdown: true
}
}
}

/// The user-facing name for this simulator, e.g. iPhone 11 Pro Max.
Expand Down Expand Up @@ -64,7 +86,7 @@ struct Simulator: Identifiable, Comparable, Hashable {
let deviceType: DeviceType?

/// The current state of the simulator
let state: State
private(set) var state: State

/// Wheter this simulator is the `Default` one or not
var isDefault: Bool {
Expand Down Expand Up @@ -100,81 +122,91 @@ struct Simulator: Identifiable, Comparable, Hashable {
self.image = typeIdentifier.icon
}

func urlForFilePath(_ filePath: FilePathKind) -> URL {

if filePath == .root {
return URL(fileURLWithPath: dataPath)
}

let containerPath = dataPath + "/Containers/Shared/AppGroup/"

guard let containerContents = try? FileManager.default.contentsOfDirectory(atPath: containerPath) else {
print("could not find any subfolders in '\(containerPath)'")
return URL(fileURLWithPath: "")
}

for content in containerContents {

if content.hasSuffix("DS_Store") { continue }

let subDirectoryPath = containerPath + content
let plistUrl = URL(fileURLWithPath: subDirectoryPath)

guard let subDirectoryContents = try? FileManager.default.contentsOfDirectory(atPath: subDirectoryPath),
let plistFile = subDirectoryContents.first(where: { $0.hasSuffix("plist")}),
let plistData = try? Data(contentsOf: plistUrl.appendingPathComponent(plistFile)),
let plist = try? PropertyListSerialization.propertyList(from: plistData, options: [], format: nil) as? NSDictionary else {
print("could not find or decode the plist file in '\(subDirectoryPath)'")
return URL(fileURLWithPath: "")
}

for value in plist.allValues {
if let value = value as? String {
if value.hasSuffix(filePath.storageType) {
return URL(fileURLWithPath: subDirectoryPath).appendingPathComponent("File Provider Storage", isDirectory: true)
}
}
}
}
print("could not find folder of type '\(filePath)' in '\(containerPath)'")
return URL(fileURLWithPath: "")
}

func copyFilesFromProviders(_ providers: [NSItemProvider], toFilePath filePath: FilePathKind) -> Bool {
for provider in providers {
provider.loadItem(forTypeIdentifier: "public.file-url", options: nil) { (urlData, error) in
if let urlData = urlData as? Data {
let sourceUrl = NSURL(absoluteURLWithDataRepresentation: urlData, relativeTo: nil) as URL
do {
try FileManager.default.copyItem(at: sourceUrl, to: urlForFilePath(filePath).appendingPathComponent(sourceUrl.lastPathComponent))
NSSound(named: "Glass")?.play()
} catch {
NSSound(named: "Sosumi")?.play()
print(error.localizedDescription)
}
} else {
NSSound(named: "Sosumi")?.play()
}
sleep(1) // if multiple files are dropped, allow user to distinguish success/error sounds
}
}
return true
}

enum FilePathKind {
case root, files // photos is complicated, and you can't just drop files there anyway

var storageType: String {
switch self {
case .root:
print("Storage type is not applicable to the root path")
return ""
case .files:
return "LocalStorage"
}
}
}
mutating func update(state: State) {
self.state = state
}

func open(_ filePath: FilePathKind) {
NSWorkspace.shared.activateFileViewerSelecting([urlForFilePath(filePath)])
}

func urlForFilePath(_ filePath: FilePathKind) -> URL {

if filePath == .root {
return URL(fileURLWithPath: dataPath)
}

let containerPath = dataPath + "/Containers/Shared/AppGroup/"

guard let containerContents = try? FileManager.default.contentsOfDirectory(atPath: containerPath) else {
print("could not find any subfolders in '\(containerPath)'")
return URL(fileURLWithPath: "")
}

for content in containerContents {

if content.hasSuffix("DS_Store") { continue }

let subDirectoryPath = containerPath + content
let plistUrl = URL(fileURLWithPath: subDirectoryPath)

guard let subDirectoryContents = try? FileManager.default.contentsOfDirectory(atPath: subDirectoryPath),
let plistFile = subDirectoryContents.first(where: { $0.hasSuffix("plist")}),
let plistData = try? Data(contentsOf: plistUrl.appendingPathComponent(plistFile)),
let plist = try? PropertyListSerialization.propertyList(from: plistData, options: [], format: nil) as? NSDictionary else {
print("could not find or decode the plist file in '\(subDirectoryPath)'")
return URL(fileURLWithPath: "")
}

for value in plist.allValues {
if let value = value as? String {
if value.hasSuffix(filePath.storageType) {
return URL(fileURLWithPath: subDirectoryPath).appendingPathComponent("File Provider Storage", isDirectory: true)
}
}
}
}
print("could not find folder of type '\(filePath)' in '\(containerPath)'")
return URL(fileURLWithPath: "")
}

func copyFilesFromProviders(_ providers: [NSItemProvider], toFilePath filePath: FilePathKind) -> Bool {
for provider in providers {
provider.loadItem(forTypeIdentifier: "public.file-url", options: nil) { (urlData, error) in
if let urlData = urlData as? Data {
let sourceUrl = NSURL(absoluteURLWithDataRepresentation: urlData, relativeTo: nil) as URL
do {
try FileManager.default.copyItem(at: sourceUrl, to: urlForFilePath(filePath).appendingPathComponent(sourceUrl.lastPathComponent))
NSSound(named: "Glass")?.play()
} catch {
NSSound(named: "Sosumi")?.play()
print(error.localizedDescription)
}
} else {
NSSound(named: "Sosumi")?.play()
}
sleep(1) // if multiple files are dropped, allow user to distinguish success/error sounds
}
}
return true
}

enum FilePathKind {
case root, files // photos is complicated, and you can't just drop files there anyway

var storageType: String {
switch self {
case .root:
print("Storage type is not applicable to the root path")
return ""
case .files:
return "LocalStorage"
}
}
}
}

extension Simulator: Hashable {
/// Sort simulators alphabetically, and then by OS version.
static func < (lhs: Simulator, rhs: Simulator) -> Bool {
if lhs.name == rhs.name,
Expand All @@ -184,7 +216,10 @@ struct Simulator: Identifiable, Comparable, Hashable {
}
return lhs.name < rhs.name
}
}

/// Preview
extension Simulator {
/// An example simulator for Xcode preview purposes
static let example = Simulator(name: "iPhone 11 Pro max", udid: UUID().uuidString, state: .booted, runtime: .unknown, deviceType: nil, dataPath: "<example data path>")

Expand Down
26 changes: 17 additions & 9 deletions ControlRoom/Main Window/SimulatorAction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,41 @@
import struct SwiftUI.LocalizedStringKey

enum Action: Int, Identifiable {
case power
case rename
case clone
case delete
case openRoot

var id: Int { rawValue }

var sheetTitle: LocalizedStringKey {
switch self {
case .rename: return "Rename Simulator"
case .clone: return "Clone Simulator"
case .delete: return "Delete Simulator"
case .power: ""
case .rename: "Rename Simulator"
case .clone: "Clone Simulator"
case .delete: "Delete Simulator"
case .openRoot: ""
}
}

var sheetMessage: LocalizedStringKey {
switch self {
case .rename: return "Enter a new name for this simulator. It may be the same as the name of an existing simulator, but a unique name will make it easier to identify."
case .clone: return "Enter a name for the new simulator. It may be the same as the name of an existing simulator, but a unique name will make it easier to identify."
case .delete: return "Are you sure you want to delete this simulator? You will not be able to undo this action."
case .power: ""
case .rename: "Enter a new name for this simulator. It may be the same as the name of an existing simulator, but a unique name will make it easier to identify."
case .clone: "Enter a name for the new simulator. It may be the same as the name of an existing simulator, but a unique name will make it easier to identify."
case .delete: "Are you sure you want to delete this simulator? You will not be able to undo this action."
case .openRoot: ""
}
}

var saveActionTitle: LocalizedStringKey {
switch self {
case .rename: return "Rename"
case .clone: return "Clone"
case .delete: return "Delete"
case .power: "Power"
case .rename: "Rename"
case .clone: "Clone"
case .delete: "Delete"
case .openRoot: ""
}
}
}
37 changes: 26 additions & 11 deletions ControlRoom/Main Window/SimulatorSidebarView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import KeyboardShortcuts

/// Shows one simulator in the sidebar.
struct SimulatorSidebarView: View {
let simulator: Simulator
var simulator: Simulator
let canShowContextualMenu: Bool

@State private var action: Action?
Expand Down Expand Up @@ -56,26 +56,26 @@ struct SimulatorSidebarView: View {
.padding(.top, 2)
.shadow(color: .primary, radius: 1)
Text(simulatorSummary)
Spacer()
}
.frame(alignment: .leading)
.contextMenu(
ContextMenu(shouldDisplay: canShowContextualMenu) {
Button("\(simulator.state.menuActionName)") { performAction(.power) }
.disabled(!simulator.state.isActionAllowed)
Divider()
Button("Rename...") { action = .rename }
Button("Clone...") { action = .clone }
.disabled(simulator.state == .booted)
Button("Delete...") { action = .delete }
Divider()
Button("Open in Finder") { performAction(.openRoot) }
}
)
.sheet(item: $action) { action in
if action == .delete {
SimulatorActionSheet(
icon: simulator.image,
message: action.sheetTitle,
informativeText: action.sheetMessage,
confirmationTitle: action.saveActionTitle,
confirm: { performAction(action) }
)
} else {
switch action {
case .power, .openRoot:
EmptyView()
case .rename, .clone:
SimulatorActionSheet(
icon: simulator.image,
message: action.sheetTitle,
Expand All @@ -87,6 +87,13 @@ struct SimulatorSidebarView: View {
TextField("Name", text: $newName)
}
)
case .delete:
SimulatorActionSheet(
icon: simulator.image,
message: action.sheetTitle,
informativeText: action.sheetMessage,
confirmationTitle: action.saveActionTitle,
confirm: { performAction(action) })
}
}
}
Expand All @@ -98,6 +105,14 @@ struct SimulatorSidebarView: View {
case .rename: SimCtl.rename(simulator.udid, name: newName)
case .clone: SimCtl.clone(simulator.udid, name: newName)
case .delete: SimCtl.delete([simulator.udid])
case .power:
if simulator.state == .booted {
SimCtl.shutdown(simulator.udid)
} else if simulator.state == .shutdown {
SimCtl.boot(simulator)
}
case .openRoot:
simulator.open(.root)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ struct SystemView: View {
}

func openInFinder(_ filePath: Simulator.FilePathKind) {
NSWorkspace.shared.activateFileViewerSelecting([simulator.urlForFilePath(filePath)])
simulator.open(filePath)
}

func openInTerminal(_ filePath: Simulator.FilePathKind) {
Expand Down

0 comments on commit ccfb2a2

Please sign in to comment.