├── .gitattributes ├── .github └── sasha-logo.png ├── .gitignore ├── .sasha ├── file_templates │ ├── android-template.sketch │ └── ios-template.sketch └── project.sasha ├── .swiftlint.yml ├── .travis.yml ├── Config.xcconfig ├── LICENSE ├── Makefile ├── Package.pins ├── Package.resolved ├── Package.swift ├── README.md └── Sources ├── Sasha └── main.swift └── SashaCore ├── Commands ├── Command.swift ├── CommandRegistry.swift ├── IconsCommand.swift └── ProjectCommand.swift ├── Models ├── AndroidIcon.swift ├── AppIconSet.swift ├── Asset.swift ├── ComplicationSet.swift ├── Icon.swift ├── IconFactory.swift ├── IconRepresentable.swift ├── Info.swift └── Platform.swift ├── Services ├── FolderService.swift └── IconService.swift └── Tasks ├── IconsTask.swift └── ProjectTask.swift /.gitattributes: -------------------------------------------------------------------------------- 1 | *.yml linguist-generated 2 | *.sh linguist-generated 3 | *.podspec linguist-generated 4 | Makefile linguist-generated 5 | 6 | *.md linguist-documentation 7 | -------------------------------------------------------------------------------- /.github/sasha-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artemnovichkov/sasha/593e87a837aef61e7e86e12ab980fb16397a1009/.github/sasha-logo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | -------------------------------------------------------------------------------- /.sasha/file_templates/android-template.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artemnovichkov/sasha/593e87a837aef61e7e86e12ab980fb16397a1009/.sasha/file_templates/android-template.sketch -------------------------------------------------------------------------------- /.sasha/file_templates/ios-template.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artemnovichkov/sasha/593e87a837aef61e7e86e12ab980fb16397a1009/.sasha/file_templates/ios-template.sketch -------------------------------------------------------------------------------- /.sasha/project.sasha: -------------------------------------------------------------------------------- 1 | iOS 2 | -UI 3 | --old 4 | --png 5 | -UX 6 | --old 7 | --png 8 | Android 9 | -UI 10 | --old 11 | --png 12 | -UX 13 | --old 14 | --png 15 | references 16 | -main_screens 17 | -menu 18 | -cards 19 | -another_case 20 | stuff 21 | -logos 22 | -icons 23 | -patterns 24 | -stocks 25 | -source -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | # paths to ignore during linting. Takes precedence over `included`. 2 | 3 | excluded: 4 | - .build/ 5 | 6 | # Rules 7 | 8 | opt_in_rules: 9 | - conditional_returns_on_newline 10 | - fatal_error_message 11 | - overridden_super_call 12 | 13 | force_cast: error 14 | force_try: error 15 | 16 | colon: 17 | apply_to_dictionaries: false 18 | 19 | identifier_name: 20 | excluded: 21 | - id 22 | - URL 23 | 24 | statement_position: 25 | statement_mode: uncuddled_else 26 | 27 | # Constants 28 | 29 | file_length: 30 | warning: 1000 31 | error: 1500 32 | 33 | type_body_length: 34 | warning: 800 35 | error: 1000 36 | 37 | line_length: 140 38 | 39 | function_parameter_count: 4 40 | 41 | type_name: 42 | min_length: 2 # only warning 43 | 44 | # Custom rules 45 | 46 | custom_rules: 47 | accessors_and_observers_on_newline: 48 | name: "Property accessors and observers on newline" 49 | regex: "^\\s*(get|set|didSet|willSet)[\\h\\S]*\\{[\\h\\S]+$" 50 | message: "Property accessors and observers should always start with a newline." 51 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: osx 2 | language: swift 3 | osx_image: xcode10 4 | script: 5 | - make build 6 | 7 | -------------------------------------------------------------------------------- /Config.xcconfig: -------------------------------------------------------------------------------- 1 | OTHER_LDFLAGS = -framework CoreImage 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Artem Novichkov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BINARY?=sasha 2 | BUILD_FOLDER?=.build 3 | OS?=sierra 4 | PREFIX?=/usr/local 5 | PROJECT?=Sasha 6 | RELEASE_BINARY_FOLDER?=$(BUILD_FOLDER)/release/$(PROJECT) 7 | VERSION?=2.4 8 | 9 | build: 10 | swift build --disable-sandbox -c release -Xswiftc -static-stdlib -Xswiftc "-target" -Xswiftc "x86_64-apple-macosx10.12" 11 | 12 | test: 13 | swift test 14 | 15 | clean: 16 | swift package clean 17 | rm -rf $(BUILD_FOLDER) $(PROJECT).xcodeproj 18 | 19 | xcodeproj: 20 | swift package generate-xcodeproj --xcconfig-overrides Config.xcconfig 21 | 22 | install: build 23 | mkdir -p $(PREFIX)/bin 24 | cp -f $(RELEASE_BINARY_FOLDER) $(PREFIX)/bin/$(BINARY) 25 | cp -a .sasha ~ 26 | 27 | bottle: clean build 28 | mkdir -p $(BINARY)/$(VERSION)/bin 29 | cp README.md $(BINARY)/$(VERSION)/README.md 30 | cp LICENSE $(BINARY)/$(VERSION)/LICENSE 31 | cp -f $(RELEASE_BINARY_FOLDER) $(BINARY)/$(VERSION)/bin/$(BINARY) 32 | tar cfvz $(BINARY)-$(VERSION).$(OS).bottle.tar.gz --exclude='*/.*' $(BINARY) 33 | shasum -a 256 $(BINARY)-$(VERSION).$(OS).bottle.tar.gz 34 | rm -rf $(BINARY) 35 | 36 | sha256: 37 | wget https://github.com/artemnovichkov/$(PROJECT)/archive/$(VERSION).tar.gz -O $(PROJECT)-$(VERSION).tar.gz 38 | shasum -a 256 $(PROJECT)-$(VERSION).tar.gz 39 | rm $(PROJECT)-$(VERSION).tar.gz 40 | -------------------------------------------------------------------------------- /Package.pins: -------------------------------------------------------------------------------- 1 | { 2 | "autoPin": true, 3 | "pins": [ 4 | { 5 | "package": "Files", 6 | "reason": null, 7 | "repositoryURL": "https://github.com/johnsundell/files.git", 8 | "version": "1.11.0" 9 | }, 10 | { 11 | "package": "Swiftline", 12 | "reason": null, 13 | "repositoryURL": "https://github.com/nsomar/Swiftline.git", 14 | "version": "0.5.0" 15 | } 16 | ], 17 | "version": 1 18 | } -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "Files", 6 | "repositoryURL": "https://github.com/johnsundell/files.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "6171f9e9d8619678da22355560f3dbed6368058d", 10 | "version": "2.0.1" 11 | } 12 | }, 13 | { 14 | "package": "SwiftPM", 15 | "repositoryURL": "https://github.com/apple/swift-package-manager.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "90abb3c4c9415afee34291e66e655c58a1902784", 19 | "version": "0.1.0" 20 | } 21 | } 22 | ] 23 | }, 24 | "version": 1 25 | } 26 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.0 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "Sasha", 7 | dependencies: [ 8 | .package(url: "https://github.com/johnsundell/files.git", from: "2.0.0"), 9 | .package(url: "https://github.com/apple/swift-package-manager.git", from: "0.1.0") 10 | ], 11 | targets: [ 12 | .target( 13 | name: "Sasha", 14 | dependencies: ["SashaCore"]), 15 | .target( 16 | name: "SashaCore", 17 | dependencies: ["Files", "Utility"]) 18 | ] 19 | ) 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | sasha 3 | 4 | 5 | 6 | Swift 4.2 7 | 8 | Make 9 | 10 | Swift Package Manager 11 | 12 | 13 | Marathon 14 | 15 |

