Files
kotej/App/NodeExporter.swift
alexandrev-tibco 139f650d64 Set as base, folder pairing, copy between sides, single window
- 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
2026-07-31 13:27:59 +02:00

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
}