diff --git a/Package.resolved b/Package.resolved index a66dcf8..ae84154 100644 --- a/Package.resolved +++ b/Package.resolved @@ -19,6 +19,15 @@ "version": "0.9.0" } }, + { + "package": "swift-argument-parser", + "repositoryURL": "https://github.com/apple/swift-argument-parser", + "state": { + "branch": null, + "revision": "3d79b2b5a2e5af52c14e462044702ea7728f5770", + "version": "0.1.0" + } + }, { "package": "SwiftCLI", "repositoryURL": "https://github.com/jakeheis/SwiftCLI", diff --git a/Package.swift b/Package.swift index c1b0179..27fdc61 100644 --- a/Package.swift +++ b/Package.swift @@ -16,6 +16,10 @@ let package = Package( .package( url: "https://github.com/kylef/PathKit", from: "1.0.0" + ), + .package( + url: "https://github.com/apple/swift-argument-parser", + from: "0.1.0" ) ], targets: [ @@ -23,7 +27,11 @@ let package = Package( // Targets can depend on other targets in this package, and on products in packages which this package depends on. .target( name: "Badgy", - dependencies: ["SwiftCLI", "PathKit"]), + dependencies: [ + "SwiftCLI", + "PathKit", + .product(name: "ArgumentParser", package: "swift-argument-parser") + ]), .testTarget( name: "BadgyTests", dependencies: ["Badgy"]), diff --git a/Sources/Badgy/Commands/Badgy.swift b/Sources/Badgy/Commands/Badgy.swift new file mode 100644 index 0000000..10b7abf --- /dev/null +++ b/Sources/Badgy/Commands/Badgy.swift @@ -0,0 +1,143 @@ +// +// Badgy +// + +import Foundation +import ArgumentParser +import PathKit + +struct Badgy: ParsableCommand { + static var configuration = CommandConfiguration( + abstract: "A command-line tool to add labels to your app icon", + version: "0.1.4", + subcommands: [Long.self, Small.self], + defaultSubcommand: Long.self + ) +} + +extension Badgy { + struct Options: ParsableArguments { + @Argument(help :"Specify badge text") + var label: String + + @Argument(help :"Specify path to icon with format .png | .jpg | .appiconset", transform: IconPath.init(path:)) + var icon: IconPath + + @Option(help: "Position on which to place the badge") + var position: Position? + + @Option(help: "Specify badge color with a hexadecimal color code format '#rrbbgg' | '#rrbbggaa'") + var color: HexColor? + + @Option(help: "Specify badge text/tint color with a hexadecimal color code format '#rrbbgg' | '#rrbbggaa'") + var tintColor: HexColor? + + @Flag(help: "Indicates Badgy should replace the input icon") + var replace: Bool + + @Flag(help: "Log tech details for nerds") + var verbose: Bool + + func validate() throws { + guard DependencyManager().areDependenciesInstalled() else { + throw ValidationError("Missing dependencies. Run: 'brew install imagemagick'") + } + } + } +} + +extension Badgy { + struct Long: ParsableCommand { + static var configuration = CommandConfiguration( + abstract: "Add rectangular label to app icon" + ) + + @OptionGroup() + var options: Badgy.Options + + @Option(help: "Rotation angle of the badge") + var angle: Int? + + func validate() throws { + guard options.label.count <= 4 else { + throw ValidationError("Label should contain maximum 4 characters") + } + + if let position = options.position { + let supportedPositions: [Position] = [ + .top, .left, .bottom, .right, .center + ] + guard supportedPositions.contains(position) else { + let formatted = supportedPositions + .map { $0.rawValue } + .joined(separator: " | ") + + throw ValidationError("Invalid provided position, supported positions are: \(formatted)") + } + } + + let validAngleRange = -180...180 + if let angle = angle { + guard validAngleRange.contains(angle) else { + throw ValidationError("Angle should be within range: \(validAngleRange)") + } + } + } + } +} + +extension Badgy { + struct Small: ParsableCommand { + static var configuration = CommandConfiguration( + abstract: "Add small square label to app icon" + ) + + @OptionGroup() + var options: Badgy.Options + + func validate() throws { + guard options.label.count <= 1 else { + throw ValidationError("Label should contain maximum 1 characters") + } + } + } +} + +extension Position: ExpressibleByArgument { } + +struct HexColor: ExpressibleByArgument { + let value: String + + init?(argument: String) { + guard NSRegularExpression.hexColorCode.matches(argument) else { + return nil + } + + value = argument + } +} + +struct IconPath { + static let supportedFormats = Set([ + ".png", ".jpg", ".jpeg", ".appiconset" + ]) + + let path: Path + + init(path: String) throws { + let path = Path(path) + + guard path.exists else { + throw ValidationError("Input file doesn't exist") + } + + let isSupportedFormat = IconPath.supportedFormats.contains { + path.lastComponent.contains($0) + } + guard isSupportedFormat else { + throw ValidationError("Input file doesn't have a valid format") + } + + self.path = path + } +} diff --git a/Sources/Badgy/Helpers/Validation+HexColor.swift b/Sources/Badgy/Helpers/Validation+HexColor.swift index c4a0866..ae2590b 100644 --- a/Sources/Badgy/Helpers/Validation+HexColor.swift +++ b/Sources/Badgy/Helpers/Validation+HexColor.swift @@ -22,7 +22,7 @@ extension Validation where T == String { } } -private extension NSRegularExpression { +extension NSRegularExpression { /// `^` asserts position at start of a line /// `#` matches the character # literally ///