f85c746af0
"Cotejo" means nothing outside Spanish and is hard to pronounce elsewhere, so the project is now Kotej: same root, but it reads and sounds the same in English and Spanish. Renamed throughout — engine, targets, bundle ids and the URL scheme, which is now kotej://compare. Icon generated with Codex: two facing panels split by a seam, content lines on each side and one amber line marking a difference — the tool's job at a glance, still legible at 16pt. 34 tests still green; kotej:// verified end to end against the demo JARs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UfhDYRLTpGJSKGP1m3LVJN
111 lines
4.3 KiB
Swift
111 lines
4.3 KiB
Swift
import Cocoa
|
|
import FinderSync
|
|
|
|
/// Adds Kotej to Finder's contextual menu.
|
|
///
|
|
/// Two flows, matching how comparison tools are normally used:
|
|
/// - select two items → "Comparar los 2 elementos"
|
|
/// - select one item → "Seleccionar para comparar", then on another item
|
|
/// "Comparar con <lo seleccionado>"
|
|
///
|
|
/// The app is asked to do the work through its `kotej://` URL scheme, so the
|
|
/// extension stays tiny and holds no comparison logic.
|
|
final class FinderMenu: FIFinderSync {
|
|
/// Where the first pick is remembered between two right-clicks.
|
|
private static let pendingKey = "kotej.pendingLeft"
|
|
|
|
private var pendingLeft: URL? {
|
|
get {
|
|
guard let path = UserDefaults.standard.string(forKey: Self.pendingKey) else { return nil }
|
|
return URL(fileURLWithPath: path)
|
|
}
|
|
set {
|
|
if let path = newValue?.path {
|
|
UserDefaults.standard.set(path, forKey: Self.pendingKey)
|
|
} else {
|
|
UserDefaults.standard.removeObject(forKey: Self.pendingKey)
|
|
}
|
|
}
|
|
}
|
|
|
|
override init() {
|
|
super.init()
|
|
// Watch every mounted volume so the menu is available anywhere, which is
|
|
// what a comparison tool needs (FinderSync only offers menus for the
|
|
// directories it monitors).
|
|
FIFinderSyncController.default().directoryURLs = Set(
|
|
FileManager.default.mountedVolumeURLs(includingResourceValuesForKeys: nil,
|
|
options: [.skipHiddenVolumes]) ?? [URL(fileURLWithPath: "/")]
|
|
)
|
|
}
|
|
|
|
override func menu(for menuKind: FIMenuKind) -> NSMenu? {
|
|
guard menuKind == .contextualMenuForItems else { return nil }
|
|
let selected = FIFinderSyncController.default().selectedItemURLs() ?? []
|
|
guard !selected.isEmpty else { return nil }
|
|
|
|
let menu = NSMenu(title: "Kotej")
|
|
|
|
if selected.count >= 2 {
|
|
let item = NSMenuItem(title: String(localized: "Compare the 2 items with Kotej"),
|
|
action: #selector(compareSelection(_:)), keyEquivalent: "")
|
|
item.target = self
|
|
menu.addItem(item)
|
|
return menu
|
|
}
|
|
|
|
if let pending = pendingLeft, pending != selected[0] {
|
|
let item = NSMenuItem(title: String(localized: "Compare with “\(pending.lastPathComponent)”"),
|
|
action: #selector(compareWithPending(_:)), keyEquivalent: "")
|
|
item.target = self
|
|
menu.addItem(item)
|
|
|
|
let forget = NSMenuItem(title: String(localized: "Forget “\(pending.lastPathComponent)”"),
|
|
action: #selector(forgetPending(_:)), keyEquivalent: "")
|
|
forget.target = self
|
|
menu.addItem(forget)
|
|
} else {
|
|
let item = NSMenuItem(title: String(localized: "Select for comparison"),
|
|
action: #selector(markForComparison(_:)), keyEquivalent: "")
|
|
item.target = self
|
|
menu.addItem(item)
|
|
}
|
|
return menu
|
|
}
|
|
|
|
// MARK: Actions
|
|
|
|
@objc private func compareSelection(_ sender: AnyObject?) {
|
|
let selected = FIFinderSyncController.default().selectedItemURLs() ?? []
|
|
guard selected.count >= 2 else { return }
|
|
open(left: selected[0], right: selected[1])
|
|
}
|
|
|
|
@objc private func markForComparison(_ sender: AnyObject?) {
|
|
pendingLeft = FIFinderSyncController.default().selectedItemURLs()?.first
|
|
}
|
|
|
|
@objc private func compareWithPending(_ sender: AnyObject?) {
|
|
guard let left = pendingLeft,
|
|
let right = FIFinderSyncController.default().selectedItemURLs()?.first else { return }
|
|
pendingLeft = nil
|
|
open(left: left, right: right)
|
|
}
|
|
|
|
@objc private func forgetPending(_ sender: AnyObject?) {
|
|
pendingLeft = nil
|
|
}
|
|
|
|
private func open(left: URL, right: URL) {
|
|
var components = URLComponents()
|
|
components.scheme = "kotej"
|
|
components.host = "compare"
|
|
components.queryItems = [
|
|
URLQueryItem(name: "left", value: left.path),
|
|
URLQueryItem(name: "right", value: right.path),
|
|
]
|
|
guard let url = components.url else { return }
|
|
NSWorkspace.shared.open(url)
|
|
}
|
|
}
|