From 3bd789ef84d49b5d2418c77abea40deb7c9914bc Mon Sep 17 00:00:00 2001 From: egor mihneivch Date: Sun, 21 Jun 2020 08:44:42 +0300 Subject: [PATCH 01/25] drafting arguments parser adoption --- Package.resolved | 9 ++ Package.swift | 10 +- Sources/Badgy/Commands/Badgy.swift | 143 ++++++++++++++++++ .../Badgy/Helpers/Validation+HexColor.swift | 2 +- 4 files changed, 162 insertions(+), 2 deletions(-) create mode 100644 Sources/Badgy/Commands/Badgy.swift 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 4abe92b..1d853e5 100644 --- a/Sources/Badgy/Helpers/Validation+HexColor.swift +++ b/Sources/Badgy/Helpers/Validation+HexColor.swift @@ -12,7 +12,7 @@ extension Validation where T == String { } } -private extension NSRegularExpression { +extension NSRegularExpression { /// Regular expression that matches '#rrbbgg' and '#rrbbggaa' formats /// /// `^` asserts position at start of a line From 3af86b66b2fe574144e0780a14ae44c8379c72c0 Mon Sep 17 00:00:00 2001 From: egor mihneivch Date: Sun, 21 Jun 2020 09:10:26 +0300 Subject: [PATCH 02/25] support named colors arguments cli --- Sources/Badgy/Commands/Badgy.swift | 20 +-- Sources/Badgy/Commands/ColorCode+Hex.swift | 34 +++++ Sources/Badgy/Commands/ColorCode+Name.swift | 127 ++++++++++++++++++ Sources/Badgy/Commands/ColorCode.swift | 24 ++++ .../Badgy/Helpers/Validation+ColorName.swift | 117 +--------------- .../Badgy/Helpers/Validation+HexColor.swift | 19 +-- 6 files changed, 191 insertions(+), 150 deletions(-) create mode 100644 Sources/Badgy/Commands/ColorCode+Hex.swift create mode 100644 Sources/Badgy/Commands/ColorCode+Name.swift create mode 100644 Sources/Badgy/Commands/ColorCode.swift diff --git a/Sources/Badgy/Commands/Badgy.swift b/Sources/Badgy/Commands/Badgy.swift index 10b7abf..8e316ab 100644 --- a/Sources/Badgy/Commands/Badgy.swift +++ b/Sources/Badgy/Commands/Badgy.swift @@ -26,11 +26,11 @@ extension Badgy { @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 color with a hexadecimal color code format '#rrbbgg' | '#rrbbggaa' or a named color format ('red', 'white', etc.)") + var color: ColorCode? - @Option(help: "Specify badge text/tint color with a hexadecimal color code format '#rrbbgg' | '#rrbbggaa'") - var tintColor: HexColor? + @Option(help: "Specify badge text/tint color with a hexadecimal color code format ('#rrbbgg' | '#rrbbggaa') or a named color format ('red', 'white', etc.)") + var tintColor: ColorCode? @Flag(help: "Indicates Badgy should replace the input icon") var replace: Bool @@ -105,18 +105,6 @@ extension Badgy { 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" diff --git a/Sources/Badgy/Commands/ColorCode+Hex.swift b/Sources/Badgy/Commands/ColorCode+Hex.swift new file mode 100644 index 0000000..9db389d --- /dev/null +++ b/Sources/Badgy/Commands/ColorCode+Hex.swift @@ -0,0 +1,34 @@ +// +// File.swift +// +// +// Created by yahor mikhnevich on 6/21/20. +// + +import Foundation + +extension ColorCode { + /// Checks whether the `input` matches the valid color formats + /// + /// Valid colors formats are `#rrbbgg` | `#rrbbggaa` + static func isHexColor(_ hex: String) -> Bool { + NSRegularExpression.hexColorCode.matches(hex) + } +} + +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/Commands/ColorCode+Name.swift b/Sources/Badgy/Commands/ColorCode+Name.swift new file mode 100644 index 0000000..71ab914 --- /dev/null +++ b/Sources/Badgy/Commands/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/Commands/ColorCode.swift b/Sources/Badgy/Commands/ColorCode.swift new file mode 100644 index 0000000..59f5746 --- /dev/null +++ b/Sources/Badgy/Commands/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/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 1d853e5..45870eb 100644 --- a/Sources/Badgy/Helpers/Validation+HexColor.swift +++ b/Sources/Badgy/Helpers/Validation+HexColor.swift @@ -8,23 +8,6 @@ import SwiftCLI extension Validation where T == String { /// Checks whether the `input` matches '#rrbbgg' | '#rrbbggaa' color formats static func isHexColor(_ input: String) -> Bool { - NSRegularExpression.hexColorCode.matches(input) + ColorCode.isHexColor(input) } } - -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}$") -} From 2f7ed2961342e3bb6bd744b64af096e27c2068d7 Mon Sep 17 00:00:00 2001 From: egor mihneivch Date: Sun, 21 Jun 2020 10:22:07 +0300 Subject: [PATCH 03/25] streamline processing by stripping factory completions --- Sources/Badgy/Commands/Long.swift | 74 ++++++++++------------- Sources/Badgy/Commands/Small.swift | 73 ++++++++++------------ Sources/Badgy/Helpers/Factory+Small.swift | 9 ++- Sources/Badgy/Helpers/Factory.swift | 59 +++++++++--------- 4 files changed, 97 insertions(+), 118 deletions(-) diff --git a/Sources/Badgy/Commands/Long.swift b/Sources/Badgy/Commands/Long.swift index c647290..5cdd34e 100644 --- a/Sources/Badgy/Commands/Long.swift +++ b/Sources/Badgy/Commands/Long.swift @@ -110,48 +110,40 @@ final class Long: DependencyManager, Command, IconSetDelegate { private func process(baseIcon: String) throws { let folder = Path("Badgy") - 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) + do { + defer { try? self.factory.cleanUp(folder: folder) } + + _ = try factory.makeBadge( + with: labelText, + colorHexCode: color, + tintColorHexCode: tintColor, + angle: angleInt, + inFolder: folder + ) + + let filename = try factory.appendBadge( + to: baseIcon, + folder: folder, + label: self.labelText, + position: Position(rawValue: self.position ?? "bottom") + ) + + let filePath = Path(filename) + guard filePath.exists else { + logger.logError("❌ ", item: "Failed to create badge") + return } - }) - } - - private func resize(filePath: Path) { - factory.resize(filename: filePath) - } - - private func replace(iconSet: IconSetImages, with newBadgeFile: Path) { - factory.replace(iconSet, with: newBadgeFile) + + logger.logInfo(item: "Icon with badge '\(labelText)' created at '\(filePath.absolute().description)'") + + if ReplaceFlag.value, let iconSet = iconSetImages { + factory.replace(iconSet, with: filePath) + } else { + factory.resize(filename: filePath) + } + } catch { + throw CLI.Error(message: error.localizedDescription) + } } } diff --git a/Sources/Badgy/Commands/Small.swift b/Sources/Badgy/Commands/Small.swift index ece50c0..6cf7fd6 100644 --- a/Sources/Badgy/Commands/Small.swift +++ b/Sources/Badgy/Commands/Small.swift @@ -95,47 +95,38 @@ final class Small: DependencyManager, Command, IconSetDelegate { 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) + do { + defer { try? self.factory.cleanUp(folder: folder) } + + _ = try factory.makeSmall( + with: char, + colorHexCode: color, + tintColorHexCode: tintColor, + inFolder: folder + ) + + let filename = try factory.appendBadge( + to: baseIcon, + folder: folder, + label: self.char, + position: Position(rawValue: self.position ?? "bottomLeft") + ) + + let filePath = Path(filename) + guard filePath.exists else { + logger.logError("❌ ", item: "Failed to create badge") + return } - }) - } - - private func resize(filePath: Path) { - factory.resize(filename: filePath) - } - - private func replace(iconSet: IconSetImages, with newBadgeFile: Path) { - factory.replace(iconSet, with: newBadgeFile) + + logger.logInfo(item: "Icon with badge '\(char)' created at '\(filePath.absolute().description)'") + + if ReplaceFlag.value, let iconSet = iconSetImages { + factory.replace(iconSet, with: filePath) + } else { + factory.resize(filename: filePath) + } + } catch { + throw CLI.Error(message: error.localizedDescription) + } } } 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 { From 7f7920e768afbad7d9b650878fe31f4973439a69 Mon Sep 17 00:00:00 2001 From: egor mihneivch Date: Sun, 21 Jun 2020 11:28:00 +0300 Subject: [PATCH 04/25] support icon set in arguments parser --- Sources/Badgy/Commands/Badgy.swift | 29 +----------- Sources/Badgy/Commands/Icon.swift | 35 ++++++++++++++ Sources/Badgy/Commands/IconSet.swift | 68 ++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 27 deletions(-) create mode 100644 Sources/Badgy/Commands/Icon.swift create mode 100644 Sources/Badgy/Commands/IconSet.swift diff --git a/Sources/Badgy/Commands/Badgy.swift b/Sources/Badgy/Commands/Badgy.swift index 8e316ab..a1b9b63 100644 --- a/Sources/Badgy/Commands/Badgy.swift +++ b/Sources/Badgy/Commands/Badgy.swift @@ -20,8 +20,8 @@ extension Badgy { @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 + @Argument(help :"Specify path to icon with format .png | .jpg | .appiconset", transform: Icon.init(path:)) + var icon: Icon @Option(help: "Position on which to place the badge") var position: Position? @@ -104,28 +104,3 @@ extension Badgy { } extension Position: ExpressibleByArgument { } - -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/Commands/Icon.swift b/Sources/Badgy/Commands/Icon.swift new file mode 100644 index 0000000..a10f60b --- /dev/null +++ b/Sources/Badgy/Commands/Icon.swift @@ -0,0 +1,35 @@ +// +// File.swift +// +// +// Created by yahor mikhnevich on 6/21/20. +// + +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 { + throw ValidationError("Input file or directory doesn't exist") + } + + if let set = IconSet.makeFromFolder(at: path) { + self = .set(set) + return + } + + if IconSet.imageExtensions.contains(path.lastComponent) { + self = .plain(path) + return + } + + throw ValidationError("Input file or directory doesn't have a valid format") + } +} diff --git a/Sources/Badgy/Commands/IconSet.swift b/Sources/Badgy/Commands/IconSet.swift new file mode 100644 index 0000000..f590cda --- /dev/null +++ b/Sources/Badgy/Commands/IconSet.swift @@ -0,0 +1,68 @@ +// +// Badgy +// + +import Foundation +import PathKit +import SwiftCLI + +struct IconSet { + typealias ImageInfo = (image: Path, size: ImageSize) + + var 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.lastComponent.contains(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(images: images) + } +} + +private extension ImageSize { + static func makeFromImage(at imagePath: Path) -> ImageSize? { + guard IconSet.imageExtensions.contains(imagePath.lastComponent) 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 + } + } +} From cc3c7aa5cd4da6e239b083a3f7e36624af89bbb3 Mon Sep 17 00:00:00 2001 From: egor mihneivch Date: Sun, 21 Jun 2020 11:51:29 +0300 Subject: [PATCH 05/25] switch to iconset struct --- Sources/Badgy/Commands/Icon.swift | 11 ++++++ Sources/Badgy/Commands/Long.swift | 30 +++++------------ Sources/Badgy/Commands/Small.swift | 37 ++++++++------------- Sources/Badgy/Helpers/Factory+Resizer.swift | 17 ++++++++++ 4 files changed, 50 insertions(+), 45 deletions(-) diff --git a/Sources/Badgy/Commands/Icon.swift b/Sources/Badgy/Commands/Icon.swift index a10f60b..9259fcd 100644 --- a/Sources/Badgy/Commands/Icon.swift +++ b/Sources/Badgy/Commands/Icon.swift @@ -33,3 +33,14 @@ enum Icon { throw ValidationError("Input file or directory doesn't have a valid format") } } + +extension Icon { + var base: Path? { + switch self { + case .plain(let path): + return path + case .set(let set): + return set.largest() + } + } +} diff --git a/Sources/Badgy/Commands/Long.swift b/Sources/Badgy/Commands/Long.swift index 5cdd34e..1d9752d 100644 --- a/Sources/Badgy/Commands/Long.swift +++ b/Sources/Badgy/Commands/Long.swift @@ -87,29 +87,17 @@ final class Long: DependencyManager, Command, IconSetDelegate { } } - 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) + let baseIcon = try Icon(path: icon) + try process(baseIcon) } - private func process(baseIcon: String) throws { + private func process(_ icon: Icon) throws { let folder = Path("Badgy") + guard let baseIcon = icon.base else { + throw CLI.Error(message: "Couldn't find the largest image in the set") + } + do { defer { try? self.factory.cleanUp(folder: folder) } @@ -122,7 +110,7 @@ final class Long: DependencyManager, Command, IconSetDelegate { ) let filename = try factory.appendBadge( - to: baseIcon, + to: baseIcon.absolute().description, folder: folder, label: self.labelText, position: Position(rawValue: self.position ?? "bottom") @@ -136,7 +124,7 @@ final class Long: DependencyManager, Command, IconSetDelegate { logger.logInfo(item: "Icon with badge '\(labelText)' created at '\(filePath.absolute().description)'") - if ReplaceFlag.value, let iconSet = iconSetImages { + if ReplaceFlag.value, case .set(let iconSet) = icon { factory.replace(iconSet, with: filePath) } else { factory.resize(filename: filePath) diff --git a/Sources/Badgy/Commands/Small.swift b/Sources/Badgy/Commands/Small.swift index 6cf7fd6..c6ab6c0 100644 --- a/Sources/Badgy/Commands/Small.swift +++ b/Sources/Badgy/Commands/Small.swift @@ -68,33 +68,22 @@ final class Small: DependencyManager, Command, IconSetDelegate { public func execute() throws { 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) + let baseIcon = try Icon(path: icon) + try process(baseIcon) } - - private func process(baseIcon: String) throws { + + private func process(_ icon: Icon) throws { let folder = Path("Badgy") + + guard let baseIcon = icon.base else { + throw CLI.Error(message: "Couldn't find the largest image in the set") + } + do { defer { try? self.factory.cleanUp(folder: folder) } @@ -106,7 +95,7 @@ final class Small: DependencyManager, Command, IconSetDelegate { ) let filename = try factory.appendBadge( - to: baseIcon, + to: baseIcon.absolute().description, folder: folder, label: self.char, position: Position(rawValue: self.position ?? "bottomLeft") @@ -120,7 +109,7 @@ final class Small: DependencyManager, Command, IconSetDelegate { logger.logInfo(item: "Icon with badge '\(char)' created at '\(filePath.absolute().description)'") - if ReplaceFlag.value, let iconSet = iconSetImages { + if ReplaceFlag.value, case .set(let iconSet) = icon { factory.replace(iconSet, with: filePath) } else { factory.resize(filename: filePath) 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)") + } + } + } } From 176892d3d2a3dece79c9126b7dae2d1b08916889 Mon Sep 17 00:00:00 2001 From: egor mihneivch Date: Sun, 21 Jun 2020 12:01:45 +0300 Subject: [PATCH 06/25] fix path extension handling --- Sources/Badgy/Commands/Badgy.swift | 2 +- Sources/Badgy/Commands/Icon.swift | 2 +- Sources/Badgy/Commands/IconSet.swift | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/Badgy/Commands/Badgy.swift b/Sources/Badgy/Commands/Badgy.swift index a1b9b63..188f438 100644 --- a/Sources/Badgy/Commands/Badgy.swift +++ b/Sources/Badgy/Commands/Badgy.swift @@ -20,7 +20,7 @@ extension Badgy { @Argument(help :"Specify badge text") var label: String - @Argument(help :"Specify path to icon with format .png | .jpg | .appiconset", transform: Icon.init(path:)) + @Argument(help :"Specify path to icon with format .png | .jpg | .jpeg | .appiconset", transform: Icon.init(path:)) var icon: Icon @Option(help: "Position on which to place the badge") diff --git a/Sources/Badgy/Commands/Icon.swift b/Sources/Badgy/Commands/Icon.swift index 9259fcd..96fa97a 100644 --- a/Sources/Badgy/Commands/Icon.swift +++ b/Sources/Badgy/Commands/Icon.swift @@ -25,7 +25,7 @@ enum Icon { return } - if IconSet.imageExtensions.contains(path.lastComponent) { + if IconSet.imageExtensions.contains(path.extension ?? "") { self = .plain(path) return } diff --git a/Sources/Badgy/Commands/IconSet.swift b/Sources/Badgy/Commands/IconSet.swift index f590cda..dacdf3e 100644 --- a/Sources/Badgy/Commands/IconSet.swift +++ b/Sources/Badgy/Commands/IconSet.swift @@ -17,11 +17,11 @@ struct IconSet { } extension IconSet { - static let setExtension = ".appiconset" - static let imageExtensions = Set([".png", ".jpg", ".jpeg"]) + static let setExtension = "appiconset" + static let imageExtensions = Set(["png", "jpg", "jpeg"]) static func makeFromFolder(at path: Path) -> IconSet? { - guard path.isDirectory && path.lastComponent.contains(setExtension) else { + guard path.isDirectory && path.extension == setExtension else { return nil } @@ -41,7 +41,7 @@ extension IconSet { private extension ImageSize { static func makeFromImage(at imagePath: Path) -> ImageSize? { - guard IconSet.imageExtensions.contains(imagePath.lastComponent) else { + guard IconSet.imageExtensions.contains(imagePath.extension ?? "") else { return nil } From a98ea71da7700ba3327c4461ce3d3e7a42190644 Mon Sep 17 00:00:00 2001 From: egor mihneivch Date: Sun, 21 Jun 2020 12:37:24 +0300 Subject: [PATCH 07/25] extract commands' process to a pipeline --- Sources/Badgy/Commands/Icon.swift | 11 --- Sources/Badgy/Commands/Long.swift | 53 +++--------- Sources/Badgy/Commands/Small.swift | 51 +++-------- Sources/Badgy/Helpers/IconSignPipeline.swift | 91 ++++++++++++++++++++ 4 files changed, 113 insertions(+), 93 deletions(-) create mode 100644 Sources/Badgy/Helpers/IconSignPipeline.swift diff --git a/Sources/Badgy/Commands/Icon.swift b/Sources/Badgy/Commands/Icon.swift index 96fa97a..e0efa4c 100644 --- a/Sources/Badgy/Commands/Icon.swift +++ b/Sources/Badgy/Commands/Icon.swift @@ -33,14 +33,3 @@ enum Icon { throw ValidationError("Input file or directory doesn't have a valid format") } } - -extension Icon { - var base: Path? { - switch self { - case .plain(let path): - return path - case .set(let set): - return set.largest() - } - } -} diff --git a/Sources/Badgy/Commands/Long.swift b/Sources/Badgy/Commands/Long.swift index 1d9752d..fc11d4c 100644 --- a/Sources/Badgy/Commands/Long.swift +++ b/Sources/Badgy/Commands/Long.swift @@ -86,52 +86,21 @@ final class Long: DependencyManager, Command, IconSetDelegate { throw CLI.Error(message: "Angle should be within range -180 ... 180") } } - - let baseIcon = try Icon(path: icon) - try process(baseIcon) } - private func process(_ icon: Icon) throws { - let folder = Path("Badgy") + private func process() throws { + var pipeline = IconSignPipeline( + icon: try Icon(path: icon), + label: labelText + ) - guard let baseIcon = icon.base else { - throw CLI.Error(message: "Couldn't find the largest image in the set") - } + pipeline.position = Position(rawValue: position ?? "bottom") + pipeline.color = color + pipeline.tintColor = color + pipeline.angle = angleInt + pipeline.replace = ReplaceFlag.value - do { - defer { try? self.factory.cleanUp(folder: folder) } - - _ = try factory.makeBadge( - with: labelText, - colorHexCode: color, - tintColorHexCode: tintColor, - angle: angleInt, - inFolder: folder - ) - - let filename = try factory.appendBadge( - to: baseIcon.absolute().description, - folder: folder, - label: self.labelText, - position: Position(rawValue: self.position ?? "bottom") - ) - - let filePath = Path(filename) - guard filePath.exists else { - logger.logError("❌ ", item: "Failed to create badge") - return - } - - logger.logInfo(item: "Icon with badge '\(labelText)' created at '\(filePath.absolute().description)'") - - if ReplaceFlag.value, case .set(let iconSet) = icon { - factory.replace(iconSet, with: filePath) - } else { - factory.resize(filename: filePath) - } - } catch { - throw CLI.Error(message: error.localizedDescription) - } + try pipeline.execute() } } diff --git a/Sources/Badgy/Commands/Small.swift b/Sources/Badgy/Commands/Small.swift index c6ab6c0..97a4efe 100644 --- a/Sources/Badgy/Commands/Small.swift +++ b/Sources/Badgy/Commands/Small.swift @@ -73,49 +73,20 @@ final class Small: DependencyManager, Command, IconSetDelegate { } logger.logSection("$ ", item: "badgy small \"\(char)\" \"\(icon)\"", color: .ios) - let baseIcon = try Icon(path: icon) - try process(baseIcon) + try process() } - private func process(_ icon: Icon) throws { - let folder = Path("Badgy") + private func process() throws { + var pipeline = IconSignPipeline( + icon: try Icon(path: icon), + label: char + ) - guard let baseIcon = icon.base else { - throw CLI.Error(message: "Couldn't find the largest image in the set") - } + pipeline.position = Position(rawValue: self.position ?? "bottomLeft") + pipeline.color = color + pipeline.tintColor = color + pipeline.replace = ReplaceFlag.value - do { - defer { try? self.factory.cleanUp(folder: folder) } - - _ = try factory.makeSmall( - with: char, - colorHexCode: color, - tintColorHexCode: tintColor, - inFolder: folder - ) - - let filename = try factory.appendBadge( - to: baseIcon.absolute().description, - folder: folder, - label: self.char, - position: Position(rawValue: self.position ?? "bottomLeft") - ) - - let filePath = Path(filename) - guard filePath.exists else { - logger.logError("❌ ", item: "Failed to create badge") - return - } - - logger.logInfo(item: "Icon with badge '\(char)' created at '\(filePath.absolute().description)'") - - if ReplaceFlag.value, case .set(let iconSet) = icon { - factory.replace(iconSet, with: filePath) - } else { - factory.resize(filename: filePath) - } - } catch { - throw CLI.Error(message: error.localizedDescription) - } + try pipeline.execute() } } diff --git a/Sources/Badgy/Helpers/IconSignPipeline.swift b/Sources/Badgy/Helpers/IconSignPipeline.swift new file mode 100644 index 0000000..697e862 --- /dev/null +++ b/Sources/Badgy/Helpers/IconSignPipeline.swift @@ -0,0 +1,91 @@ +// +// 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): + guard let path = set.largest() else { + throw IconSignPipeline.Error(message: "Couldn't find the largest image in the set") + } + + return path + } + } +} From f2c87edf37dab627fa4ea824ad2857a3509a1af1 Mon Sep 17 00:00:00 2001 From: egor mihneivch Date: Sun, 21 Jun 2020 12:38:57 +0300 Subject: [PATCH 08/25] shuffle cli model files to helpers directory --- Sources/Badgy/{Commands => Helpers}/ColorCode+Hex.swift | 0 Sources/Badgy/{Commands => Helpers}/ColorCode+Name.swift | 0 Sources/Badgy/{Commands => Helpers}/ColorCode.swift | 0 Sources/Badgy/{Commands => Helpers}/Icon.swift | 0 Sources/Badgy/{Commands => Helpers}/IconSet.swift | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename Sources/Badgy/{Commands => Helpers}/ColorCode+Hex.swift (100%) rename Sources/Badgy/{Commands => Helpers}/ColorCode+Name.swift (100%) rename Sources/Badgy/{Commands => Helpers}/ColorCode.swift (100%) rename Sources/Badgy/{Commands => Helpers}/Icon.swift (100%) rename Sources/Badgy/{Commands => Helpers}/IconSet.swift (100%) diff --git a/Sources/Badgy/Commands/ColorCode+Hex.swift b/Sources/Badgy/Helpers/ColorCode+Hex.swift similarity index 100% rename from Sources/Badgy/Commands/ColorCode+Hex.swift rename to Sources/Badgy/Helpers/ColorCode+Hex.swift diff --git a/Sources/Badgy/Commands/ColorCode+Name.swift b/Sources/Badgy/Helpers/ColorCode+Name.swift similarity index 100% rename from Sources/Badgy/Commands/ColorCode+Name.swift rename to Sources/Badgy/Helpers/ColorCode+Name.swift diff --git a/Sources/Badgy/Commands/ColorCode.swift b/Sources/Badgy/Helpers/ColorCode.swift similarity index 100% rename from Sources/Badgy/Commands/ColorCode.swift rename to Sources/Badgy/Helpers/ColorCode.swift diff --git a/Sources/Badgy/Commands/Icon.swift b/Sources/Badgy/Helpers/Icon.swift similarity index 100% rename from Sources/Badgy/Commands/Icon.swift rename to Sources/Badgy/Helpers/Icon.swift diff --git a/Sources/Badgy/Commands/IconSet.swift b/Sources/Badgy/Helpers/IconSet.swift similarity index 100% rename from Sources/Badgy/Commands/IconSet.swift rename to Sources/Badgy/Helpers/IconSet.swift From 389470f14021797323ad277faea9192f5790a8e1 Mon Sep 17 00:00:00 2001 From: egor mihneivch Date: Sun, 21 Jun 2020 12:56:05 +0300 Subject: [PATCH 09/25] use pipeline in arguments CLI --- Sources/Badgy/Commands/Badgy.swift | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/Sources/Badgy/Commands/Badgy.swift b/Sources/Badgy/Commands/Badgy.swift index 188f438..6abbdb3 100644 --- a/Sources/Badgy/Commands/Badgy.swift +++ b/Sources/Badgy/Commands/Badgy.swift @@ -83,6 +83,14 @@ extension Badgy { } } } + + func run() throws { + var pipeline = IconSignPipeline.make(withOptions: options) + pipeline.position = options.position ?? Position.bottom + pipeline.angle = angle + + try pipeline.execute() + } } } @@ -100,7 +108,27 @@ extension Badgy { throw ValidationError("Label should contain maximum 1 characters") } } + + func run() throws { + var pipeline = IconSignPipeline.make(withOptions: options) + pipeline.position = options.position ?? Position.bottom + + 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.position = options.position + pipeline.color = options.color?.value + pipeline.tintColor = options.tintColor?.value + pipeline.replace = options.replace + + return pipeline + } +} From 511f4a667f580b8b6880e66ab8f5a54846aad9a6 Mon Sep 17 00:00:00 2001 From: egor mihneivch Date: Sun, 21 Jun 2020 12:59:47 +0300 Subject: [PATCH 10/25] hack around logger's verbose flag --- Sources/Badgy/Commands/Badgy.swift | 2 ++ Sources/Badgy/Commands/Long.swift | 2 ++ Sources/Badgy/Commands/Small.swift | 2 ++ Sources/Badgy/Loggers/Logger.swift | 2 ++ Sources/Badgy/Loggers/VerboseLogger.swift | 2 -- 5 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Sources/Badgy/Commands/Badgy.swift b/Sources/Badgy/Commands/Badgy.swift index 6abbdb3..5b7d5c3 100644 --- a/Sources/Badgy/Commands/Badgy.swift +++ b/Sources/Badgy/Commands/Badgy.swift @@ -42,6 +42,8 @@ extension Badgy { guard DependencyManager().areDependenciesInstalled() else { throw ValidationError("Missing dependencies. Run: 'brew install imagemagick'") } + + Logger.shared.verbose = verbose } } } diff --git a/Sources/Badgy/Commands/Long.swift b/Sources/Badgy/Commands/Long.swift index fc11d4c..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'") diff --git a/Sources/Badgy/Commands/Small.swift b/Sources/Badgy/Commands/Small.swift index 97a4efe..fec692f 100644 --- a/Sources/Badgy/Commands/Small.swift +++ b/Sources/Badgy/Commands/Small.swift @@ -67,6 +67,8 @@ 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'") 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 } From d675b461855d5084e518c64a81424d05539e0465 Mon Sep 17 00:00:00 2001 From: egor mihneivch Date: Sun, 21 Jun 2020 13:10:21 +0300 Subject: [PATCH 11/25] add compilation flag to swift on to arguments parser --- Sources/Badgy/main.swift | 4 ++++ 1 file changed, 4 insertions(+) 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 From c6174987c8942b1067a5da724c31aeded2c2e91d Mon Sep 17 00:00:00 2001 From: egor mihneivch Date: Sun, 21 Jun 2020 13:21:41 +0300 Subject: [PATCH 12/25] add initial execution log --- Sources/Badgy/Commands/Badgy.swift | 4 ++++ Sources/Badgy/Helpers/Icon.swift | 9 +++++++++ Sources/Badgy/Helpers/IconSet.swift | 5 +++-- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/Sources/Badgy/Commands/Badgy.swift b/Sources/Badgy/Commands/Badgy.swift index 5b7d5c3..f123f07 100644 --- a/Sources/Badgy/Commands/Badgy.swift +++ b/Sources/Badgy/Commands/Badgy.swift @@ -87,6 +87,8 @@ extension Badgy { } func run() throws { + Logger.shared.logSection("$ ", item: "badgy long \"\(options.label)\" \"\(options.icon.path)\"", color: .ios) + var pipeline = IconSignPipeline.make(withOptions: options) pipeline.position = options.position ?? Position.bottom pipeline.angle = angle @@ -112,6 +114,8 @@ extension Badgy { } func run() throws { + Logger.shared.logSection("$ ", item: "badgy small \"\(options.label)\" \"\(options.icon.path)\"", color: .ios) + var pipeline = IconSignPipeline.make(withOptions: options) pipeline.position = options.position ?? Position.bottom diff --git a/Sources/Badgy/Helpers/Icon.swift b/Sources/Badgy/Helpers/Icon.swift index e0efa4c..9cca7f9 100644 --- a/Sources/Badgy/Helpers/Icon.swift +++ b/Sources/Badgy/Helpers/Icon.swift @@ -33,3 +33,12 @@ enum Icon { throw ValidationError("Input file or directory doesn't have a valid format") } } + +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 index dacdf3e..f48da9d 100644 --- a/Sources/Badgy/Helpers/IconSet.swift +++ b/Sources/Badgy/Helpers/IconSet.swift @@ -9,7 +9,8 @@ import SwiftCLI struct IconSet { typealias ImageInfo = (image: Path, size: ImageSize) - var images: [ImageInfo] + let path: Path + let images: [ImageInfo] func largest() -> Path? { images.max { lhs, rhs in lhs.size.width < rhs.size.width }.flatMap { $0.image } @@ -35,7 +36,7 @@ extension IconSet { } } - return IconSet(images: images) + return IconSet(path: path, images: images) } } From adc5a14d48c93745fa78a1e28cb1c872169a9f85 Mon Sep 17 00:00:00 2001 From: egor mihneivch Date: Sun, 21 Jun 2020 13:30:11 +0300 Subject: [PATCH 13/25] match logging with original cli --- Sources/Badgy/Helpers/Icon.swift | 10 +++++++--- Sources/Badgy/Helpers/IconSignPipeline.swift | 4 ++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Sources/Badgy/Helpers/Icon.swift b/Sources/Badgy/Helpers/Icon.swift index 9cca7f9..da05a90 100644 --- a/Sources/Badgy/Helpers/Icon.swift +++ b/Sources/Badgy/Helpers/Icon.swift @@ -17,7 +17,9 @@ enum Icon { let path = Path(path) guard path.exists else { - throw ValidationError("Input file or directory doesn't exist") + let message = "Input file or directory doesn't exist" + Logger.shared.logError("❌ ", item: message) + throw ValidationError(message) } if let set = IconSet.makeFromFolder(at: path) { @@ -29,8 +31,10 @@ enum Icon { self = .plain(path) return } - - throw ValidationError("Input file or directory doesn't have a valid format") + + let message = "Input file or directory doesn't have a valid format" + Logger.shared.logError("❌ ", item: message) + throw ValidationError(message) } } diff --git a/Sources/Badgy/Helpers/IconSignPipeline.swift b/Sources/Badgy/Helpers/IconSignPipeline.swift index 697e862..edcdf33 100644 --- a/Sources/Badgy/Helpers/IconSignPipeline.swift +++ b/Sources/Badgy/Helpers/IconSignPipeline.swift @@ -81,10 +81,14 @@ private extension Icon { 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 } } From 49fb264cd80fc11087940cf75e1ddc7340ab2c7b Mon Sep 17 00:00:00 2001 From: egor mihneivch Date: Sun, 21 Jun 2020 13:49:04 +0300 Subject: [PATCH 14/25] tweak colors help section --- Sources/Badgy/Commands/Badgy.swift | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/Sources/Badgy/Commands/Badgy.swift b/Sources/Badgy/Commands/Badgy.swift index f123f07..4ae6dd8 100644 --- a/Sources/Badgy/Commands/Badgy.swift +++ b/Sources/Badgy/Commands/Badgy.swift @@ -26,10 +26,20 @@ extension Badgy { @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' or a named color format ('red', 'white', etc.)") + @Option(help: """ + Specify a valid hex color code in a case insensitive format: '#rrbbgg' | '#rrbbggaa' + 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 badge text/tint color with a hexadecimal color code format ('#rrbbgg' | '#rrbbggaa') or a named color format ('red', 'white', etc.)") + @Option(help: """ + Specify a valid hex color code in a case insensitive format: '#rrbbgg' | '#rrbbggaa' + 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") From 27e073377fc52ab27445112fe53efa01659a8fd6 Mon Sep 17 00:00:00 2001 From: egor mihneivch Date: Mon, 22 Jun 2020 10:38:37 +0300 Subject: [PATCH 15/25] replace colors order misspell --- Sources/Badgy/Commands/Badgy.swift | 4 ++-- Sources/Badgy/Helpers/ColorCode+Hex.swift | 4 ++-- Sources/Badgy/Helpers/Validation+Color.swift | 2 +- Sources/Badgy/Helpers/Validation+HexColor.swift | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/Badgy/Commands/Badgy.swift b/Sources/Badgy/Commands/Badgy.swift index 4ae6dd8..8c38245 100644 --- a/Sources/Badgy/Commands/Badgy.swift +++ b/Sources/Badgy/Commands/Badgy.swift @@ -27,7 +27,7 @@ extension Badgy { var position: Position? @Option(help: """ - 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 @@ -35,7 +35,7 @@ extension Badgy { var color: ColorCode? @Option(help: """ - 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/ColorCode+Hex.swift b/Sources/Badgy/Helpers/ColorCode+Hex.swift index 9db389d..348eee6 100644 --- a/Sources/Badgy/Helpers/ColorCode+Hex.swift +++ b/Sources/Badgy/Helpers/ColorCode+Hex.swift @@ -10,14 +10,14 @@ import Foundation extension ColorCode { /// Checks whether the `input` matches the valid color formats /// - /// Valid colors formats are `#rrbbgg` | `#rrbbggaa` + /// Valid colors formats are `#rrggbb` | `#rrggbbaa` static func isHexColor(_ hex: String) -> Bool { NSRegularExpression.hexColorCode.matches(hex) } } private extension NSRegularExpression { - /// Regular expression that matches '#rrbbgg' and '#rrbbggaa' formats + /// Regular expression that matches '#rrggbb' and '#rrggbbaa' formats /// /// `^` asserts position at start of a line /// `#` matches the character # literally 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+HexColor.swift b/Sources/Badgy/Helpers/Validation+HexColor.swift index 45870eb..e609b13 100644 --- a/Sources/Badgy/Helpers/Validation+HexColor.swift +++ b/Sources/Badgy/Helpers/Validation+HexColor.swift @@ -6,7 +6,7 @@ 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 { ColorCode.isHexColor(input) } From 9953f2b8520ad34d15261bbca578688bcd94dd1f Mon Sep 17 00:00:00 2001 From: egor mihneivch Date: Mon, 22 Jun 2020 10:57:32 +0300 Subject: [PATCH 16/25] tweak angle option flag comments --- Sources/Badgy/Commands/Badgy.swift | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Sources/Badgy/Commands/Badgy.swift b/Sources/Badgy/Commands/Badgy.swift index 8c38245..ab51647 100644 --- a/Sources/Badgy/Commands/Badgy.swift +++ b/Sources/Badgy/Commands/Badgy.swift @@ -67,8 +67,8 @@ extension Badgy { @OptionGroup() var options: Badgy.Options - @Option(help: "Rotation angle of the badge") - var angle: Int? + @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 { @@ -89,10 +89,8 @@ extension Badgy { } let validAngleRange = -180...180 - if let angle = angle { - guard validAngleRange.contains(angle) else { - throw ValidationError("Angle should be within range: \(validAngleRange)") - } + guard validAngleRange.contains(angle) else { + throw ValidationError("Angle should be within range: \(validAngleRange)") } } From 2a2345d260026ddbe14de766a2c91b1c355242c6 Mon Sep 17 00:00:00 2001 From: egor mihneivch Date: Mon, 22 Jun 2020 11:37:13 +0300 Subject: [PATCH 17/25] redo CLI position argument handling specify default position value extract valid positions --- Sources/Badgy/Commands/Badgy.swift | 32 ++++++++++++++---------------- Sources/Badgy/Position.swift | 6 ++++++ 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/Sources/Badgy/Commands/Badgy.swift b/Sources/Badgy/Commands/Badgy.swift index ab51647..a88bfdb 100644 --- a/Sources/Badgy/Commands/Badgy.swift +++ b/Sources/Badgy/Commands/Badgy.swift @@ -23,8 +23,8 @@ extension Badgy { @Argument(help :"Specify path to icon with format .png | .jpg | .jpeg | .appiconset", transform: Icon.init(path:)) var icon: Icon - @Option(help: "Position on which to place the badge") - var position: Position? + @Option(default: .bottom, help: "Position on which to place the badge") + var position: Position @Option(help: """ Specify a valid hex color code in a case insensitive format: '#rrggbb' | '#rrggbbaa' @@ -75,17 +75,8 @@ extension Badgy { 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)") - } + guard options.position.isValidForLongLabels else { + throw ValidationError("Invalid provided position, supported positions are: \(Position.longLabelPositions.formatted())") } let validAngleRange = -180...180 @@ -98,7 +89,6 @@ extension Badgy { Logger.shared.logSection("$ ", item: "badgy long \"\(options.label)\" \"\(options.icon.path)\"", color: .ios) var pipeline = IconSignPipeline.make(withOptions: options) - pipeline.position = options.position ?? Position.bottom pipeline.angle = angle try pipeline.execute() @@ -124,9 +114,7 @@ extension Badgy { func run() throws { Logger.shared.logSection("$ ", item: "badgy small \"\(options.label)\" \"\(options.icon.path)\"", color: .ios) - var pipeline = IconSignPipeline.make(withOptions: options) - pipeline.position = options.position ?? Position.bottom - + let pipeline = IconSignPipeline.make(withOptions: options) try pipeline.execute() } } @@ -146,3 +134,13 @@ private extension IconSignPipeline { 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/Position.swift b/Sources/Badgy/Position.swift index 88a7f37..4b0eb08 100644 --- a/Sources/Badgy/Position.swift +++ b/Sources/Badgy/Position.swift @@ -35,3 +35,9 @@ enum Position: String { } } } + +extension Sequence where Element == Position { + func formatted() -> String { + map { $0.rawValue}.joined(separator: " | ") + } +} From 75fec0c6cc6e6b632b0c02f9f4f3757ef94e787b Mon Sep 17 00:00:00 2001 From: egor mihneivch Date: Mon, 22 Jun 2020 11:42:29 +0300 Subject: [PATCH 18/25] trim a detailed explanation of hex regexp --- Sources/Badgy/Helpers/ColorCode+Hex.swift | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/Sources/Badgy/Helpers/ColorCode+Hex.swift b/Sources/Badgy/Helpers/ColorCode+Hex.swift index 348eee6..562a4b1 100644 --- a/Sources/Badgy/Helpers/ColorCode+Hex.swift +++ b/Sources/Badgy/Helpers/ColorCode+Hex.swift @@ -19,16 +19,6 @@ extension ColorCode { private extension NSRegularExpression { /// Regular expression that matches '#rrggbb' and '#rrggbbaa' 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/) + /// Additional explanation at [regex101](https://regex101.com/r/j0MDnb/1/tests) static let hexColorCode = NSRegularExpression("^#(?:[0-9a-fA-F]{2}){3,4}$") } From b733332e441a5e9eebb88f96501ebd96d3985a33 Mon Sep 17 00:00:00 2001 From: egor mihneivch Date: Mon, 22 Jun 2020 11:53:18 +0300 Subject: [PATCH 19/25] by command position argument handling restore bottomLeft default position for `small` list supported positions list in help --- Sources/Badgy/Commands/Badgy.swift | 17 +++++++++++------ Sources/Badgy/Position.swift | 2 +- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/Sources/Badgy/Commands/Badgy.swift b/Sources/Badgy/Commands/Badgy.swift index a88bfdb..f5bd360 100644 --- a/Sources/Badgy/Commands/Badgy.swift +++ b/Sources/Badgy/Commands/Badgy.swift @@ -23,9 +23,6 @@ extension Badgy { @Argument(help :"Specify path to icon with format .png | .jpg | .jpeg | .appiconset", transform: Icon.init(path:)) var icon: Icon - @Option(default: .bottom, help: "Position on which to place the badge") - var position: Position - @Option(help: """ Specify a valid hex color code in a case insensitive format: '#rrggbb' | '#rrggbbaa' or @@ -67,6 +64,9 @@ extension Badgy { @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 @@ -75,7 +75,7 @@ extension Badgy { throw ValidationError("Label should contain maximum 4 characters") } - guard options.position.isValidForLongLabels else { + guard position.isValidForLongLabels else { throw ValidationError("Invalid provided position, supported positions are: \(Position.longLabelPositions.formatted())") } @@ -89,6 +89,7 @@ extension Badgy { 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() @@ -105,6 +106,9 @@ extension Badgy { @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 characters") @@ -114,7 +118,9 @@ extension Badgy { func run() throws { Logger.shared.logSection("$ ", item: "badgy small \"\(options.label)\" \"\(options.icon.path)\"", color: .ios) - let pipeline = IconSignPipeline.make(withOptions: options) + var pipeline = IconSignPipeline.make(withOptions: options) + pipeline.position = position + try pipeline.execute() } } @@ -126,7 +132,6 @@ private extension IconSignPipeline { static func make(withOptions options: Badgy.Options) -> IconSignPipeline { var pipeline = IconSignPipeline(icon: options.icon, label: options.label) - pipeline.position = options.position pipeline.color = options.color?.value pipeline.tintColor = options.tintColor?.value pipeline.replace = options.replace diff --git a/Sources/Badgy/Position.swift b/Sources/Badgy/Position.swift index 4b0eb08..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 From 8efb0a61fe223c2dbbf367889ef49c863e3b76fe Mon Sep 17 00:00:00 2001 From: egor mihneivch Date: Mon, 22 Jun 2020 11:55:41 +0300 Subject: [PATCH 20/25] fix validation error misspell --- Sources/Badgy/Commands/Badgy.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Badgy/Commands/Badgy.swift b/Sources/Badgy/Commands/Badgy.swift index f5bd360..c787ce8 100644 --- a/Sources/Badgy/Commands/Badgy.swift +++ b/Sources/Badgy/Commands/Badgy.swift @@ -111,7 +111,7 @@ extension Badgy { func validate() throws { guard options.label.count <= 1 else { - throw ValidationError("Label should contain maximum 1 characters") + throw ValidationError("Label should contain maximum 1 character") } } From cf0957dc4c1c775aa3bf287d89b631a91822f149 Mon Sep 17 00:00:00 2001 From: egor mihneivch Date: Mon, 22 Jun 2020 12:03:16 +0300 Subject: [PATCH 21/25] cleaned up header comments --- Sources/Badgy/Helpers/ColorCode+Hex.swift | 5 +---- Sources/Badgy/Helpers/Icon.swift | 7 ++----- Sources/Badgy/Helpers/IconSignPipeline.swift | 1 - Sources/Badgy/Helpers/Validation+Any.swift | 1 - 4 files changed, 3 insertions(+), 11 deletions(-) diff --git a/Sources/Badgy/Helpers/ColorCode+Hex.swift b/Sources/Badgy/Helpers/ColorCode+Hex.swift index 562a4b1..efad36e 100644 --- a/Sources/Badgy/Helpers/ColorCode+Hex.swift +++ b/Sources/Badgy/Helpers/ColorCode+Hex.swift @@ -1,9 +1,6 @@ // -// File.swift +// Badgy // -// -// Created by yahor mikhnevich on 6/21/20. -// import Foundation diff --git a/Sources/Badgy/Helpers/Icon.swift b/Sources/Badgy/Helpers/Icon.swift index da05a90..752074b 100644 --- a/Sources/Badgy/Helpers/Icon.swift +++ b/Sources/Badgy/Helpers/Icon.swift @@ -1,9 +1,6 @@ // -// File.swift -// -// -// Created by yahor mikhnevich on 6/21/20. -// +// Badgy +// import Foundation import ArgumentParser diff --git a/Sources/Badgy/Helpers/IconSignPipeline.swift b/Sources/Badgy/Helpers/IconSignPipeline.swift index edcdf33..c1b7c38 100644 --- a/Sources/Badgy/Helpers/IconSignPipeline.swift +++ b/Sources/Badgy/Helpers/IconSignPipeline.swift @@ -2,7 +2,6 @@ // Badgy // - import Foundation import PathKit 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 From 577b54a2afea73edd6f25b91ad871c9f8ba4f1a7 Mon Sep 17 00:00:00 2001 From: egor mihneivch Date: Sat, 27 Jun 2020 09:06:50 +0300 Subject: [PATCH 22/25] drop SwiftCLI arguments handling --- Sources/Badgy/Commands/Long.swift | 108 ------------------ Sources/Badgy/Commands/Small.swift | 94 --------------- Sources/Badgy/Helpers/Validation+Any.swift | 18 --- Sources/Badgy/Helpers/Validation+Color.swift | 20 ---- .../Badgy/Helpers/Validation+ColorName.swift | 13 --- .../Badgy/Helpers/Validation+HexColor.swift | 13 --- Sources/Badgy/Loggers/Logger.swift | 1 - Sources/Badgy/Loggers/VerboseLogger.swift | 2 - Sources/Badgy/main.swift | 21 ---- 9 files changed, 290 deletions(-) delete mode 100644 Sources/Badgy/Commands/Long.swift delete mode 100644 Sources/Badgy/Commands/Small.swift delete mode 100644 Sources/Badgy/Helpers/Validation+Any.swift delete mode 100644 Sources/Badgy/Helpers/Validation+Color.swift delete mode 100644 Sources/Badgy/Helpers/Validation+ColorName.swift delete mode 100644 Sources/Badgy/Helpers/Validation+HexColor.swift diff --git a/Sources/Badgy/Commands/Long.swift b/Sources/Badgy/Commands/Long.swift deleted file mode 100644 index f98e37c..0000000 --- a/Sources/Badgy/Commands/Long.swift +++ /dev/null @@ -1,108 +0,0 @@ -// -// Badgy -// -// Created by Arthur Alves on 30/05/2020. -// - -import Foundation -import PathKit -import SwiftCLI - -final class Long: DependencyManager, Command, IconSetDelegate { - // -------------- - // MARK: Command information - - let name: String = "long" - let shortDescription: String = "Add rectangular label to app icon" - - // -------------- - // MARK: Configuration Properties - @Param(validation: Validation.custom("Label should contain maximum 4 characters") { - (input) in return input.count <= 4 - }) - var labelText: String - - @Param(validation: Validation - .custom("Specify valid icon with format .png | .jpg | .appiconset") { - (input) in - - let path = Path(input) - let formats = [".png", ".jpg", ".jpeg", ".appiconset"] - - guard path.exists, formats.contains(where: { path.lastComponent.contains($0) }) - else { - Logger.shared.logError("❌ ", - item: "Input file doesn't exist or doesn't have a valid format") - return false - } - return true - }) - var icon: String - - @Key("-a", "--angle", description: "Rotation angle of the badge") - var angle: String? - var angleInt: Int = 0 - - @Key("-p", "--position", - description: "Position on which to place the badge", - validation: [Validation.allowing(Position.top.rawValue, Position.left.rawValue, - Position.bottom.rawValue, Position.right.rawValue, - Position.center.rawValue)] - ) - var position: String? - - @Key("-c", "--color", - description: "Specify badge color with a hexadecimal color code or a named color", - validation: [Validation.colorCode()] - ) - var color: String? - - @Key("-t", "--tint-color", - description: "Specify badge text/tint color with a hexadecimal color code or a named color", - validation: [Validation.colorCode()] - ) - var tintColor: String? - - let logger = Logger.shared - let factory = Factory() - - 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 label \"\(labelText)\" \"\(icon)\"", color: .ios) - if - let stringAngle = angle, - let angle = Int(String(stringAngle.replacingOccurrences(of: "\\", with: ""))) { - switch angle { - case -180...180: - angleInt = angle - logger.logDebug(item: "Acceptable angle") - default: - throw CLI.Error(message: "Angle should be within range -180 ... 180") - } - } - } - - private func process() throws { - var pipeline = IconSignPipeline( - icon: try Icon(path: icon), - label: labelText - ) - - 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 deleted file mode 100644 index fec692f..0000000 --- a/Sources/Badgy/Commands/Small.swift +++ /dev/null @@ -1,94 +0,0 @@ -// -// Badgy -// -// Created by Arthur Alves on 30/05/2020. -// - -import Foundation -import PathKit -import SwiftCLI - -final class Small: DependencyManager, Command, IconSetDelegate { - // -------------- - // MARK: Command information - - let name: String = "small" - let shortDescription: String = "Add small square label to app icon" - - // -------------- - // MARK: Configuration Properties - @Param(validation: Validation.custom("This parameter should be 1 char only.") { - (input) in return input.count == 1 - }) - var char: String - - @Param(validation: Validation - .custom("Specify valid icon with format .png | .jpg | .appiconset") { - (input) in - - let path = Path(input) - let formats = [".png", ".jpg", ".jpeg", ".appiconset"] - - guard path.exists, formats.contains(where: { path.lastComponent.contains($0) }) - else { - Logger.shared.logError("❌ ", - item: "Input file doesn't exist or doesn't have a valid format") - return false - } - return true - }) - var icon: String - - @Key("-p", "--position", - description: "Position on which to place the badge", - validation: [Validation.allowing(Position.top.rawValue, Position.left.rawValue, - Position.bottom.rawValue, Position.right.rawValue, - Position.topLeft.rawValue, Position.topRight.rawValue, - Position.bottomLeft.rawValue, Position.bottomRight.rawValue, - Position.center.rawValue)] - ) - var position: String? - - @Key("-c", "--color", - description: "Specify badge color with a hexadecimal color code or a named color", - validation: [Validation.colorCode()] - ) - var color: String? - - @Key("-t", "--tint-color", - description: "Specify badge text/tint color with a hexadecimal color code or a named color", - validation: [Validation.colorCode()] - ) - var tintColor: String? - - let logger = Logger.shared - let factory = Factory() - - 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) - - try process() - } - - 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/Validation+Any.swift b/Sources/Badgy/Helpers/Validation+Any.swift deleted file mode 100644 index 01ace11..0000000 --- a/Sources/Badgy/Helpers/Validation+Any.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// Badgy -// - -import Foundation -import SwiftCLI - -extension Validation { - static func any(_ message: String, validations: Validation.ValidatorBlock...) -> Validation { - return Validation.custom(message) { (input) in - let isInputValid = validations.contains { validation in validation(input) } - if !isInputValid { - Logger.shared.logError("❌ ", item: message) - } - return isInputValid - } - } -} diff --git a/Sources/Badgy/Helpers/Validation+Color.swift b/Sources/Badgy/Helpers/Validation+Color.swift deleted file mode 100644 index 703694f..0000000 --- a/Sources/Badgy/Helpers/Validation+Color.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// Badgy -// - -import Foundation -import SwiftCLI - -extension Validation where T == String { - static func colorCode() -> Self { - let message = """ - - 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 - """ - - return Validation.any(message, validations: isHexColor, isColorName) - } -} diff --git a/Sources/Badgy/Helpers/Validation+ColorName.swift b/Sources/Badgy/Helpers/Validation+ColorName.swift deleted file mode 100644 index 8eb2836..0000000 --- a/Sources/Badgy/Helpers/Validation+ColorName.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// Badgy -// - -import Foundation -import SwiftCLI - -extension Validation where T == String { - /// Check whether the `input` is a known color name - static func isColorName(_ input: String) -> Bool { - return ColorCode.isColorName(input) - } -} diff --git a/Sources/Badgy/Helpers/Validation+HexColor.swift b/Sources/Badgy/Helpers/Validation+HexColor.swift deleted file mode 100644 index e609b13..0000000 --- a/Sources/Badgy/Helpers/Validation+HexColor.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// Badgy -// - -import Foundation -import SwiftCLI - -extension Validation where T == String { - /// Checks whether the `input` matches '#rrggbb' | '#rrggbbaa' color formats - static func isHexColor(_ input: String) -> Bool { - ColorCode.isHexColor(input) - } -} diff --git a/Sources/Badgy/Loggers/Logger.swift b/Sources/Badgy/Loggers/Logger.swift index dacc19b..34947e8 100644 --- a/Sources/Badgy/Loggers/Logger.swift +++ b/Sources/Badgy/Loggers/Logger.swift @@ -5,7 +5,6 @@ // import Foundation -import SwiftCLI public class Logger: VerboseLogger { static let shared = Logger() diff --git a/Sources/Badgy/Loggers/VerboseLogger.swift b/Sources/Badgy/Loggers/VerboseLogger.swift index e8cfae3..b07f882 100644 --- a/Sources/Badgy/Loggers/VerboseLogger.swift +++ b/Sources/Badgy/Loggers/VerboseLogger.swift @@ -7,8 +7,6 @@ import SwiftCLI import Foundation -let VerboseFlag = Flag("-v", "--verbose", description: "Log tech details for nerds") - public enum ShellColor: String { case blue = "\\033[0;34m" case red = "\\033[0;31m" diff --git a/Sources/Badgy/main.swift b/Sources/Badgy/main.swift index a515bbf..640bb26 100644 --- a/Sources/Badgy/main.swift +++ b/Sources/Badgy/main.swift @@ -1,23 +1,2 @@ -import SwiftCLI -let ReplaceFlag = Flag("-r", "--replace", description: "Indicates Badgy should replace the input icon") - -let cli = CLI( - name: "badgy", - version: "0.1.4", - description: "A command-line tool to add labels to your app icon" -) - -cli.commands = [ - Small(), - Long() -] - -cli.globalOptions.append(ReplaceFlag) -cli.globalOptions.append(VerboseFlag) - -#if ARGUMENT_PARSER Badgy.main() -#else -_ = cli.go() -#endif From 1b437526e691c256c3021ea02ea6a28ef580ed6b Mon Sep 17 00:00:00 2001 From: egor mihneivch Date: Sat, 27 Jun 2020 09:24:51 +0300 Subject: [PATCH 23/25] explicitly specify default values for color arguments --- Sources/Badgy/Commands/Badgy.swift | 7 ++++--- Sources/Badgy/Helpers/ColorCode.swift | 11 +++++++++++ Sources/Badgy/Helpers/Factory+Small.swift | 2 +- Sources/Badgy/Helpers/Factory.swift | 4 ++-- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/Sources/Badgy/Commands/Badgy.swift b/Sources/Badgy/Commands/Badgy.swift index c787ce8..5031ea5 100644 --- a/Sources/Badgy/Commands/Badgy.swift +++ b/Sources/Badgy/Commands/Badgy.swift @@ -28,16 +28,17 @@ extension Badgy { or Provide a named color: 'snow' | 'snow1' | ... Complete list of named colors: https://imagemagick.org/script/color.php#color_names + (default: randomly selected from \(Factory.colors.joined(separator: " | ")) """) var color: ColorCode? - @Option(help: """ + @Option(default: "white", 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? + var tintColor: ColorCode @Flag(help: "Indicates Badgy should replace the input icon") var replace: Bool @@ -133,7 +134,7 @@ private extension IconSignPipeline { var pipeline = IconSignPipeline(icon: options.icon, label: options.label) pipeline.color = options.color?.value - pipeline.tintColor = options.tintColor?.value + pipeline.tintColor = options.tintColor.value pipeline.replace = options.replace return pipeline diff --git a/Sources/Badgy/Helpers/ColorCode.swift b/Sources/Badgy/Helpers/ColorCode.swift index 59f5746..cfbe5f6 100644 --- a/Sources/Badgy/Helpers/ColorCode.swift +++ b/Sources/Badgy/Helpers/ColorCode.swift @@ -22,3 +22,14 @@ extension ColorCode: ExpressibleByArgument { } } +extension ColorCode: CustomStringConvertible { + var description: String { + return value + } +} + +extension ColorCode: ExpressibleByStringLiteral { + init(stringLiteral value: StringLiteralType) { + self.init(argument: value)! + } +} diff --git a/Sources/Badgy/Helpers/Factory+Small.swift b/Sources/Badgy/Helpers/Factory+Small.swift index ff98a01..50fe7bf 100644 --- a/Sources/Badgy/Helpers/Factory+Small.swift +++ b/Sources/Badgy/Helpers/Factory+Small.swift @@ -14,7 +14,7 @@ extension Factory { tintColorHexCode: String? = nil, inFolder folder: Path) throws -> String { - let color = colorHexCode ?? colors.randomElement()! + let color = colorHexCode ?? Factory.colors.randomElement()! let tintColor = tintColorHexCode ?? "white" do { diff --git a/Sources/Badgy/Helpers/Factory.swift b/Sources/Badgy/Helpers/Factory.swift index 9f9e64c..44a496e 100644 --- a/Sources/Badgy/Helpers/Factory.swift +++ b/Sources/Badgy/Helpers/Factory.swift @@ -11,7 +11,7 @@ import PathKit typealias BadgeProductionResponse = ((Result) throws -> Void) struct Factory { - let colors = ["#EE6D6D", "#A36DEE", "#4C967E", "#3B8A4B", "#CABA0E", "#E68C31", "#E11818"] + static let colors = ["#EE6D6D", "#A36DEE", "#4C967E", "#3B8A4B", "#CABA0E", "#E68C31", "#E11818"] func makeBadge(with label: String, colorHexCode: String? = nil, @@ -20,7 +20,7 @@ struct Factory { inFolder folder: Path) throws -> String { do { - let color = colorHexCode ?? colors.randomElement()! + let color = colorHexCode ?? Factory.colors.randomElement()! let tintColor = tintColorHexCode ?? "white" let folderBase = folder.absolute().description From 22e6e36365ef78efa466e9b6f599397046fad893 Mon Sep 17 00:00:00 2001 From: egor mihneivch Date: Sat, 27 Jun 2020 09:29:41 +0300 Subject: [PATCH 24/25] close default bracket --- Sources/Badgy/Commands/Badgy.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Badgy/Commands/Badgy.swift b/Sources/Badgy/Commands/Badgy.swift index 5031ea5..3718c7f 100644 --- a/Sources/Badgy/Commands/Badgy.swift +++ b/Sources/Badgy/Commands/Badgy.swift @@ -28,7 +28,7 @@ extension Badgy { or Provide a named color: 'snow' | 'snow1' | ... Complete list of named colors: https://imagemagick.org/script/color.php#color_names - (default: randomly selected from \(Factory.colors.joined(separator: " | ")) + (default: randomly selected from \(Factory.colors.joined(separator: " | "))) """) var color: ColorCode? From f32222e18a6a15f67158cf24f6ec4df3a38d97a4 Mon Sep 17 00:00:00 2001 From: Arthur Alves Date: Tue, 14 Jul 2020 12:41:59 +0000 Subject: [PATCH 25/25] formatting: SwiftFormat adjustments --- Sources/Badgy/Commands/Badgy.swift | 80 +++++++++++++------- Sources/Badgy/Helpers/ColorCode+Hex.swift | 26 ++++++- Sources/Badgy/Helpers/ColorCode+Name.swift | 2 +- Sources/Badgy/Helpers/ColorCode.swift | 26 ++++++- Sources/Badgy/Helpers/Factory+Resizer.swift | 6 +- Sources/Badgy/Helpers/Factory+Small.swift | 9 +-- Sources/Badgy/Helpers/Factory.swift | 16 ++-- Sources/Badgy/Helpers/Icon.swift | 40 +++++++--- Sources/Badgy/Helpers/IconSet.swift | 42 +++++++--- Sources/Badgy/Helpers/IconSignPipeline.swift | 66 ++++++++++------ Sources/Badgy/Loggers/Logger.swift | 2 +- Sources/Badgy/Loggers/VerboseLogger.swift | 1 - Sources/Badgy/Position.swift | 2 +- 13 files changed, 224 insertions(+), 94 deletions(-) diff --git a/Sources/Badgy/Commands/Badgy.swift b/Sources/Badgy/Commands/Badgy.swift index 3718c7f..bb114f4 100644 --- a/Sources/Badgy/Commands/Badgy.swift +++ b/Sources/Badgy/Commands/Badgy.swift @@ -1,9 +1,31 @@ // -// Badgy +// Badgy.swift +// Badgy // +// MIT License +// +// Copyright (c) 2020 Arthur Alves +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. -import Foundation import ArgumentParser +import Foundation import PathKit struct Badgy: ParsableCommand { @@ -17,12 +39,12 @@ struct Badgy: ParsableCommand { extension Badgy { struct Options: ParsableArguments { - @Argument(help :"Specify badge text") + @Argument(help: "Specify badge text") var label: String - - @Argument(help :"Specify path to icon with format .png | .jpg | .jpeg | .appiconset", transform: Icon.init(path:)) + + @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 @@ -31,7 +53,7 @@ extension Badgy { (default: randomly selected from \(Factory.colors.joined(separator: " | "))) """) var color: ColorCode? - + @Option(default: "white", help: """ Specify a valid hex color code in a case insensitive format: '#rrggbb' | '#rrggbbaa' or @@ -39,18 +61,18 @@ extension Badgy { 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 } } @@ -61,34 +83,34 @@ extension Badgy { 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 + + 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 @@ -103,40 +125,40 @@ extension Badgy { 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 { } +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 } } @@ -145,7 +167,7 @@ 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/Helpers/ColorCode+Hex.swift b/Sources/Badgy/Helpers/ColorCode+Hex.swift index efad36e..4602d29 100644 --- a/Sources/Badgy/Helpers/ColorCode+Hex.swift +++ b/Sources/Badgy/Helpers/ColorCode+Hex.swift @@ -1,6 +1,28 @@ // -// Badgy -// +// ColorCode+Hex.swift +// Badgy +// +// MIT License +// +// Copyright (c) 2020 Arthur Alves +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. import Foundation diff --git a/Sources/Badgy/Helpers/ColorCode+Name.swift b/Sources/Badgy/Helpers/ColorCode+Name.swift index da0ba43..acb7589 100644 --- a/Sources/Badgy/Helpers/ColorCode+Name.swift +++ b/Sources/Badgy/Helpers/ColorCode+Name.swift @@ -1,5 +1,5 @@ // -// Validation+ColorName.swift +// ColorCode+Name.swift // Badgy // // MIT License diff --git a/Sources/Badgy/Helpers/ColorCode.swift b/Sources/Badgy/Helpers/ColorCode.swift index cfbe5f6..d86e1de 100644 --- a/Sources/Badgy/Helpers/ColorCode.swift +++ b/Sources/Badgy/Helpers/ColorCode.swift @@ -1,9 +1,31 @@ // -// Badgy +// ColorCode.swift +// Badgy // +// MIT License +// +// Copyright (c) 2020 Arthur Alves +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. -import Foundation import ArgumentParser +import Foundation struct ColorCode { var value: String diff --git a/Sources/Badgy/Helpers/Factory+Resizer.swift b/Sources/Badgy/Helpers/Factory+Resizer.swift index d8d3937..101a8be 100644 --- a/Sources/Badgy/Helpers/Factory+Resizer.swift +++ b/Sources/Badgy/Helpers/Factory+Resizer.swift @@ -72,10 +72,10 @@ extension Factory { } } } - + func replace(_ iconSet: IconSet, with newBadgeFile: Path) { iconSet.images - .forEach { (info) in + .forEach { info in Logger.shared.logInfo("Replacing: ", item: info.image, color: .purple) do { try Task.run( @@ -87,6 +87,6 @@ extension Factory { 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 64dda98..a20ebdb 100644 --- a/Sources/Badgy/Helpers/Factory+Small.swift +++ b/Sources/Badgy/Helpers/Factory+Small.swift @@ -30,10 +30,9 @@ import SwiftCLI extension Factory { func makeSmall(with label: String, - colorHexCode: String? = nil, - tintColorHexCode: String? = nil, - inFolder folder: Path) throws -> String { - + colorHexCode: String? = nil, + tintColorHexCode: String? = nil, + inFolder folder: Path) throws -> String { let color = colorHexCode ?? Factory.colors.randomElement()! let tintColor = tintColorHexCode ?? "white" @@ -69,7 +68,7 @@ extension Factory { "convert", "\(folderBase)/top.png", "\(folderBase)/bottom.png", "-append", "\(folderBase)/badge.png" ) - + return "\(folderBase)/badge.png" } catch { print("FAILED: \(error.localizedDescription)") diff --git a/Sources/Badgy/Helpers/Factory.swift b/Sources/Badgy/Helpers/Factory.swift index 4754cd2..e100b42 100644 --- a/Sources/Badgy/Helpers/Factory.swift +++ b/Sources/Badgy/Helpers/Factory.swift @@ -37,11 +37,11 @@ struct Factory { colorHexCode: String? = nil, tintColorHexCode: String? = nil, angle: Int? = nil, - inFolder folder: Path) throws -> String { + inFolder folder: Path) throws -> String { do { let color = colorHexCode ?? Factory.colors.randomElement()! let tintColor = tintColorHexCode ?? "white" - + let folderBase = folder.absolute().description if !folder.isDirectory { try Task.run("mkdir", folderBase) @@ -82,36 +82,36 @@ struct Factory { "\(folderBase)/badge.png" ) } - + return "\(folderBase)/badge.png" } catch { print("FAILED: \(error.localizedDescription)") throw error } } - + func appendBadge(to baseIcon: String, folder: Path, label: String, position: Position?) throws -> String { let position = position ?? .bottom - + 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 } diff --git a/Sources/Badgy/Helpers/Icon.swift b/Sources/Badgy/Helpers/Icon.swift index 752074b..5c963e2 100644 --- a/Sources/Badgy/Helpers/Icon.swift +++ b/Sources/Badgy/Helpers/Icon.swift @@ -1,29 +1,51 @@ // -// Badgy -// +// Icon.swift +// Badgy +// +// MIT License +// +// Copyright (c) 2020 Arthur Alves +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. -import Foundation import ArgumentParser +import Foundation 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 @@ -38,8 +60,8 @@ enum Icon { extension Icon { var path: Path { switch self { - case .plain(let path): return path - case .set(let set): return set.path + case let .plain(path): return path + case let .set(set): return set.path } } } diff --git a/Sources/Badgy/Helpers/IconSet.swift b/Sources/Badgy/Helpers/IconSet.swift index f48da9d..d509be9 100644 --- a/Sources/Badgy/Helpers/IconSet.swift +++ b/Sources/Badgy/Helpers/IconSet.swift @@ -1,6 +1,28 @@ // -// Badgy +// IconSet.swift +// Badgy // +// MIT License +// +// Copyright (c) 2020 Arthur Alves +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. import Foundation import PathKit @@ -8,10 +30,10 @@ 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 } } @@ -22,20 +44,20 @@ extension IconSet { static let imageExtensions = Set(["png", "jpg", "jpeg"]) static func makeFromFolder(at path: Path) -> IconSet? { - guard path.isDirectory && path.extension == setExtension else { + 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) } } @@ -45,7 +67,7 @@ private extension ImageSize { guard IconSet.imageExtensions.contains(imagePath.extension ?? "") else { return nil } - + do { let result = try Task.capture( "identify", arguments: [ @@ -53,11 +75,11 @@ private extension ImageSize { 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: ", diff --git a/Sources/Badgy/Helpers/IconSignPipeline.swift b/Sources/Badgy/Helpers/IconSignPipeline.swift index c1b7c38..fd025a7 100644 --- a/Sources/Badgy/Helpers/IconSignPipeline.swift +++ b/Sources/Badgy/Helpers/IconSignPipeline.swift @@ -1,6 +1,28 @@ // -// Badgy +// IconSignPipeline.swift +// Badgy // +// MIT License +// +// Copyright (c) 2020 Arthur Alves +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. import Foundation import PathKit @@ -9,29 +31,29 @@ 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, @@ -41,34 +63,34 @@ struct IconSignPipeline { inFolder: folder ) } - + private func appendBadge(to icon: Path) throws -> Path { - let signedIconFilename = try factory.appendBadge( + 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) - } + if replace, case let .set(iconSet) = icon { + factory.replace(iconSet, with: signedIcon) + } else { + factory.resize(filename: signedIcon) + } } - + private func cleanUp() { try? factory.cleanUp(folder: folder) } @@ -77,16 +99,16 @@ struct IconSignPipeline { private extension Icon { func base() throws -> Path { switch self { - case .plain(let path): + case let .plain(path): return path - case .set(let set): + case let .set(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/Loggers/Logger.swift b/Sources/Badgy/Loggers/Logger.swift index 90761ef..4d694e1 100644 --- a/Sources/Badgy/Loggers/Logger.swift +++ b/Sources/Badgy/Loggers/Logger.swift @@ -30,7 +30,7 @@ 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 ef91663..94a8467 100644 --- a/Sources/Badgy/Loggers/VerboseLogger.swift +++ b/Sources/Badgy/Loggers/VerboseLogger.swift @@ -64,7 +64,6 @@ extension Date { } extension VerboseLogger { - 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 3cb8d05..10d3443 100644 --- a/Sources/Badgy/Position.swift +++ b/Sources/Badgy/Position.swift @@ -58,6 +58,6 @@ enum Position: String, CaseIterable { extension Sequence where Element == Position { func formatted() -> String { - map { $0.rawValue}.joined(separator: " | ") + map { $0.rawValue }.joined(separator: " | ") } }