From 0f7ee6e29f70017dd4fdfe2dd3a6106f7204f04f Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Thu, 02 Jul 2026 02:40:40 +0800
Subject: [PATCH] Document Raycast extension backlog
---
src/Apptag/DataLayer.swift | 308 +++++++++++++++++++++++++++++++++++++++++++++++----
1 files changed, 283 insertions(+), 25 deletions(-)
diff --git a/src/Apptag/DataLayer.swift b/src/Apptag/DataLayer.swift
index 6177fd6..ac1650c 100644
--- a/src/Apptag/DataLayer.swift
+++ b/src/Apptag/DataLayer.swift
@@ -237,6 +237,52 @@
// MARK: - Tag Color Mapping
+struct TagCustomColor: Codable, Equatable {
+ var red: Double
+ var green: Double
+ var blue: Double
+ var alpha: Double
+
+ init(red: Double, green: Double, blue: Double, alpha: Double = 1.0) {
+ self.red = Self.clamp(red)
+ self.green = Self.clamp(green)
+ self.blue = Self.clamp(blue)
+ self.alpha = Self.clamp(alpha)
+ }
+
+ init(nsColor: NSColor) {
+ let rgbColor = nsColor.usingColorSpace(.sRGB) ?? nsColor
+ self.init(
+ red: Double(rgbColor.redComponent),
+ green: Double(rgbColor.greenComponent),
+ blue: Double(rgbColor.blueComponent),
+ alpha: Double(rgbColor.alphaComponent)
+ )
+ }
+
+ var nsColor: NSColor {
+ NSColor(
+ srgbRed: CGFloat(red),
+ green: CGFloat(green),
+ blue: CGFloat(blue),
+ alpha: CGFloat(alpha)
+ )
+ }
+
+ var signature: String {
+ [
+ String(format: "%.4f", red),
+ String(format: "%.4f", green),
+ String(format: "%.4f", blue),
+ String(format: "%.4f", alpha)
+ ].joined(separator: ":")
+ }
+
+ private static func clamp(_ value: Double) -> Double {
+ min(1.0, max(0.0, value))
+ }
+}
+
enum TagColor {
static func nsColor(for index: Int) -> NSColor {
switch index {
@@ -251,6 +297,20 @@
default: return NSColor.systemGray
}
}
+
+ static func nsColor(for index: Int, customColor: TagCustomColor?) -> NSColor {
+ customColor?.nsColor ?? nsColor(for: index)
+ }
+
+ static func prefersDarkText(for color: NSColor) -> Bool {
+ let rgbColor = color.usingColorSpace(.sRGB) ?? color
+ let red = Double(rgbColor.redComponent)
+ let green = Double(rgbColor.greenComponent)
+ let blue = Double(rgbColor.blueComponent)
+ let luminance = (0.299 * red) + (0.587 * green) + (0.114 * blue)
+ return luminance > 0.62
+ }
+
static let allIndices: [Int] = [1, 2, 3, 4, 5, 6, 7]
}
@@ -963,6 +1023,8 @@
struct TagDef: Codable, Equatable {
var color: Int
var systemCategoryID: SmartCategoryID? = nil
+ var customColor: TagCustomColor? = nil
+ var isCustomMaintained: Bool? = nil
}
struct Store: Codable {
@@ -1113,6 +1175,40 @@
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
guard let data = try? encoder.encode(normalizedStore) else { return }
try? data.write(to: storeURL, options: .atomic)
+ }
+
+ static func customMaintainedContainerCount(in store: Store? = nil) -> Int {
+ customMaintainedContainerNames(in: store).count
+ }
+
+ static func customMaintainedContainerNames(in store: Store? = nil) -> [String] {
+ let effectiveStore = store ?? load()
+ return effectiveStore.tags
+ .filter { isCustomMaintainedTag(name: $0.key, definition: $0.value) }
+ .map(\.key)
+ .sorted()
+ }
+
+ static func isCustomMaintainedTag(name: String, definition: TagDef) -> Bool {
+ guard let systemCategoryID = definition.systemCategoryID else {
+ return true
+ }
+ if definition.isCustomMaintained == true {
+ return true
+ }
+ if definition.customColor != nil {
+ return true
+ }
+ return definition.color != systemCategoryID.defaultColorIndex
+ }
+
+ static func markCustomMaintainedIfNeeded(_ tagName: String, in store: inout Store) {
+ guard var definition = store.tags[tagName],
+ definition.systemCategoryID != nil,
+ !isCustomMaintainedTag(name: tagName, definition: definition)
+ else { return }
+ definition.isCustomMaintained = true
+ store.tags[tagName] = definition
}
static func normalizedAppOrderPaths(_ paths: [String]) -> [String] {
@@ -1467,6 +1563,7 @@
for (tagName, tagDef) in store.tags {
guard let categoryID = tagDef.systemCategoryID else { continue }
+ guard !isCustomMaintainedTag(name: tagName, definition: tagDef) else { continue }
let targetName = categoryID.localizedDisplayName
guard targetName != tagName else { continue }
if let existing = store.tags[targetName],
@@ -1494,7 +1591,9 @@
) -> (name: String, created: Bool)? {
if let existingName = systemTagName(for: categoryID, in: store) {
let targetName = categoryID.localizedDisplayName
- if existingName != targetName {
+ let existingDefinition = store.tags[existingName]
+ if existingName != targetName,
+ existingDefinition.map({ !isCustomMaintainedTag(name: existingName, definition: $0) }) ?? true {
if let existing = store.tags[targetName],
existing.systemCategoryID != categoryID {
return (existingName, false)
@@ -1755,12 +1854,11 @@
guard !FileManager.default.fileExists(atPath: storeURL.path) else { return }
let starterCategoryIDs: [SmartCategoryID] = [
- .uiPrototyping,
- .ide,
- .writing,
- .game,
+ .design,
+ .development,
+ .office,
.entertainment,
- .system,
+ .systemEnhancement,
.gtd
]
@@ -1772,9 +1870,55 @@
}
}
+enum TagEditorMutationResult {
+ case saved
+ case noChange
+ case blockedCustomContainerQuota(ProCustomContainerQuotaStatus)
+
+ var didSave: Bool {
+ if case .saved = self {
+ return true
+ }
+ return false
+ }
+}
+
// MARK: - Tag Editor (CRUD against local TagDatabase)
enum TagEditor {
+ private static func customContainerCandidates(
+ _ tagNames: Set<String>,
+ in store: TagDatabase.Store
+ ) -> Set<String> {
+ Set(tagNames.filter { tagName in
+ guard let definition = store.tags[tagName] else {
+ return true
+ }
+ return !TagDatabase.isCustomMaintainedTag(name: tagName, definition: definition)
+ })
+ }
+
+ private static func customContainerMaintenanceResult(
+ for tagNames: Set<String>,
+ in store: TagDatabase.Store
+ ) -> TagEditorMutationResult? {
+ let addedCount = customContainerCandidates(tagNames, in: store).count
+ switch ProEntitlementPolicy.customContainerMaintenanceDecision(
+ addingCustomContainerCount: addedCount,
+ in: store
+ ) {
+ case .allow:
+ return nil
+ case .blocked(let status):
+ return .blockedCustomContainerQuota(status)
+ }
+ }
+
+ private static func markCustomMaintained(_ tagNames: Set<String>, in store: inout TagDatabase.Store) {
+ for tagName in tagNames {
+ TagDatabase.markCustomMaintainedIfNeeded(tagName, in: &store)
+ }
+ }
// MARK: Read
@@ -1797,7 +1941,9 @@
}
/// Persist a new tag display order.
- static func reorderTags(_ names: [String]) {
+ @discardableResult
+ static func reorderTags(_ names: [String]) -> Bool {
+ guard ProEntitlementPolicy.isUnlocked(.persistentAppSorting) else { return false }
var store = TagDatabase.load()
let previousStore = store
store.tagOrder = names
@@ -1806,6 +1952,7 @@
previous: previousStore,
reason: "reorder-tags"
)
+ return true
}
/// Persist the visible app order for one stable container.
@@ -1836,9 +1983,13 @@
}
/// Create a new tag definition (no app assignments yet).
- static func createTag(_ name: String, color: Int) {
+ @discardableResult
+ static func createTag(_ name: String, color: Int) -> TagEditorMutationResult {
var store = TagDatabase.load()
- guard store.tags[name] == nil else { return }
+ guard store.tags[name] == nil else { return .noChange }
+ if let blocked = customContainerMaintenanceResult(for: [name], in: store) {
+ return blocked
+ }
let previousStore = store
store.tags[name] = TagDatabase.TagDef(color: color, systemCategoryID: nil)
if !store.tagOrder.contains(name) { store.tagOrder.insert(name, at: 0) }
@@ -1847,6 +1998,7 @@
previous: previousStore,
reason: "create-tag"
)
+ return .saved
}
/// Annotate scanned apps with tags from the database.
@@ -1875,8 +2027,12 @@
// MARK: Write
/// Assign a tag to multiple apps. Preserves existing tags.
- static func assignTag(_ tag: String, color: Int, to paths: [String]) {
+ @discardableResult
+ static func assignTag(_ tag: String, color: Int, to paths: [String]) -> TagEditorMutationResult {
var store = TagDatabase.load()
+ if let blocked = customContainerMaintenanceResult(for: [tag], in: store) {
+ return blocked
+ }
let previousStore = store
// Ensure tag definition exists
if store.tags[tag] == nil {
@@ -1884,6 +2040,7 @@
// New tag: append to display order
if !store.tagOrder.contains(tag) { store.tagOrder.insert(tag, at: 0) }
}
+ markCustomMaintained([tag], in: &store)
for path in paths {
var current = store.appTags[path] ?? []
if !current.contains(tag) {
@@ -1896,14 +2053,27 @@
previous: previousStore,
reason: "assign-tag"
)
+ return .saved
}
/// Replace the full editable tag set for multiple apps.
- static func setTags(_ tags: [String], to paths: [String]) {
+ @discardableResult
+ static func setTags(_ tags: [String], to paths: [String]) -> TagEditorMutationResult {
var store = TagDatabase.load()
- let previousStore = store
let selectedUncommon = tags.contains(TagDatabase.uncommonTagKey)
let validTags = tags.filter { store.tags[$0] != nil }
+ let nextValidTags = Set(validTags)
+ var touchedTags = Set<String>()
+ for path in paths {
+ let currentTags = Set(store.appTags[path] ?? [])
+ touchedTags.formUnion(currentTags.symmetricDifference(nextValidTags))
+ }
+ touchedTags = touchedTags.filter { store.tags[$0] != nil }
+ if let blocked = customContainerMaintenanceResult(for: touchedTags, in: store) {
+ return blocked
+ }
+ let previousStore = store
+ markCustomMaintained(touchedTags, in: &store)
var uncommonPaths = Set(store.uncommonAppPaths)
for path in paths {
if validTags.isEmpty {
@@ -1925,16 +2095,29 @@
previous: previousStore,
reason: "set-tags"
)
+ return .saved
}
/// Append tags to multiple apps without disturbing their existing tag sets.
- static func appendTags(_ tags: [String], to paths: [String]) {
- guard !tags.isEmpty, !paths.isEmpty else { return }
+ @discardableResult
+ static func appendTags(_ tags: [String], to paths: [String]) -> TagEditorMutationResult {
+ guard !tags.isEmpty, !paths.isEmpty else { return .noChange }
var store = TagDatabase.load()
- let previousStore = store
let selectedUncommon = tags.contains(TagDatabase.uncommonTagKey)
let validTags = tags.filter { store.tags[$0] != nil }
+ var touchedTags = Set<String>()
+ for path in paths {
+ let current = Set(store.appTags[path] ?? [])
+ for tag in validTags where !current.contains(tag) {
+ touchedTags.insert(tag)
+ }
+ }
+ if let blocked = customContainerMaintenanceResult(for: touchedTags, in: store) {
+ return blocked
+ }
+ let previousStore = store
+ markCustomMaintained(touchedTags, in: &store)
var uncommonPaths = Set(store.uncommonAppPaths)
for path in paths {
@@ -1958,16 +2141,27 @@
previous: previousStore,
reason: "append-tags"
)
+ return .saved
}
/// Remove tags from multiple apps while preserving every unrelated tag.
- static func removeTags(_ tags: [String], from paths: [String]) {
- guard !tags.isEmpty, !paths.isEmpty else { return }
+ @discardableResult
+ static func removeTags(_ tags: [String], from paths: [String]) -> TagEditorMutationResult {
+ guard !tags.isEmpty, !paths.isEmpty else { return .noChange }
var store = TagDatabase.load()
- let previousStore = store
let selectedUncommon = tags.contains(TagDatabase.uncommonTagKey)
let validTags = Set(tags.filter { store.tags[$0] != nil })
+ var touchedTags = Set<String>()
+ for path in paths {
+ let current = Set(store.appTags[path] ?? [])
+ touchedTags.formUnion(validTags.intersection(current))
+ }
+ if let blocked = customContainerMaintenanceResult(for: touchedTags, in: store) {
+ return blocked
+ }
+ let previousStore = store
+ markCustomMaintained(touchedTags, in: &store)
var uncommonPaths = Set(store.uncommonAppPaths)
for path in paths {
@@ -1992,6 +2186,7 @@
previous: previousStore,
reason: "remove-tags"
)
+ return .saved
}
static func reconcileScannedApps(_ apps: [AppInfo]) -> TagDatabase.Store {
@@ -2185,13 +2380,30 @@
return decision
}
- static func moveApp(path: String, from sourceTag: String, to targetTag: String, color: Int, copy: Bool) {
+ @discardableResult
+ static func moveApp(path: String, from sourceTag: String, to targetTag: String, color: Int, copy: Bool) -> TagEditorMutationResult {
var store = TagDatabase.load()
+ var touchedTags = Set<String>()
+ let currentTags = Set(store.appTags[path] ?? [])
+ if store.tags[targetTag] == nil || !currentTags.contains(targetTag) {
+ touchedTags.insert(targetTag)
+ }
+ if !copy,
+ !sourceTag.isEmpty,
+ sourceTag != "Mac自带",
+ sourceTag != tr("group.appleBuiltIn"),
+ currentTags.contains(sourceTag) {
+ touchedTags.insert(sourceTag)
+ }
+ if let blocked = customContainerMaintenanceResult(for: touchedTags, in: store) {
+ return blocked
+ }
let previousStore = store
if store.tags[targetTag] == nil {
store.tags[targetTag] = TagDatabase.TagDef(color: color, systemCategoryID: nil)
if !store.tagOrder.contains(targetTag) { store.tagOrder.insert(targetTag, at: 0) }
}
+ markCustomMaintained(touchedTags, in: &store)
var current = store.appTags[path] ?? []
if !copy, !sourceTag.isEmpty, sourceTag != "Mac自带", sourceTag != tr("group.appleBuiltIn") {
@@ -2211,17 +2423,31 @@
previous: previousStore,
reason: "move-app"
)
+ return .saved
}
/// Rename a tag on all apps that have it.
- static func renameTag(from oldName: String, to newName: String) {
+ @discardableResult
+ static func renameTag(from oldName: String, to newName: String) -> TagEditorMutationResult {
var store = TagDatabase.load()
- guard oldName != newName, store.tags[newName] == nil else { return }
+ guard oldName != newName, store.tags[newName] == nil else { return .noChange }
+ guard let oldDefinition = store.tags[oldName] else { return .noChange }
+ let needsCustomContainerSlot = !TagDatabase.isCustomMaintainedTag(
+ name: oldName,
+ definition: oldDefinition
+ )
+ if needsCustomContainerSlot,
+ let blocked = customContainerMaintenanceResult(for: [oldName], in: store) {
+ return blocked
+ }
let previousStore = store
// Update tag definition
- if let def = store.tags.removeValue(forKey: oldName) {
+ if var def = store.tags.removeValue(forKey: oldName) {
let oldContainerID = AppContainerID.forTag(oldName, definition: def)
let newContainerID = AppContainerID.forTag(newName, definition: def)
+ if def.systemCategoryID != nil {
+ def.isCustomMaintained = true
+ }
store.tags[newName] = def
TagDatabase.migrateContainerAppOrderKey(
in: &store,
@@ -2243,6 +2469,7 @@
previous: previousStore,
reason: "rename-tag"
)
+ return .saved
}
/// Delete a tag completely (from all apps and tag definitions).
@@ -2283,16 +2510,47 @@
}
/// Set the color for a tag.
- static func setColor(_ color: Int, for tag: String) {
+ @discardableResult
+ static func setColor(_ color: Int, for tag: String) -> TagEditorMutationResult {
var store = TagDatabase.load()
- let previousStore = store
- var tagDef = store.tags[tag] ?? TagDatabase.TagDef(color: color, systemCategoryID: nil)
+ let existingDefinition = store.tags[tag]
+ var tagDef = existingDefinition ?? TagDatabase.TagDef(color: color, systemCategoryID: nil)
+ let wasCustomMaintained = existingDefinition.map {
+ TagDatabase.isCustomMaintainedTag(name: tag, definition: $0)
+ } ?? false
tagDef.color = color
+ tagDef.customColor = nil
+ let willBeCustomMaintained = TagDatabase.isCustomMaintainedTag(name: tag, definition: tagDef)
+ if !wasCustomMaintained, willBeCustomMaintained,
+ let blocked = customContainerMaintenanceResult(for: [tag], in: store) {
+ return blocked
+ }
+ let previousStore = store
store.tags[tag] = tagDef
TagDatabase.saveUserCategorySchemeMutation(
store,
previous: previousStore,
reason: "set-tag-color"
)
+ return .saved
+ }
+
+ /// Set a Pro custom color for a tag without changing its fallback base color.
+ @discardableResult
+ static func setCustomColor(_ customColor: TagCustomColor, for tag: String) -> Bool {
+ guard ProEntitlementPolicy.isUnlocked(.customTagColors) else {
+ return false
+ }
+ var store = TagDatabase.load()
+ let previousStore = store
+ var tagDef = store.tags[tag] ?? TagDatabase.TagDef(color: 0, systemCategoryID: nil)
+ tagDef.customColor = customColor
+ store.tags[tag] = tagDef
+ TagDatabase.saveUserCategorySchemeMutation(
+ store,
+ previous: previousStore,
+ reason: "set-tag-custom-color"
+ )
+ return true
}
}
--
Gitblit v1.9.3