Files
alexandrev-tibco f85c746af0 Rename to Kotej and add the app icon
"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
2026-07-27 17:24:05 +02:00

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)
}
}