diff --git a/.swiftlint.yml b/.swiftlint.yml
index e45c7ed..4dc454e 100644
--- a/.swiftlint.yml
+++ b/.swiftlint.yml
@@ -1,3 +1,7 @@
+excluded:
+ - Sources/*/FileSystemTestHelper.swift
+ - Tests/*/*.swift
+
opt_in_rules:
- array_init
- attributes
@@ -104,6 +108,24 @@ line_length:
ignores_urls: true
ignores_function_declarations: true
ignores_comments: true
+ ignores_interpolated_strings: true
+ excluded_lines_patterns:
+ # String format
+ - 'NSLocalizedString\('
+ - 'String\.localizedStringWithFormat\('
+ - 'String\(format'
+ - 'format: ".*",$'
+ - 'fatalError\('
+ - 'NSLog\('
+ - 'print\('
+ - 'NSPredicate\(format'
+
+ # Constraints
+ - 'constraint\(equalTo'
+
+ # App Specific
+ - 'folder-\d+(-open)?'
+ - 'FileSizeFormatter\.default'
multiline_arguments:
first_argument_location: next_line
diff --git a/AppDefaults/VDDefaults.plist b/AppDefaults/VDDefaults.plist
index 280aab5..048a5ac 100644
--- a/AppDefaults/VDDefaults.plist
+++ b/AppDefaults/VDDefaults.plist
@@ -47,7 +47,7 @@
foldersDifferenceNavigatorCenterInWindow
comparatorBinaryBufferSize
- 131072
+ 2097152
filesStatusBarShowMessageTimeout
3
tabWidth
diff --git a/Sources/App/AppDelegate.swift b/Sources/App/AppDelegate.swift
index 832777a..f5143f1 100644
--- a/Sources/App/AppDelegate.swift
+++ b/Sources/App/AppDelegate.swift
@@ -55,7 +55,11 @@ class AppDelegate: NSObject, NSApplicationDelegate {
private func initDefaults() {
if let defaultsPath = Bundle.main.url(forResource: "VDDefaults", withExtension: "plist"),
let data = try? Data(contentsOf: defaultsPath),
- let defaultsDict = try? PropertyListSerialization.propertyList(from: data, options: [], format: nil) as? [String: Any] {
+ let defaultsDict = try? PropertyListSerialization.propertyList(
+ from: data,
+ options: [],
+ format: nil
+ ) as? [String: Any] {
UserDefaults.standard.register(defaults: defaultsDict)
}
}
diff --git a/Sources/Core/Common/ColorScheme.swift b/Sources/Core/Common/ColorScheme.swift
index bfe6e4f..a56fcf4 100644
--- a/Sources/Core/Common/ColorScheme.swift
+++ b/Sources/Core/Common/ColorScheme.swift
@@ -6,6 +6,8 @@
// Copyright (c) 2025 visualdiffer.com
//
+import os.log
+
struct ColorSet {
enum Key: String {
case text = "textColor"
@@ -47,7 +49,7 @@ extension ColorSet {
case .background:
tempBackground = color
default:
- NSLog("Found invalid color type: \(key)")
+ Logger.general.error("Found invalid color type: \(key)")
}
}
diff --git a/Sources/Core/CommonPrefs/CommonPrefs+FolderCompare.swift b/Sources/Core/CommonPrefs/CommonPrefs+FolderCompare.swift
index 69f7e84..90e919d 100644
--- a/Sources/Core/CommonPrefs/CommonPrefs+FolderCompare.swift
+++ b/Sources/Core/CommonPrefs/CommonPrefs+FolderCompare.swift
@@ -6,7 +6,7 @@
// Copyright (c) 2025 visualdiffer.com
//
-let defaultComparatorBinaryBufferSize = 128 * 1024
+let defaultComparatorBinaryBufferSize = 2 * 1024 * 1024
enum FolderColorAttribute: String {
case unknown
@@ -114,16 +114,6 @@ extension CommonPrefs {
}
}
- var checkResourceForks: Bool {
- get {
- fileExtraOptions.hasCheckResourceForks
- }
-
- set {
- fileExtraOptions = fileExtraOptions.changeCheckResourceForks(newValue)
- }
- }
-
// MARK: - Filters
var defaultFileFilters: String? {
diff --git a/Sources/Core/CommonPrefs/CommonPrefs.swift b/Sources/Core/CommonPrefs/CommonPrefs.swift
index e2101cc..43f52ca 100644
--- a/Sources/Core/CommonPrefs/CommonPrefs.swift
+++ b/Sources/Core/CommonPrefs/CommonPrefs.swift
@@ -6,6 +6,8 @@
// Copyright (c) 2025 visualdiffer.com
//
+import os.log
+
private let darkColorSchemeFileName: String = "colorsDark"
private let lightColorSchemeFileName: String = "colors"
@@ -65,7 +67,7 @@ public class CommonPrefs: @unchecked Sendable {
do {
return try readJSON(configPath) as? [String: Any]
} catch {
- NSLog("Error while loading colors from %@, error %@. Try to load default colors config", configPath, error.localizedDescription)
+ Logger.general.error("Error while loading colors from \(configPath), error \(error.localizedDescription). Try to load default colors config")
// fallback to defaults
if let path = defaultColorsConfigPath {
return try? readJSON(path) as? [String: Any]
diff --git a/Sources/Core/Document/VDDocument.swift b/Sources/Core/Document/VDDocument.swift
index 607f748..51cadcc 100644
--- a/Sources/Core/Document/VDDocument.swift
+++ b/Sources/Core/Document/VDDocument.swift
@@ -6,6 +6,8 @@
// Copyright (c) 2010 visualdiffer.com
//
+import os.log
+
public protocol DocumentWindowControllerDelegate: AnyObject {
func canClose(_ document: VDDocument) -> Bool
}
@@ -181,7 +183,7 @@ public protocol DocumentWindowControllerDelegate: AnyObject {
addWindowController(fwc)
fwc.startComparison()
default:
- NSLog("Invalid session type")
+ Logger.general.error("Invalid session type")
}
// disable restoration for new documents
// otherwise when the application restarts it restores an invalid document
diff --git a/Sources/Core/Document/Window/DocumentWindow.swift b/Sources/Core/Document/Window/DocumentWindow.swift
index 7194089..dd3bf80 100644
--- a/Sources/Core/Document/Window/DocumentWindow.swift
+++ b/Sources/Core/Document/Window/DocumentWindow.swift
@@ -192,7 +192,8 @@ class DocumentWindow: NSWindow, FileDropImageViewDelegate, HistoryControllerDele
leftPathChooser.addPath(leftPath)
rightPathChooser.addPath(rightPath)
- if !userChosenPreferences, HistorySessionManager.shared.containsHistory(leftPath: leftPath, rightPath: rightPath) {
+ if !userChosenPreferences,
+ HistorySessionManager.shared.containsHistory(leftPath: leftPath, rightPath: rightPath) {
try? HistorySessionManager.shared.openDocument(leftPath: leftPath, rightPath: rightPath)
} else {
// Create a new document
diff --git a/Sources/Core/History/HistorySessionManager.swift b/Sources/Core/History/HistorySessionManager.swift
index ca3dc08..bc87d08 100644
--- a/Sources/Core/History/HistorySessionManager.swift
+++ b/Sources/Core/History/HistorySessionManager.swift
@@ -6,6 +6,8 @@
// Copyright (c) 2015 visualdiffer.com
//
+import os.log
+
class HistorySessionManager: @unchecked Sendable {
static let defaultMaxItemsCount = 50
@@ -25,7 +27,10 @@ class HistorySessionManager: @unchecked Sendable {
}
func maxItemCountPref() -> Int {
- let maxItemCount = CommonPrefs.shared.number(forKey: .History.maximumHistoryCount, Self.defaultMaxItemsCount).intValue
+ let maxItemCount = CommonPrefs.shared.number(
+ forKey: .History.maximumHistoryCount,
+ Self.defaultMaxItemsCount
+ ).intValue
if maxItemCount >= 0 {
return maxItemCount
@@ -80,14 +85,17 @@ class HistorySessionManager: @unchecked Sendable {
do {
try historyMOC.save()
} catch {
- NSLog("Unable to save history \(error)")
+ Logger.general.error("Unable to save history \(error)")
}
documents.insert(document.uuid)
}
}
func createNewHistory() -> HistoryEntity? {
- guard let history = NSEntityDescription.insertNewObject(forEntityName: HistoryEntity.name, into: historyMOC) as? HistoryEntity else {
+ guard let history = NSEntityDescription.insertNewObject(
+ forEntityName: HistoryEntity.name,
+ into: historyMOC
+ ) as? HistoryEntity else {
return nil
}
return history
@@ -98,7 +106,9 @@ class HistorySessionManager: @unchecked Sendable {
rightPath: String
) -> Bool {
do {
- let count = try historyMOC.count(for: HistoryEntity.searchPathRequest(leftPath: leftPath, rightPath: rightPath))
+ let count = try historyMOC.count(
+ for: HistoryEntity.searchPathRequest(leftPath: leftPath, rightPath: rightPath)
+ )
return (count == NSNotFound || count == 0) ? false : true
} catch {}
return false
@@ -140,7 +150,7 @@ class HistorySessionManager: @unchecked Sendable {
do {
return try historyMOC.fetch(HistoryEntity.searchPathRequest(leftPath: leftPath, rightPath: rightPath)).first
} catch {
- NSLog("Error fetching objects: \(error.localizedDescription)")
+ Logger.general.error("Error fetching objects: \(error.localizedDescription)")
}
return nil
}
@@ -192,7 +202,7 @@ class HistorySessionManager: @unchecked Sendable {
withIntermediateDirectories: true
)
} catch {
- NSLog("Unable to create history directory \(historyPath), reason \(error)")
+ Logger.general.error("Unable to create history directory \(historyPath), reason \(error)")
return nil
}
}
diff --git a/Sources/Core/SessionDiff/SessionDiff+AlignRule.swift b/Sources/Core/SessionDiff/SessionDiff+AlignRule.swift
index ae05a3b..92da9dc 100644
--- a/Sources/Core/SessionDiff/SessionDiff+AlignRule.swift
+++ b/Sources/Core/SessionDiff/SessionDiff+AlignRule.swift
@@ -6,6 +6,8 @@
// Copyright (c) 2025 visualdiffer.com
//
+import os.log
+
extension SessionDiff {
var fileNameAlignments: [AlignRule]? {
get {
@@ -25,7 +27,7 @@ extension SessionDiff {
)
return (data as? [[String: Any]])?.compactMap { AlignRule($0) }
} catch {
- NSLog("Unable to unarchive file name alignments array \(error)")
+ Logger.general.error("Unable to unarchive file name alignments array \(error)")
return nil
}
@@ -40,7 +42,7 @@ extension SessionDiff {
requiringSecureCoding: false
)
} catch {
- NSLog("Unable to save file name alignments array \(error)")
+ Logger.general.error("Unable to save file name alignments array \(error)")
fileNameAlignmentsData = nil
}
} else {
diff --git a/Sources/Features/FilesCompare/Controller/FilesWindowController+NSToolbarDelegate.swift b/Sources/Features/FilesCompare/Controller/FilesWindowController+NSToolbarDelegate.swift
index a1e0938..6580589 100644
--- a/Sources/Features/FilesCompare/Controller/FilesWindowController+NSToolbarDelegate.swift
+++ b/Sources/Features/FilesCompare/Controller/FilesWindowController+NSToolbarDelegate.swift
@@ -208,7 +208,9 @@ extension FilesWindowController: NSToolbarDelegate, NSToolbarItemValidation {
@MainActor func updateToolbarButton(_ item: NSToolbarItem) {
if item.itemIdentifier == .Files.wordWrap {
- item.image = NSImage(named: rowHeightCalculator.isWordWrapEnabled ? VDImageNameWordWrapOn : VDImageNameWordWrapOff)
+ item.image = NSImage(
+ named: rowHeightCalculator.isWordWrapEnabled ? VDImageNameWordWrapOn : VDImageNameWordWrapOff
+ )
return
}
switch lastUsedView.side {
diff --git a/Sources/Features/FilesCompare/Controller/FilesWindowController+Navigate.swift b/Sources/Features/FilesCompare/Controller/FilesWindowController+Navigate.swift
index 03fd5b3..629c2a1 100644
--- a/Sources/Features/FilesCompare/Controller/FilesWindowController+Navigate.swift
+++ b/Sources/Features/FilesCompare/Controller/FilesWindowController+Navigate.swift
@@ -71,9 +71,17 @@ extension FilesWindowController {
}
if navigateToNext {
- parentSession.nextDifferenceFiles(from: sessionDiff.leftPath, rightPath: sessionDiff.rightPath, block: block)
+ parentSession.nextDifferenceFiles(
+ from: sessionDiff.leftPath,
+ rightPath: sessionDiff.rightPath,
+ block: block
+ )
} else {
- parentSession.prevDifferenceFiles(from: sessionDiff.leftPath, rightPath: sessionDiff.rightPath, block: block)
+ parentSession.prevDifferenceFiles(
+ from: sessionDiff.leftPath,
+ rightPath: sessionDiff.rightPath,
+ block: block
+ )
}
}
diff --git a/Sources/Features/FilesCompare/Controller/FilesWindowController+TableView.swift b/Sources/Features/FilesCompare/Controller/FilesWindowController+TableView.swift
index 6850476..be3de94 100644
--- a/Sources/Features/FilesCompare/Controller/FilesWindowController+TableView.swift
+++ b/Sources/Features/FilesCompare/Controller/FilesWindowController+TableView.swift
@@ -27,7 +27,10 @@
let arr = diffSide.lines
let diffLine = arr[row]
- let view = tableView.makeView(withIdentifier: identifier, owner: nil) as? LineNumberTableCellView ?? LineNumberTableCellView()
+ let view = tableView.makeView(
+ withIdentifier: identifier,
+ owner: nil
+ ) as? LineNumberTableCellView ?? LineNumberTableCellView()
view.diffLine = diffLine
view.font = treeViewFont()
@@ -52,7 +55,11 @@
let visibleRows = tableView.rows(in: tableView.visibleRect)
for row in visibleRows.location ..< NSMaxRange(visibleRows) {
- if let cellView = tableView.view(atColumn: 0, row: row, makeIfNecessary: false) as? LineNumberTableCellView {
+ if let cellView = tableView.view(
+ atColumn: 0,
+ row: row,
+ makeIfNecessary: false
+ ) as? LineNumberTableCellView {
cellView.isSelected = tableView.isRowSelected(row)
}
}
@@ -75,7 +82,10 @@
let path1 = arr[0].osPath
var isDir = ObjCBool(false)
- let isValidPath1 = FileManager.default.fileExists(atPath: path1, isDirectory: &isDir) && isDir.boolValue == false
+ let isValidPath1 = FileManager.default.fileExists(
+ atPath: path1,
+ isDirectory: &isDir
+ ) && isDir.boolValue == false
if arr.count < 2 {
if isValidPath1 {
@@ -83,7 +93,10 @@
}
} else {
let path2 = arr[1].osPath
- let isValidPath2 = FileManager.default.fileExists(atPath: path2, isDirectory: &isDir) && isDir.boolValue == false
+ let isValidPath2 = FileManager.default.fileExists(
+ atPath: path2,
+ isDirectory: &isDir
+ ) && isDir.boolValue == false
if isValidPath1, isValidPath2 {
result = .copy
}
diff --git a/Sources/Features/FoldersCompare/CompareItem/CompareItem+Description.swift b/Sources/Features/FoldersCompare/CompareItem/CompareItem+Description.swift
index b3549e4..ea5889e 100644
--- a/Sources/Features/FoldersCompare/CompareItem/CompareItem+Description.swift
+++ b/Sources/Features/FoldersCompare/CompareItem/CompareItem+Description.swift
@@ -9,7 +9,6 @@
extension CompareItem {
override public var description: String {
String(
- // swiftlint:disable:next line_length
format: "Path %@, isFileValid %d, isFolder %d, isFiltered %d, isDisp %d, subs , type %@, older %ld, changed %ld, orphan %ld, matched %ld, tags = %ld, labels = %ld, linkedItem %@",
path ?? "",
isValidFile,
diff --git a/Sources/Features/FoldersCompare/CompareItem/CompareItem+VisibleItem.swift b/Sources/Features/FoldersCompare/CompareItem/CompareItem+VisibleItem.swift
index 8f7bce9..7f8c499 100644
--- a/Sources/Features/FoldersCompare/CompareItem/CompareItem+VisibleItem.swift
+++ b/Sources/Features/FoldersCompare/CompareItem/CompareItem+VisibleItem.swift
@@ -121,7 +121,10 @@ extension CompareItem {
setAttributes(try? fm.attributesOfItem(atPath: path), fileExtraOptions: filterConfig.fileExtraOptions)
}
if let path = destRoot.path {
- destRoot.setAttributes(try? fm.attributesOfItem(atPath: path), fileExtraOptions: filterConfig.fileExtraOptions)
+ destRoot.setAttributes(
+ try? fm.attributesOfItem(atPath: path),
+ fileExtraOptions: filterConfig.fileExtraOptions
+ )
}
srcDiffSize = fileSize - srcDiffSize
diff --git a/Sources/Features/FoldersCompare/CompareItem/CompareItem.swift b/Sources/Features/FoldersCompare/CompareItem/CompareItem.swift
index 9049371..8fb49ed 100644
--- a/Sources/Features/FoldersCompare/CompareItem/CompareItem.swift
+++ b/Sources/Features/FoldersCompare/CompareItem/CompareItem.swift
@@ -8,9 +8,8 @@
typealias CompareItemComparison = (CompareItem, CompareItem) -> ComparisonResult
-// swiftlint:disable file_length
public class CompareItem: NSObject {
- struct FileOptions: OptionSet {
+ struct FileOptions: FlagSet {
var rawValue: Int
static let isFile = FileOptions(rawValue: 1 << 0)
@@ -20,20 +19,6 @@ public class CompareItem: NSObject {
static let isResourceFork = FileOptions(rawValue: 1 << 4)
static let isLocked = FileOptions(rawValue: 1 << 5)
static let isValidFile = FileOptions(rawValue: 1 << 6)
-
- subscript(option: FileOptions) -> Bool {
- get {
- contains(option)
- }
-
- set {
- if newValue {
- insert(option)
- } else {
- remove(option)
- }
- }
- }
}
var linkedItem: CompareItem?
@@ -401,5 +386,3 @@ public class CompareItem: NSObject {
isFiltered = false
}
}
-
-// swiftlint:enable file_length
diff --git a/Sources/Features/FoldersCompare/CompareItem/FileExtraOptions.swift b/Sources/Features/FoldersCompare/CompareItem/FileExtraOptions.swift
index be04113..f96599c 100644
--- a/Sources/Features/FoldersCompare/CompareItem/FileExtraOptions.swift
+++ b/Sources/Features/FoldersCompare/CompareItem/FileExtraOptions.swift
@@ -6,7 +6,7 @@
// Copyright (c) 2025 visualdiffer.com
//
-public struct FileExtraOptions: OptionSet, Sendable {
+public struct FileExtraOptions: FlagSet, Sendable {
public let rawValue: Int
static let resourceFork = FileExtraOptions(rawValue: 1 << 0)
@@ -15,17 +15,3 @@ public struct FileExtraOptions: OptionSet, Sendable {
self.rawValue = rawValue
}
}
-
-public extension FileExtraOptions {
- var hasCheckResourceForks: Bool {
- contains(.resourceFork)
- }
-
- func changeCheckResourceForks(_ isOn: Bool) -> Self {
- if isOn {
- union(.resourceFork)
- } else {
- subtracting(.resourceFork)
- }
- }
-}
diff --git a/Sources/Features/FoldersCompare/Components/CompareItemTableCellView.swift b/Sources/Features/FoldersCompare/Components/CompareItemTableCellView.swift
index 6380d9b..6480d45 100644
--- a/Sources/Features/FoldersCompare/Components/CompareItemTableCellView.swift
+++ b/Sources/Features/FoldersCompare/Components/CompareItemTableCellView.swift
@@ -98,7 +98,12 @@ class CompareItemTableCellView: NSView {
text.lineBreakMode = .byTruncatingMiddle
}
text.font = font
- icon?.image = IconUtils.shared.icon(for: item, size: 16, isExpanded: isExpanded, hideEmptyFolders: hideEmptyFolders)
+ icon?.image = IconUtils.shared.icon(
+ for: item,
+ size: 16,
+ isExpanded: isExpanded,
+ hideEmptyFolders: hideEmptyFolders
+ )
}
func fileSize(
diff --git a/Sources/Features/FoldersCompare/Components/FoldersOutlineView/FoldersOutlineView+Columns.swift b/Sources/Features/FoldersCompare/Components/FoldersOutlineView/FoldersOutlineView+Columns.swift
index fe3e0a1..4e22c9a 100644
--- a/Sources/Features/FoldersCompare/Components/FoldersOutlineView/FoldersOutlineView+Columns.swift
+++ b/Sources/Features/FoldersCompare/Components/FoldersOutlineView/FoldersOutlineView+Columns.swift
@@ -67,7 +67,8 @@ extension FoldersOutlineView {
+ NSScroller.scrollerWidth(for: .regular, scrollerStyle: .legacy)
let fileSizeFormatter = FileSizeFormatter(showInBytes: true, showUnitForBytes: false)
- let sizeCellWidth = (fileSizeFormatter.string(from: NSNumber(value: 999_999_999_999)) as? NSString)?.size(withAttributes: textAttrs).width ?? 0
+ let sizeCellWidth = (fileSizeFormatter.string(from: NSNumber(value: 999_999_999_999)) as? NSString)?
+ .size(withAttributes: textAttrs).width ?? 0
tableColumn(withIdentifier: .Folders.cellName)?.width = totalWidth - dateCellWidth - sizeCellWidth
tableColumn(withIdentifier: .Folders.cellSize)?.width = sizeCellWidth
diff --git a/Sources/Features/FoldersCompare/Components/FoldersOutlineView/IconUtils+CompareItemIconUtils.swift b/Sources/Features/FoldersCompare/Components/FoldersOutlineView/IconUtils+CompareItemIconUtils.swift
index 17ec79f..40c7e78 100644
--- a/Sources/Features/FoldersCompare/Components/FoldersOutlineView/IconUtils+CompareItemIconUtils.swift
+++ b/Sources/Features/FoldersCompare/Components/FoldersOutlineView/IconUtils+CompareItemIconUtils.swift
@@ -19,7 +19,11 @@
let url = item.toUrl() {
if item.isLocked {
if item.isFolder {
- let name = ColoredFoldersManager.shared.iconName(item, isExpanded: isExpanded, hideEmptyFolders: hideEmptyFolders)
+ let name = ColoredFoldersManager.shared.iconName(
+ item,
+ isExpanded: isExpanded,
+ hideEmptyFolders: hideEmptyFolders
+ )
let url = URL(filePath: name)
icon = self.icon(forLockedFile: url, size: size)
} else {
@@ -27,7 +31,11 @@
}
} else if item.isSymbolicLink {
if item.isFolder {
- let name = ColoredFoldersManager.shared.iconName(item, isExpanded: isExpanded, hideEmptyFolders: hideEmptyFolders)
+ let name = ColoredFoldersManager.shared.iconName(
+ item,
+ isExpanded: isExpanded,
+ hideEmptyFolders: hideEmptyFolders
+ )
let url = URL(filePath: name)
icon = self.icon(forSymbolicLink: url, size: size)
} else {
@@ -35,7 +43,12 @@
}
} else {
if item.isFolder {
- icon = ColoredFoldersManager.shared.icon(forFolder: item, size: size, isExpanded: isExpanded, hideEmptyFolders: hideEmptyFolders)
+ icon = ColoredFoldersManager.shared.icon(
+ forFolder: item,
+ size: size,
+ isExpanded: isExpanded,
+ hideEmptyFolders: hideEmptyFolders
+ )
} else {
// get the icon from path because for some files (eg resource forks)
// the file type should be irrelevant
diff --git a/Sources/Features/FoldersCompare/Controller/FileSystemController/FileSystemController.swift b/Sources/Features/FoldersCompare/Controller/FileSystemController/FileSystemController.swift
index 058039d..8062b83 100644
--- a/Sources/Features/FoldersCompare/Controller/FileSystemController/FileSystemController.swift
+++ b/Sources/Features/FoldersCompare/Controller/FileSystemController/FileSystemController.swift
@@ -262,7 +262,10 @@ public class FileSystemController: NSWindowCon
totalSize = fileCount.subfoldersSize
if includesFiltered {
- totalFiles += fileFilteredCount.olderFiles + fileFilteredCount.changedFiles + fileFilteredCount.orphanFiles + fileFilteredCount.matchedFiles
+ totalFiles += fileFilteredCount.olderFiles
+ + fileFilteredCount.changedFiles
+ + fileFilteredCount.orphanFiles
+ + fileFilteredCount.matchedFiles
totalFolders += fileFilteredCount.folders
totalSize += fileFilteredCount.subfoldersSize
}
@@ -341,7 +344,10 @@ public class FileSystemController: NSWindowCon
func itemsCount() -> Int {
var count = fileCount.olderFiles + fileCount.changedFiles + fileCount.orphanFiles + fileCount.matchedFiles
count += fileCount.folders
- count += fileFilteredCount.olderFiles + fileFilteredCount.changedFiles + fileFilteredCount.orphanFiles + fileFilteredCount.matchedFiles
+ count += fileFilteredCount.olderFiles
+ + fileFilteredCount.changedFiles
+ + fileFilteredCount.orphanFiles
+ + fileFilteredCount.matchedFiles
count += fileFilteredCount.folders
return count
diff --git a/Sources/Features/FoldersCompare/Controller/FileSystemController/FileSystemTestHelper.swift b/Sources/Features/FoldersCompare/Controller/FileSystemController/FileSystemTestHelper.swift
new file mode 100644
index 0000000..9366bf1
--- /dev/null
+++ b/Sources/Features/FoldersCompare/Controller/FileSystemController/FileSystemTestHelper.swift
@@ -0,0 +1,450 @@
+//
+// FileSystemTestHelper.swift
+// VisualDiffer
+//
+// Created by davide ficano on 25/12/25.
+// Copyright (c) 2025 visualdiffer.com
+//
+
+// swiftlint:disable identifier_name force_unwrapping force_try file_length
+#if DEBUG
+
+ private let fakeFile = "12345678901234567890"
+
+ private let headerPattern =
+ """
+ let comparatorDelegate = MockItemComparatorDelegate()
+ let comparator = ItemComparator(
+ options: %@,
+ delegate: comparatorDelegate,
+ bufferSize: 8192,
+ isLeftCaseSensitive: %@,
+ isRightCaseSensitive: %@
+ )
+ let filterConfig = FilterConfig(
+ showFilteredFiles: %@,
+ hideEmptyFolders: %@,
+ followSymLinks: %@,
+ skipPackages: %@,
+ traverseFilteredFolders: %@,
+ predicate: defaultPredicate,
+ fileExtraOptions: [],
+ displayOptions: %@
+ )
+ let folderReaderDelegate = MockFolderReaderDelegate(isRunning: true)
+ let folderReader = FolderReader(
+ with: folderReaderDelegate,
+ comparator: comparator,
+ filterConfig: filterConfig,
+ refreshInfo: RefreshInfo(initState: true)
+ )
+
+ try removeItem("l")
+ try removeItem("r")
+
+ """
+
+ private let readFolderPattern =
+ """
+ folderReader.start(
+ withLeftRoot: nil,
+ rightRoot: nil,
+ leftPath: appendFolder("l"),
+ rightPath: appendFolder("r")
+ )
+
+ let rootL = folderReader.leftRoot!
+ let rootR = folderReader.rightRoot!
+ let vi = rootL.visibleItem!
+ """
+
+ class FileSystemTestHelper {
+ var strFolders = "// create folders\n"
+ var strFiles = "// create files\n"
+ var strAssert = ""
+ var strAssertVisibleItems = "// VisibleItems\n"
+ var leftPos = 0
+ var rightPos = 0
+ var childNum = 1
+
+ var header = ""
+
+ private static let comparatorStrings = [
+ ComparatorOptions.timestamp.rawValue: ".timestamp",
+ ComparatorOptions.size.rawValue: ".size",
+ ComparatorOptions.content.rawValue: ".content",
+ ComparatorOptions.contentTimestamp.rawValue: ".contentTimestamp",
+ ComparatorOptions.asText.rawValue: ".asText",
+ ComparatorOptions.finderLabel.rawValue: ".finderLabel",
+ ComparatorOptions.finderTags.rawValue: ".finderTags",
+ ComparatorOptions.filename.rawValue: ".filename",
+ ComparatorOptions.alignFileSystemCase.rawValue: ".alignFileSystemCase",
+ ComparatorOptions.alignMatchCase.rawValue: ".alignMatchCase",
+ ComparatorOptions.alignIgnoreCase.rawValue: ".alignIgnoreCase",
+ ]
+ private static let displayFiltersStrings = [
+ DisplayOptions.onlyMatches.rawValue: ".onlyMatches",
+ DisplayOptions.onlyLeftSideNewer.rawValue: ".onlyLeftSideNewer",
+ DisplayOptions.onlyLeftSideOrphans.rawValue: ".onlyLeftSideOrphans",
+ DisplayOptions.onlyRightSideNewer.rawValue: ".onlyRightSideNewer",
+ DisplayOptions.onlyRightSideOrphans.rawValue: ".onlyRightSideOrphans",
+ DisplayOptions.mismatchesButNoOrphans.rawValue: ".mismatchesButNoOrphans",
+ DisplayOptions.leftNewerAndLeftOrphans.rawValue: ".leftNewerAndLeftOrphans",
+ DisplayOptions.rightNewerAndRightOrphans.rawValue: ".rightNewerAndRightOrphans",
+ DisplayOptions.onlyMismatches.rawValue: ".onlyMismatches",
+ DisplayOptions.noOrphan.rawValue: ".noOrphan",
+ DisplayOptions.onlyOrphans.rawValue: ".onlyOrphans",
+ DisplayOptions.showAll.rawValue: ".showAll",
+ DisplayOptions.dontFollowSymlinks.rawValue: ".dontFollowSymlinks",
+ ]
+
+ private var comparatorFlags: ComparatorOptions = []
+
+ init(sessionDiff: SessionDiff) {
+ comparatorFlags = sessionDiff.comparatorOptions
+
+ leftPos = URL(filePath: sessionDiff.leftPath!).deletingLastPathComponent().osPath.count + 1
+ rightPos = URL(filePath: sessionDiff.rightPath!).deletingLastPathComponent().osPath.count + 1
+ header = Self.formatHeader(sessionDiff)
+ }
+
+ private static func formatHeader(_ sessionDiff: SessionDiff) -> String {
+ let leftURL = URL(filePath: sessionDiff.leftPath!)
+ let rightURL = URL(filePath: sessionDiff.rightPath!)
+
+ return String(
+ format: headerPattern,
+ Self.stringify(flag: sessionDiff.comparatorOptions.rawValue, stringNumberDictionary: Self.comparatorStrings),
+ Self.bool2String(try! leftURL.volumeSupportsCaseSensitive()),
+ Self.bool2String(try! rightURL.volumeSupportsCaseSensitive()),
+ Self.bool2String(false),
+ Self.bool2String(true),
+ Self.bool2String(sessionDiff.followSymLinks),
+ Self.bool2String(sessionDiff.skipPackages),
+ Self.bool2String(sessionDiff.traverseFilteredFolders),
+ Self.stringify(flag: sessionDiff.displayOptions.rawValue, stringNumberDictionary: Self.displayFiltersStrings)
+ )
+ }
+
+ private static func bool2String(_ value: Bool) -> String { value ? "true" : "false" }
+
+ private static func stringify(
+ flag: Int,
+ stringNumberDictionary: [Int: String]
+ ) -> String {
+ var orFlags = [String]()
+
+ if let str = stringNumberDictionary[flag] {
+ orFlags.append(str)
+ } else {
+ var currentFlag = 1
+ var flag = flag
+
+ while flag != 0 {
+ if (currentFlag & flag) != 0, let value = stringNumberDictionary[currentFlag] {
+ orFlags.append(value)
+ flag &= ~currentFlag
+ }
+ currentFlag <<= 1
+ }
+ }
+ let stringFlags = orFlags.joined(separator: ", ")
+
+ if orFlags.count > 1 {
+ return "[\(stringFlags)]"
+ }
+ return stringFlags
+ }
+
+ @MainActor static func createTestCode(
+ _ view: FoldersOutlineView,
+ sessionDiff: SessionDiff
+ ) {
+ guard let vi = view.dataSource?.outlineView?(view, child: 0, ofItem: nil) as? VisibleItem,
+ let fs = vi.item.parent else {
+ return
+ }
+ let testHelper = FileSystemTestHelper(sessionDiff: sessionDiff)
+ testHelper.generateTest(fs)
+ testHelper.generateTest(fs.visibleItem!)
+ testHelper.copyTestToClipboard()
+ }
+
+ // MARK: - CompareItem generators
+
+ private func generateTest(_ root: CompareItem) {
+ childNum = 1
+ generateTest(root, parentVarName: "rootL", index: 0)
+ }
+
+ private func generateTest(
+ _ root: CompareItem,
+ parentVarName: String,
+ index: Int
+ ) {
+ createFolders(root)
+ createAssert(root, parentVarName: parentVarName, outString: &strAssert, index: index)
+
+ let parentName = String(format: "child%ld", childNum)
+ for (index, fs) in root.children.enumerated() {
+ childNum += 1
+
+ if fs.isFolder {
+ generateTest(fs, parentVarName: parentName, index: index)
+ } else {
+ createFolders(fs)
+ createAssert(fs, parentVarName: parentName, outString: &strAssert, index: index)
+ }
+ }
+ }
+
+ // MARK: - VisibleItem generators
+
+ private func generateTest(_ root: VisibleItem) {
+ childNum = 1
+
+ generateTest(root, parentVarName: "vi", index: 0)
+ }
+
+ private func generateTest(
+ _ root: VisibleItem,
+ parentVarName: String,
+ index: Int
+ ) {
+ let childVarName = String(format: "childVI%ld", childNum)
+
+ // root element for simplicity is assigned to child and printed out
+ if parentVarName == "vi" {
+ strAssertVisibleItems.append(String(
+ format: "let %@ = vi // %@ <--> %@\n",
+ childVarName,
+ root.item.fileName!,
+ root.item.linkedItem!.fileName!
+ ))
+ } else {
+ strAssertVisibleItems.append(String(
+ format: "let %@ = %@.children[%ld] // %@ <--> %@\n",
+ childVarName,
+ parentVarName,
+ index,
+ root.item.parent!.fileName!,
+ root.item.linkedItem!.parent!.fileName!
+ ))
+ }
+ strAssertVisibleItems.append(String(format: "assertArrayCount(%@.children, %ld)\n", childVarName, root.children.count))
+ createAssert(root, parentVarName: childVarName, outString: &strAssertVisibleItems, index: index)
+
+ for (index, vi) in root.children.enumerated() {
+ childNum += 1
+
+ generateTest(vi, parentVarName: childVarName, index: index)
+ }
+ }
+
+ // MARK: - Assert generator methods
+
+ private func createAssert(
+ _ fsOrVi: AnyObject,
+ parentVarName: String,
+ outString: inout String,
+ index: Int
+ ) {
+ var fs: CompareItem
+ var stringFormat = ""
+
+ if let vi = fsOrVi as? VisibleItem {
+ fs = vi.item
+ stringFormat = "let child%ld = %@.item // %@ <-> %@\n"
+ outString.append(String(
+ format: stringFormat,
+ childNum,
+ parentVarName,
+ fs.parent?.fileName ?? "nil",
+ fs.parent?.linkedItem?.fileName ?? "nil"
+ ))
+ } else if let fsOrVi = fsOrVi as? CompareItem {
+ fs = fsOrVi
+ if parentVarName == "rootL" {
+ stringFormat = "let child%ld = %@ // %@ <-> %@\n"
+ outString.append(String(
+ format: stringFormat,
+ childNum,
+ parentVarName,
+ fs.fileName ?? "nil",
+ fs.linkedItem?.fileName ?? "nil"
+ ))
+ } else {
+ stringFormat = "let child%ld = %@.children[%ld] // %@ <-> %@\n"
+ outString.append(String(
+ format: stringFormat,
+ childNum,
+ parentVarName,
+ index,
+ fs.parent?.fileName ?? "nil",
+ fs.parent?.linkedItem?.fileName ?? "nil"
+ ))
+ }
+ } else {
+ return
+ }
+
+ createAssert(fs, outString: &outString, linked: false)
+ createAssert(fs.linkedItem!, outString: &outString, linked: true)
+
+ outString.append("\n")
+ }
+
+ private func createAssert(
+ _ fs: CompareItem,
+ outString: inout String,
+ linked: Bool
+ ) {
+ let fileName = if let fileName = fs.fileName {
+ String(format: "\"%@\"", fileName)
+ } else {
+ "nil"
+ }
+
+ let linkedString = linked ? ".linkedItem" : ""
+
+ outString.append(String(
+ format: "assertItem(child%ld%@, %ld, %ld, %ld, %ld, %ld, %@, .%@, %lld)\n",
+ childNum,
+ linkedString,
+ fs.olderFiles,
+ fs.changedFiles,
+ fs.orphanFiles,
+ fs.matchedFiles,
+ fs.children.count,
+ fileName,
+ fs.type.description,
+ fs.isFolder ? fs.subfoldersSize : fs.fileSize
+ ))
+ if fs.isValidFile, fs.isFolder {
+ let linkedStringForceUnwrapping = linked ? ".linkedItem!" : ""
+ outString.append(String(
+ format: "#expect(child%ld%@.orphanFolders == %ld, \"OrphanFolder: Expected count %ld found \\(child%ld%@.orphanFolders)\")\n",
+ childNum,
+ linkedStringForceUnwrapping,
+ fs.orphanFolders,
+ fs.orphanFolders,
+ childNum,
+ linkedStringForceUnwrapping
+ ))
+ }
+
+ if comparatorFlags.contains(.finderTags) {
+ outString.append(String(format: "assertFolderTags(child%ld%@, %@, %@)\n", childNum, linkedString, Self.bool2String(fs.summary.hasMetadataTags), fileName))
+ outString.append(String(format: "assertMismatchingTags(child%ld%@, %ld, %@)\n", childNum, linkedString, fs.mismatchingTags, fileName))
+ }
+ if comparatorFlags.contains(.finderLabel) {
+ outString.append(String(format: "assertFolderLabels(child%ld%@, %@, %@)\n", childNum, linkedString, Self.bool2String(fs.summary.hasMetadataLabels), fileName))
+ outString.append(String(format: "assertMismatchingLabels(child%ld%@, %ld, %@)\n", childNum, linkedString, fs.mismatchingLabels, fileName))
+ addAssertLabelsOnDisk(&outString, fs: fs, linked: linked, index: childNum)
+ }
+ }
+
+ private func addAssertLabelsOnDisk(
+ _ outString: inout String,
+ fs: CompareItem,
+ linked: Bool,
+ index: Int
+ ) {
+ guard fs.isValidFile else {
+ return
+ }
+ let path = fs.path!
+ if let labelNumber = URL(filePath: path).labelNumber() {
+ let linkedString = linked ? ".linkedItem" : ""
+ let startIndex = path.index(path.startIndex, offsetBy: linked ? rightPos : leftPos)
+ let subPath = String(path[startIndex ..< path.endIndex])
+ outString.append(String(
+ format: "assertResourceFileLabels(child%ld%@, %ld, appendFolder(\"%@\"))\n",
+ index,
+ linkedString,
+ labelNumber,
+ subPath
+ ))
+ }
+ }
+
+ // MARK: - Folder, size, metadata
+
+ private func createFolders(_ fs: CompareItem) {
+ appendCreateFoldersExpressions(fs, pathIndex: leftPos)
+ appendCreateFoldersExpressions(fs.linkedItem!, pathIndex: rightPos)
+ }
+
+ private func appendCreateFoldersExpressions(
+ _ fs: CompareItem,
+ pathIndex: Int
+ ) {
+ guard fs.isValidFile,
+ let path = fs.path else {
+ return
+ }
+ let startIndex = path.index(path.startIndex, offsetBy: pathIndex)
+ let subPath = String(path[startIndex ..< path.endIndex])
+ if fs.isFolder {
+ strFolders.append(String(format: "try createFolder(\"%@\")\n", subPath))
+ } else {
+ if fs.olderFiles > 0 {
+ strFiles.append(String(format: "try createFile(\"%@\", \"%@\")\n", subPath, fakeFileBySize(fs.fileSize, subPath)))
+ strFiles.append(String(format: "try setFileTimestamp(\"%@\", \"2001-03-24 10:45:32 +0600\")\n", subPath))
+ } else {
+ strFiles.append(String(format: "try createFile(\"%@\", \"%@\")\n", subPath, fakeFileBySize(fs.fileSize, subPath)))
+ }
+ }
+ if comparatorFlags.contains(.finderTags) {
+ addTags(fs, subPath: subPath)
+ }
+ if comparatorFlags.contains(.finderLabel) {
+ addLabels(fs, subPath: subPath)
+ }
+ }
+
+ private func fakeFileBySize(_ size: Int64, _ path: String) -> String {
+ assert(size < fakeFile.count, "File size (\(size)) is greater then fakeFile length (\(fakeFile.count)) for \(path)")
+
+ return String(fakeFile[.. NSView? {
guard let tableColumn,
let itemCompare = (item as? VisibleItem)?.item,
- let result = outlineView.makeView(withIdentifier: tableColumn.identifier, owner: nil) as? CompareItemTableCellView else {
+ let result = outlineView.makeView(
+ withIdentifier: tableColumn.identifier,
+ owner: nil
+ ) as? CompareItemTableCellView else {
return nil
}
diff --git a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+MenuDelegate.swift b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+MenuDelegate.swift
index a47eb8a..108044f 100644
--- a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+MenuDelegate.swift
+++ b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+MenuDelegate.swift
@@ -49,7 +49,10 @@ extension FoldersWindowController: NSMenuDelegate,
return fsi.validateCopyFiles(sessionDiff)
} else if action == #selector(deleteFiles) {
return fsi.validateDeleteFiles(sessionDiff)
- } else if action == #selector(copyFullPaths) || action == #selector(copyFileNames) || action == #selector(copy(_:)) || action == #selector(copyUrls) {
+ } else if action == #selector(copyFullPaths)
+ || action == #selector(copyFileNames)
+ || action == #selector(copy(_:))
+ || action == #selector(copyUrls) {
return fsi.validateClipboardCopy()
} else if action == #selector(setAsBaseFolder) {
return fsi.validateSetAsBaseFolder()
diff --git a/Sources/Features/FoldersCompare/Controller/FoldersWindowController.swift b/Sources/Features/FoldersCompare/Controller/FoldersWindowController.swift
index 8ec927d..3da7896 100644
--- a/Sources/Features/FoldersCompare/Controller/FoldersWindowController.swift
+++ b/Sources/Features/FoldersCompare/Controller/FoldersWindowController.swift
@@ -8,6 +8,7 @@
import Quartz
import UserNotifications
+import os.log
// the heights are in flipped coordinates
private let splitViewMinHeight: CGFloat = 160.0
@@ -263,4 +264,19 @@ public class FoldersWindowController: NSWindowController,
@objc func zoomResetFont(_: AnyObject) {
fontZoomFactor = 0
}
+
+ #if DEBUG
+ override public func keyDown(with event: NSEvent) {
+ // Cmd + F12 pressed, create the test code
+ if event.modifierFlags.contains(.command),
+ event.charactersIgnoringModifiers?.unicodeScalars.first?.value == UInt32(NSF12FunctionKey) {
+ FileSystemTestHelper.createTestCode(leftView, sessionDiff: sessionDiff)
+ Logger.ui.info("Unit Test generated and copied on clipboard")
+
+ return
+ }
+
+ super.keyDown(with: event)
+ }
+ #endif
}
diff --git a/Sources/Features/FoldersCompare/FileManager/CopyCompareItem.swift b/Sources/Features/FoldersCompare/FileManager/CopyCompareItem.swift
index 32f8ba7..d72db14 100644
--- a/Sources/Features/FoldersCompare/FileManager/CopyCompareItem.swift
+++ b/Sources/Features/FoldersCompare/FileManager/CopyCompareItem.swift
@@ -380,7 +380,11 @@ class CopyCompareItem: NSObject {
// set the timestamps at the end so we are sure they are not 'overwritten'
// For example modification date for folders changes after a file is copied into it
- try fm.setFileAttributes(operationManager.timestampAttributesFrom(attributes), ofItemAtPath: destFullPath, volumeType: volumeType)
+ try fm.setFileAttributes(
+ operationManager.timestampAttributesFrom(attributes),
+ ofItemAtPath: destFullPath,
+ volumeType: volumeType
+ )
// the delete above reset path and file to nil
// here we assign again the copied values
let destAttrs = try fm.attributesOfItem(atPath: destFullPath.osPath)
diff --git a/Sources/Features/FoldersCompare/FileManager/MoveCompareItem.swift b/Sources/Features/FoldersCompare/FileManager/MoveCompareItem.swift
index fdc7651..c5ce545 100644
--- a/Sources/Features/FoldersCompare/FileManager/MoveCompareItem.swift
+++ b/Sources/Features/FoldersCompare/FileManager/MoveCompareItem.swift
@@ -446,7 +446,10 @@ public class MoveCompareItem: NSObject {
}
// update file information after move
destRoot.path = destFullPath.osPath
- destRoot.setAttributes(try? FileManager.default.attributesOfItem(atPath: destFullPath.osPath), fileExtraOptions: operationManager.filterConfig.fileExtraOptions)
+ destRoot.setAttributes(
+ try? FileManager.default.attributesOfItem(atPath: destFullPath.osPath),
+ fileExtraOptions: operationManager.filterConfig.fileExtraOptions
+ )
if destRoot.isFiltered,
let filter = operationManager.filterConfig.predicate {
diff --git a/Sources/Features/FoldersCompare/Services/FolderReader/FolderReader.swift b/Sources/Features/FoldersCompare/Services/FolderReader/FolderReader.swift
index 8d7dc70..78824ae 100644
--- a/Sources/Features/FoldersCompare/Services/FolderReader/FolderReader.swift
+++ b/Sources/Features/FoldersCompare/Services/FolderReader/FolderReader.swift
@@ -356,16 +356,18 @@ public class FolderReader: @unchecked Sendable {
// but the order can change from a different sort in the delegate
// so we get the current order and use it to traverse the folders
var traversalOrder = [CompareItem]()
+ var seenItems = Set()
if let visibleItem = leftItem.visibleItem {
for vi in visibleItem.children {
traversalOrder.append(vi.item)
+ seenItems.insert(ObjectIdentifier(vi.item))
}
}
// Only VisibleItems are ordered so filtered elements are not present inside the array
// We must iterate also the not filtered items to correctly update folders informations like the subfolders size
// We add the missing CompareItem to the end, this is correct because they are not visible
- for item in leftItem.children where !traversalOrder.contains(where: { $0 == item }) {
+ for item in leftItem.children where !seenItems.contains(ObjectIdentifier(item)) {
traversalOrder.append(item)
}
diff --git a/Sources/Features/FoldersCompare/Services/ItemComparator/ItemComparator+AlignRegularExpression.swift b/Sources/Features/FoldersCompare/Services/ItemComparator/ItemComparator+AlignRegularExpression.swift
index 9e1e6f1..82345e8 100644
--- a/Sources/Features/FoldersCompare/Services/ItemComparator/ItemComparator+AlignRegularExpression.swift
+++ b/Sources/Features/FoldersCompare/Services/ItemComparator/ItemComparator+AlignRegularExpression.swift
@@ -20,7 +20,10 @@ extension ItemComparator {
var lIndex = leftIndex
var rIndex = rightIndex
- let result = leftChild.compare(rightChild, followSymLinks: alignConfig.followSymLinks) { self.compareByRegularExpression(
+ let result = leftChild.compare(
+ rightChild,
+ followSymLinks: alignConfig.followSymLinks
+ ) { self.compareByRegularExpression(
lhs: $0,
rhs: $1,
leftRoot: leftRoot,
diff --git a/Sources/Features/FoldersCompare/Services/ItemComparator/ItemComparator+Compare.swift b/Sources/Features/FoldersCompare/Services/ItemComparator/ItemComparator+Compare.swift
index 94f5467..baf9556 100644
--- a/Sources/Features/FoldersCompare/Services/ItemComparator/ItemComparator+Compare.swift
+++ b/Sources/Features/FoldersCompare/Services/ItemComparator/ItemComparator+Compare.swift
@@ -97,7 +97,9 @@ extension ItemComparator {
return .orderedSame
}
- var res: ComparisonResult = leftTags.count == rightTags.count ? .orderedSame : leftTags.count < rightTags.count ? .orderedAscending : .orderedDescending
+ var res: ComparisonResult = leftTags.count == rightTags.count
+ ? .orderedSame
+ : leftTags.count < rightTags.count ? .orderedAscending : .orderedDescending
if res == .orderedSame {
for (index, item) in leftTags.enumerated() where res == .orderedSame {
res = item.caseInsensitiveCompare(rightTags[index])
diff --git a/Sources/Features/Preferences/Box/ComparisonStandardUserDataSource.swift b/Sources/Features/Preferences/Box/ComparisonStandardUserDataSource.swift
index 9f08ae2..a428a86 100644
--- a/Sources/Features/Preferences/Box/ComparisonStandardUserDataSource.swift
+++ b/Sources/Features/Preferences/Box/ComparisonStandardUserDataSource.swift
@@ -13,7 +13,7 @@ class ComparisonStandardUserDataSource: StandardUserPreferencesBoxDataSource {
) -> Bool {
switch key {
case .virtualResourceFork:
- CommonPrefs.shared.checkResourceForks
+ CommonPrefs.shared.fileExtraOptions[.resourceFork]
case .virtualFinderLabel:
CommonPrefs.shared.finderLabel
case .virtualFinderTags:
@@ -30,7 +30,7 @@ class ComparisonStandardUserDataSource: StandardUserPreferencesBoxDataSource {
) {
switch key {
case .virtualResourceFork:
- CommonPrefs.shared.checkResourceForks = value
+ CommonPrefs.shared.fileExtraOptions[.resourceFork] = value
case .virtualFinderLabel:
CommonPrefs.shared.finderLabel = value
case .virtualFinderTags:
diff --git a/Sources/Features/Preferences/Main/Controllers/BasePreferences.swift b/Sources/Features/Preferences/Main/Controllers/BasePreferences.swift
index 0dbd9f2..d4e4404 100644
--- a/Sources/Features/Preferences/Main/Controllers/BasePreferences.swift
+++ b/Sources/Features/Preferences/Main/Controllers/BasePreferences.swift
@@ -210,8 +210,17 @@ class BasePreferences: NSWindowController, NSToolbarDelegate, NSTabViewDelegate,
func resize() {
let windowFrame = NSWindow.contentRect(forFrameRect: prefPanel.frame, styleMask: prefPanel.styleMask)
let height = minWindowHeight()
- let frameRect = NSRect(x: windowFrame.origin.x, y: windowFrame.origin.y + windowFrame.size.height - height, width: windowFrame.size.width, height: height)
- prefPanel.setFrame(NSWindow.frameRect(forContentRect: frameRect, styleMask: prefPanel.styleMask), display: true, animate: prefPanel.isVisible)
+ let frameRect = NSRect(
+ x: windowFrame.origin.x,
+ y: windowFrame.origin.y + windowFrame.size.height - height,
+ width: windowFrame.size.width,
+ height: height
+ )
+ prefPanel.setFrame(
+ NSWindow.frameRect(forContentRect: frameRect, styleMask: prefPanel.styleMask),
+ display: true,
+ animate: prefPanel.isVisible
+ )
}
func tabView(_: NSTabView, didSelect _: NSTabViewItem?) {
diff --git a/Sources/Features/Preferences/Session/Controllers/SessionPreferencesWindow.swift b/Sources/Features/Preferences/Session/Controllers/SessionPreferencesWindow.swift
index 85b25b4..3643aa2 100644
--- a/Sources/Features/Preferences/Session/Controllers/SessionPreferencesWindow.swift
+++ b/Sources/Features/Preferences/Session/Controllers/SessionPreferencesWindow.swift
@@ -205,7 +205,7 @@ class SessionPreferencesWindow: NSWindowController, NSTabViewDelegate, @preconcu
func preferenceBox(_: PreferencesBox, boolForKey key: CommonPrefs.Name) -> Bool {
switch key {
case .virtualResourceFork:
- currentPreferences.fileExtraOptions.hasCheckResourceForks
+ currentPreferences.fileExtraOptions[.resourceFork]
case .virtualFinderLabel:
currentPreferences.comparatorOptions.hasFinderLabel
case .virtualFinderTags:
@@ -226,7 +226,7 @@ class SessionPreferencesWindow: NSWindowController, NSTabViewDelegate, @preconcu
func preferenceBox(_: PreferencesBox, setBool value: Bool, forKey key: CommonPrefs.Name) {
switch key {
case .virtualResourceFork:
- currentPreferences.fileExtraOptions = currentPreferences.fileExtraOptions.changeCheckResourceForks(value)
+ currentPreferences.fileExtraOptions[.resourceFork] = value
case .virtualFinderLabel:
currentPreferences.comparatorOptions = currentPreferences.comparatorOptions.changeFinderLabel(value)
case .virtualFinderTags:
diff --git a/Sources/Legacy/TOPMacros.h b/Sources/Legacy/TOPMacros.h
deleted file mode 100644
index b260bd5..0000000
--- a/Sources/Legacy/TOPMacros.h
+++ /dev/null
@@ -1,30 +0,0 @@
-//
-// TOPMacros.h
-// TernaryOp
-//
-// Created by davide ficano on 21/11/14.
-// Copyright (c) 2014 visualdiffer.com
-//
-
-#ifndef TernaryOp_TOPMacros_h
-#define TernaryOp_TOPMacros_h
-
-// Output log only when in debug mode showing the method name
-#if defined(DEBUG)
-#define TOP_DEBUG_METHOD_LOG(text, ...) NSLog(@"%@ " text, NSStringFromSelector(_cmd), ## __VA_ARGS__)
-#else
-#define TOP_DEBUG_METHOD_LOG(text, ...)
-#endif
-
-#define TOP_LOG(text, ...) NSLog(text, ## __VA_ARGS__)
-#define TOP_METHOD_LOG(text, ...) NSLog(@"%@ " text, NSStringFromSelector(_cmd), ## __VA_ARGS__)
-
-#define TOP_IS_FLAG_ON(wholeFlags, flagToTest) ((wholeFlags & flagToTest) == flagToTest)
-#define TOP_SET_FLAG_ON(wholeFlags, flagToSet) (wholeFlags | flagToSet)
-#define TOP_SET_FLAG_OFF(wholeFlags, flagToSet) (wholeFlags & ~flagToSet)
-#define TOP_TOGGLE_FLAG(wholeFlags, flagToTest) ((wholeFlags & flagToTest) == flagToTest ? (wholeFlags & ~flagToTest) : (wholeFlags | flagToTest))
-// If flagToTest is on then return NSControlStateValueOn otherwise NSControlStateValueOff
-#define TOP_BUTTON_STATE(wholeFlags, flagToTest) ((wholeFlags & flagToTest) == flagToTest ? NSControlStateValueOn : NSControlStateValueOff)
-#define TOP_TOGGLE_BUTTON_STATE(wholeFlags, flagToTest, state) (state == NSControlStateValueOn ? TOP_SET_FLAG_ON(wholeFlags, flagToTest) : TOP_SET_FLAG_OFF(wholeFlags, flagToTest))
-
-#endif
diff --git a/Sources/SharedKit/Extensions/Color/NSColor+Hex.swift b/Sources/SharedKit/Extensions/Color/NSColor+Hex.swift
index b5c8ded..205ff35 100644
--- a/Sources/SharedKit/Extensions/Color/NSColor+Hex.swift
+++ b/Sources/SharedKit/Extensions/Color/NSColor+Hex.swift
@@ -10,7 +10,12 @@ import Cocoa
extension NSColor {
@objc static func colorRGBA(_ red: UInt, green: UInt, blue: UInt, alpha: UInt) -> NSColor {
- NSColor(calibratedRed: CGFloat(red) / 255.0, green: CGFloat(green) / 255.0, blue: CGFloat(blue) / 255.0, alpha: CGFloat(alpha) / 255.0)
+ NSColor(
+ calibratedRed: CGFloat(red) / 255.0,
+ green: CGFloat(green) / 255.0,
+ blue: CGFloat(blue) / 255.0,
+ alpha: CGFloat(alpha) / 255.0
+ )
}
@objc static func colorFromHexRGBA(_ inColorString: String) -> NSColor? {
diff --git a/Sources/SharedKit/Extensions/Image/NSImage+Tint.swift b/Sources/SharedKit/Extensions/Image/NSImage+Tint.swift
index 33a5bb1..3a94498 100644
--- a/Sources/SharedKit/Extensions/Image/NSImage+Tint.swift
+++ b/Sources/SharedKit/Extensions/Image/NSImage+Tint.swift
@@ -100,8 +100,14 @@ public extension NSImage {
return nil
}
- compositingFilter.setValue(colorFilter.value(forKey: kCIOutputImageKey), forKey: kCIInputImageKey)
- compositingFilter.setValue(monochromeFilter.value(forKey: kCIOutputImageKey), forKey: kCIInputBackgroundImageKey)
+ compositingFilter.setValue(
+ colorFilter.value(forKey: kCIOutputImageKey),
+ forKey: kCIInputImageKey
+ )
+ compositingFilter.setValue(
+ monochromeFilter.value(forKey: kCIOutputImageKey),
+ forKey: kCIInputBackgroundImageKey
+ )
return compositingFilter
}
diff --git a/Sources/SharedKit/Extensions/OptionSet/FlagSet.swift b/Sources/SharedKit/Extensions/OptionSet/FlagSet.swift
new file mode 100644
index 0000000..62d86a3
--- /dev/null
+++ b/Sources/SharedKit/Extensions/OptionSet/FlagSet.swift
@@ -0,0 +1,25 @@
+//
+// FlagSet.swift
+// VisualDiffer
+//
+// Created by davide ficano on 31/12/25.
+// Copyright (c) 2025 visualdiffer.com
+//
+
+public protocol FlagSet: OptionSet {}
+
+public extension FlagSet {
+ @inlinable
+ subscript(option: Self.Element) -> Bool {
+ get {
+ contains(option)
+ }
+ set {
+ if newValue {
+ insert(option)
+ } else {
+ remove(option)
+ }
+ }
+ }
+}
diff --git a/Sources/SharedKit/Extensions/TableView/NSTableView+Row.swift b/Sources/SharedKit/Extensions/TableView/NSTableView+Row.swift
index 4be4ce3..cdc32c1 100644
--- a/Sources/SharedKit/Extensions/TableView/NSTableView+Row.swift
+++ b/Sources/SharedKit/Extensions/TableView/NSTableView+Row.swift
@@ -91,9 +91,15 @@ extension NSTableView {
let point = if center,
let scrollView {
- NSPoint(x: 0, y: rowRect.origin.y - headerHeight + (rowRect.size.height / 2) - (scrollView.frame.size.height / 2))
+ NSPoint(
+ x: 0,
+ y: rowRect.origin.y - headerHeight + (rowRect.size.height / 2) - (scrollView.frame.size.height / 2)
+ )
} else {
- NSPoint(x: 0, y: rowRect.origin.y - headerHeight)
+ NSPoint(
+ x: 0,
+ y: rowRect.origin.y - headerHeight
+ )
}
scroll(point)
}
diff --git a/Sources/SharedKit/Extensions/URL/URL+Path.swift b/Sources/SharedKit/Extensions/URL/URL+Path.swift
index d180599..fa9e426 100644
--- a/Sources/SharedKit/Extensions/URL/URL+Path.swift
+++ b/Sources/SharedKit/Extensions/URL/URL+Path.swift
@@ -48,7 +48,10 @@ public extension URL {
return destBaseURL
.appending(path: String(relativePath))
- .appending(path: destURL.lastPathComponent, directoryHint: destURL.hasDirectoryPath ? .isDirectory : .notDirectory)
+ .appending(
+ path: destURL.lastPathComponent,
+ directoryHint: destURL.hasDirectoryPath ? .isDirectory : .notDirectory
+ )
}
let trailingPath = String(srcPath[srcBaseDir.endIndex...]).trimmingPrefix("/")
return destBaseURL
diff --git a/Sources/SharedKit/Extensions/URL/URL+StructuredContent.swift b/Sources/SharedKit/Extensions/URL/URL+StructuredContent.swift
index e092452..b22390e 100644
--- a/Sources/SharedKit/Extensions/URL/URL+StructuredContent.swift
+++ b/Sources/SharedKit/Extensions/URL/URL+StructuredContent.swift
@@ -34,8 +34,14 @@ extension URL {
return text.string
}
// try fallback encoding NSWindowsCP1252StringEncoding
- let optionsWithBestEncoding = options.merging([.characterEncoding: NSWindowsCP1252StringEncoding]) { _, new in new }
- if let text = try readHandlingInapplicableStringEncoding(options: optionsWithBestEncoding, documentAttributes: &docAttrs) {
+ let optionsWithBestEncoding = options.merging(
+ [.characterEncoding: NSWindowsCP1252StringEncoding]
+ ) { _, new in new }
+
+ if let text = try readHandlingInapplicableStringEncoding(
+ options: optionsWithBestEncoding,
+ documentAttributes: &docAttrs
+ ) {
return text.string
}
// try removing encoding
diff --git a/Sources/SharedKit/Extensions/View/TextView/NSTextView+Style.swift b/Sources/SharedKit/Extensions/View/TextView/NSTextView+Style.swift
index 8bcbabb..0400533 100644
--- a/Sources/SharedKit/Extensions/View/TextView/NSTextView+Style.swift
+++ b/Sources/SharedKit/Extensions/View/TextView/NSTextView+Style.swift
@@ -11,7 +11,7 @@
guard let font else {
return
}
- guard let paragraphStyle: NSMutableParagraphStyle = defaultParagraphStyle?.mutableCopy() as? NSMutableParagraphStyle
+ guard let paragraphStyle = defaultParagraphStyle?.mutableCopy() as? NSMutableParagraphStyle
?? NSParagraphStyle.default.mutableCopy() as? NSMutableParagraphStyle else {
return
}
diff --git a/Sources/SharedKit/Utilities/Document/SecureBookmark.swift b/Sources/SharedKit/Utilities/Document/SecureBookmark.swift
index da265bd..40095ee 100644
--- a/Sources/SharedKit/Utilities/Document/SecureBookmark.swift
+++ b/Sources/SharedKit/Utilities/Document/SecureBookmark.swift
@@ -7,6 +7,7 @@
//
import Foundation
+import os.log
private let sandboxedPaths = "sandboxedPaths"
@@ -48,7 +49,7 @@ class SecureBookmark: @unchecked Sendable {
dict![path.osPath] = bookmark
UserDefaults.standard.set(dict, forKey: sandboxedPaths)
} catch {
- NSLog("Secure bookmark failed \(error)")
+ Logger.general.error("Secure bookmark failed \(error)")
return false
}
}
@@ -77,7 +78,7 @@ class SecureBookmark: @unchecked Sendable {
}
return url
} catch {
- NSLog("Secure bookmark error while resolving bookmark \(error)")
+ Logger.general.error("Secure bookmark error while resolving bookmark \(error)")
}
return nil
}
diff --git a/Sources/SharedKit/Utilities/IO/BufferedInputStream.swift b/Sources/SharedKit/Utilities/IO/BufferedInputStream.swift
index e75542b..24b0631 100644
--- a/Sources/SharedKit/Utilities/IO/BufferedInputStream.swift
+++ b/Sources/SharedKit/Utilities/IO/BufferedInputStream.swift
@@ -110,7 +110,11 @@ public class BufferedInputStream: InputStream {
]
do {
let line = try withUnsafeMutablePointer(to: &docAttrs) { docAttrsPointer in
- try NSAttributedString(data: data, options: options, documentAttributes: AutoreleasingUnsafeMutablePointer(docAttrsPointer)).string
+ try NSAttributedString(
+ data: data,
+ options: options,
+ documentAttributes: AutoreleasingUnsafeMutablePointer(docAttrsPointer)
+ ).string
}
if let val = docAttrs[NSAttributedString.DocumentAttributeKey.characterEncoding] as? NSNumber {
diff --git a/Sources/SharedKit/Utilities/IO/FileError.swift b/Sources/SharedKit/Utilities/IO/FileError.swift
index 4e89b28..fa23cd4 100644
--- a/Sources/SharedKit/Utilities/IO/FileError.swift
+++ b/Sources/SharedKit/Utilities/IO/FileError.swift
@@ -14,7 +14,6 @@ public enum FileError: Error, Equatable {
case unknownVolumeType
}
-// swiftlint:disable line_length
extension FileError: LocalizedError {
public var errorDescription: String? {
switch self {
@@ -49,5 +48,3 @@ extension FileError: LocalizedError {
}
}
}
-
-// swiftlint:enable line_length
diff --git a/Sources/SharedKit/Utilities/OpenEditor/OpenEditor.swift b/Sources/SharedKit/Utilities/OpenEditor/OpenEditor.swift
index 61387c2..ef90759 100644
--- a/Sources/SharedKit/Utilities/OpenEditor/OpenEditor.swift
+++ b/Sources/SharedKit/Utilities/OpenEditor/OpenEditor.swift
@@ -6,6 +6,8 @@
// Copyright (c) 2025 visualdiffer.com
//
+import os.log
+
struct OpenEditor {
let attributes: [OpenEditorAttribute]
@@ -62,7 +64,7 @@ extension OpenEditor {
configuration: NSWorkspace.OpenConfiguration()
) { _, error in
if let error {
- NSLog("Unable to open file \(item.path): \(error)")
+ Logger.general.error("Unable to open file \(item.path): \(error)")
}
}
}
@@ -93,7 +95,7 @@ extension OpenEditor {
let task = try NSUserUnixTask(url: scriptURL)
task.execute(withArguments: arguments()) { taskError in
if let taskError {
- NSLog("Unable to launch \(scriptURL.osPath), \(taskError)")
+ Logger.general.error("Unable to launch \(scriptURL.osPath), \(taskError)")
}
}
} catch {
diff --git a/Sources/SharedKit/Utilities/OpenEditor/OpenEditorError.swift b/Sources/SharedKit/Utilities/OpenEditor/OpenEditorError.swift
index 41e3d3f..ce688e7 100644
--- a/Sources/SharedKit/Utilities/OpenEditor/OpenEditorError.swift
+++ b/Sources/SharedKit/Utilities/OpenEditor/OpenEditorError.swift
@@ -11,7 +11,6 @@ enum OpenEditorError: Error {
case missingExecutePermission(URL)
}
-// swiftlint:disable line_length
extension OpenEditorError: LocalizedError {
var errorDescription: String? {
switch self {
@@ -24,5 +23,3 @@ extension OpenEditorError: LocalizedError {
}
}
}
-
-// swiftlint:enable line_length
diff --git a/Sources/SharedKit/Utilities/View/Encoding/SelectEncodingsPanel.swift b/Sources/SharedKit/Utilities/View/Encoding/SelectEncodingsPanel.swift
index 3c2e070..aeb12a4 100644
--- a/Sources/SharedKit/Utilities/View/Encoding/SelectEncodingsPanel.swift
+++ b/Sources/SharedKit/Utilities/View/Encoding/SelectEncodingsPanel.swift
@@ -187,7 +187,10 @@ class SelectEncodingsPanel: NSWindow, NSTableViewDataSource, NSTableViewDelegate
guard let identifier = tableColumn?.identifier else {
return nil
}
- let cell = tableView.makeView(withIdentifier: identifier, owner: self) as? NSTableCellView ?? createCell(identifier)
+ let cell = tableView.makeView(
+ withIdentifier: identifier,
+ owner: self
+ ) as? NSTableCellView ?? createCell(identifier)
cell.textField?.stringValue = String.localizedName(of: encodingsList[row])
diff --git a/Sources/SharedUI/Cells/FilePathTableCellView.swift b/Sources/SharedUI/Cells/FilePathTableCellView.swift
index 0962119..e165582 100644
--- a/Sources/SharedUI/Cells/FilePathTableCellView.swift
+++ b/Sources/SharedUI/Cells/FilePathTableCellView.swift
@@ -85,7 +85,10 @@ class FilePathTableCellView: NSTableCellView {
}
if pathTextField.fileExists {
- imageView.image = IconUtils.shared.icon(forFile: URL(filePath: path, directoryHint: .notDirectory), size: 16.0)
+ imageView.image = IconUtils.shared.icon(
+ forFile: URL(filePath: path, directoryHint: .notDirectory),
+ size: 16.0
+ )
} else {
imageView.image = NSImage(named: NSImage.cautionName)
imageView.image?.size = NSSize(width: 16.0, height: 16.0)
diff --git a/VisualDiffer.xcodeproj/project.pbxproj b/VisualDiffer.xcodeproj/project.pbxproj
index 740593f..fd309e2 100644
--- a/VisualDiffer.xcodeproj/project.pbxproj
+++ b/VisualDiffer.xcodeproj/project.pbxproj
@@ -463,6 +463,7 @@
55C1CC012E8BB755004FE322 /* visdiff in Copy Visdiff to Helpers */ = {isa = PBXBuildFile; fileRef = 55C1CBE22E8BB6A2004FE322 /* visdiff */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
55C1CC072E8BBD85004FE322 /* DocumentWaiter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55C1CC062E8BBAD1004FE322 /* DocumentWaiter.swift */; };
55C84A712DA93ADB00BA39DA /* UserNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 55C84A702DA93ADB00BA39DA /* UserNotifications.framework */; };
+ 55D352E72EFD0C5B00C944D5 /* FileSystemTestHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55D352E62EFD0C4D00C944D5 /* FileSystemTestHelper.swift */; };
55D71FD62EE584B7007E0FA0 /* AppUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55D71FD52EE584B7007E0FA0 /* AppUpdater.swift */; };
55DF3DD22E924BB10044CC0C /* BaseTests+FileSystemOperations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55DF3DC52E924BB10044CC0C /* BaseTests+FileSystemOperations.swift */; };
55DF3DD32E924BB10044CC0C /* FiltersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55DF3DBF2E924BB10044CC0C /* FiltersTests.swift */; };
@@ -507,6 +508,7 @@
55F9E79A2EDD5DBD001218C7 /* DiffResult.Options.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55F9E7992EDD5DAD001218C7 /* DiffResult.Options.swift */; };
55F9E79C2EDD6221001218C7 /* WhitespacesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55F9E79B2EDD6221001218C7 /* WhitespacesTests.swift */; };
55FD7E28180C484500CF473C /* prefs_folder@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 55FD7E25180C484500CF473C /* prefs_folder@2x.png */; };
+ 55FE894A2F054B260003864A /* FlagSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55FE89492F054B260003864A /* FlagSet.swift */; };
775DFF38067A968500C5B868 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7A7FEA54F5311CA2CBB /* Cocoa.framework */; };
8D15AC2C0486D014006FF6A4 /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 2A37F4B9FDCFA73011CA2CEA /* Credits.rtf */; };
8D15AC2F0486D014006FF6A4 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 089C165FFE840EACC02AAC07 /* InfoPlist.strings */; };
@@ -869,7 +871,6 @@
553740D92E9372DF00AB56D0 /* TOPFileSizePredicateEditorRowTemplate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPFileSizePredicateEditorRowTemplate.m; sourceTree = ""; };
553740DA2E9372DF00AB56D0 /* TOPTimestampPredicateEditorRowTemplate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPTimestampPredicateEditorRowTemplate.h; sourceTree = ""; };
553740DB2E9372DF00AB56D0 /* TOPTimestampPredicateEditorRowTemplate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPTimestampPredicateEditorRowTemplate.m; sourceTree = ""; };
- 553740E02E9372DF00AB56D0 /* TOPMacros.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPMacros.h; sourceTree = ""; };
553740E22E9372DF00AB56D0 /* OpenDiffCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenDiffCommand.swift; sourceTree = ""; };
553740E52E9372DF00AB56D0 /* NSAppearance+DarkMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSAppearance+DarkMode.swift"; sourceTree = ""; };
553740E72E9372DF00AB56D0 /* Array+MoveIndexes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+MoveIndexes.swift"; sourceTree = ""; };
@@ -1056,6 +1057,7 @@
55C1CC062E8BBAD1004FE322 /* DocumentWaiter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentWaiter.swift; sourceTree = ""; };
55C84A702DA93ADB00BA39DA /* UserNotifications.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotifications.framework; path = System/Library/Frameworks/UserNotifications.framework; sourceTree = SDKROOT; };
55D26E84140E2EAD00646F50 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; };
+ 55D352E62EFD0C4D00C944D5 /* FileSystemTestHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileSystemTestHelper.swift; sourceTree = ""; };
55D71FD52EE584B7007E0FA0 /* AppUpdater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppUpdater.swift; sourceTree = ""; };
55D71FD72EE584FF007E0FA0 /* AppConfig-Sparkle.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "AppConfig-Sparkle.xcconfig"; sourceTree = ""; };
55D71FD82EE584FF007E0FA0 /* VisualDiffer-Sparkle.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "VisualDiffer-Sparkle.entitlements"; sourceTree = ""; };
@@ -1106,6 +1108,7 @@
55F9E79B2EDD6221001218C7 /* WhitespacesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhitespacesTests.swift; sourceTree = ""; };
55FC15F22557CD000075E30D /* ScriptingBridge.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ScriptingBridge.framework; path = System/Library/Frameworks/ScriptingBridge.framework; sourceTree = SDKROOT; };
55FD7E25180C484500CF473C /* prefs_folder@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "prefs_folder@2x.png"; sourceTree = ""; };
+ 55FE89492F054B260003864A /* FlagSet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlagSet.swift; sourceTree = ""; };
7788DA0506752A1600599AAD /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = /System/Library/Frameworks/CoreData.framework; sourceTree = ""; };
8D15AC360486D014006FF6A4 /* VisualDiffer-Info.plist */ = {isa = PBXFileReference; explicitFileType = text.plist.xml; fileEncoding = 4; path = "VisualDiffer-Info.plist"; sourceTree = ""; };
8D15AC370486D014006FF6A4 /* VisualDiffer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = VisualDiffer.app; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -1671,14 +1674,15 @@
children = (
55373FFC2E9372DF00AB56D0 /* Copy */,
55373FFE2E9372DF00AB56D0 /* Delete */,
- 553740002E9372DF00AB56D0 /* Move */,
- 5537400B2E9372DF00AB56D0 /* Progress */,
- 553740102E9372DF00AB56D0 /* Sync */,
- 553740152E9372DF00AB56D0 /* Touch */,
553740162E9372DF00AB56D0 /* FileOperationExecutor.swift */,
553740172E9372DF00AB56D0 /* FileSummaryView.swift */,
553740182E9372DF00AB56D0 /* FileSystemController.swift */,
+ 55D352E62EFD0C4D00C944D5 /* FileSystemTestHelper.swift */,
+ 553740002E9372DF00AB56D0 /* Move */,
553740192E9372DF00AB56D0 /* OperationSummaryView.swift */,
+ 5537400B2E9372DF00AB56D0 /* Progress */,
+ 553740102E9372DF00AB56D0 /* Sync */,
+ 553740152E9372DF00AB56D0 /* Touch */,
);
path = FileSystemController;
sourceTree = "";
@@ -2149,7 +2153,6 @@
553740C92E9372DF00AB56D0 /* FileManager */,
553740D32E9372DF00AB56D0 /* MGScopeBar */,
553740DC2E9372DF00AB56D0 /* Predicate */,
- 553740E02E9372DF00AB56D0 /* TOPMacros.h */,
);
path = Legacy;
sourceTree = "";
@@ -2253,6 +2256,7 @@
553740FA2E9372DF00AB56D0 /* OptionSet */ = {
isa = PBXGroup;
children = (
+ 55FE89492F054B260003864A /* FlagSet.swift */,
553740F92E9372DF00AB56D0 /* OptionSet+Toggle.swift */,
);
path = OptionSet;
@@ -3220,6 +3224,7 @@
55916A102E8A814200ED6629 /* MyDocument.xcdatamodeld in Sources */,
553741792E9372DF00AB56D0 /* NSAppearance+DarkMode.swift in Sources */,
5537417A2E9372DF00AB56D0 /* QLPreviewPanel.swift in Sources */,
+ 55FE894A2F054B260003864A /* FlagSet.swift in Sources */,
5537417B2E9372DF00AB56D0 /* TrustedPathsPreferencesPanel.swift in Sources */,
5537417C2E9372DF00AB56D0 /* TouchPickersStackView.swift in Sources */,
5537417D2E9372DF00AB56D0 /* BasePreferences.swift in Sources */,
@@ -3251,6 +3256,7 @@
553741982E9372DF00AB56D0 /* CommonPrefs+FolderCompare.swift in Sources */,
553741992E9372DF00AB56D0 /* FoldersWindowController+PathControlDelegate.swift in Sources */,
551DE3B82ED439AF0067AB18 /* PreferencesBoxDataSource+Default.swift in Sources */,
+ 55D352E72EFD0C5B00C944D5 /* FileSystemTestHelper.swift in Sources */,
5537419A2E9372DF00AB56D0 /* FontBox.swift in Sources */,
5537419B2E9372DF00AB56D0 /* FilesTableViewFindTextDelegate.swift in Sources */,
5537419C2E9372DF00AB56D0 /* DeleteFileOperationExecutor.swift in Sources */,
diff --git a/VisualDiffer_Prefix.pch b/VisualDiffer_Prefix.pch
index 0c2776c..e0d8658 100644
--- a/VisualDiffer_Prefix.pch
+++ b/VisualDiffer_Prefix.pch
@@ -2,8 +2,6 @@
// Prefix header for all source files of the 'VisualDiffer' target in the 'VisualDiffer' project
//
-#import "TOPMacros.h"
-
#ifdef __OBJC__
#import
#import
diff --git a/scripts/changelog.sh b/scripts/changelog.sh
new file mode 100755
index 0000000..d8c55f9
--- /dev/null
+++ b/scripts/changelog.sh
@@ -0,0 +1,68 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+XC_CONFIG_FILE="Versions.local.xcconfig"
+CONTEXT_JSON="changelog-context.local.json"
+
+get_key_from_xcconfig() {
+ local xcconfig_file="$1"
+ local key="${2:-APP_VERSION}"
+
+ if [[ ! -f "$xcconfig_file" ]]; then
+ echo "Error: file not found: $xcconfig_file" >&2
+ return 1
+ fi
+
+ local value
+ value="$(sed -nE "s/^[[:space:]]*$key[[:space:]]*=[[:space:]]*(.*)\$/\1/p" "$xcconfig_file")"
+ value="${value%%[[:space:]]*}"
+
+ if [[ -z "$value" ]]; then
+ echo "Error: $key not found in $xcconfig_file" >&2
+ return 1
+ fi
+
+ echo "$value"
+}
+
+update_json_property() {
+ local json_file="$1"
+ local property="$2"
+ local prop_value="$3"
+
+ if [[ ! -f "$json_file" ]]; then
+ echo "Error: file not found: $json_file" >&2
+ return 1
+ fi
+
+ sed -i '' -E \
+ 's/^([[:space:]]*"'$property'":[[:space:]]*")[^"]*(")/\1'"$prop_value"'\2/' \
+ "$json_file"
+}
+
+rename_json_property() {
+ local json_file="$1"
+ local old_name="$2"
+ local new_name="$3"
+
+ if [[ ! -f "$json_file" ]]; then
+ echo "Error: file not found: $json_file" >&2
+ return 1
+ fi
+
+ sed -i '' -E \
+ 's/^([[:space:]]*")'$old_name'(":)/\1'"$new_name"'\2/' \
+ "$json_file"
+}
+
+
+version="$(get_key_from_xcconfig $XC_CONFIG_FILE)"
+update_json_property $CONTEXT_JSON "appVersion" "v$version"
+
+if [ "$1" == "all" ]; then
+ rename_json_property $CONTEXT_JSON "excludeTypes" "_excludeTypes"
+else
+ rename_json_property $CONTEXT_JSON "_excludeTypes" "excludeTypes"
+fi
+
+npx conventional-changelog -p visualdiffer -c $CONTEXT_JSON