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..c787ce8 --- /dev/null +++ b/Sources/Badgy/Commands/Badgy.swift @@ -0,0 +1,151 @@ +// +// 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 | .jpeg | .appiconset", transform: Icon.init(path:)) + var icon: Icon + + @Option(help: """ + Specify a valid hex color code in a case insensitive format: '#rrggbb' | '#rrggbbaa' + or + Provide a named color: 'snow' | 'snow1' | ... + Complete list of named colors: https://imagemagick.org/script/color.php#color_names + """) + var color: ColorCode? + + @Option(help: """ + Specify a valid hex color code in a case insensitive format: '#rrggbb' | '#rrggbbaa' + or + Provide a named color: 'snow' | 'snow1' | ... + Complete list of named colors: https://imagemagick.org/script/color.php#color_names + """) + var tintColor: ColorCode? + + @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'") + } + + Logger.shared.verbose = verbose + } + } +} + +extension Badgy { + struct Long: ParsableCommand { + static var configuration = CommandConfiguration( + abstract: "Add rectangular label to app icon" + ) + + @OptionGroup() + var options: Badgy.Options + + @Option(default: .bottom, help: "Position on which to place the badge. Supported positions: \(Position.longLabelPositions.formatted())") + var position: Position + + @Option(default: 0, help: "The rotation angle of the badge in degrees range of -180 ... 180") + var angle: Int + + func validate() throws { + guard options.label.count <= 4 else { + throw ValidationError("Label should contain maximum 4 characters") + } + + guard position.isValidForLongLabels else { + throw ValidationError("Invalid provided position, supported positions are: \(Position.longLabelPositions.formatted())") + } + + let validAngleRange = -180...180 + guard validAngleRange.contains(angle) else { + throw ValidationError("Angle should be within range: \(validAngleRange)") + } + } + + func run() throws { + Logger.shared.logSection("$ ", item: "badgy long \"\(options.label)\" \"\(options.icon.path)\"", color: .ios) + + var pipeline = IconSignPipeline.make(withOptions: options) + pipeline.position = position + pipeline.angle = angle + + try pipeline.execute() + } + } +} + +extension Badgy { + struct Small: ParsableCommand { + static var configuration = CommandConfiguration( + abstract: "Add small square label to app icon" + ) + + @OptionGroup() + var options: Badgy.Options + + @Option(default: .bottomLeft, help: "Position on which to place the badge. Supported positions: \(Position.allCases.formatted())") + var position: Position + + func validate() throws { + guard options.label.count <= 1 else { + throw ValidationError("Label should contain maximum 1 character") + } + } + + func run() throws { + Logger.shared.logSection("$ ", item: "badgy small \"\(options.label)\" \"\(options.icon.path)\"", color: .ios) + + var pipeline = IconSignPipeline.make(withOptions: options) + pipeline.position = position + + try pipeline.execute() + } + } +} + +extension Position: ExpressibleByArgument { } + +private extension IconSignPipeline { + static func make(withOptions options: Badgy.Options) -> IconSignPipeline { + var pipeline = IconSignPipeline(icon: options.icon, label: options.label) + + pipeline.color = options.color?.value + pipeline.tintColor = options.tintColor?.value + pipeline.replace = options.replace + + return pipeline + } +} + +private extension Position { + static let longLabelPositions: Set = Set([ + .top, .left, .bottom, .right, .center + ]) + + var isValidForLongLabels: Bool { + Position.longLabelPositions.contains(self) + } +} diff --git a/Sources/Badgy/Commands/Long.swift b/Sources/Badgy/Commands/Long.swift index c647290..f98e37c 100644 --- a/Sources/Badgy/Commands/Long.swift +++ b/Sources/Badgy/Commands/Long.swift @@ -69,6 +69,8 @@ final class Long: DependencyManager, Command, IconSetDelegate { var iconSetImages: IconSetImages? public func execute() throws { + Logger.shared.verbose = VerboseFlag.value + guard areDependenciesInstalled() else { throw CLI.Error(message: "Missing dependencies. Run: 'brew install imagemagick'") @@ -86,72 +88,21 @@ final class Long: DependencyManager, Command, IconSetDelegate { throw CLI.Error(message: "Angle should be within range -180 ... 180") } } - - var baseIcon = icon - if isIconSet(Path(icon)) { - logger.logDebug("", item: "Finding the largest image in the .appiconset", color: .purple) - - iconSetImages = iconSetImages(for: Path(icon)) - - guard - let largest = iconSetImages?.largest, - largest.size.width > 0 - else { - logger.logError("❌ ", item: "Couldn't find the largest image in the set") - exit(1) - } - baseIcon = largest.image.absolute().description - logger.logDebug("Found: ", item: baseIcon, color: .purple) - } - - try process(baseIcon: baseIcon) } - private func process(baseIcon: String) throws { - let folder = Path("Badgy") + private func process() throws { + var pipeline = IconSignPipeline( + icon: try Icon(path: icon), + label: labelText + ) - try factory.makeBadge(with: labelText, colorHexCode: color, tintColorHexCode: tintColor, angle: angleInt, inFolder: folder, completion: { (result) in - switch result { - case .success(_): - try self.factory.appendBadge(to: baseIcon, - folder: folder, - label: self.labelText, - position: Position(rawValue: self.position ?? "bottom")) { - (result) in - switch result { - case .success(let filename): - let filePath = Path(filename) - guard filePath.exists - else { - self.logger.logError("❌ ", item: "Failed to create badge") - return - } - self.logger.logInfo(item: "Icon with badge '\(self.labelText)' created at '\(filePath.absolute().description)'") - try self.factory.cleanUp(folder: folder) - - if ReplaceFlag.value, let iconSet = self.iconSetImages { - self.replace(iconSet: iconSet, with: filePath) - } else { - self.resize(filePath: filePath) - } - case .failure(let error): - try self.factory.cleanUp(folder: folder) - throw CLI.Error(message: error.localizedDescription) - } - } - case .failure(let error): - try self.factory.cleanUp(folder: folder) - throw CLI.Error(message: error.localizedDescription) - } - }) - } - - private func resize(filePath: Path) { - factory.resize(filename: filePath) - } - - private func replace(iconSet: IconSetImages, with newBadgeFile: Path) { - factory.replace(iconSet, with: newBadgeFile) + pipeline.position = Position(rawValue: position ?? "bottom") + pipeline.color = color + pipeline.tintColor = color + pipeline.angle = angleInt + pipeline.replace = ReplaceFlag.value + + try pipeline.execute() } } diff --git a/Sources/Badgy/Commands/Small.swift b/Sources/Badgy/Commands/Small.swift index ece50c0..fec692f 100644 --- a/Sources/Badgy/Commands/Small.swift +++ b/Sources/Badgy/Commands/Small.swift @@ -67,75 +67,28 @@ final class Small: DependencyManager, Command, IconSetDelegate { var iconSetImages: IconSetImages? public func execute() throws { + Logger.shared.verbose = VerboseFlag.value + guard areDependenciesInstalled() - else { - throw CLI.Error(message: "Missing dependencies. Run: 'brew install imagemagick'") - } - logger.logSection("$ ", item: "badgy small \"\(char)\" \"\(icon)\"", color: .ios) - - var baseIcon = icon - if isIconSet(Path(icon)) { - logger.logDebug("", item: "Finding the largest image in the .appiconset", color: .purple) - - iconSetImages = iconSetImages(for: Path(icon)) - - guard - let largest = iconSetImages?.largest, - largest.size.width > 0 else { - logger.logError("❌ ", item: "Couldn't find the largest image in the set") - exit(1) - } - baseIcon = largest.image.absolute().description - logger.logDebug("Found: ", item: baseIcon, color: .purple) + throw CLI.Error(message: "Missing dependencies. Run: 'brew install imagemagick'") } + logger.logSection("$ ", item: "badgy small \"\(char)\" \"\(icon)\"", color: .ios) - try process(baseIcon: baseIcon) + try process() } - - private func process(baseIcon: String) throws { - let folder = Path("Badgy") - factory.makeSmall(with: char, colorHexCode: color, tintColorHexCode: tintColor, inFolder: folder, completion: { (result) in - switch result { - case .success(_): - try self.factory.appendBadge(to: baseIcon, - folder: folder, - label: self.char, - position: Position(rawValue: self.position ?? "bottomLeft")) { - (result) in - switch result { - case .success(let filename): - let filePath = Path(filename) - guard filePath.exists - else { - self.logger.logError("❌ ", item: "Failed to create badge") - return - } - self.logger.logInfo(item: "Icon with badge '\(self.char)' created at '\(filePath.absolute().description)'") - try self.factory.cleanUp(folder: folder) - - if ReplaceFlag.value, let iconSet = self.iconSetImages { - self.replace(iconSet: iconSet, with: filePath) - } else { - self.resize(filePath: filePath) - } - case .failure(let error): - try self.factory.cleanUp(folder: folder) - throw CLI.Error(message: error.localizedDescription) - } - } - case .failure(let error): - try self.factory.cleanUp(folder: folder) - throw CLI.Error(message: error.localizedDescription) - } - }) - } - - private func resize(filePath: Path) { - factory.resize(filename: filePath) - } - - private func replace(iconSet: IconSetImages, with newBadgeFile: Path) { - factory.replace(iconSet, with: newBadgeFile) + + private func process() throws { + var pipeline = IconSignPipeline( + icon: try Icon(path: icon), + label: char + ) + + pipeline.position = Position(rawValue: self.position ?? "bottomLeft") + pipeline.color = color + pipeline.tintColor = color + pipeline.replace = ReplaceFlag.value + + try pipeline.execute() } } diff --git a/Sources/Badgy/Helpers/ColorCode+Hex.swift b/Sources/Badgy/Helpers/ColorCode+Hex.swift new file mode 100644 index 0000000..efad36e --- /dev/null +++ b/Sources/Badgy/Helpers/ColorCode+Hex.swift @@ -0,0 +1,21 @@ +// +// Badgy +// + +import Foundation + +extension ColorCode { + /// Checks whether the `input` matches the valid color formats + /// + /// Valid colors formats are `#rrggbb` | `#rrggbbaa` + static func isHexColor(_ hex: String) -> Bool { + NSRegularExpression.hexColorCode.matches(hex) + } +} + +private extension NSRegularExpression { + /// Regular expression that matches '#rrggbb' and '#rrggbbaa' formats + /// + /// Additional explanation at [regex101](https://regex101.com/r/j0MDnb/1/tests) + static let hexColorCode = NSRegularExpression("^#(?:[0-9a-fA-F]{2}){3,4}$") +} diff --git a/Sources/Badgy/Helpers/ColorCode+Name.swift b/Sources/Badgy/Helpers/ColorCode+Name.swift new file mode 100644 index 0000000..71ab914 --- /dev/null +++ b/Sources/Badgy/Helpers/ColorCode+Name.swift @@ -0,0 +1,127 @@ +// +// Badgy +// + +import Foundation + +extension ColorCode { + /// Checks whether `name` is a known color name + static func isColorName(_ name: String) -> Bool { + colorNames.contains(name) + } + + /// The set below provides a list of named colors recognized by + /// [ImageMagick as color names](https://imagemagick.org/script/color.php#color_names) + private static let colorNames = Set([ + "snow", "snow1", "snow2", "RosyBrown1", "RosyBrown2", "snow3", "LightCoral", + "IndianRed1", "RosyBrown3", "IndianRed2", "RosyBrown", "brown1", "firebrick1", + "brown2", "IndianRed", "IndianRed3", "firebrick2", "snow4", "brown3", "red", + "red1", "RosyBrown4", "firebrick3", "red2", "firebrick", "brown", "red3", + "IndianRed4", "brown4", "firebrick4", "DarkRed", "red4", "maroon", "LightPink1", + "LightPink3", "LightPink4", "LightPink2", "LightPink", "pink", "crimson", + "pink1", "pink2", "pink3", "pink4", "PaleVioletRed4", "PaleVioletRed", + "PaleVioletRed2", "PaleVioletRed1", "PaleVioletRed3", "LavenderBlush", + "LavenderBlush1", "LavenderBlush3", "LavenderBlush2", "LavenderBlush4", + "maroon", "HotPink3", "VioletRed3", "VioletRed1", "VioletRed2", "VioletRed4", + "HotPink2", "HotPink1", "HotPink4", "HotPink", "DeepPink", "DeepPink1", + "DeepPink2", "DeepPink3", "DeepPink4", "maroon1", "maroon2", "maroon3", + "maroon4", "MediumVioletRed", "VioletRed", "orchid2", "orchid", "orchid1", + "orchid3", "orchid4", "thistle1", "thistle2", "plum1", "plum2", "thistle", + "thistle3", "plum", "violet", "plum3", "thistle4", "fuchsia", "magenta", + "magenta1", "plum4", "magenta2", "magenta3", "DarkMagenta", "magenta4", + "purple", "MediumOrchid", "MediumOrchid1", "MediumOrchid2", "MediumOrchid3", + "MediumOrchid4", "DarkViolet", "DarkOrchid", "DarkOrchid1", "DarkOrchid3", + "DarkOrchid2", "DarkOrchid4", "purple", "indigo", "BlueViolet", "purple2", + "purple3", "purple4", "purple1", "MediumPurple", "MediumPurple1", + "MediumPurple2", "MediumPurple3", "MediumPurple4", "DarkSlateBlue", + "LightSlateBlue", "MediumSlateBlue", "SlateBlue", "SlateBlue1", "SlateBlue2", + "SlateBlue3", "SlateBlue4", "GhostWhite", "lavender", "blue", "blue1", "blue2", + "blue3", "MediumBlue", "blue4", "DarkBlue", "MidnightBlue", "navy", "NavyBlue", + "RoyalBlue", "RoyalBlue1", "RoyalBlue2", "RoyalBlue3", "RoyalBlue4", + "CornflowerBlue", "LightSteelBlue", "LightSteelBlue1", "LightSteelBlue2", + "LightSteelBlue3", "LightSteelBlue4", "SlateGray4", "SlateGray1", "SlateGray2", + "SlateGray3", "LightSlateGray", "LightSlateGrey", "SlateGray", "SlateGrey", + "DodgerBlue", "DodgerBlue1", "DodgerBlue2", "DodgerBlue4", "DodgerBlue3", + "AliceBlue", "SteelBlue4", "SteelBlue", "SteelBlue1", "SteelBlue2", + "SteelBlue3", "SkyBlue4", "SkyBlue1", "SkyBlue2", "SkyBlue3", "LightSkyBlue", + "LightSkyBlue4", "LightSkyBlue1", "LightSkyBlue2", "LightSkyBlue3", "SkyBlue", + "LightBlue3", "DeepSkyBlue", "DeepSkyBlue1", "DeepSkyBlue2", "DeepSkyBlue4", + "DeepSkyBlue3", "LightBlue1", "LightBlue2", "LightBlue", "LightBlue4", + "PowderBlue", "CadetBlue1", "CadetBlue2", "CadetBlue3", "CadetBlue4", + "turquoise1", "turquoise2", "turquoise3", "turquoise4", "cadet", "CadetBlue", + "DarkTurquoise", "azure", "azure1", "LightCyan", "LightCyan1", "azure2", + "LightCyan2", "PaleTurquoise1", "PaleTurquoise", "PaleTurquoise2", + "DarkSlateGray1", "azure3", "LightCyan3", "DarkSlateGray2", "PaleTurquoise3", + "DarkSlateGray3", "azure4", "LightCyan4", "aqua", "cyan", "cyan1", + "PaleTurquoise4", "cyan2", "DarkSlateGray4", "cyan3", "cyan4", "DarkCyan", + "teal", "DarkSlateGray", "DarkSlateGrey", "MediumTurquoise", "LightSeaGreen", + "turquoise", "aquamarine4", "aquamarine", "aquamarine1", "aquamarine2", + "aquamarine3", "MediumAquamarine", "MediumSpringGreen", "MintCream", + "SpringGreen", "SpringGreen1", "SpringGreen2", "SpringGreen3", "SpringGreen4", + "MediumSeaGreen", "SeaGreen", "SeaGreen3", "SeaGreen1", "SeaGreen4", + "SeaGreen2", "MediumForestGreen", "honeydew", "honeydew1", "honeydew2", + "DarkSeaGreen1", "DarkSeaGreen2", "PaleGreen1", "PaleGreen", "honeydew3", + "LightGreen", "PaleGreen2", "DarkSeaGreen3", "DarkSeaGreen", "PaleGreen3", + "honeydew4", "green1", "lime", "LimeGreen", "DarkSeaGreen4", "green2", + "PaleGreen4", "green3", "ForestGreen", "green4", "green", "DarkGreen", + "LawnGreen", "chartreuse", "chartreuse1", "chartreuse2", "chartreuse3", + "chartreuse4", "GreenYellow", "DarkOliveGreen3", "DarkOliveGreen1", + "DarkOliveGreen2", "DarkOliveGreen4", "DarkOliveGreen", "OliveDrab", + "OliveDrab1", "OliveDrab2", "OliveDrab3", "YellowGreen", "OliveDrab4", "ivory", + "ivory1", "LightYellow", "LightYellow1", "beige", "ivory2", + "LightGoldenrodYellow", "LightYellow2", "ivory3", "LightYellow3", "ivory4", + "LightYellow4", "yellow", "yellow1", "yellow2", "yellow3", "yellow4", "olive", + "DarkKhaki", "khaki2", "LemonChiffon4", "khaki1", "khaki3", "khaki4", + "PaleGoldenrod", "LemonChiffon", "LemonChiffon1", "khaki", "LemonChiffon3", + "LemonChiffon2", "MediumGoldenRod", "cornsilk4", "gold", "gold1", "gold2", + "gold3", "gold4", "LightGoldenrod", "LightGoldenrod4", "LightGoldenrod1", + "LightGoldenrod3", "LightGoldenrod2", "cornsilk3", "cornsilk2", "cornsilk", + "cornsilk1", "goldenrod", "goldenrod1", "goldenrod2", "goldenrod3", + "goldenrod4", "DarkGoldenrod", "DarkGoldenrod1", "DarkGoldenrod2", + "DarkGoldenrod3", "DarkGoldenrod4", "FloralWhite", "wheat2", "OldLace", "wheat", + "wheat1", "wheat3", "orange", "orange1", "orange2", "orange3", "orange4", + "wheat4", "moccasin", "PapayaWhip", "NavajoWhite3", "BlanchedAlmond", + "NavajoWhite", "NavajoWhite1", "NavajoWhite2", "NavajoWhite4", "AntiqueWhite4", + "AntiqueWhite", "tan", "bisque4", "burlywood", "AntiqueWhite2", "burlywood1", + "burlywood3", "burlywood2", "AntiqueWhite1", "burlywood4", "AntiqueWhite3", + "DarkOrange", "bisque2", "bisque", "bisque1", "bisque3", "DarkOrange1", "linen", + "DarkOrange2", "DarkOrange3", "DarkOrange4", "peru", "tan1", "tan2", "tan3", + "tan4", "PeachPuff", "PeachPuff1", "PeachPuff4", "PeachPuff2", "PeachPuff3", + "SandyBrown", "seashell4", "seashell2", "seashell3", "chocolate", "chocolate1", + "chocolate2", "chocolate3", "chocolate4", "SaddleBrown", "seashell", + "seashell1", "sienna4", "sienna", "sienna1", "sienna2", "sienna3", + "LightSalmon3", "LightSalmon", "LightSalmon1", "LightSalmon4", "LightSalmon2", + "coral", "OrangeRed", "OrangeRed1", "OrangeRed2", "OrangeRed3", "OrangeRed4", + "DarkSalmon", "salmon1", "salmon2", "salmon3", "salmon4", "coral1", "coral2", + "coral3", "coral4", "tomato4", "tomato", "tomato1", "tomato2", "tomato3", + "MistyRose4", "MistyRose2", "MistyRose", "MistyRose1", "salmon", "MistyRose3", + "white", "gray100", "grey100", "grey100", "gray99", "grey99", "gray98", + "grey98", "gray97", "grey97", "gray96", "grey96", "WhiteSmoke", "gray95", + "grey95", "gray94", "grey94", "gray93", "grey93", "gray92", "grey92", "gray91", + "grey91", "gray90", "grey90", "gray89", "grey89", "gray88", "grey88", "gray87", + "grey87", "gainsboro", "gray86", "grey86", "gray85", "grey85", "gray84", + "grey84", "gray83", "grey83", "LightGray", "LightGrey", "gray82", "grey82", + "gray81", "grey81", "gray80", "grey80", "gray79", "grey79", "gray78", "grey78", + "gray77", "grey77", "gray76", "grey76", "silver", "gray75", "grey75", "gray74", + "grey74", "gray73", "grey73", "gray72", "grey72", "gray71", "grey71", "gray70", + "grey70", "gray69", "grey69", "gray68", "grey68", "gray67", "grey67", + "DarkGray", "DarkGrey", "gray66", "grey66", "gray65", "grey65", "gray64", + "grey64", "gray63", "grey63", "gray62", "grey62", "gray61", "grey61", "gray60", + "grey60", "gray59", "grey59", "gray58", "grey58", "gray57", "grey57", "gray56", + "grey56", "gray55", "grey55", "gray54", "grey54", "gray53", "grey53", "gray52", + "grey52", "gray51", "grey51", "fractal", "gray50", "grey50", "gray", "gray49", + "grey49", "gray48", "grey48", "gray47", "grey47", "gray46", "grey46", "gray45", + "grey45", "gray44", "grey44", "gray43", "grey43", "gray42", "grey42", "DimGray", + "DimGrey", "gray41", "grey41", "gray40", "grey40", "gray39", "grey39", "gray38", + "grey38", "gray37", "grey37", "gray36", "grey36", "gray35", "grey35", "gray34", + "grey34", "gray33", "grey33", "gray32", "grey32", "gray31", "grey31", "gray30", + "grey30", "gray29", "grey29", "gray28", "grey28", "gray27", "grey27", "gray26", + "grey26", "gray25", "grey25", "gray24", "grey24", "gray23", "grey23", "gray22", + "grey22", "gray21", "grey21", "gray20", "grey20", "gray19", "grey19", "gray18", + "grey18", "gray17", "grey17", "gray16", "grey16", "gray15", "grey15", "gray14", + "grey14", "gray13", "grey13", "gray12", "grey12", "gray11", "grey11", "gray10", + "grey10", "gray9", "grey9", "gray8", "grey8", "gray7", "grey7", "gray6", + "grey6", "gray5", "grey5", "gray4", "grey4", "gray3", "grey3", "gray2", "grey2", + "gray1", "grey1", "black", "gray0", "grey0", "opaque", "none", "transparent" + ]) +} diff --git a/Sources/Badgy/Helpers/ColorCode.swift b/Sources/Badgy/Helpers/ColorCode.swift new file mode 100644 index 0000000..59f5746 --- /dev/null +++ b/Sources/Badgy/Helpers/ColorCode.swift @@ -0,0 +1,24 @@ +// +// Badgy +// + +import Foundation +import ArgumentParser + +struct ColorCode { + var value: String +} + +extension ColorCode: ExpressibleByArgument { + init?(argument: String) { + switch argument { + case let hex where ColorCode.isHexColor(argument): + value = hex + case let name where ColorCode.isColorName(argument): + value = name + default: + return nil + } + } +} + diff --git a/Sources/Badgy/Helpers/Factory+Resizer.swift b/Sources/Badgy/Helpers/Factory+Resizer.swift index 376e448..9ff3531 100644 --- a/Sources/Badgy/Helpers/Factory+Resizer.swift +++ b/Sources/Badgy/Helpers/Factory+Resizer.swift @@ -52,4 +52,21 @@ extension Factory { } } } + + func replace(_ iconSet: IconSet, with newBadgeFile: Path) { + iconSet.images + .forEach { (info) in + Logger.shared.logInfo("Replacing: ", item: info.image, color: .purple) + do { + try Task.run( + "convert", newBadgeFile.absolute().description, + "-resize", info.size.description(), + info.image.absolute().description + ) + } catch { + Logger.shared.logError("❌ ", + item: "Failed to replace \(info.image)") + } + } + } } diff --git a/Sources/Badgy/Helpers/Factory+Small.swift b/Sources/Badgy/Helpers/Factory+Small.swift index a5bd647..ff98a01 100644 --- a/Sources/Badgy/Helpers/Factory+Small.swift +++ b/Sources/Badgy/Helpers/Factory+Small.swift @@ -12,8 +12,7 @@ extension Factory { func makeSmall(with label: String, colorHexCode: String? = nil, tintColorHexCode: String? = nil, - inFolder folder: Path, - completion: @escaping BadgeProductionResponse) { + inFolder folder: Path) throws -> String { let color = colorHexCode ?? colors.randomElement()! let tintColor = tintColorHexCode ?? "white" @@ -50,11 +49,11 @@ extension Factory { "convert", "\(folderBase)/top.png", "\(folderBase)/bottom.png", "-append", "\(folderBase)/badge.png" ) - try completion(.success("\(folderBase)/badge.png")) - } catch let error { + return "\(folderBase)/badge.png" + } catch { print("FAILED: \(error.localizedDescription)") - try? completion(.failure(error)) + throw error } } } diff --git a/Sources/Badgy/Helpers/Factory.swift b/Sources/Badgy/Helpers/Factory.swift index 446c716..9f9e64c 100644 --- a/Sources/Badgy/Helpers/Factory.swift +++ b/Sources/Badgy/Helpers/Factory.swift @@ -17,13 +17,12 @@ struct Factory { colorHexCode: String? = nil, tintColorHexCode: String? = nil, angle: Int? = nil, - inFolder folder: Path, - completion: @escaping BadgeProductionResponse) throws { - - let color = colorHexCode ?? colors.randomElement()! - let tintColor = tintColorHexCode ?? "white" + inFolder folder: Path) throws -> String { do { + let color = colorHexCode ?? colors.randomElement()! + let tintColor = tintColorHexCode ?? "white" + let folderBase = folder.absolute().description if !folder.isDirectory { try Task.run("mkdir", folderBase) @@ -64,39 +63,37 @@ struct Factory { "\(folderBase)/badge.png" ) } - try completion(.success("\(folderBase)/badge.png")) - } catch let error { + return "\(folderBase)/badge.png" + } catch { print("FAILED: \(error.localizedDescription)") - try? completion(.failure(error)) + throw error } } - func appendBadge(to baseIcon: String, folder: Path, label: String, - position: Position?, completion: @escaping BadgeProductionResponse) throws { + func appendBadge(to baseIcon: String, + folder: Path, + label: String, + position: Position?) throws -> String { let position = position ?? .bottom - do { - let folderBase = folder.absolute().description - let finalFilename = "\(folderBase)/\(label).png" - - try Task.run( - "convert", baseIcon, - "-resize", "1024x", - finalFilename - ) - - try Task.run( - "convert", "-composite", - "-gravity", "\(position.cardinal)", - finalFilename, "\(folderBase)/badge.png", - finalFilename - ) - - try completion(.success(finalFilename)) - } catch let error { - try? completion(.failure(error)) - } + let folderBase = folder.absolute().description + let finalFilename = "\(folderBase)/\(label).png" + + try Task.run( + "convert", baseIcon, + "-resize", "1024x", + finalFilename + ) + + try Task.run( + "convert", "-composite", + "-gravity", "\(position.cardinal)", + finalFilename, "\(folderBase)/badge.png", + finalFilename + ) + + return finalFilename } func cleanUp(folder: Path) throws { diff --git a/Sources/Badgy/Helpers/Icon.swift b/Sources/Badgy/Helpers/Icon.swift new file mode 100644 index 0000000..752074b --- /dev/null +++ b/Sources/Badgy/Helpers/Icon.swift @@ -0,0 +1,45 @@ +// +// Badgy +// + +import Foundation +import ArgumentParser +import PathKit + +enum Icon { + case plain(Path) + case set(IconSet) + + init(path: String) throws { + let path = Path(path) + + guard path.exists else { + let message = "Input file or directory doesn't exist" + Logger.shared.logError("❌ ", item: message) + throw ValidationError(message) + } + + if let set = IconSet.makeFromFolder(at: path) { + self = .set(set) + return + } + + if IconSet.imageExtensions.contains(path.extension ?? "") { + self = .plain(path) + return + } + + let message = "Input file or directory doesn't have a valid format" + Logger.shared.logError("❌ ", item: message) + throw ValidationError(message) + } +} + +extension Icon { + var path: Path { + switch self { + case .plain(let path): return path + case .set(let set): return set.path + } + } +} diff --git a/Sources/Badgy/Helpers/IconSet.swift b/Sources/Badgy/Helpers/IconSet.swift new file mode 100644 index 0000000..f48da9d --- /dev/null +++ b/Sources/Badgy/Helpers/IconSet.swift @@ -0,0 +1,69 @@ +// +// Badgy +// + +import Foundation +import PathKit +import SwiftCLI + +struct IconSet { + typealias ImageInfo = (image: Path, size: ImageSize) + + let path: Path + let images: [ImageInfo] + + func largest() -> Path? { + images.max { lhs, rhs in lhs.size.width < rhs.size.width }.flatMap { $0.image } + } +} + +extension IconSet { + static let setExtension = "appiconset" + static let imageExtensions = Set(["png", "jpg", "jpeg"]) + + static func makeFromFolder(at path: Path) -> IconSet? { + guard path.isDirectory && path.extension == setExtension else { + return nil + } + + guard let content = try? path.children() else { + return nil + } + + let images = content.compactMap { path in + ImageSize.makeFromImage(at: path).flatMap { size in + (path, size) + } + } + + return IconSet(path: path, images: images) + } +} + +private extension ImageSize { + static func makeFromImage(at imagePath: Path) -> ImageSize? { + guard IconSet.imageExtensions.contains(imagePath.extension ?? "") else { + return nil + } + + do { + let result = try Task.capture( + "identify", arguments: [ + "-format", "{\"width\":%[fx:w],\"height\":%[fx:h]}", + imagePath.absolute().description + ] + ) + + guard let data = result.stdout.data(using: .utf8) else { + throw CLI.Error(message: "Failed to get image size") + } + + return try JSONDecoder().decode(ImageSize.self, from: data) + } catch { + Logger.shared.logDebug("Warning: ", + item: "Failed to capture size for image: \(imagePath)", + color: .purple) + return nil + } + } +} diff --git a/Sources/Badgy/Helpers/IconSignPipeline.swift b/Sources/Badgy/Helpers/IconSignPipeline.swift new file mode 100644 index 0000000..c1b7c38 --- /dev/null +++ b/Sources/Badgy/Helpers/IconSignPipeline.swift @@ -0,0 +1,94 @@ +// +// Badgy +// + +import Foundation +import PathKit + +struct IconSignPipeline { + struct Error: Swift.Error { + var message: String + } + + var icon: Icon + var label: String + + var position: Position? + var color: String? + var tintColor: String? + var angle: Int? = nil + var replace: Bool = false + + let logger = Logger.shared + let factory = Factory() + let folder = Path("Badgy") + + func execute() throws { + defer { cleanUp() } + + let baseIcon = try icon.base() + try makeBadge() + let signedIcon = try appendBadge(to: baseIcon) + postprocess(signedIcon) + } + + private func makeBadge() throws { + _ = try factory.makeBadge( + with: label, + colorHexCode: color, + tintColorHexCode: tintColor, + angle: angle, + inFolder: folder + ) + } + + private func appendBadge(to icon: Path) throws -> Path { + let signedIconFilename = try factory.appendBadge( + to: icon.absolute().description, + folder: folder, + label: label, + position: position + ) + + let signedIcon = Path(signedIconFilename) + guard signedIcon.exists else { + logger.logError("❌ ", item: "Failed to create badge") + throw IconSignPipeline.Error(message: "Failed to create badge") + } + + logger.logInfo(item: "Icon with badge '\(label)' created at '\(signedIcon.absolute().description)'") + + return signedIcon + } + + private func postprocess(_ signedIcon: Path) { + if replace, case .set(let iconSet) = icon { + factory.replace(iconSet, with: signedIcon) + } else { + factory.resize(filename: signedIcon) + } + } + + private func cleanUp() { + try? factory.cleanUp(folder: folder) + } +} + +private extension Icon { + func base() throws -> Path { + switch self { + case .plain(let path): + return path + case .set(let set): + Logger.shared.logDebug("", item: "Finding the largest image in the .appiconset", color: .purple) + + guard let path = set.largest() else { + Logger.shared.logError("❌ ", item: "Couldn't find the largest image in the set") + throw IconSignPipeline.Error(message: "Couldn't find the largest image in the set") + } + + Logger.shared.logDebug("Found: ", item: path, color: .purple) + return path + } + } +} diff --git a/Sources/Badgy/Helpers/Validation+Any.swift b/Sources/Badgy/Helpers/Validation+Any.swift index b16dbdf..01ace11 100644 --- a/Sources/Badgy/Helpers/Validation+Any.swift +++ b/Sources/Badgy/Helpers/Validation+Any.swift @@ -2,7 +2,6 @@ // Badgy // - import Foundation import SwiftCLI diff --git a/Sources/Badgy/Helpers/Validation+Color.swift b/Sources/Badgy/Helpers/Validation+Color.swift index 6ee7ded..703694f 100644 --- a/Sources/Badgy/Helpers/Validation+Color.swift +++ b/Sources/Badgy/Helpers/Validation+Color.swift @@ -9,7 +9,7 @@ extension Validation where T == String { static func colorCode() -> Self { let message = """ - Specify a valid hex color code in a case insensitive format: '#rrbbgg' | '#rrbbggaa' + Specify a valid hex color code in a case insensitive format: '#rrggbb' | '#rrggbbaa' or Provide a named color: 'snow' | 'snow1' | ... Complete list of named colors: https://imagemagick.org/script/color.php#color_names diff --git a/Sources/Badgy/Helpers/Validation+ColorName.swift b/Sources/Badgy/Helpers/Validation+ColorName.swift index bad817b..8eb2836 100644 --- a/Sources/Badgy/Helpers/Validation+ColorName.swift +++ b/Sources/Badgy/Helpers/Validation+ColorName.swift @@ -8,121 +8,6 @@ import SwiftCLI extension Validation where T == String { /// Check whether the `input` is a known color name static func isColorName(_ input: String) -> Bool { - return colorNames.contains(input) + return ColorCode.isColorName(input) } - - /// The set below provides a list of named colors recognized by - /// [ImageMagick as color names](https://imagemagick.org/script/color.php#color_names) - private static let colorNames = Set([ - "snow", "snow1", "snow2", "RosyBrown1", "RosyBrown2", "snow3", "LightCoral", - "IndianRed1", "RosyBrown3", "IndianRed2", "RosyBrown", "brown1", "firebrick1", - "brown2", "IndianRed", "IndianRed3", "firebrick2", "snow4", "brown3", "red", - "red1", "RosyBrown4", "firebrick3", "red2", "firebrick", "brown", "red3", - "IndianRed4", "brown4", "firebrick4", "DarkRed", "red4", "maroon", "LightPink1", - "LightPink3", "LightPink4", "LightPink2", "LightPink", "pink", "crimson", - "pink1", "pink2", "pink3", "pink4", "PaleVioletRed4", "PaleVioletRed", - "PaleVioletRed2", "PaleVioletRed1", "PaleVioletRed3", "LavenderBlush", - "LavenderBlush1", "LavenderBlush3", "LavenderBlush2", "LavenderBlush4", - "maroon", "HotPink3", "VioletRed3", "VioletRed1", "VioletRed2", "VioletRed4", - "HotPink2", "HotPink1", "HotPink4", "HotPink", "DeepPink", "DeepPink1", - "DeepPink2", "DeepPink3", "DeepPink4", "maroon1", "maroon2", "maroon3", - "maroon4", "MediumVioletRed", "VioletRed", "orchid2", "orchid", "orchid1", - "orchid3", "orchid4", "thistle1", "thistle2", "plum1", "plum2", "thistle", - "thistle3", "plum", "violet", "plum3", "thistle4", "fuchsia", "magenta", - "magenta1", "plum4", "magenta2", "magenta3", "DarkMagenta", "magenta4", - "purple", "MediumOrchid", "MediumOrchid1", "MediumOrchid2", "MediumOrchid3", - "MediumOrchid4", "DarkViolet", "DarkOrchid", "DarkOrchid1", "DarkOrchid3", - "DarkOrchid2", "DarkOrchid4", "purple", "indigo", "BlueViolet", "purple2", - "purple3", "purple4", "purple1", "MediumPurple", "MediumPurple1", - "MediumPurple2", "MediumPurple3", "MediumPurple4", "DarkSlateBlue", - "LightSlateBlue", "MediumSlateBlue", "SlateBlue", "SlateBlue1", "SlateBlue2", - "SlateBlue3", "SlateBlue4", "GhostWhite", "lavender", "blue", "blue1", "blue2", - "blue3", "MediumBlue", "blue4", "DarkBlue", "MidnightBlue", "navy", "NavyBlue", - "RoyalBlue", "RoyalBlue1", "RoyalBlue2", "RoyalBlue3", "RoyalBlue4", - "CornflowerBlue", "LightSteelBlue", "LightSteelBlue1", "LightSteelBlue2", - "LightSteelBlue3", "LightSteelBlue4", "SlateGray4", "SlateGray1", "SlateGray2", - "SlateGray3", "LightSlateGray", "LightSlateGrey", "SlateGray", "SlateGrey", - "DodgerBlue", "DodgerBlue1", "DodgerBlue2", "DodgerBlue4", "DodgerBlue3", - "AliceBlue", "SteelBlue4", "SteelBlue", "SteelBlue1", "SteelBlue2", - "SteelBlue3", "SkyBlue4", "SkyBlue1", "SkyBlue2", "SkyBlue3", "LightSkyBlue", - "LightSkyBlue4", "LightSkyBlue1", "LightSkyBlue2", "LightSkyBlue3", "SkyBlue", - "LightBlue3", "DeepSkyBlue", "DeepSkyBlue1", "DeepSkyBlue2", "DeepSkyBlue4", - "DeepSkyBlue3", "LightBlue1", "LightBlue2", "LightBlue", "LightBlue4", - "PowderBlue", "CadetBlue1", "CadetBlue2", "CadetBlue3", "CadetBlue4", - "turquoise1", "turquoise2", "turquoise3", "turquoise4", "cadet", "CadetBlue", - "DarkTurquoise", "azure", "azure1", "LightCyan", "LightCyan1", "azure2", - "LightCyan2", "PaleTurquoise1", "PaleTurquoise", "PaleTurquoise2", - "DarkSlateGray1", "azure3", "LightCyan3", "DarkSlateGray2", "PaleTurquoise3", - "DarkSlateGray3", "azure4", "LightCyan4", "aqua", "cyan", "cyan1", - "PaleTurquoise4", "cyan2", "DarkSlateGray4", "cyan3", "cyan4", "DarkCyan", - "teal", "DarkSlateGray", "DarkSlateGrey", "MediumTurquoise", "LightSeaGreen", - "turquoise", "aquamarine4", "aquamarine", "aquamarine1", "aquamarine2", - "aquamarine3", "MediumAquamarine", "MediumSpringGreen", "MintCream", - "SpringGreen", "SpringGreen1", "SpringGreen2", "SpringGreen3", "SpringGreen4", - "MediumSeaGreen", "SeaGreen", "SeaGreen3", "SeaGreen1", "SeaGreen4", - "SeaGreen2", "MediumForestGreen", "honeydew", "honeydew1", "honeydew2", - "DarkSeaGreen1", "DarkSeaGreen2", "PaleGreen1", "PaleGreen", "honeydew3", - "LightGreen", "PaleGreen2", "DarkSeaGreen3", "DarkSeaGreen", "PaleGreen3", - "honeydew4", "green1", "lime", "LimeGreen", "DarkSeaGreen4", "green2", - "PaleGreen4", "green3", "ForestGreen", "green4", "green", "DarkGreen", - "LawnGreen", "chartreuse", "chartreuse1", "chartreuse2", "chartreuse3", - "chartreuse4", "GreenYellow", "DarkOliveGreen3", "DarkOliveGreen1", - "DarkOliveGreen2", "DarkOliveGreen4", "DarkOliveGreen", "OliveDrab", - "OliveDrab1", "OliveDrab2", "OliveDrab3", "YellowGreen", "OliveDrab4", "ivory", - "ivory1", "LightYellow", "LightYellow1", "beige", "ivory2", - "LightGoldenrodYellow", "LightYellow2", "ivory3", "LightYellow3", "ivory4", - "LightYellow4", "yellow", "yellow1", "yellow2", "yellow3", "yellow4", "olive", - "DarkKhaki", "khaki2", "LemonChiffon4", "khaki1", "khaki3", "khaki4", - "PaleGoldenrod", "LemonChiffon", "LemonChiffon1", "khaki", "LemonChiffon3", - "LemonChiffon2", "MediumGoldenRod", "cornsilk4", "gold", "gold1", "gold2", - "gold3", "gold4", "LightGoldenrod", "LightGoldenrod4", "LightGoldenrod1", - "LightGoldenrod3", "LightGoldenrod2", "cornsilk3", "cornsilk2", "cornsilk", - "cornsilk1", "goldenrod", "goldenrod1", "goldenrod2", "goldenrod3", - "goldenrod4", "DarkGoldenrod", "DarkGoldenrod1", "DarkGoldenrod2", - "DarkGoldenrod3", "DarkGoldenrod4", "FloralWhite", "wheat2", "OldLace", "wheat", - "wheat1", "wheat3", "orange", "orange1", "orange2", "orange3", "orange4", - "wheat4", "moccasin", "PapayaWhip", "NavajoWhite3", "BlanchedAlmond", - "NavajoWhite", "NavajoWhite1", "NavajoWhite2", "NavajoWhite4", "AntiqueWhite4", - "AntiqueWhite", "tan", "bisque4", "burlywood", "AntiqueWhite2", "burlywood1", - "burlywood3", "burlywood2", "AntiqueWhite1", "burlywood4", "AntiqueWhite3", - "DarkOrange", "bisque2", "bisque", "bisque1", "bisque3", "DarkOrange1", "linen", - "DarkOrange2", "DarkOrange3", "DarkOrange4", "peru", "tan1", "tan2", "tan3", - "tan4", "PeachPuff", "PeachPuff1", "PeachPuff4", "PeachPuff2", "PeachPuff3", - "SandyBrown", "seashell4", "seashell2", "seashell3", "chocolate", "chocolate1", - "chocolate2", "chocolate3", "chocolate4", "SaddleBrown", "seashell", - "seashell1", "sienna4", "sienna", "sienna1", "sienna2", "sienna3", - "LightSalmon3", "LightSalmon", "LightSalmon1", "LightSalmon4", "LightSalmon2", - "coral", "OrangeRed", "OrangeRed1", "OrangeRed2", "OrangeRed3", "OrangeRed4", - "DarkSalmon", "salmon1", "salmon2", "salmon3", "salmon4", "coral1", "coral2", - "coral3", "coral4", "tomato4", "tomato", "tomato1", "tomato2", "tomato3", - "MistyRose4", "MistyRose2", "MistyRose", "MistyRose1", "salmon", "MistyRose3", - "white", "gray100", "grey100", "grey100", "gray99", "grey99", "gray98", - "grey98", "gray97", "grey97", "gray96", "grey96", "WhiteSmoke", "gray95", - "grey95", "gray94", "grey94", "gray93", "grey93", "gray92", "grey92", "gray91", - "grey91", "gray90", "grey90", "gray89", "grey89", "gray88", "grey88", "gray87", - "grey87", "gainsboro", "gray86", "grey86", "gray85", "grey85", "gray84", - "grey84", "gray83", "grey83", "LightGray", "LightGrey", "gray82", "grey82", - "gray81", "grey81", "gray80", "grey80", "gray79", "grey79", "gray78", "grey78", - "gray77", "grey77", "gray76", "grey76", "silver", "gray75", "grey75", "gray74", - "grey74", "gray73", "grey73", "gray72", "grey72", "gray71", "grey71", "gray70", - "grey70", "gray69", "grey69", "gray68", "grey68", "gray67", "grey67", - "DarkGray", "DarkGrey", "gray66", "grey66", "gray65", "grey65", "gray64", - "grey64", "gray63", "grey63", "gray62", "grey62", "gray61", "grey61", "gray60", - "grey60", "gray59", "grey59", "gray58", "grey58", "gray57", "grey57", "gray56", - "grey56", "gray55", "grey55", "gray54", "grey54", "gray53", "grey53", "gray52", - "grey52", "gray51", "grey51", "fractal", "gray50", "grey50", "gray", "gray49", - "grey49", "gray48", "grey48", "gray47", "grey47", "gray46", "grey46", "gray45", - "grey45", "gray44", "grey44", "gray43", "grey43", "gray42", "grey42", "DimGray", - "DimGrey", "gray41", "grey41", "gray40", "grey40", "gray39", "grey39", "gray38", - "grey38", "gray37", "grey37", "gray36", "grey36", "gray35", "grey35", "gray34", - "grey34", "gray33", "grey33", "gray32", "grey32", "gray31", "grey31", "gray30", - "grey30", "gray29", "grey29", "gray28", "grey28", "gray27", "grey27", "gray26", - "grey26", "gray25", "grey25", "gray24", "grey24", "gray23", "grey23", "gray22", - "grey22", "gray21", "grey21", "gray20", "grey20", "gray19", "grey19", "gray18", - "grey18", "gray17", "grey17", "gray16", "grey16", "gray15", "grey15", "gray14", - "grey14", "gray13", "grey13", "gray12", "grey12", "gray11", "grey11", "gray10", - "grey10", "gray9", "grey9", "gray8", "grey8", "gray7", "grey7", "gray6", - "grey6", "gray5", "grey5", "gray4", "grey4", "gray3", "grey3", "gray2", "grey2", - "gray1", "grey1", "black", "gray0", "grey0", "opaque", "none", "transparent" - ]) } diff --git a/Sources/Badgy/Helpers/Validation+HexColor.swift b/Sources/Badgy/Helpers/Validation+HexColor.swift index 4abe92b..e609b13 100644 --- a/Sources/Badgy/Helpers/Validation+HexColor.swift +++ b/Sources/Badgy/Helpers/Validation+HexColor.swift @@ -6,25 +6,8 @@ import Foundation import SwiftCLI extension Validation where T == String { - /// Checks whether the `input` matches '#rrbbgg' | '#rrbbggaa' color formats + /// Checks whether the `input` matches '#rrggbb' | '#rrggbbaa' color formats static func isHexColor(_ input: String) -> Bool { - NSRegularExpression.hexColorCode.matches(input) + ColorCode.isHexColor(input) } } - -private extension NSRegularExpression { - /// Regular expression that matches '#rrbbgg' and '#rrbbggaa' formats - /// - /// `^` asserts position at start of a line - /// `#` matches the character # literally - /// - /// `(:?[0-9a-fA-F]{2})` - /// - `(:?)` denotes a non-capturing group - /// - `[0-9a-fA-F]` match a single character present in the list below - /// - `{2}` - matches exactly 2 times - /// - /// `{3,4}` - matches between 3 and 4 times, as many times as possible - /// - /// Additional explanation at [regex101](https://regex101.com/) - static let hexColorCode = NSRegularExpression("^#(?:[0-9a-fA-F]{2}){3,4}$") -} diff --git a/Sources/Badgy/Loggers/Logger.swift b/Sources/Badgy/Loggers/Logger.swift index 857fa6e..dacc19b 100644 --- a/Sources/Badgy/Loggers/Logger.swift +++ b/Sources/Badgy/Loggers/Logger.swift @@ -10,6 +10,8 @@ import SwiftCLI public class Logger: VerboseLogger { static let shared = Logger() + public var verbose: Bool = false + func logError(_ prefix: Any = "", item: Any, color: ShellColor = .red) { log(item: "--------------------------------------------------------------------------------------", logLevel: .error) log(prefix, item: item, color: color, logLevel: .error) diff --git a/Sources/Badgy/Loggers/VerboseLogger.swift b/Sources/Badgy/Loggers/VerboseLogger.swift index 183d42d..e8cfae3 100644 --- a/Sources/Badgy/Loggers/VerboseLogger.swift +++ b/Sources/Badgy/Loggers/VerboseLogger.swift @@ -46,8 +46,6 @@ extension Date { } extension VerboseLogger { - public var verbose: Bool { VerboseFlag.value } - public func log(_ prefix: Any = "", item: Any, indentationLevel: Int = 0, color: ShellColor = .neutral, logLevel: LogLevel = .none) { if logLevel == .verbose { guard verbose else { return } diff --git a/Sources/Badgy/Position.swift b/Sources/Badgy/Position.swift index 88a7f37..e381d3b 100644 --- a/Sources/Badgy/Position.swift +++ b/Sources/Badgy/Position.swift @@ -6,7 +6,7 @@ import Foundation -enum Position: String { +enum Position: String, CaseIterable { case top, left, bottom, right case topLeft, topRight case bottomLeft, bottomRight @@ -35,3 +35,9 @@ enum Position: String { } } } + +extension Sequence where Element == Position { + func formatted() -> String { + map { $0.rawValue}.joined(separator: " | ") + } +} diff --git a/Sources/Badgy/main.swift b/Sources/Badgy/main.swift index 87082ed..a515bbf 100644 --- a/Sources/Badgy/main.swift +++ b/Sources/Badgy/main.swift @@ -16,4 +16,8 @@ cli.commands = [ cli.globalOptions.append(ReplaceFlag) cli.globalOptions.append(VerboseFlag) +#if ARGUMENT_PARSER +Badgy.main() +#else _ = cli.go() +#endif