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
This commit is contained in:
alexandrev-tibco
2026-07-27 17:24:05 +02:00
parent c8d0af0783
commit f85c746af0
32 changed files with 72 additions and 54 deletions
@@ -0,0 +1,15 @@
{
"images" : [
{ "idiom" : "mac", "scale" : "1x", "size" : "16x16", "filename" : "icon_16.png" },
{ "idiom" : "mac", "scale" : "2x", "size" : "16x16", "filename" : "icon_32.png" },
{ "idiom" : "mac", "scale" : "1x", "size" : "32x32", "filename" : "icon_32.png" },
{ "idiom" : "mac", "scale" : "2x", "size" : "32x32", "filename" : "icon_64.png" },
{ "idiom" : "mac", "scale" : "1x", "size" : "128x128", "filename" : "icon_128.png" },
{ "idiom" : "mac", "scale" : "2x", "size" : "128x128", "filename" : "icon_256.png" },
{ "idiom" : "mac", "scale" : "1x", "size" : "256x256", "filename" : "icon_256.png" },
{ "idiom" : "mac", "scale" : "2x", "size" : "256x256", "filename" : "icon_512.png" },
{ "idiom" : "mac", "scale" : "1x", "size" : "512x512", "filename" : "icon_512.png" },
{ "idiom" : "mac", "scale" : "2x", "size" : "512x512", "filename" : "icon_1024.png" }
],
"info" : { "author" : "xcode", "version" : 1 }
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 859 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

+1
View File
@@ -0,0 +1 @@
{ "info" : { "author" : "xcode", "version" : 1 } }
+1 -1
View File
@@ -1,6 +1,6 @@
import Foundation
import Observation
import CotejoEngine
import KotejEngine
/// Drives one comparison: what's on each side, the resulting tree, and the
/// options. Scanning runs off the main actor so big trees don't freeze the UI.
+1 -1
View File
@@ -1,6 +1,6 @@
import SwiftUI
import UniformTypeIdentifiers
import CotejoEngine
import KotejEngine
struct ContentView: View {
@Bindable var model: ComparisonModel
+1 -1
View File
@@ -1,5 +1,5 @@
import SwiftUI
import CotejoEngine
import KotejEngine
/// A row ready to render: the filter is applied up front so the outline can use a
/// plain key path for its children.
+1 -1
View File
@@ -1,5 +1,5 @@
import SwiftUI
import CotejoEngine
import KotejEngine
/// Side-by-side contents of the selected row. Text (and JSON/XML) is aligned line
/// by line; binaries just report their verdict, since a hex view would be noise.
+4 -4
View File
@@ -5,7 +5,7 @@
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>Cotejo</string>
<string>Kotej</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
@@ -27,16 +27,16 @@
<array>
<dict>
<key>CFBundleURLName</key>
<string>com.alexandrev.cotejo</string>
<string>com.alexandrev.kotej</string>
<key>CFBundleURLSchemes</key>
<array>
<string>cotejo</string>
<string>kotej</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSHumanReadableCopyright</key>
<string>Cotejo</string>
<string>Kotej</string>
</dict>
</plist>
+4 -4
View File
@@ -1,15 +1,15 @@
import SwiftUI
import CotejoEngine
import KotejEngine
@main
struct CotejoApp: App {
struct KotejApp: App {
@State private var tabs = TabsModel()
var body: some Scene {
WindowGroup {
RootView(tabs: tabs)
.frame(minWidth: 900, minHeight: 560)
// cotejo://compare?left=&right= how the Finder extension asks
// kotej://compare?left=&right= how the Finder extension asks
// for a comparison; it always lands in a tab.
.onOpenURL { url in handle(url) }
}
@@ -34,7 +34,7 @@ struct CotejoApp: App {
}
private func handle(_ url: URL) {
guard url.scheme == "cotejo", url.host == "compare",
guard url.scheme == "kotej", url.host == "compare",
let items = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems else { return }
func path(_ name: String) -> URL? {
items.first { $0.name == name }?.value.map { URL(fileURLWithPath: $0) }
+6 -6
View File
@@ -1,18 +1,18 @@
import Cocoa
import FinderSync
/// Adds Cotejo to Finder's contextual menu.
/// 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 `cotejo://` URL scheme, so the
/// 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 = "cotejo.pendingLeft"
private static let pendingKey = "kotej.pendingLeft"
private var pendingLeft: URL? {
get {
@@ -44,10 +44,10 @@ final class FinderMenu: FIFinderSync {
let selected = FIFinderSyncController.default().selectedItemURLs() ?? []
guard !selected.isEmpty else { return nil }
let menu = NSMenu(title: "Cotejo")
let menu = NSMenu(title: "Kotej")
if selected.count >= 2 {
let item = NSMenuItem(title: String(localized: "Compare the 2 items with Cotejo"),
let item = NSMenuItem(title: String(localized: "Compare the 2 items with Kotej"),
action: #selector(compareSelection(_:)), keyEquivalent: "")
item.target = self
menu.addItem(item)
@@ -98,7 +98,7 @@ final class FinderMenu: FIFinderSync {
private func open(left: URL, right: URL) {
var components = URLComponents()
components.scheme = "cotejo"
components.scheme = "kotej"
components.host = "compare"
components.queryItems = [
URLQueryItem(name: "left", value: left.path),
+2 -2
View File
@@ -5,7 +5,7 @@
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Cotejo</string>
<string>Kotej</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
@@ -25,7 +25,7 @@
<key>NSExtensionPointIdentifier</key>
<string>com.apple.FinderSync</string>
<key>NSExtensionPrincipalClass</key>
<string>CotejoFinder.FinderMenu</string>
<string>KotejFinder.FinderMenu</string>
</dict>
</dict>
</plist>
@@ -1,4 +1,4 @@
"Compare the 2 items with Cotejo" = "Compare the 2 items with Cotejo";
"Compare the 2 items with Kotej" = "Compare the 2 items with Kotej";
"Select for comparison" = "Select for comparison";
"Compare with “%@”" = "Compare with “%@”";
"Forget “%@”" = "Forget “%@”";
@@ -1,4 +1,4 @@
"Compare the 2 items with Cotejo" = "Comparar los 2 elementos con Cotejo";
"Compare the 2 items with Kotej" = "Comparar los 2 elementos con Kotej";
"Select for comparison" = "Seleccionar para comparar";
"Compare with “%@”" = "Comparar con «%@»";
"Forget “%@”" = "Olvidar «%@»";
+4 -4
View File
@@ -2,14 +2,14 @@
import PackageDescription
let package = Package(
name: "Cotejo",
name: "Kotej",
platforms: [.macOS(.v14)],
products: [
// The engine has no UI dependencies so it can also back a CLI later.
.library(name: "CotejoEngine", targets: ["CotejoEngine"]),
.library(name: "KotejEngine", targets: ["KotejEngine"]),
],
targets: [
.target(name: "CotejoEngine"),
.testTarget(name: "CotejoEngineTests", dependencies: ["CotejoEngine"]),
.target(name: "KotejEngine"),
.testTarget(name: "KotejEngineTests", dependencies: ["KotejEngine"]),
]
)
+5 -5
View File
@@ -1,4 +1,4 @@
# Cotejo
# Kotej
Comparador de ficheros, carpetas y **artefactos** (ZIP/JAR/EAR/WAR) para macOS.
Ligero, sin dependencias externas y con el motor aislado del interfaz.
@@ -28,13 +28,13 @@ Por defecto cada fichero usa el modo que le corresponde según su tipo
## Integración con Finder
Con la app instalada, activa la extensión en **Ajustes del Sistema → General →
Elementos de inicio y extensiones → Extensiones de Finder → Cotejo**. Después,
Elementos de inicio y extensiones → Extensiones de Finder → Kotej**. Después,
en el menú contextual del Finder:
- Selecciona **dos** elementos → *Comparar los 2 elementos con Cotejo*
- Selecciona **dos** elementos → *Comparar los 2 elementos con Kotej*
- O selecciona uno → *Seleccionar para comparar*, y en otro → *Comparar con “…”*
También responde a `cotejo://compare?left=/ruta/a&right=/ruta/b`, que abre la
También responde a `kotej://compare?left=/ruta/a&right=/ruta/b`, que abre la
comparación en una pestaña.
## Pestañas
@@ -46,7 +46,7 @@ comparando.
## Estado
-**Motor** (`CotejoEngine`): lector ZIP propio, escaneo de árboles, archivos
-**Motor** (`KotejEngine`): lector ZIP propio, escaneo de árboles, archivos
anidados, comparación por niveles, comparadores binario/texto/JSON/XML y diff
de líneas.
-**App** SwiftUI: **pestañas** (varias comparaciones en una ventana),
@@ -172,7 +172,7 @@ public enum TreeScanner {
private static func extractNested(reader: ZipReader, entry: ZipReader.Entry, name: String) throws -> URL {
let directory = URL(fileURLWithPath: NSTemporaryDirectory())
.appendingPathComponent("cotejo-nested-\(UUID().uuidString)")
.appendingPathComponent("kotej-nested-\(UUID().uuidString)")
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
let url = directory.appendingPathComponent(name)
try reader.data(for: entry).write(to: url)
@@ -1,5 +1,5 @@
import XCTest
@testable import CotejoEngine
@testable import KotejEngine
final class ContentComparerTests: XCTestCase {
private func data(_ text: String) -> Data { Data(text.utf8) }
@@ -1,12 +1,12 @@
import XCTest
@testable import CotejoEngine
@testable import KotejEngine
final class DirectoryComparerTests: XCTestCase {
private var scratch: URL!
override func setUpWithError() throws {
scratch = URL(fileURLWithPath: NSTemporaryDirectory())
.appendingPathComponent("cotejo-\(UUID().uuidString)")
.appendingPathComponent("kotej-\(UUID().uuidString)")
try FileManager.default.createDirectory(at: scratch, withIntermediateDirectories: true)
}
@@ -1,5 +1,5 @@
import XCTest
@testable import CotejoEngine
@testable import KotejEngine
final class TextDiffTests: XCTestCase {
func testIdenticalTextIsAllEqual() {
@@ -1,12 +1,12 @@
import XCTest
@testable import CotejoEngine
@testable import KotejEngine
final class ZipReaderTests: XCTestCase {
/// Builds a real archive with the system `zip`, so the reader is tested
/// against the format as produced in the wild rather than a hand-made blob.
private func makeArchive(_ files: [String: String], name: String = "test.zip") throws -> URL {
let root = URL(fileURLWithPath: NSTemporaryDirectory())
.appendingPathComponent("cotejo-\(UUID().uuidString)")
.appendingPathComponent("kotej-\(UUID().uuidString)")
let content = root.appendingPathComponent("content")
try FileManager.default.createDirectory(at: content, withIntermediateDirectories: true)
@@ -84,7 +84,7 @@ final class ZipReaderTests: XCTestCase {
func testRejectsNonZip() throws {
let url = URL(fileURLWithPath: NSTemporaryDirectory())
.appendingPathComponent("cotejo-\(UUID().uuidString).zip")
.appendingPathComponent("kotej-\(UUID().uuidString).zip")
try Data("definitely not a zip".utf8).write(to: url)
XCTAssertThrowsError(try ZipReader(url: url))
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 859 KiB

+17 -15
View File
@@ -1,4 +1,4 @@
name: Cotejo
name: Kotej
options:
bundleIdPrefix: com.alexandrev
createIntermediateGroups: true
@@ -13,41 +13,43 @@ settings:
DEVELOPMENT_TEAM: "2825Q76T7H"
CODE_SIGN_STYLE: Automatic
packages:
CotejoEngine:
KotejEngine:
path: .
targets:
Cotejo:
Kotej:
type: application
platform: macOS
deploymentTarget: "14.0"
sources:
- App
- path: App/Assets.xcassets
buildPhase: resources
- path: Resources
buildPhase: resources
dependencies:
- package: CotejoEngine
product: CotejoEngine
- target: CotejoFinder
- package: KotejEngine
product: KotejEngine
- target: KotejFinder
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: com.alexandrev.cotejo
PRODUCT_BUNDLE_IDENTIFIER: com.alexandrev.kotej
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
ENABLE_APP_SANDBOX: NO
ENABLE_HARDENED_RUNTIME: YES
info:
path: App/Info.plist
properties:
CFBundleDisplayName: Cotejo
CFBundleDisplayName: Kotej
CFBundleShortVersionString: "$(MARKETING_VERSION)"
CFBundleVersion: "$(CURRENT_PROJECT_VERSION)"
CFBundleDevelopmentRegion: en
CFBundleLocalizations: [en, es]
NSHumanReadableCopyright: "Cotejo"
NSHumanReadableCopyright: "Kotej"
CFBundleURLTypes:
- CFBundleURLName: com.alexandrev.cotejo
CFBundleURLSchemes: [cotejo]
- CFBundleURLName: com.alexandrev.kotej
CFBundleURLSchemes: [kotej]
CotejoFinder:
KotejFinder:
type: app-extension
platform: macOS
deploymentTarget: "14.0"
@@ -58,15 +60,15 @@ targets:
buildPhase: resources
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: com.alexandrev.cotejo.finder
PRODUCT_BUNDLE_IDENTIFIER: com.alexandrev.kotej.finder
ENABLE_APP_SANDBOX: NO
ENABLE_HARDENED_RUNTIME: YES
info:
path: FinderExtension/Info.plist
properties:
CFBundleDisplayName: Cotejo
CFBundleDisplayName: Kotej
CFBundleShortVersionString: "$(MARKETING_VERSION)"
CFBundleVersion: "$(CURRENT_PROJECT_VERSION)"
NSExtension:
NSExtensionPointIdentifier: com.apple.FinderSync
NSExtensionPrincipalClass: CotejoFinder.FinderMenu
NSExtensionPrincipalClass: KotejFinder.FinderMenu