diff --git a/Makefile b/Makefile index bb613fd..7586fbe 100644 --- a/Makefile +++ b/Makefile @@ -72,7 +72,7 @@ build-linux-docker: docker-compose run -w /work swift @echo "\nLinux version built at bin/linux/guaka\n" -build-all-local: clean build-linux-docker build-project-darwin +build-all-local: clone-swiftline clean build-linux-docker build-project-darwin @echo "Binaries built at bin/\n" release-local: @@ -93,6 +93,15 @@ release-publish-local: make release-local make publish-local +clone-swiftline: + @echo "Removing old Swiftline" + @echo "" + rm -rf SwiftLineTemp + + @echo "Clone new Swiftline from oarrabi/linux! branch" + git clone -b oarrabi/linux! https://github.com/oarrabi/Swiftline.git SwiftLineTemp + mv SwiftLineTemp/Sources/*.* Sources/Swiftline/ + rm -rf SwiftLineTemp release-and-deploy: if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then make build-project-darwin release-darwin VERSION=${TRAVIS_TAG} GITHUB_TOKEN=${GITHUB_TOKEN} ; fi diff --git a/Package.swift b/Package.swift index a93c9ab..77c881f 100644 --- a/Package.swift +++ b/Package.swift @@ -4,7 +4,8 @@ let package = Package( name: "guaka-cli", targets: [ Target(name: "guaka-cli", dependencies: ["GuakaClILib"]), - Target(name: "GuakaClILib"), + Target(name: "Swiftline"), + Target(name: "GuakaClILib", dependencies: ["Swiftline"]), ], dependencies: [ .Package(url: "https://github.com/oarrabi/Guaka.git", majorVersion: 0), diff --git a/Sources/GuakaClILib/GeneratorParts.swift b/Sources/GuakaClILib/GeneratorParts.swift index 9a30315..c574959 100644 --- a/Sources/GuakaClILib/GeneratorParts.swift +++ b/Sources/GuakaClILib/GeneratorParts.swift @@ -29,7 +29,8 @@ public enum GeneratorParts { generatedComment, "func setupCommands() {", comamndAddingPlaceholder, - "}" + "}", + "" ].joined(separator: "\n") } @@ -41,6 +42,7 @@ public enum GeneratorParts { "setupCommands()", "", "rootCommand.execute()", + "" ].joined(separator: "\n") } @@ -66,6 +68,7 @@ public enum GeneratorParts { " // Execute code here", " print(\"\(commandName) called\")", "}", + "" ].joined(separator: "\n") } @@ -77,7 +80,8 @@ public enum GeneratorParts { " dependencies: [", " .Package(url: \"\(guakaURL)\", majorVersion: \(guakaVersion)),", " ]", - ")" + ")", + "" ].joined(separator: "\n") } diff --git a/Sources/GuakaClILib/MiscTypes.swift b/Sources/GuakaClILib/MiscTypes.swift index 1644f88..ed2a57b 100644 --- a/Sources/GuakaClILib/MiscTypes.swift +++ b/Sources/GuakaClILib/MiscTypes.swift @@ -5,6 +5,8 @@ // Created by Omar Abdelhafith on 27/11/2016. // // +import Swiftline + public struct GuakaCliConfig { public static var dir: DirectoryType.Type = FileSystemDirectory.self @@ -20,6 +22,7 @@ public enum GuakaError: Error { case cannotReadFile(String) case setupFileAltered case notAGuakaProject + case commandAlreadyExist(String, String) case missingCommandName case tooManyArgsPassed case wrongCommandNameFormat(String) @@ -27,28 +30,45 @@ public enum GuakaError: Error { public var error: String { switch self { case .wrongDirectoryGiven(let path): - return "The path given cannot be used \(path)" + return [ + "Wrong path given:", + " \(path)", + "The path must be an empty directory" + ].joined(separator: "\n").f.red + case .triedToCreateProjectInNonEmptyDirectory(let path): - return "Cannot create project in non empty directory \(path)\n" + return "Cannot create project in non empty directory: \(path)\n".f.red + case .failedCreatingFolder(let path): - return "Failed creating directory \(path)" + return "Failed creating directory \(path)".f.red + case .cannotCreateFile (let name): - return "Cannot generate \(name) file" + return "Cannot generate \(name) file".f.red + case .cannotReadFile(let path): - return "Cannot read contents of file \(path)" + return "Cannot read contents of file \(path)".f.red + case .setupFileAltered: - return "Guaka setup.swift file has been altered.\nThe placeholder used to insert commands cannot be found \(GeneratorParts.comamndAddingPlaceholder).\nYou can try to add it yourself by updating `setup.swift` to look like\n\n\(GeneratorParts.setupFileContent())\n\nAdding command wont be possible." + return "Guaka setup.swift file has been altered.\nThe placeholder used to insert commands cannot be found \(GeneratorParts.comamndAddingPlaceholder).\nYou can try to add it yourself by updating `setup.swift` to look like\n\n\(GeneratorParts.setupFileContent())\n\nAdding command won't be possible.".f.red + case .notAGuakaProject: - return "This command can only be executed in a Guaka project.\nThe following directory does not contain guaka files" + return "This command can only be executed inside a Guaka project.".f.red + case .missingCommandName: return [ - "`guaka add` requires a command that was not given.", + "Missing CommandName for `guaka add`.".f.red, + "", "Call `guaka add CommandName` to create a new command.", "" ].joined(separator: "\n") + case .commandAlreadyExist(let name, let path): + return [ "The command `\(name)` already exist:".f.red, + " \(path)".f.red, + "Please use a differnt command name"].joined(separator: "\n") + case .wrongCommandNameFormat(let name): - return [ "The command name passed `\(name)` is incorrect.", + return [ "The command name passed `\(name)` is incorrect.".f.red, "Please use only letters, numbers, underscodes and dashes.", "", "Valid examples:", @@ -57,8 +77,10 @@ public enum GuakaError: Error { " guaka new my-command", " guaka new my_command", " guaka new myCommand"].joined(separator: "\n") + case .tooManyArgsPassed: - return "Too many arguments passed to command." + return "Too many arguments passed to command.".f.red + } } diff --git a/Sources/GuakaClILib/Paths.swift b/Sources/GuakaClILib/Paths.swift index adebd2e..b31c282 100644 --- a/Sources/GuakaClILib/Paths.swift +++ b/Sources/GuakaClILib/Paths.swift @@ -51,5 +51,9 @@ public struct Paths { return false } + + public func isNewCommand(commandName: String) -> Bool { + return GuakaCliConfig.file.exists(atPath: path(forSwiftFile: commandName)) == false + } } diff --git a/Sources/Swiftline/Agree.swift b/Sources/Swiftline/Agree.swift new file mode 100644 index 0000000..12f7ba4 --- /dev/null +++ b/Sources/Swiftline/Agree.swift @@ -0,0 +1,23 @@ +// +// Agree.swift +// Agree +// +// Created by Omar Abdelhafith on 03/11/2015. +// Copyright © 2015 Omar Abdelhafith. All rights reserved. +// + +/** + Displays a yes/no prompt to the user + + - parameter prompt: The prompt to display + - returns: the user decision + */ +public func agree(_ prompt: String) -> Bool { + PromptSettings.print(prompt, terminator: " ") + let value = readStringOrEmpty() + + let settings = AgreeSettings(prompt: prompt) + let validatedValue = askForValidatedItem(originalValue: value, validator: settings) + + return settings.isPositive(validatedValue) +} diff --git a/Sources/Swiftline/AgreeSettings.swift b/Sources/Swiftline/AgreeSettings.swift new file mode 100644 index 0000000..7c70803 --- /dev/null +++ b/Sources/Swiftline/AgreeSettings.swift @@ -0,0 +1,41 @@ +// +// AgreeSettings.swift +// AgreeSettings +// +// Created by Omar Abdelhafith on 03/11/2015. +// Copyright © 2015 Omar Abdelhafith. All rights reserved. +// + + +class AgreeSettings: AskerValidator { + + let positiveValues = ["Yes", "yes", "Y", "y"] + let negativeValues = ["No", "no", "N", "n"] + + let prompt: String + + init(prompt: String) { + self.prompt = prompt + } + + func validatedItem(forString string: String) -> String { + return string + } + + func invalidItemMessage(_ string: String?) -> String? { + if let message = string , positiveValues.contains(message) || negativeValues.contains(message) { + return nil + } + + return "Please enter \"yes\" or \"no\"." + } + + func newItemPromptMessage() -> String { + return "\(prompt) " + } + + func isPositive(_ item: String) -> Bool { + return positiveValues.contains(item) + } + +} diff --git a/Sources/Swiftline/ArgConvertible.swift b/Sources/Swiftline/ArgConvertible.swift new file mode 100644 index 0000000..67a874c --- /dev/null +++ b/Sources/Swiftline/ArgConvertible.swift @@ -0,0 +1,63 @@ +// +// ArgConvertible.swift +// ArgConvertible +// +// Created by Omar Abdelhafith on 02/11/2015. +// Copyright © 2015 Omar Abdelhafith. All rights reserved. +// + + +/** + * Any type that extends ArgConvertibleType can be used in ask and choose + */ +public protocol ArgConvertibleType { + + /// Create an instance out of a string + static func fromString(_ string: String) -> Self? + + /// Return the display name of a type + static func typeName() -> String +} + + +extension Int: ArgConvertibleType { + public static func fromString(_ string: String) -> Int? { + return Int(string) + } + + public static func typeName() -> String { + return "Integer" + } +} + + +extension Double: ArgConvertibleType { + public static func fromString(_ string: String) -> Double? { + return Double(string) + } + + public static func typeName() -> String { + return "Double" + } +} + +extension Float: ArgConvertibleType { + public static func fromString(_ string: String) -> Float? { + return Float(string) + } + + public static func typeName() -> String { + return "Float" + } +} + + +extension String: ArgConvertibleType { + public static func fromString(_ string: String) -> String? { + return string + } + + public static func typeName() -> String { + return "String" + } +} diff --git a/Sources/Swiftline/Args.swift b/Sources/Swiftline/Args.swift new file mode 100644 index 0000000..040104c --- /dev/null +++ b/Sources/Swiftline/Args.swift @@ -0,0 +1,66 @@ +// +// Args.swift +// Swiftline +// +// Created by Omar Abdelhafith on 25/11/2015. +// Copyright © 2015 Omar Abdelhafith. All rights reserved. +// + + +/// Return the command line arguments passed to the script +public class Args { + + /// Return the list of arguments passed to the script + public static var all: [String] { + return CommandLine.arguments + } + + static var cachedResults: ParsedArgs? + + /// Return a parsed list of arguments containing the flags and the parameters passed to the scripts + /// The flags are recognized as short flags `-f` or long flags `--force` + /// The flag value will be the argument that follows the flag + /// `--` is used to mark the terminatin of the flags + public static var parsed: ParsedArgs { + let result = parse(argumens: all, cachedResults: cachedResults) + cachedResults = result + return result + } + + static func parse(argumens: [String], cachedResults: ParsedArgs?) -> ParsedArgs { + if let result = cachedResults { + return result + } + + var parsedFlags = [String: String]() + let parsedArgs = ArgsParser.parseFlags(argumens) + + parsedArgs.0.forEach { + parsedFlags[$0.argument.name] = $0.value ?? "" + } + + var arguments = parsedArgs.1 + + // the first argument is always the executable's name + var commandName = "" + if let firstArgument = arguments.first { // just in case! + commandName = firstArgument + arguments.removeFirst(1) + } + + return ParsedArgs(command: commandName, flags: parsedFlags, parameters: arguments) + } + +} + + +public struct ParsedArgs { + /// The name of the executable that was invoked from the command line + public let command: String + + /// Parsed flags will be prepred in a dictionary, the key is the flag and the value is the flag value + public let flags: [String: String] + + /// List of parameters passed to the script + public let parameters: [String] +} diff --git a/Sources/Swiftline/ArgsParser.swift b/Sources/Swiftline/ArgsParser.swift new file mode 100644 index 0000000..3ce0c13 --- /dev/null +++ b/Sources/Swiftline/ArgsParser.swift @@ -0,0 +1,53 @@ +// +// ArgsParser.swift +// Swiftline +// +// Created by Omar Abdelhafith on 27/11/2015. +// Copyright © 2015 Omar Abdelhafith. All rights reserved. +// + +class ArgsParser { + + static func parseFlags(_ args: [String]) -> ([Option], [String]) { + var options = [Option]() + var others = [String]() + var previousArgument: Argument? + var argsTerminated = false + + for argumentString in args { + let argument = Argument(argumentString) + defer { previousArgument = argument } + + if argsTerminated { + others += [argumentString] + continue + } + + if argument.isFlagTerminator { + argsTerminated = true + continue + } + + if argument.isFlag { + options += [Option(argument: argument)] + continue + } + + if let previousArgument = previousArgument , previousArgument.isFlag { + updatelastOption(forArray: &options, withValue: argumentString) + } else { + others += [argument.name] + } + } + + return (options, others) + + } + + static func updatelastOption(forArray array: inout [Option], withValue value: String) { + var previousOption = array.last! + previousOption.value = value + array.removeLast() + array += [previousOption] + } +} diff --git a/Sources/Swiftline/Argument.swift b/Sources/Swiftline/Argument.swift new file mode 100644 index 0000000..5aa8f59 --- /dev/null +++ b/Sources/Swiftline/Argument.swift @@ -0,0 +1,80 @@ +// +// Argument.swift +// Swiftline +// +// Created by Omar Abdelhafith on 26/11/2015. +// Copyright © 2015 Omar Abdelhafith. All rights reserved. +// +import StringScanner + + +struct Option { + let argument: Argument + var value: String? + + init(argument: Argument, value: String? = nil) { + self.argument = argument + self.value = value + } +} + +struct Argument { + + enum ArgumentType { + case shortFlag + case longFlag + case notAFlag + case flagsTerminator + + var isFlag: Bool { + return self != .notAFlag + } + + var isFlagTerminator: Bool { + return self == .flagsTerminator + } + + init(_ argument: String) { + + if argument == "--" { + self = .flagsTerminator + } else if argument.isPrefixed(by: "--") { + self = .longFlag + } else if argument.isPrefixed(by: "-") { + self = .shortFlag + } else { + self = .notAFlag + } + } + } + + let type: ArgumentType + let argument: String + + init(_ argument: String) { + self.type = ArgumentType(argument) + self.argument = argument + } + + var isFlag: Bool { + return type.isFlag + } + + var isFlagTerminator: Bool { + return type.isFlagTerminator + } + + var name: String { + switch type { + case .notAFlag: + return argument + case .shortFlag: + return argument[with: 1..) -> Void)? = nil) -> String { + return ask(prompt, type: String.self, customizationBlock: customizationBlock) +} + + +/** + Display a promt to the user + + - parameter prompt:The message to display + - parameter type: The value type to be expected from the user + - parameter customizationBlock: The block to costumize the prompt before displaying + + - returns: The string casted to the type requested + - discussion: If the user enters a wrong type, ask will keep prompting until a correct value has been entered + */ +public func ask(_ prompt: String, type: T.Type, customizationBlock: ((AskSettings) -> Void)? = nil) -> T { + + PromptSettings.print(prompt) + + let settings = getSettings(customizationBlock) + + if settings.confirm { + return getValidatedStringWithConfirmation(settings) + } else { + return getValidatedString(settings) + } +} + + +// MARK:- Internal functions + + +func getValidatedString(_ validator: W) -> T where W.Item == T { + let stringOrEmpty = readStringOrEmpty() + return askForValidatedItem(originalValue: stringOrEmpty, validator: validator) +} + + +func getValidatedStringWithConfirmation(_ validator: W) -> T where W.Item == T { + + while true { + let stringOrEmpty = readStringOrEmpty() + let answer = askForValidatedItem(originalValue: stringOrEmpty, validator: validator) + + if agree("Are you sure?") { + return answer + } else { + PromptSettings.print("? ", terminator: "") + } + } +} + + +func getSettings(_ callback: ((AskSettings) -> Void)?) -> AskSettings { + let settings = AskSettings() + callback?(settings) + return settings +} + + +func readStringOrEmpty() -> String { + return PromptSettings.read() ?? "" +} diff --git a/Sources/Swiftline/AskSettings.swift b/Sources/Swiftline/AskSettings.swift new file mode 100644 index 0000000..f1cdcba --- /dev/null +++ b/Sources/Swiftline/AskSettings.swift @@ -0,0 +1,82 @@ +// +// AskSettings.swift +// AskSettings +// +// Created by Omar Abdelhafith on 03/11/2015. +// Copyright © 2015 Omar Abdelhafith. All rights reserved. +// + + +/// Settings used to costumize the behaviour of ask() +public class AskSettings { + + /// Default value to set incase the user entered a blank + public var defaultValue: T? + + /// If set to true, another message will follow successful user entry asking the user to confirm + /// his selection + public var confirm = false + + var invalidClousures: [((T) -> Bool, String)] = [] + + + /** + Add an invalid entry case + + - parameter description: The string to be printed to the stdout if the case is invalid + - parameter invalidIfTrue: If true is returned, then the user input was invalid, if false, the + user input was valid. + */ + public func addInvalidCase(_ description: String, invalidIfTrue: @escaping ((T) -> Bool)) { + invalidClousures.append((invalidIfTrue, description)) + } + + func preparedItem(originalString string: String) -> T { + if string.isEmpty && defaultValue != nil { + return defaultValue! + } + + return T.fromString(string)! + } +} + + +// MARK:- Internal extension + +extension AskSettings: AskerValidator { + + func invalidItemMessage(_ string: String?) -> String? { + guard let string = string else { + return "You provided an empty message, pelase enter anything!" + } + + guard let validatedItem = T.fromString(string) else { + return "You must enter a valid \(T.typeName())." + } + + guard let validationError = firstValidationError(validatedItem) else { + return nil + } + + return validationError + } + + func newItemPromptMessage() -> String { + return "? " + } + + func validatedItem(forString string: String) -> T { + return T.fromString(string)! + } + + func firstValidationError(_ item: T) -> String? { + + for (isInvalid, validationError) in invalidClousures { + if isInvalid(item) { + return validationError + } + } + + return nil + } +} diff --git a/Sources/Swiftline/AskerValidator.swift b/Sources/Swiftline/AskerValidator.swift new file mode 100644 index 0000000..ee1ce3e --- /dev/null +++ b/Sources/Swiftline/AskerValidator.swift @@ -0,0 +1,36 @@ +// +// ConsoleEntryValidator.swift +// AskerValidator +// +// Created by Omar Abdelhafith on 02/11/2015. +// Copyright © 2015 Omar Abdelhafith. All rights reserved. +// + + +protocol AskerValidator { + associatedtype Item + + func invalidItemMessage(_ string: String?) -> String? + func newItemPromptMessage() -> String + + func validatedItem(forString string: String) -> Item +} + +func askForValidatedItem + (originalValue value: String, validator: W) -> T where W.Item == T { + + var validatedValue: String = value + + while true { + guard let invalidMessage = validator.invalidItemMessage(validatedValue) else { + break + } + + PromptSettings.print(invalidMessage) + PromptSettings.print(validator.newItemPromptMessage(), terminator: "") + + validatedValue = readStringOrEmpty() + } + + return validator.validatedItem(forString: validatedValue) +} diff --git a/Sources/Swiftline/Choose.swift b/Sources/Swiftline/Choose.swift new file mode 100644 index 0000000..079b012 --- /dev/null +++ b/Sources/Swiftline/Choose.swift @@ -0,0 +1,93 @@ +// +// Chooser.swift +// Choose +// +// Created by Omar Abdelhafith on 03/11/2015. +// Copyright © 2015 Omar Abdelhafith. All rights reserved. +// + + + +/** + Presents a user with a menu of items to choose from + + - parameter prompt: The menu prompt message + - parameter choices: List of choices + + - returns: The user selected item + */ +public func choose(_ prompt: String, choices: String...) -> String { + return choose(prompt, type: String.self) { + for choice in choices { + $0.addChoice(choice) { return choice } + } + } +} + + +/** + Presents a user with a menu of items to choose from + + - parameter costumizationBlock: Closure to be called with a ChooseSettings, changes to the settings are reflected to the prompt + + - returns: The user selected item + */ +public func choose(_ costumizationBlock: ((ChooseSettings) -> Void)) -> T { + + let settings = getChooseSettings(costumizationBlock) + return choose(settings, type: T.self) +} + + +/** + Presents a user with a menu of items to choose from + + - parameter prompt: The menu prompt message + - parameter type: The value type to be expected from the user + - parameter costumizationBlock: Closure to be called with a ChooseSettings, changes to the settings are reflected to the prompt + + - returns: The user selected item + */ +public func choose(_ prompt: String, type: T.Type, costumizationBlock: ((ChooseSettings) -> Void)) -> T { + + let settings = getChooseSettings(costumizationBlock) + settings.promptQuestion = prompt + return choose(settings, type: type) +} + + +/** + Presents a user with a menu of items to choose from + + - parameter type: The value type to be expected from the user + - parameter costumizationBlock: Closure to be called with a ChooseSettings, changes to the settings are reflected to the prompt + + - returns: The user selected item + */ +public func choose(_ type: T.Type, costumizationBlock: ((ChooseSettings) -> Void)) -> T { + + let settings = getChooseSettings(costumizationBlock) + return choose(settings, type: type) +} + + +// MARK :- Internal functions + + +func choose(_ settings: ChooseSettings, type: T.Type) -> T { + + let items = settings.preparePromptItems() + + items.forEach { PromptSettings.print($0) } + PromptSettings.print("\(settings.promptQuestion)", terminator: "") + + let stringRead = readStringOrEmpty() + + return askForValidatedItem(originalValue: stringRead, validator: settings) +} + +func getChooseSettings(_ costumizationBlock: (ChooseSettings) -> Void) -> ChooseSettings { + let settings = ChooseSettings() + costumizationBlock(settings) + return settings +} diff --git a/Sources/Swiftline/ChooseSettings.swift b/Sources/Swiftline/ChooseSettings.swift new file mode 100644 index 0000000..b033e78 --- /dev/null +++ b/Sources/Swiftline/ChooseSettings.swift @@ -0,0 +1,128 @@ +// +// ChooseSettings.swift +// ChooseSettings +// +// Created by Omar Abdelhafith on 03/11/2015. +// Copyright © 2015 Omar Abdelhafith. All rights reserved. +// + + +/** + Choice index type + + - Letters: Use letters as choice index (a. b. c.) + - Numbers: Use numbers as choice index (1. 2. 3.) + */ +public enum ChoiceIndexType { + case letters + case numbers +} + + + +/// Settings to costumize the behavior of choose +public class ChooseSettings { + typealias Item = T + + var choices: [(choice: String, callback: (Void) -> T)] = [] + + /// Prompt message to use + public var promptQuestion = "" + + /// Choice index used for choose items + public var index = ChoiceIndexType.numbers + + /// Index suffix used between the index and the item + public var indexSuffix = ". " + + /** + Add a new item to the list of choices + + - parameter choice: Item name + - parameter callback: callback called when the item is selected, the value returned from this call back will be returned from choose + */ + public func addChoice(_ choice: String..., callback: @escaping (Void) -> T) { + choice.forEach { + choices.append(($0, callback)) + } + } + + // MARK:- Internal + + func validChoices() -> [String] { + let validChoices = Array(1...choices.count).map { "\($0)" } + return validChoices + stringChoices() + } + + func stringChoices() -> [String] { + return choices.map { $0.choice } + } + + func choiceForInput(_ item: String) -> T? { + if let value = Int(item) { + return choiceWithIntValue(value) + } else { + return choiceWithStringValue(item) + } + } + + func choiceWithIntValue(_ value: Int) -> T? { + let index = value - 1 + if index >= 0 && index < choices.count { + return choices[index].callback() + } + + return nil + } + + func choiceWithStringValue(_ value: String) -> T? { + let possibleIndex = choices.index { $0.choice == value } + if let index = possibleIndex { + return choices[index].callback() + } + + return nil + } + + func preparePromptItems() -> [String] { + return zip(indexChoices(), stringChoices()).map { index, string in + return "\(index)\(indexSuffix)\(string)" + } + } + + func indexChoices() -> [String] { + return stringChoices().enumerated().map { itemIndex, string in + + if index == .numbers { + return "\(itemIndex + 1)" + } else { + let character = "a".unicodeScalars.first!.value + UInt32(itemIndex) + return String(Character(UnicodeScalar(character)!)) + } + } + } + +} + +// MARK:- Internal Class + +extension ChooseSettings: AskerValidator { + func validatedItem(forString string: String) -> T { + return choiceForInput(string)! + } + + func invalidItemMessage(_ string: String?) -> String? { + if choiceForInput(string!) != nil { + return nil + } + + let baseMessage = "You must choose one of" + let choicesString = validChoices().joined(separator: ", ") + + return "\(baseMessage) [\(choicesString)]." + } + + func newItemPromptMessage() -> String { + return "? " + } +} diff --git a/Sources/Swiftline/Colorizer.swift b/Sources/Swiftline/Colorizer.swift new file mode 100644 index 0000000..c6e9f8c --- /dev/null +++ b/Sources/Swiftline/Colorizer.swift @@ -0,0 +1,59 @@ +// +// Colorizer.swift +// Colorizer +// +// Created by Omar Abdelhafith on 30/10/2015. +// Copyright © 2015 Omar Abdelhafith. All rights reserved. +// + + +extension String { + + /// Access the methods to change the foreground color + public var f: StringForegroundColorizer { + return foreground + } + + /// Access the methods to change the foreground color + public var foreground: StringForegroundColorizer { + return StringForegroundColorizer(string: self) + } + + /// Access the methods to change the background color + public var b: StringBackgroundColorizer { + return background + } + + /// Access the methods to change the background color + public var background: StringBackgroundColorizer { + return StringBackgroundColorizer(string: self) + } + + /// Access the methods to change the text style + public var s: StringStyleColorizer { + return style + } + + /// Access the methods to change the text style + public var style: StringStyleColorizer { + return StringStyleColorizer(string: self) + } + +} + + // MARK- Internal + +class Colorizer: CustomStringConvertible { + + let color: StringStyle + let string: String + + init(string: String, color: StringStyle) { + self.string = string + self.color = color + } + + var description: String { + return color.colorize(string: string) + } +} \ No newline at end of file diff --git a/Sources/Swiftline/PromptPrinter.swift b/Sources/Swiftline/PromptPrinter.swift new file mode 100644 index 0000000..32a2743 --- /dev/null +++ b/Sources/Swiftline/PromptPrinter.swift @@ -0,0 +1,28 @@ +// +// PromptPrinter.swift +// PromptPrinter +// +// Created by Omar Abdelhafith on 02/11/2015. +// Copyright © 2015 Omar Abdelhafith. All rights reserved. +// + + +protocol PromptPrinter { + func printString(_ string: String, terminator: String) +} + +class ConsolePromptPrinter: PromptPrinter { + + func printString(_ string: String, terminator: String = "\n") { + return print(string, separator: "", terminator: terminator) + } +} + +class DummyPromptPrinter: PromptPrinter { + + var printed = "" + + func printString(_ string: String, terminator: String = "\n") { + printed += string + terminator + } +} diff --git a/Sources/Swiftline/PromptReader.swift b/Sources/Swiftline/PromptReader.swift new file mode 100644 index 0000000..b884fb2 --- /dev/null +++ b/Sources/Swiftline/PromptReader.swift @@ -0,0 +1,32 @@ +// +// PromptReader.swift +// PromptReader +// +// Created by Omar Abdelhafith on 31/10/2015. +// Copyright © 2015 Omar Abdelhafith. All rights reserved. +// + + +protocol PromptReader { + func read() -> String? +} + +class ConsolePromptReader: PromptReader { + + func read() -> String? { + return readLine() + } +} + +class DummyPromptReader: PromptReader { + + var parametersToReturn: [String] + + init(toReturn: String...) { + self.parametersToReturn = toReturn + } + + func read() -> String? { + return parametersToReturn.removeFirst() + } +} \ No newline at end of file diff --git a/Sources/Swiftline/PromptSettings.swift b/Sources/Swiftline/PromptSettings.swift new file mode 100644 index 0000000..fa0c0a7 --- /dev/null +++ b/Sources/Swiftline/PromptSettings.swift @@ -0,0 +1,22 @@ +// +// PromptSettings.swift +// PromptSettings +// +// Created by Omar Abdelhafith on 02/11/2015. +// Copyright © 2015 Omar Abdelhafith. All rights reserved. +// + + +class PromptSettings { + + static var reader: PromptReader = ConsolePromptReader() + static var printer: PromptPrinter = ConsolePromptPrinter() + + class func read() -> String? { + return reader.read() + } + + class func print(_ string: String, terminator: String = "\n") { + return printer.printString(string, terminator: terminator) + } +} diff --git a/Sources/Swiftline/Readme.md b/Sources/Swiftline/Readme.md new file mode 100644 index 0000000..e86a2bf --- /dev/null +++ b/Sources/Swiftline/Readme.md @@ -0,0 +1,11 @@ +This folder is autogenerated. Dont edit it. +To generate this folder again run + +``` +make clone-swiftline +``` + +## Why this hack? +I needed to use a different branch of `Swiftline` and since Swift Package Manager does not allow to specify the branch to use. A similar hack is needed. + +This hack will be removed when `oarrabi/linux!` branch is merge into swiftline diff --git a/Sources/Swiftline/StringBackgroundColorizer.swift b/Sources/Swiftline/StringBackgroundColorizer.swift new file mode 100644 index 0000000..3998e88 --- /dev/null +++ b/Sources/Swiftline/StringBackgroundColorizer.swift @@ -0,0 +1,48 @@ +// +// StringBackgroundColorizer.swift +// StringBackgroundColorizer +// +// Created by Omar Abdelhafith on 31/10/2015. +// Copyright © 2015 Omar Abdelhafith. All rights reserved. +// + + +extension String { + + public struct StringBackgroundColorizer { + + let string: String + + public var black: String { + return Colorizer(string: string, color: BackgroundColor.black).description + } + + public var red: String { + return Colorizer(string: string, color: BackgroundColor.red).description + } + + public var green: String { + return Colorizer(string: string, color: BackgroundColor.green).description + } + + public var yellow: String { + return Colorizer(string: string, color: BackgroundColor.yellow).description + } + + public var blue: String { + return Colorizer(string: string, color: BackgroundColor.blue).description + } + + public var magenta: String { + return Colorizer(string: string, color: BackgroundColor.magenta).description + } + + public var cyan: String { + return Colorizer(string: string, color: BackgroundColor.cyan).description + } + + public var white: String { + return Colorizer(string: string, color: BackgroundColor.white).description + } + } +} diff --git a/Sources/Swiftline/StringForegroundColorizer.swift b/Sources/Swiftline/StringForegroundColorizer.swift new file mode 100644 index 0000000..880b131 --- /dev/null +++ b/Sources/Swiftline/StringForegroundColorizer.swift @@ -0,0 +1,49 @@ +// +// StringForegroundColorizer.swift +// StringForegroundColorizer +// +// Created by Omar Abdelhafith on 31/10/2015. +// Copyright © 2015 Omar Abdelhafith. All rights reserved. +// + + +extension String { + + public struct StringForegroundColorizer { + + let string: String + + public var black: String { + return Colorizer(string: string, color: ForegroundColor.black).description + } + + public var red: String { + return Colorizer(string: string, color: ForegroundColor.red).description + } + + public var green: String { + return Colorizer(string: string, color: ForegroundColor.green).description + } + + public var yellow: String { + return Colorizer(string: string, color: ForegroundColor.yellow).description + } + + public var blue: String { + return Colorizer(string: string, color: ForegroundColor.blue).description + } + + public var magenta: String { + return Colorizer(string: string, color: ForegroundColor.magenta).description + } + + public var cyan: String { + return Colorizer(string: string, color: ForegroundColor.cyan).description + } + + public var white: String { + return Colorizer(string: string, color: ForegroundColor.white).description + } + } + +} diff --git a/Sources/Swiftline/StringStyle.swift b/Sources/Swiftline/StringStyle.swift new file mode 100644 index 0000000..90dd6c7 --- /dev/null +++ b/Sources/Swiftline/StringStyle.swift @@ -0,0 +1,137 @@ +// +// StringStyle.swift +// StringStyle +// +// Created by Omar Abdelhafith on 31/10/2015. +// Copyright © 2015 Omar Abdelhafith. All rights reserved. +// +import StringScanner + + +let startOfCode = "\u{001B}[" +let endOfCode = "m" +let codesSeperators = ";" + +protocol StringStyle { + var rawValue: Int { get } + func colorize(string: String) -> String +} + + +extension StringStyle { + + func colorize(string: String) -> String { + + if hasAnyStyleCode(string) { + return colorizeStringAndAddCodeSeperators(string) + } else { + return colorizeStringWithoutPriorCode(string) + } + } + + fileprivate func colorizeStringWithoutPriorCode(_ string: String) -> String { + return "\(preparedColorCode(self.rawValue))\(string)\(endingColorCode())" + } + + fileprivate func colorizeStringAndAddCodeSeperators(_ string: String) -> String { + //To refactor and use regex matching instead of replacing strings and using tricks + let stringByRemovingEnding = removeEndingCode(string) + let sringwWithStart = "\(preparedColorCode(self.rawValue))\(stringByRemovingEnding)" + + let stringByAddingCodeSeperator = addCommandSeperators(sringwWithStart) + + return "\(stringByAddingCodeSeperator)\(endingColorCode())" + } + + fileprivate func preparedColorCode(_ color: Int) -> String { + return "\(startOfCode)\(color)\(endOfCode)" + } + + fileprivate func hasAnyStyleCode(_ string: String) -> Bool { + return string.find(string: startOfCode) != nil + } + + fileprivate func addCommandSeperators(_ string: String) -> String { + var rangeWithInset = (string.characters.index(after: string.startIndex) ..< string.characters.index(before: string.endIndex)) + + let newString = string.replacing(subString: startOfCode, withString: ";", inRange: rangeWithInset) + + rangeWithInset = (newString.characters.index(after: newString.startIndex) ..< newString.characters.index(before: newString.endIndex)) + return newString.replacing(subString: "m;", withString: ";", inRange: rangeWithInset) + } + + fileprivate func removeEndingCode(_ string: String) -> String { + let rangeWithInset = (string.characters.index(after: string.startIndex) ..< string.endIndex) + return string.replacing(subString: endingColorCode(), withString: "", inRange: rangeWithInset) + } + + fileprivate func endingColorCode() -> String { + return preparedColorCode(0) + } +} + +// TODO: think about the performance of these functions +extension String { + + func replacing(subString: String, withString: String) -> String { + var mutableStr = self + + while let startIndex = mutableStr.find(string: subString) { + let endIndex = mutableStr.index(startIndex, offsetBy: subString.characters.count) + mutableStr.replaceSubrange(startIndex..) -> String { + let beforeString = self[self.startIndex..