139f650d64
- Set as base: both sides of a row become the new comparison, which is how you descend into a folder pair. Works on folders and archives, not just files. - Marking for comparison no longer refuses folders, so a folder inside a JAR can be paired with one on disk; archive subtrees are extracted whole to do it. - Copy left → right and right → left, from the toolbar, the context menu or cmd-option-arrow, re-comparing afterwards. Only offered where the destination is a real folder: an entry inside an archive has no path to write to, and the button is disabled rather than failing silently. - The scene is now a single Window instead of a WindowGroup: a comparison opened from Finder joins the window already open as a new tab instead of spawning a second window. Verified with two consecutive kotej:// opens — one window. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UfhDYRLTpGJSKGP1m3LVJN
65 lines
2.4 KiB
Swift
65 lines
2.4 KiB
Swift
import Foundation
|
|
import KotejEngine
|
|
|
|
/// Gets a real file path for any node, so an arbitrary pair can be re-compared as
|
|
/// a new base.
|
|
///
|
|
/// Nodes living inside an archive have no path of their own, so they're written
|
|
/// out to a temporary location first — that's what makes it possible to set a
|
|
/// folder inside a JAR as one side of a comparison and something on disk as the
|
|
/// other.
|
|
enum NodeExporter {
|
|
/// Returns a URL for the node, extracting it if it only exists inside an
|
|
/// archive. Directories are extracted whole, preserving their layout.
|
|
static func url(for node: Node) throws -> URL {
|
|
switch node.source {
|
|
case .fileSystem(let url), .extracted(let url, _):
|
|
return url
|
|
case .archiveEntry:
|
|
return try node.isDirectory ? extractTree(node) : extractFile(node)
|
|
}
|
|
}
|
|
|
|
private static func scratchDirectory() throws -> URL {
|
|
let url = URL(fileURLWithPath: NSTemporaryDirectory())
|
|
.appendingPathComponent("kotej-pick-\(UUID().uuidString)")
|
|
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
|
|
return url
|
|
}
|
|
|
|
private static func extractFile(_ node: Node) throws -> URL {
|
|
let url = try scratchDirectory().appendingPathComponent(node.name)
|
|
try node.data().write(to: url)
|
|
return url
|
|
}
|
|
|
|
/// Writes a whole subtree out of an archive, so a folder inside a JAR can be
|
|
/// compared like any other folder.
|
|
private static func extractTree(_ node: Node) throws -> URL {
|
|
let root = try scratchDirectory().appendingPathComponent(node.name)
|
|
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
|
|
try write(node, into: root)
|
|
return root
|
|
}
|
|
|
|
private static func write(_ node: Node, into directory: URL) throws {
|
|
for child in node.children {
|
|
let destination = directory.appendingPathComponent(child.name)
|
|
if child.isDirectory {
|
|
try FileManager.default.createDirectory(at: destination, withIntermediateDirectories: true)
|
|
try write(child, into: destination)
|
|
} else if let data = try? child.data() {
|
|
try data.write(to: destination)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// One side of a manual pairing, kept while the other side is chosen.
|
|
struct PendingPick: Equatable {
|
|
let path: String
|
|
let name: String
|
|
let url: URL
|
|
let isDirectory: Bool
|
|
}
|