Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ on:
jobs:
test:
if: github.event.pull_request.draft == false
runs-on: macos-14
runs-on: macos-26
timeout-minutes: 10

steps:
- uses: actions/checkout@v4
- name: Select Xcode
run: sudo xcode-select -s /Applications/Xcode_15.3.app/Contents/Developer
run: sudo xcode-select -s /Applications/Xcode_26.3.app/Contents/Developer
- name: Run tests
run: sudo swift test --parallel -Xswiftc -DCI -v
7 changes: 4 additions & 3 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version: 5.9
// swift-tools-version: 6.2

import PackageDescription

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ A tool that finds Xcode color assets by their hex codes. The idea behind this to

<p align="center">
<br />
<img alt-text="Swift Version" src="https://img.shields.io/badge/Swift-5.9-orange.svg">
<img alt-text="Swift Version" src="https://img.shields.io/badge/Swift-6.2-orange.svg">
<a href="https://github.com/artem-y/hexcode/releases/latest"><img alt-text="GitHub Release" src="https://img.shields.io/github/v/release/artem-y/hexcode"></a>
<img alt-text="Minimal macOS Version" src="https://img.shields.io/badge/macOS-13%2B-e08416">
</p>
Expand Down
2 changes: 1 addition & 1 deletion Sources/hexcode/Commands/Hexcode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ struct Hexcode: AsyncParsableCommand {
by their hexadecimal codes.
""",
usage: "hexcode <color-hex> [--directory <directory>]",
version: "hexcode 0.2.1",
version: "hexcode 0.2.2",
subcommands: [
FindColor.self,
FindDuplicates.self,
Expand Down
22 changes: 12 additions & 10 deletions Sources/hexcode/Controllers/AssetCollector.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,21 @@ import Foundation

/// Finds color asset folders and converts them to Swift objects.
protocol AssetCollecting {
/// `FileManager` instance used for the search.
var fileManager: FileManager { get set }
/// Finds all color sets in the directory and its subdirectories.
/// - parameter directory: Path to the directory to search for color sets.
/// - throws: Errors when valid directory not found at given path or can't read content.
/// - returns: Named color sets found in given directory, sorted by name.
func collectAssets(in directory: String) async throws -> [NamedColorSet]
}

final class AssetCollector: AssetCollecting {
var fileManager: FileManager = .default
final class AssetCollector: AssetCollecting, Sendable {
/// Judging from Apple's documentation, the use of the default shared `FileManager` from multiple threads is fine.
nonisolated(unsafe)
let fileManager: FileManager

init(fileManager: FileManager = .default) {
self.fileManager = fileManager
}

func collectAssets(in directory: String) async throws -> [NamedColorSet] {
var isDirectory: ObjCBool = false
Expand Down Expand Up @@ -55,7 +59,6 @@ extension AssetCollector {
private func findColorSets(
at paths: [String],
in searchRootDirectory: String,
alreadyFoundColorSets: [NamedColorSet] = []
) async -> [NamedColorSet] {
let colorSets = await withTaskGroup(of: [NamedColorSet].self) { group in
for path in paths {
Expand All @@ -64,7 +67,7 @@ extension AssetCollector {

switch contentAtPath {
case .colorSet(let colorSet):
return self.makeNamedColorset(
return Self.makeNamedColorset(
from: colorSet,
at: path,
in: searchRootDirectory
Expand All @@ -76,7 +79,6 @@ extension AssetCollector {
let colorSetsFromSubdirectory = await self.findColorSets(
at: fullSubpaths,
in: searchRootDirectory,
alreadyFoundColorSets: alreadyFoundColorSets
)
return colorSetsFromSubdirectory

Expand All @@ -86,13 +88,13 @@ extension AssetCollector {
}
}

return await group.reduce(alreadyFoundColorSets, +)
return await group.reduce([], +)
}

return colorSets
}

private func makeNamedColorset(
private static func makeNamedColorset(
from colorSet: ColorSet,
at path: String,
in searchRootDirectory: String
Expand All @@ -119,7 +121,7 @@ extension AssetCollector {
try fileManager.contentsOfDirectory(atPath: directory)
}

private func getAssetName(from path: String, in searchRootDirectory: String) -> String {
private static func getAssetName(from path: String, in searchRootDirectory: String) -> String {
let trimmedPath = String(path.trimmingPrefix(searchRootDirectory + "/"))
return URL(filePath: trimmedPath)
.deletingPathExtension()
Expand Down
6 changes: 2 additions & 4 deletions Sources/hexcode/Controllers/ColorFinder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ final class ColorFinder: ColorFinding {
for currentColor in currentColors {
let rgbHex = currentColor.color.rgbHex

if rgbHex.isEmpty || duplicates.keys.contains(rgbHex) {
if rgbHex.isEmpty || duplicates[rgbHex] != nil {
continue
}

Expand Down Expand Up @@ -81,9 +81,7 @@ final class ColorFinder: ColorFinding {
)
colorNames.append(currentColorSetName)

if duplicates[rgbHex] == nil {
duplicates[rgbHex] = colorNames.sorted()
}
duplicates[rgbHex] = colorNames.sorted()
}

}
Expand Down
7 changes: 2 additions & 5 deletions Sources/hexcode/HexcodeApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,14 @@ final class HexcodeApp {

init(
fileManager: FileManager = .default,
assetCollector: AssetCollecting = AssetCollector(),
assetCollector: AssetCollecting? = nil,
colorFinder: ColorFinding = ColorFinder(),
outputFunction: @escaping ((String) -> Void) = { print($0) }
) {
self.fileManager = fileManager
self.colorFinder = colorFinder
self.output = outputFunction

var assetCollector = assetCollector
assetCollector.fileManager = fileManager
self.assetCollector = assetCollector
self.assetCollector = assetCollector ?? AssetCollector(fileManager: fileManager)
}

/// Entry point for the default `find-color` subcommand logic.
Expand Down
14 changes: 6 additions & 8 deletions Sources/hexcode/Models/ColorAsset+Color.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,11 @@ extension ColorAsset.Color {
}

private func isValidHexComponent(_ component: String) -> Bool {
matchRegex(component, pattern: "0x[0-9a-fA-F]{2}")
}

private func matchRegex(_ string: String, pattern: String) -> Bool {
guard let regex = try? NSRegularExpression(pattern: pattern) else { return false }
let foundMatches = regex.matches(
in: string,
let foundMatches = Self.hexRegex.matches(
in: component,
range: NSRange(
location: 0,
length: string.count
length: component.count
)
)
return foundMatches.count == 1
Expand Down Expand Up @@ -87,6 +82,9 @@ extension ColorAsset.Color {
private static let floatConversionMultiplier: Float = 255.2
private static let hexFormatString = "%02X"

private static let hexRegex = try! NSRegularExpression(
pattern: "0x[0-9a-fA-F]{2}"
)
private static let formatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.decimalSeparator = "."
Expand Down
16 changes: 14 additions & 2 deletions Tests/hexcodeTests/AssetCollectorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,24 @@ final class AssetCollectorTests: XCTestCase {

override func setUp() {
mocks = Mocks()
sut = SUT()
sut.fileManager = mocks.fileManager
sut = SUT(fileManager: mocks.fileManager)
}

// MARK: - Tests

func test_init_withDefaultParameters_setsFileManagerToDefault() async throws {
// Given, When
sut = SUT()

// Then
XCTAssertTrue(sut.fileManager === FileManager.default)
}

func test_init_withNonDefaultFileManagerPassed_setsTheGivenFileManager() async throws {
// Then
XCTAssertTrue(sut.fileManager === mocks.fileManager)
}

func test_collectAssets_inAssetCatalogDirectory_findsAssets() async throws {
// Given
let catalogPath = "Assets.xcassets"
Expand Down
18 changes: 0 additions & 18 deletions Tests/hexcodeTests/HexcodeAppTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,6 @@ final class HexcodeAppTests: XCTestCase {
sut = makeSUT()
}

// MARK: - Test init

func test_init_withoutFileManager_passesDefaultFileManagerIntoAssetCollector() {
// When
_ = SUT(assetCollector: mocks.assetCollector)

// Then
XCTAssertEqual(mocks.assetCollector.calls, [.setFileManager(.default)])
}

func test_init_passingFileManager_passesDefaultFileManagerIntoAssetCollector() {
// When
_ = SUT(fileManager: mocks.fileManager, assetCollector: mocks.assetCollector)

// Then
XCTAssertEqual(mocks.assetCollector.calls, [.setFileManager(mocks.fileManager)])
}

// MARK: - Test runFindColor

func test_runFindColor_withoutDirectory_runsInCurrentDirectoryFromFileManager() async throws {
Expand Down
14 changes: 0 additions & 14 deletions Tests/hexcodeTests/Mocks/AssetCollectorMock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,10 @@ import Foundation
final class AssetCollectorMock {
enum Call: Equatable {
case collectAssetsIn(directory: String)
case getFileManager
case setFileManager(FileManager)
}

struct CallResults {
var collectAssets: Result<[NamedColorSet], Error> = .success([])
var fileManager: FileManager = FileManagerMock()
}

private(set) var calls: [Call] = []
Expand All @@ -25,17 +22,6 @@ final class AssetCollectorMock {
// MARK: - AssetCollecting

extension AssetCollectorMock: AssetCollecting {
var fileManager: FileManager {
get {
calls.append(.getFileManager)
return results.fileManager
}

set {
calls.append(.setFileManager(newValue))
}
}

func collectAssets(in directory: String) throws -> [NamedColorSet] {
calls.append(.collectAssetsIn(directory: directory))
return try results.collectAssets.get()
Expand Down
Loading