16 | Sasha is an easy-to-use CLI app for routine designer tasks. 17 | 18 |

19 | FeaturesUsingInstallingAuthorLicense 20 |

21 | 22 | ## Features 23 | - Icon slicing for different platforms: 24 | - iOS 25 | - watchOS including 40 and 44 mm versions 26 | - watchOS Complications 27 | - macOS 28 | - Carplay 29 | - Android 30 | - Android Wear 31 | - Project folder tree generation 32 | 33 | ## Using 34 | 35 | ### Icons 36 | 37 | Sasha has two main commands - `icons` and `project`. 38 | 39 | ```bash 40 | $ sasha icons --platform iOS --name icon.png 41 | ``` 42 | Sasha generates icons in needed resolutions as well. For Apple platforms Sasha generates `AppIcon.appiconset`, which iOS developer can drag and drop right into `Images.xcassets` without manual icon sorting 👨🏻‍💻👍. 43 | 44 | There is a [service](https://github.com/artemnovichkov/sasha/issues/5#issuecomment-358310264) for Sasha. Right click on an original icon, select `Services > Sasha, make me iOS icons`. 45 | 46 | ### Project generation 47 | ```bash 48 | $ sasha project --name ProjectName 49 | ``` 50 | Sasha generates folder tree with name passed via `--name` option. By default Sasha uses this project structure: 51 | 52 | ``` 53 | iOS 54 | -UI 55 | --old 56 | --png 57 | -UX 58 | --old 59 | --png 60 | Android 61 | -UI 62 | --old 63 | --png 64 | -UX 65 | --old 66 | --png 67 | references 68 | -main_screens 69 | -menu 70 | -cards 71 | -another_case 72 | stuff 73 | -logos 74 | -icons 75 | -patterns 76 | -stocks 77 | -source 78 | ``` 79 | To change it, open `~/.sasha/project.sasha` file in your favourite text editor and make custom project structure. 80 | 81 | ## Installing 82 | 83 | ### Homebrew (recommended): 84 | 85 | ```bash 86 | $ brew install artemnovichkov/projects/sasha 87 | ``` 88 | 89 | ### Make: 90 | 91 | ```bash 92 | $ git clone https://github.com/artemnovichkov/sasha.git 93 | $ cd sasha 94 | $ make 95 | ``` 96 | 97 | ### Swift Package Manager: 98 | 99 | ```bash 100 | $ git clone https://github.com/artemnovichkov/sasha.git 101 | $ cd sasha 102 | $ make build 103 | $ cp -f .build/release/sasha /usr/local/bin/sasha 104 | $ cp -r .sasha ~ 105 | ``` 106 | 107 | ### Marathon: 108 | 109 | - Install [Marathon](https://github.com/johnsundell/marathon#installing). 110 | - Add Files using `$ marathon add https://github.com/artemnovichkov/sasha.git`. 111 | - Run your script using `$ marathon run `. 112 | 113 | ## Author 114 | 115 | Artem Novichkov, novichkoff93@gmail.com 116 | 117 | [![Get help on Codementor](https://cdn.codementor.io/badges/get_help_github.svg)](https://www.codementor.io/artemnovichkov?utm_source=github&utm_medium=button&utm_term=artemnovichkov&utm_campaign=github) 118 | 119 | 120 | ## License 121 | 122 | Sasha is available under the MIT license. See the LICENSE file for more info. 123 | 124 | -------------------------------------------------------------------------------- /Sources/Sasha/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2017 Artem Novichkov. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | import SashaCore 7 | 8 | var registry = CommandRegistry(usage: " ", 9 | overview: "👨‍💼 Reduce daily designer routine with sasha") 10 | registry.register(ProjectCommand.self, IconsCommand.self) 11 | registry.run() 12 | -------------------------------------------------------------------------------- /Sources/SashaCore/Commands/Command.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2017 Artem Novichkov. All rights reserved. 3 | // 4 | 5 | import Utility 6 | 7 | public protocol Command { 8 | var command: String { get } 9 | var overview: String { get } 10 | 11 | init(parser: ArgumentParser) 12 | func run(with arguments: ArgumentParser.Result) throws 13 | } 14 | -------------------------------------------------------------------------------- /Sources/SashaCore/Commands/CommandRegistry.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2017 Artem Novichkov. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | import Utility 7 | import Basic 8 | 9 | public struct CommandRegistry { 10 | 11 | private let parser: ArgumentParser 12 | private var commands: [Command] = [] 13 | 14 | public init(usage: String, overview: String) { 15 | parser = ArgumentParser(usage: usage, overview: overview) 16 | } 17 | 18 | public mutating func register(_ command: Command.Type) { 19 | commands.append(command.init(parser: parser)) 20 | } 21 | 22 | public mutating func register(_ commands: Command.Type...) { 23 | commands.forEach { register($0) } 24 | } 25 | 26 | public func run() { 27 | do { 28 | let arguments = try parse() 29 | try process(arguments) 30 | } 31 | catch { 32 | print(error) 33 | } 34 | } 35 | 36 | private func parse() throws -> ArgumentParser.Result { 37 | let arguments = Array(CommandLine.arguments.dropFirst()) 38 | return try parser.parse(arguments) 39 | } 40 | 41 | private func process(_ arguments: ArgumentParser.Result) throws { 42 | guard let subparser = arguments.subparser(parser), 43 | let command = commands.first(where: { $0.command == subparser }) else { 44 | parser.printUsage(on: stdoutStream) 45 | return 46 | } 47 | try command.run(with: arguments) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/SashaCore/Commands/IconsCommand.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2017 Artem Novichkov. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | import Utility 7 | 8 | public struct IconsCommand: Command { 9 | 10 | enum Error: Swift.Error { 11 | case missingOptions 12 | } 13 | 14 | public let command = "icons" 15 | public let overview = "Generates an icon set for selected platform." 16 | private let name: OptionArgument 17 | private let output: OptionArgument 18 | private let platform: OptionArgument 19 | private let idioms: OptionArgument<[Icon.Idiom]> 20 | 21 | public init(parser: ArgumentParser) { 22 | let subparser = parser.add(subparser: command, overview: overview) 23 | name = subparser.add(option: "--name", 24 | shortName: "-n", 25 | kind: String.self, 26 | usage: "Filename of original image. You can use full path of name of image file in current folder.") 27 | output = subparser.add(option: "--output", 28 | shortName: "-o", 29 | kind: String.self, 30 | usage: "Output path for generated icons.") 31 | platform = subparser.add(option: "--platform", 32 | shortName: "-p", 33 | kind: Platform.self, 34 | usage: "Platform for icon.") 35 | idioms = subparser.add(option: "--idioms", 36 | shortName: "-i", 37 | kind: [Icon.Idiom].self, 38 | usage: "A set of additional idioms.") 39 | } 40 | 41 | public func run(with arguments: ArgumentParser.Result) throws { 42 | guard let platform = arguments.get(platform), 43 | let fileName = arguments.get(name) else { 44 | throw Error.missingOptions 45 | } 46 | let idioms = arguments.get(self.idioms) 47 | let output = arguments.get(self.output) 48 | try IconsTask().generateIcons(for: platform, idioms: idioms, path: fileName, output: output) 49 | } 50 | } 51 | 52 | extension IconsCommand.Error: CustomStringConvertible { 53 | 54 | var description: String { 55 | switch self { 56 | case .missingOptions: 57 | return "Missing --platform or --name options." 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Sources/SashaCore/Commands/ProjectCommand.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2017 Artem Novichkov. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | import Utility 7 | 8 | public struct ProjectCommand: Command { 9 | 10 | enum Error: Swift.Error { 11 | case missingOptions 12 | } 13 | 14 | public let command = "project" 15 | public let overview = "Generates a project folders tree according to project.sasha file." 16 | private let projectName: OptionArgument 17 | 18 | public init(parser: ArgumentParser) { 19 | let subparser = parser.add(subparser: command, overview: overview) 20 | projectName = subparser.add(option: "--name", 21 | shortName: "-n", 22 | kind: String.self, 23 | usage: "Name of the project.") 24 | } 25 | 26 | public func run(with arguments: ArgumentParser.Result) throws { 27 | guard let projectName = arguments.get(projectName) else { 28 | throw Error.missingOptions 29 | } 30 | try ProjectTask().createProject(withName: projectName) 31 | } 32 | } 33 | 34 | extension ProjectCommand.Error: CustomStringConvertible { 35 | 36 | var description: String { 37 | switch self { 38 | case .missingOptions: 39 | return "Missing --name option." 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/SashaCore/Models/AndroidIcon.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2017 Artem Novichkov. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | 7 | final class AndroidIcon { 8 | 9 | let size: Float 10 | let name: String 11 | 12 | init(size: Float, name: String) { 13 | self.size = size 14 | self.name = name 15 | } 16 | } 17 | 18 | extension AndroidIcon: IconRepresentable { 19 | 20 | var iconSize: Float { 21 | return size 22 | } 23 | 24 | var iconName: String { 25 | return name 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/SashaCore/Models/AppIconSet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2017 Artem Novichkov. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | 7 | final class AppIconSet: Codable { 8 | 9 | enum CodingKeys: String, CodingKey { 10 | case icons = "images" 11 | case info 12 | } 13 | 14 | let icons: [Icon] 15 | let info: Info 16 | 17 | init(icons: [Icon], info: Info = .default) { 18 | self.icons = icons 19 | self.info = info 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/SashaCore/Models/Asset.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2017 Artem Novichkov. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | 7 | final class Asset: Codable { 8 | 9 | let idiom: Icon.Idiom 10 | let filename: String 11 | let role: Icon.Role? 12 | 13 | init(idiom: Icon.Idiom, 14 | filename: String, 15 | role: Icon.Role? = nil) { 16 | self.idiom = idiom 17 | self.filename = filename 18 | self.role = role 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/SashaCore/Models/ComplicationSet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2017 Artem Novichkov. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | 7 | final class ComplicationSet: Codable { 8 | 9 | let assets: [Asset] 10 | let info: Info 11 | 12 | init(assets: [Asset], info: Info = .default) { 13 | self.assets = assets 14 | self.info = info 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/SashaCore/Models/Icon.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2017 Artem Novichkov. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | import Utility 7 | 8 | final class Icon: Codable { 9 | 10 | enum CodingKeys: String, CodingKey { 11 | case idiom 12 | case size 13 | case filename 14 | case screenWidth = "screen-width" 15 | case scale 16 | case role 17 | case subtype 18 | } 19 | 20 | /// Idioms of icons. 21 | enum Idiom: String, Codable, ArgumentKind { 22 | 23 | case iphone 24 | case ipad 25 | case iosMarketing = "ios-marketing" 26 | case carplay = "car" 27 | case mac 28 | case watch 29 | case watchMarketing = "watch-marketing" 30 | case complicationCircular = "circular" 31 | case complicationExtraLarge = "extra large" 32 | case complicationGraphicBezel = "graphic bezel" 33 | case complicationGraphicCircular = "graphic circular" 34 | case complicationGraphicCorner = "graphic corner" 35 | case complicationGraphicLargeRectangular = "graphic large rectangular" 36 | case complicationModular = "modular" 37 | case complicationUtilitarian = "utilitarian" 38 | 39 | static let completion: ShellCompletion = .none 40 | 41 | static let complications: [Idiom] = [.complicationCircular, 42 | .complicationExtraLarge, 43 | .complicationGraphicBezel, 44 | .complicationGraphicCircular, 45 | .complicationGraphicCorner, 46 | .complicationGraphicLargeRectangular, 47 | .complicationModular, 48 | .complicationUtilitarian] 49 | 50 | init(argument: String) throws { 51 | guard let idiom = Idiom(rawValue: argument) else { 52 | throw Error.invalidIdiom 53 | } 54 | self = idiom 55 | } 56 | } 57 | 58 | enum Error: Swift.Error { 59 | case invalidIdiom 60 | } 61 | 62 | /// Roles of watchOS icons. 63 | enum Role: String, Codable { 64 | case notificationCenter 65 | case companionSettings 66 | case appLauncher 67 | case quickLook 68 | case circular 69 | case extraLarge = "extra-large" 70 | case graphicBezel = "graphic-bezel" 71 | case graphicCircular = "graphic-circular" 72 | case graphicCorner = "graphic-corner" 73 | case graphicLargeRectangular = "graphic-large-rectangular" 74 | case modular 75 | case utilitarian 76 | } 77 | 78 | /// Subtypes of watchOS icons with `notificationCenter` role. 79 | enum Subtype: String, Codable { 80 | case mm38 = "38mm", mm40 = "40mm", mm42 = "42mm", mm44 = "44mm" 81 | } 82 | 83 | let idiom: Idiom 84 | let size: Float 85 | let filename: String 86 | let screenWidth: String? 87 | let scale: Float 88 | let role: Role? 89 | let subtype: Subtype? 90 | 91 | init(idiom: Idiom, 92 | size: Float, 93 | filename: String, 94 | screenWidth: String? = nil, 95 | scale: Float, 96 | role: Role? = nil, 97 | subtype: Subtype? = nil) { 98 | self.idiom = idiom 99 | self.size = size 100 | let sizeString = String(format: "%g", size) 101 | let scaleString = String(format: "%g", scale) 102 | self.filename = filename + "-\(sizeString)x\(sizeString)@\(scaleString)x.png" 103 | self.screenWidth = screenWidth 104 | self.scale = scale 105 | self.role = role 106 | self.subtype = subtype 107 | } 108 | 109 | func encode(to encoder: Encoder) throws { 110 | var container = encoder.container(keyedBy: CodingKeys.self) 111 | let isComplication = Idiom.complications.contains(idiom) 112 | if isComplication { 113 | try container.encode(Idiom.watch, forKey: .idiom) 114 | } 115 | else { 116 | try container.encode(idiom, forKey: .idiom) 117 | } 118 | let scaleString = String(format: "%g", scale) 119 | if !isComplication { 120 | let sizeString = String(format: "%g", size) 121 | try container.encode("\(sizeString)x\(sizeString)", forKey: .size) 122 | } 123 | try container.encode(filename, forKey: .filename) 124 | if isComplication { 125 | try container.encodeIfPresent(screenWidth, forKey: .screenWidth) 126 | } 127 | try container.encode("\(scaleString)x", forKey: .scale) 128 | try container.encodeIfPresent(role, forKey: .role) 129 | try container.encodeIfPresent(subtype, forKey: .subtype) 130 | } 131 | } 132 | 133 | extension Icon: IconRepresentable { 134 | 135 | var iconSize: Float { 136 | return size * scale 137 | } 138 | 139 | var iconName: String { 140 | return filename 141 | } 142 | } 143 | 144 | extension Icon.Error: CustomStringConvertible { 145 | 146 | var description: String { 147 | switch self { 148 | case .invalidIdiom: 149 | return "Invalid idiom." 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /Sources/SashaCore/Models/IconFactory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2017 Artem Novichkov. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | 7 | final class IconFactory { 8 | 9 | /// Makes a set of icons. The set contains an icons only for selected idioms. 10 | /// 11 | /// - Parameters: 12 | /// - name: The name of generated icons. 13 | /// - idioms: The idioms of icons. 14 | /// - Returns: The set of icons. 15 | func makeSet(withName name: String, idioms: [Icon.Idiom]) -> AppIconSet { 16 | let icons = idioms.flatMap { idiom in 17 | makeIcons(forName: name, idiom: idiom) 18 | } 19 | return AppIconSet(icons: icons) 20 | } 21 | 22 | func makeComplicationSet() -> ComplicationSet { 23 | let assets = [Asset(idiom: .watch, filename: "Circular.imageset", role: .circular), 24 | Asset(idiom: .watch, filename: "Extra Large.imageset", role: .extraLarge), 25 | Asset(idiom: .watch, filename: "Graphic Bezel.imageset", role: .graphicBezel), 26 | Asset(idiom: .watch, filename: "Graphic Circular.imageset", role: .graphicCircular), 27 | Asset(idiom: .watch, filename: "Graphic Corner.imageset", role: .graphicCorner), 28 | Asset(idiom: .watch, filename: "Graphic Large Rectangular.imageset", role: .graphicLargeRectangular), 29 | Asset(idiom: .watch, filename: "Modular.imageset", role: .modular), 30 | Asset(idiom: .watch, filename: "Utilitarian.imageset", role: .utilitarian)] 31 | return ComplicationSet(assets: assets) 32 | } 33 | 34 | /// Makes an array of icons for Android. The names of icons contain full paths including needed folders. 35 | /// 36 | /// - Returns: The array of icons. 37 | func makeAndroidIcons() -> [AndroidIcon] { 38 | return [AndroidIcon(size: 72, name: "mipmap-hdpi/ic_launcher.png"), 39 | AndroidIcon(size: 36, name: "mipmap-ldpi/ic_launcher.png"), 40 | AndroidIcon(size: 48, name: "mipmap-mdpi/ic_launcher.png"), 41 | AndroidIcon(size: 96, name: "mipmap-xhdpi/ic_launcher.png"), 42 | AndroidIcon(size: 144, name: "mipmap-xxhdpi/ic_launcher.png"), 43 | AndroidIcon(size: 192, name: "mipmap-xxxhdpi/ic_launcher.png"), 44 | AndroidIcon(size: 512, name: "playstore-icon.png")] 45 | } 46 | 47 | /// Makes an array of icons for Android Wear. The names of icons contain full paths including needed folders. 48 | /// 49 | /// - Returns: The array of icons. 50 | func makeAndroidWearIcons() -> [AndroidIcon] { 51 | return [AndroidIcon(size: 72, name: "mipmap-hdpi/ic_launcher.png"), 52 | AndroidIcon(size: 48, name: "mipmap-mdpi/ic_launcher.png"), 53 | AndroidIcon(size: 96, name: "mipmap-xhdpi/ic_launcher.png"), 54 | AndroidIcon(size: 144, name: "mipmap-xxhdpi/ic_launcher.png")] 55 | } 56 | 57 | /// Makes a set of icons. The set contains an icons only for selected idiom. 58 | /// 59 | /// - Parameters: 60 | /// - name: The name of generated icons. 61 | /// - idiom: The idiom of icons. 62 | /// - Returns: The array of generated icons. 63 | private func makeIcons(forName name: String, idiom: Icon.Idiom) -> [Icon] { 64 | switch idiom { 65 | case .iphone: 66 | return [Icon(idiom: idiom, size: 20, filename: name, scale: 2), 67 | Icon(idiom: idiom, size: 20, filename: name, scale: 3), 68 | Icon(idiom: idiom, size: 29, filename: name, scale: 2), 69 | Icon(idiom: idiom, size: 29, filename: name, scale: 3), 70 | Icon(idiom: idiom, size: 40, filename: name, scale: 2), 71 | Icon(idiom: idiom, size: 40, filename: name, scale: 3), 72 | Icon(idiom: idiom, size: 60, filename: name, scale: 2), 73 | Icon(idiom: idiom, size: 60, filename: name, scale: 3)] 74 | case .ipad: 75 | return [Icon(idiom: idiom, size: 20, filename: name, scale: 1), 76 | Icon(idiom: idiom, size: 20, filename: name, scale: 2), 77 | Icon(idiom: idiom, size: 29, filename: name, scale: 1), 78 | Icon(idiom: idiom, size: 29, filename: name, scale: 2), 79 | Icon(idiom: idiom, size: 40, filename: name, scale: 1), 80 | Icon(idiom: idiom, size: 40, filename: name, scale: 2), 81 | Icon(idiom: idiom, size: 76, filename: name, scale: 1), 82 | Icon(idiom: idiom, size: 76, filename: name, scale: 2), 83 | Icon(idiom: idiom, size: 83.5, filename: name, scale: 2)] 84 | case .iosMarketing: 85 | return [Icon(idiom: idiom, size: 1024, filename: name, scale: 1)] 86 | case .carplay: 87 | return [Icon(idiom: idiom, size: 60, filename: name, scale: 2), 88 | Icon(idiom: idiom, size: 60, filename: name, scale: 3)] 89 | case .mac: 90 | return [Icon(idiom: idiom, size: 16, filename: name, scale: 1), 91 | Icon(idiom: idiom,size: 16, filename: name, scale: 2), 92 | Icon(idiom: idiom,size: 32, filename: name, scale: 1), 93 | Icon(idiom: idiom,size: 32, filename: name, scale: 2), 94 | Icon(idiom: idiom,size: 128, filename: name, scale: 1), 95 | Icon(idiom: idiom,size: 128, filename: name, scale: 2), 96 | Icon(idiom: idiom,size: 256, filename: name, scale: 1), 97 | Icon(idiom: idiom,size: 256, filename: name, scale: 2), 98 | Icon(idiom: idiom,size: 512, filename: name, scale: 1), 99 | Icon(idiom: idiom,size: 512, filename: name, scale: 2)] 100 | case .watch: 101 | return [Icon(idiom: idiom, size: 24, filename: name, scale: 2, role: .notificationCenter, subtype: .mm38), 102 | Icon(idiom: idiom, size: 27.5, filename: name, scale: 2, role: .notificationCenter, subtype: .mm42), 103 | Icon(idiom: idiom, size: 29, filename: name + "-Companion", scale: 2, role: .companionSettings), 104 | Icon(idiom: idiom, size: 29, filename: name + "-Companion", scale: 3, role: .companionSettings), 105 | Icon(idiom: idiom, size: 40, filename: name, scale: 2, role: .appLauncher, subtype: .mm38), 106 | Icon(idiom: idiom, size: 44, filename: name, scale: 2, role: .appLauncher, subtype: .mm40), 107 | Icon(idiom: idiom, size: 50, filename: name, scale: 2, role: .appLauncher, subtype: .mm44), 108 | Icon(idiom: idiom, size: 86, filename: name, scale: 2, role: .quickLook, subtype: .mm38), 109 | Icon(idiom: idiom, size: 98, filename: name, scale: 2, role: .quickLook, subtype: .mm42), 110 | Icon(idiom: idiom, size: 108, filename: name, scale: 2, role: .quickLook, subtype: .mm44)] 111 | case .watchMarketing: 112 | return [Icon(idiom: idiom, size: 1024, filename: name + "-AppStore", scale: 1)] 113 | case .complicationCircular: 114 | return [Icon(idiom: idiom, size: 16, filename: name + "-\(Icon.Subtype.mm38.rawValue)", screenWidth: "<=145", scale: 2), 115 | Icon(idiom: idiom, size: 18, filename: name + "-\(Icon.Subtype.mm42.rawValue)", screenWidth: ">161", scale: 2), 116 | Icon(idiom: idiom, size: 18, filename: name + "-\(Icon.Subtype.mm40.rawValue)", screenWidth: ">145", scale: 2), 117 | Icon(idiom: idiom, size: 18, filename: name + "-\(Icon.Subtype.mm44.rawValue)", screenWidth: ">183", scale: 2)] 118 | case .complicationExtraLarge: 119 | return [Icon(idiom: idiom, size: 91, filename: name + "-\(Icon.Subtype.mm38.rawValue)", screenWidth: "<=145", scale: 2), 120 | Icon(idiom: idiom, size: 101.5, filename: name + "-\(Icon.Subtype.mm42.rawValue)", screenWidth: ">161", scale: 2), 121 | Icon(idiom: idiom, size: 101.5, filename: name + "-\(Icon.Subtype.mm40.rawValue)", screenWidth: ">145", scale: 2), 122 | Icon(idiom: idiom, size: 112, filename: name + "-\(Icon.Subtype.mm44.rawValue)", screenWidth: ">183", scale: 2)] 123 | case .complicationGraphicBezel: 124 | return [Icon(idiom: idiom, size: 42, filename: name + "-\(Icon.Subtype.mm38.rawValue)", screenWidth: "<=145", scale: 2), 125 | Icon(idiom: idiom, size: 42, filename: name + "-\(Icon.Subtype.mm42.rawValue)", screenWidth: ">161", scale: 2), 126 | Icon(idiom: idiom, size: 42, filename: name + "-\(Icon.Subtype.mm40.rawValue)", screenWidth: ">145", scale: 2), 127 | Icon(idiom: idiom, size: 47, filename: name + "-\(Icon.Subtype.mm44.rawValue)", screenWidth: ">183", scale: 2)] 128 | case .complicationGraphicCircular: 129 | return [Icon(idiom: idiom, size: 42, filename: name + "-\(Icon.Subtype.mm38.rawValue)", screenWidth: "<=145", scale: 2), 130 | Icon(idiom: idiom, size: 42, filename: name + "-\(Icon.Subtype.mm42.rawValue)", screenWidth: ">161", scale: 2), 131 | Icon(idiom: idiom, size: 42, filename: name + "-\(Icon.Subtype.mm40.rawValue)", screenWidth: ">145", scale: 2), 132 | Icon(idiom: idiom, size: 47, filename: name + "-\(Icon.Subtype.mm44.rawValue)", screenWidth: ">183", scale: 2)] 133 | case .complicationGraphicCorner: 134 | return [Icon(idiom: idiom, size: 20, filename: name + "-\(Icon.Subtype.mm38.rawValue)", screenWidth: "<=145", scale: 2), 135 | Icon(idiom: idiom, size: 20, filename: name + "-\(Icon.Subtype.mm42.rawValue)", screenWidth: ">161", scale: 2), 136 | Icon(idiom: idiom, size: 20, filename: name + "-\(Icon.Subtype.mm40.rawValue)", screenWidth: ">145", scale: 2), 137 | Icon(idiom: idiom, size: 22, filename: name + "-\(Icon.Subtype.mm44.rawValue)", screenWidth: ">183", scale: 2)] 138 | case .complicationGraphicLargeRectangular: 139 | return [] 140 | case .complicationModular: 141 | return [Icon(idiom: idiom, size: 26, filename: name + "-\(Icon.Subtype.mm38.rawValue)", screenWidth: "<=145", scale: 2), 142 | Icon(idiom: idiom, size: 29, filename: name + "-\(Icon.Subtype.mm42.rawValue)", screenWidth: ">161", scale: 2), 143 | Icon(idiom: idiom, size: 29, filename: name + "-\(Icon.Subtype.mm40.rawValue)", screenWidth: ">145", scale: 2), 144 | Icon(idiom: idiom, size: 32, filename: name + "-\(Icon.Subtype.mm44.rawValue)", screenWidth: ">183", scale: 2)] 145 | case .complicationUtilitarian: 146 | return [Icon(idiom: idiom, size: 20, filename: name + "-\(Icon.Subtype.mm38.rawValue)", screenWidth: "<=145", scale: 2), 147 | Icon(idiom: idiom, size: 22, filename: name + "-\(Icon.Subtype.mm42.rawValue)", screenWidth: ">161", scale: 2), 148 | Icon(idiom: idiom, size: 22, filename: name + "-\(Icon.Subtype.mm40.rawValue)", screenWidth: ">145", scale: 2), 149 | Icon(idiom: idiom, size: 25, filename: name + "-\(Icon.Subtype.mm44.rawValue)", screenWidth: ">183", scale: 2)] 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /Sources/SashaCore/Models/IconRepresentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2017 Artem Novichkov. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | 7 | /// A protocol for iOS and Android icons representation. 8 | protocol IconRepresentable { 9 | 10 | var iconSize: Float { get } 11 | var iconName: String { get } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/SashaCore/Models/Info.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2017 Artem Novichkov. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | 7 | struct Info: Codable { 8 | 9 | let version: Int 10 | let author: String 11 | 12 | static let `default` = Info(version: 1, author: "sasha") 13 | } 14 | -------------------------------------------------------------------------------- /Sources/SashaCore/Models/Platform.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2017 Artem Novichkov. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | import Utility 7 | 8 | enum Platform: String, ArgumentKind, CaseIterable { 9 | 10 | enum Error: Swift.Error { 11 | case invalid 12 | } 13 | 14 | static let completion: ShellCompletion = .values(Platform.allCases.map { ($0.rawValue, $0.rawValue) }) 15 | 16 | init(argument: String) throws { 17 | guard let platform = Platform(rawValue: argument.lowercased()) else { 18 | throw Error.invalid 19 | } 20 | self = platform 21 | } 22 | 23 | case iOS = "ios" 24 | case macOS = "macos" 25 | case watchOS = "watchos" 26 | case watchOSComplication = "complication" 27 | case android 28 | case androidWear = "androidwear" 29 | } 30 | 31 | extension Platform.Error: CustomStringConvertible { 32 | 33 | var description: String { 34 | switch self { 35 | case .invalid: 36 | return "Unsupported platform." 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/SashaCore/Services/FolderService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2017 Artem Novichkov. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | 7 | final class FolderService { 8 | 9 | enum Keys { 10 | static let slash = "/" 11 | } 12 | 13 | private class Folder { 14 | let name: String 15 | let level: Int 16 | var subfolders = [Folder]() 17 | 18 | init(name: String, level: Int) { 19 | self.name = name 20 | self.level = level 21 | } 22 | 23 | func add(_ folder: Folder) { 24 | if folder.level == level + 1 { 25 | subfolders.append(folder) 26 | return 27 | } 28 | subfolders.last?.add(folder) 29 | } 30 | } 31 | 32 | /// Generates the paths for project folders from configuration string. 33 | /// 34 | /// - Parameter string: The string from `sasha.project` file. 35 | /// - Returns: The array of full paths. 36 | func paths(fromString string: String) -> [String] { 37 | let components = string.components(separatedBy: "\n") 38 | let allFolders = components.map { component -> Folder in 39 | let name = component.replacingOccurrences(of: "-", with: "") 40 | let level = component.filter { $0 == "-" } 41 | return Folder(name: name, level: level.count) 42 | } 43 | var paths = [String]() 44 | var currentLevel = 0 45 | var currentPath: String? 46 | allFolders.forEach { folder in 47 | if let path = currentPath { 48 | if folder.level > currentLevel { 49 | currentPath = path + Keys.slash + folder.name 50 | } 51 | else { 52 | var components = path.components(separatedBy: Keys.slash) 53 | var subComponents = components[0.. CIImage { 143 | if let rep = NSImageRep(contentsOf: imageURL), !rep.isOpaque { 144 | print("\u{001B}[0;33mImage has transparency... filling regions...\u{001B}[0;0m") 145 | } 146 | guard let image = CIImage(contentsOf: imageURL) else { 147 | throw Error.imageCreationFailed 148 | } 149 | guard let width = image.properties[Keys.width] as? Float, 150 | let height = image.properties[Keys.height] as? Float else { 151 | throw Error.sizeReadingFailed 152 | } 153 | guard width == Keys.defaultSize, height == Keys.defaultSize else { 154 | throw Error.wrongSizeDetected(actualWidth: width, 155 | actualHeight: height, 156 | expectedWidth: Keys.defaultSize, 157 | expectedHeight: Keys.defaultSize) 158 | } 159 | return image 160 | } 161 | 162 | /// Generates icons for Apple platforms. 163 | /// 164 | /// - Parameter iconName: The name of generated icons. 165 | /// - Parameter imageURL: The url for original image. 166 | /// - Parameter idioms: Idioms for icons. 167 | /// - Parameter output: Output path for generated icons. Default value is nil. 168 | /// - Throws: `IconService.Error` errors. 169 | private func generateIcons(withIconName iconName: String, 170 | for imageURL: URL, 171 | idioms: [Icon.Idiom], 172 | output: String? = nil) throws { 173 | let image = try self.image(for: imageURL) 174 | let iconSet = iconFactory.makeSet(withName: iconName, 175 | idioms: idioms) 176 | var folderName = Keys.iconSetName 177 | if let output = output { 178 | folderName = output + folderName 179 | } 180 | try generateIcons(from: image, icons: iconSet.icons, folderName: folderName) 181 | var path = Keys.iconSetName + "/" + Keys.contentsName 182 | if let output = output { 183 | path = output + path 184 | } 185 | try writeContents(of: iconSet, path: path) 186 | } 187 | 188 | /// Writes contents file that contains names of icons. 189 | /// 190 | /// - Parameter set: The set with icons. 191 | /// - Parameter path: Path for generated icons. 192 | /// - Throws: `File.Error.writeFailed` if the file couldn’t be written to. 193 | private func writeContents(of set: AppIconSet, path: String) throws { 194 | encoder.outputFormatting = .prettyPrinted 195 | let iconSetData = try encoder.encode(set) 196 | let contentsFile = try fileSystem.createFile(at: path) 197 | try contentsFile.write(data: iconSetData) 198 | } 199 | 200 | /// Writes contents file that contains names of icons. 201 | /// 202 | /// - Parameter set: The set with assets. 203 | /// - Parameter path: Path for generated icons. 204 | /// - Throws: `File.Error.writeFailed` if the file couldn’t be written to. 205 | private func writeContents(of set: ComplicationSet, path: String) throws { 206 | encoder.outputFormatting = .prettyPrinted 207 | let iconSetData = try encoder.encode(set) 208 | let contentsFile = try fileSystem.createFile(at: path) 209 | try contentsFile.write(data: iconSetData) 210 | } 211 | 212 | /// Generates icons from original image. The image is resized and written to image files. 213 | /// 214 | /// - Parameters: 215 | /// - image: The original image. 216 | /// - icons: The array of objects, that represent icon information. 217 | /// - folderName: The name of folder for generated icons. 218 | /// - Throws: `IconService.Error` errors. 219 | private func generateIcons(from image: CIImage, icons: [IconRepresentable], folderName: String) throws { 220 | let image = image.settingAlphaOne(in: image.extent) 221 | let filter = CIFilter(name: Keys.lanczosFilterName)! 222 | filter.setValue(image, forKey: kCIInputImageKey) 223 | filter.setValue(1, forKey: kCIInputAspectRatioKey) 224 | 225 | let context = CIContext(options: [kCIContextUseSoftwareRenderer: false]) 226 | let colorSpace = CGColorSpaceCreateDeviceRGB() 227 | 228 | try icons.forEach { icon in 229 | let scale = icon.iconSize / Keys.defaultSize 230 | filter.setValue(scale, forKey: kCIInputScaleKey) 231 | guard let outputImage = filter.value(forKey: kCIOutputImageKey) as? CIImage else { 232 | throw Error.imageRenderingFailed 233 | } 234 | guard let outputData = context.representation(of: outputImage, colorSpace: colorSpace) else { 235 | try fileSystem.currentFolder.subfolder(named: folderName).delete() 236 | throw Error.imageRenderingFailed 237 | } 238 | let path = [folderName, icon.iconName].joined(separator: "/") 239 | let file = try fileSystem.createFile(at: path) 240 | try file.write(data: outputData) 241 | } 242 | } 243 | } 244 | 245 | extension IconService.Error: CustomStringConvertible { 246 | 247 | var description: String { 248 | switch self { 249 | case .imageCreationFailed: return "Can't create an image." 250 | case .sizeReadingFailed: return "Can't get image size." 251 | case let .wrongSizeDetected(actualWidth, actualHeight, expectedWidth, expectedHeight): 252 | let actualString = String(format: "%g", actualWidth) + "x" + String(format: "%g", actualHeight) 253 | let expectedString = String(format: "%g", expectedWidth) + "x" + String(format: "%g", expectedHeight) 254 | return "Image has wrong size (actual \(actualString), expected \(expectedString))." 255 | case .imageRenderingFailed: return "Can't render the image." 256 | } 257 | } 258 | } 259 | 260 | extension CIContext { 261 | 262 | func representation(of image: CIImage, 263 | format: CIFormat = kCIFormatRGBA8, 264 | colorSpace: CGColorSpace, 265 | options: [AnyHashable: Any] = [:]) -> Data? { 266 | if #available(OSX 10.13, *) { 267 | return pngRepresentation(of: image, format: format, colorSpace: colorSpace) 268 | } 269 | else if #available(OSX 10.12, *) { 270 | return jpegRepresentation(of: image, colorSpace: colorSpace) 271 | } 272 | return nil 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /Sources/SashaCore/Tasks/IconsTask.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2017 Artem Novichkov. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | import Files 7 | 8 | final class IconsTask { 9 | 10 | private let iconService: IconService 11 | 12 | init(iconService: IconService = IconService()) { 13 | self.iconService = iconService 14 | } 15 | 16 | func generateIcons(for platform: Platform, 17 | idioms: [Icon.Idiom]? = nil, 18 | path: String, 19 | output: String? = nil) throws { 20 | let file: File 21 | if path.contains("/") { 22 | file = try File(path: path) 23 | } 24 | else { 25 | file = try Folder.current.file(named: path) 26 | } 27 | let url = URL(fileURLWithPath: file.path) 28 | switch platform { 29 | case .iOS: 30 | try iconService.generateiOSIcons(for: url, idioms: idioms, output: output) 31 | print("🎉 AppIcon.appiconset was successfully created") 32 | case .macOS: 33 | try iconService.generateMacOSIcons(for: url, output: output) 34 | print("🎉 AppIcon.appiconset was successfully created") 35 | case .watchOS: 36 | try iconService.generateWatchOSIcons(for: url, output: output) 37 | print("🎉 AppIcon.appiconset was successfully created") 38 | case .watchOSComplication: 39 | try iconService.generateWatchOSComplicationIcons(for: url, output: output) 40 | print("🎉 Complication.complicationset was successfully created") 41 | case .android: 42 | try iconService.generateAndroidIcons(for: url) 43 | print("🎉 Icons were successfully created") 44 | case .androidWear: 45 | try iconService.generateAndroidWearIcons(for: url) 46 | print("🎉 Icons were successfully created") 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/SashaCore/Tasks/ProjectTask.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2017 Artem Novichkov. All rights reserved. 3 | // 4 | 5 | import Foundation 6 | import Files 7 | 8 | final class ProjectTask { 9 | 10 | enum Error: Swift.Error { 11 | case sketchTemplatesCreationFailed 12 | } 13 | 14 | private enum Keys { 15 | static let fileTemplates = "~/.sasha/file_templates/" 16 | } 17 | 18 | private let folderService: FolderService 19 | private let fileSystem: FileSystem 20 | 21 | init(folderService: FolderService = FolderService(), 22 | fileSystem: FileSystem = FileSystem()) { 23 | self.folderService = folderService 24 | self.fileSystem = fileSystem 25 | } 26 | 27 | func createProject(withName name: String) throws { 28 | let projectFile = try File(path: "~/.sasha/project.sasha") 29 | let projectString = try projectFile.readAsString() 30 | 31 | let paths = folderService.paths(fromString: projectString) 32 | try paths.forEach { path in 33 | let finalPath = name + FolderService.Keys.slash + path 34 | try fileSystem.createFolder(at: finalPath) 35 | } 36 | 37 | let projectFolder = try Folder.current.subfolder(named: name) 38 | do { 39 | try addSketchFiles(to: projectFolder, projectName: name.lowercased()) 40 | } 41 | catch { 42 | throw Error.sketchTemplatesCreationFailed 43 | } 44 | 45 | print("🎉 Project \(name) was successfully created.") 46 | } 47 | 48 | private func addSketchFiles(to folder: Folder, projectName: String) throws { 49 | //TODO: Hardcoded paths. Think about it. 50 | try Platform.allCases.forEach { platform in 51 | let platformName = platform.rawValue 52 | let sketchFile = try File(path: Keys.fileTemplates + "\(platformName.lowercased())_template.sketch") 53 | let sketchFileData = try sketchFile.read() 54 | let fileName = "\(platformName)/UI/" + projectName + " -\(platformName.lowercased()).sketch" 55 | try folder.createFile(named: fileName, contents: sketchFileData) 56 | } 57 | } 58 | } 59 | 60 | extension ProjectTask.Error: CustomStringConvertible { 61 | 62 | var description: String { 63 | switch self { 64 | case .sketchTemplatesCreationFailed: 65 | return "Can't create Sketch project files from templates. Please check ~/.sasha/file_templates folder." 66 | } 67 | } 68 | } 69 | --------------------------------------------------------------------------------