UTMVirtualMachineEntityQuery.swift 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. //
  2. // Copyright © 2025 osy. All rights reserved.
  3. //
  4. // Licensed under the Apache License, Version 2.0 (the "License");
  5. // you may not use this file except in compliance with the License.
  6. // You may obtain a copy of the License at
  7. //
  8. // http://www.apache.org/licenses/LICENSE-2.0
  9. //
  10. // Unless required by applicable law or agreed to in writing, software
  11. // distributed under the License is distributed on an "AS IS" BASIS,
  12. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. // See the License for the specific language governing permissions and
  14. // limitations under the License.
  15. //
  16. import AppIntents
  17. @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
  18. struct UTMVirtualMachineEntityQuery: EntityQuery, EntityStringQuery {
  19. @Dependency
  20. var data: UTMData
  21. func entities(for identifiers: [UUID]) async throws -> [UTMVirtualMachineEntity] {
  22. await MainActor.run {
  23. data
  24. .virtualMachines
  25. .filter({ $0.isLoaded && identifiers.contains($0.id) })
  26. .map({ UTMVirtualMachineEntity(from: $0) })
  27. }
  28. }
  29. func entities(matching: String) async throws -> [UTMVirtualMachineEntity] {
  30. await MainActor.run {
  31. data
  32. .virtualMachines
  33. .filter({ $0.isLoaded && $0.detailsTitleLabel.localizedCaseInsensitiveContains(matching) })
  34. .map({ UTMVirtualMachineEntity(from: $0) })
  35. }
  36. }
  37. func suggestedEntities() async throws -> [UTMVirtualMachineEntity] {
  38. await MainActor.run {
  39. data
  40. .virtualMachines
  41. .filter({ $0.isLoaded })
  42. .map({ UTMVirtualMachineEntity(from: $0) })
  43. }
  44. }
  45. }
  46. @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *)
  47. extension UTMVirtualMachineEntityQuery: EntityPropertyQuery {
  48. /**
  49. The type of the comparator to use for the property query. This sample uses `Predicate`, but other apps could use `NSPredicate` (for
  50. Core Data) or an entirely custom comparator that works with an existing data model.
  51. */
  52. typealias ComparatorMappingType = Predicate<UTMVirtualMachineEntity>
  53. /**
  54. Declare the entity properties that are available for queries and in the Find intent, along with the comparator the app uses when querying the
  55. property.
  56. */
  57. static let properties = QueryProperties {
  58. Property(\UTMVirtualMachineEntity.$name) {
  59. ContainsComparator { searchValue in
  60. #Predicate<UTMVirtualMachineEntity> { $0.name.localizedStandardContains(searchValue) }
  61. }
  62. EqualToComparator { searchValue in
  63. #Predicate<UTMVirtualMachineEntity> { $0.name == searchValue }
  64. }
  65. NotEqualToComparator { searchValue in
  66. #Predicate<UTMVirtualMachineEntity> { $0.name != searchValue }
  67. }
  68. }
  69. Property(\UTMVirtualMachineEntity.$state) {
  70. EqualToComparator { searchValue in
  71. #Predicate<UTMVirtualMachineEntity> { $0.state == searchValue }
  72. }
  73. NotEqualToComparator { searchValue in
  74. #Predicate<UTMVirtualMachineEntity> { $0.state != searchValue }
  75. }
  76. }
  77. }
  78. /// Declare the entity properties available as sort criteria in the Find intent.
  79. static let sortingOptions = SortingOptions {
  80. SortableBy(\UTMVirtualMachineEntity.$name)
  81. }
  82. /// The text that people see in the Shortcuts app, describing what this intent does.
  83. static var findIntentDescription: IntentDescription? {
  84. IntentDescription("Search for a virtual machine.",
  85. searchKeywords: ["virtual machine", "vm"],
  86. resultValueName: "Virtual Machines")
  87. }
  88. /// Performs the Find intent using the predicates that the individual enters in the Shortcuts app.
  89. func entities(matching comparators: [Predicate<UTMVirtualMachineEntity>],
  90. mode: ComparatorMode,
  91. sortedBy: [EntityQuerySort<UTMVirtualMachineEntity>],
  92. limit: Int?) async throws -> [UTMVirtualMachineEntity] {
  93. logger.debug("[UTMVirtualMachineEntityQuery] Property query started")
  94. /// Get the trail entities that meet the criteria of the comparators.
  95. var matchedVms = try await virtualMachines(matching: comparators, mode: mode)
  96. /**
  97. Apply the requested sort. `EntityQuerySort` specifies the value to sort by using a `PartialKeyPath`. This key path builds a
  98. `KeyPathComparator` to use default sorting implementations for the value that the key path provides. For example, this approach uses
  99. `SortComparator.localizedStandard` when sorting key paths with a `String` value.
  100. */
  101. logger.debug("[UTMVirtualMachineEntityQuery] Sorting results")
  102. for sortOperation in sortedBy {
  103. switch sortOperation.by {
  104. case \.$name:
  105. matchedVms.sort(using: KeyPathComparator(\UTMVirtualMachineEntity.name, order: sortOperation.order.sortOrder))
  106. default:
  107. break
  108. }
  109. }
  110. /**
  111. People can optionally customize a limit to the number of results that a query returns.
  112. If your data model supports query limits, you can also use the limit parameter when querying
  113. your data model, to allow for faster searches.
  114. */
  115. if let limit, matchedVms.count > limit {
  116. logger.debug("[UTMVirtualMachineEntityQuery] Limiting results to \(limit)")
  117. matchedVms.removeLast(matchedVms.count - limit)
  118. }
  119. logger.debug("[UTMVirtualMachineEntityQuery] Property query complete")
  120. return matchedVms
  121. }
  122. /// - Returns: The trail entities that meet the criteria of `comparators` and `mode`.
  123. @MainActor
  124. private func virtualMachines(matching comparators: [Predicate<UTMVirtualMachineEntity>], mode: ComparatorMode) throws -> [UTMVirtualMachineEntity] {
  125. try data.virtualMachines.compactMap { vm in
  126. let entity = UTMVirtualMachineEntity(from: vm)
  127. /**
  128. For an AND search (criteria1 AND criteria2 AND ...), this variable starts as `true`.
  129. If any of the comparators don't match, the app sets it to `false`, allowing the comparator loop to break early because a comparator
  130. doesn't satisfy the AND requirement.
  131. For an OR search (criteria1 OR criteria2 OR ...), this variable starts as `false`.
  132. If any of the comparators match, the app sets it to `true`, allowing the comparator loop to break early because any comparator that
  133. matches satisfies the OR requirement.
  134. */
  135. var includeAsResult = mode == .and ? true : false
  136. let earlyBreakCondition = includeAsResult
  137. logger.debug("[UTMVirtualMachineEntityQuery] Starting to evaluate predicates for \(entity.name)")
  138. for comparator in comparators {
  139. guard includeAsResult == earlyBreakCondition else {
  140. logger.debug("[UTMVirtualMachineEntityQuery] Predicates matched? \(includeAsResult)")
  141. break
  142. }
  143. /// Runs the `Predicate` expression with the specific `TrailEntity` to determine whether the entity matches the conditions.
  144. includeAsResult = try comparator.evaluate(entity)
  145. }
  146. logger.debug("[UTMVirtualMachineEntityQuery] Predicates matched? \(includeAsResult)")
  147. return includeAsResult ? entity : nil
  148. }
  149. }
  150. }
  151. @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
  152. private extension EntityQuerySort.Ordering {
  153. /// Convert sort information from `EntityQuerySort` to Foundation's `SortOrder`.
  154. var sortOrder: SortOrder {
  155. switch self {
  156. case .ascending:
  157. return SortOrder.forward
  158. case .descending:
  159. return SortOrder.reverse
  160. }
  161. }
  162. }