Lors d'un hackathon sur Pnut.io j'avais tenté de code un wrapper en Bash pour cowsay pour le rendre compatible avec les posts destinés à la mini imprimante communautaire.
Le résultat était bancale, je me suis donc ensuite décider à faire une version en Swift à partir de zéro.
On commence par faire un main
pour parse les commandes avec ProcessInfo et ArgumentParser :
import SPMUtility
import Foundation
let arguments = ProcessInfo.processInfo.arguments.dropFirst()
let parser = ArgumentParser(usage: "<options> \"some text\"", overview: "A command-line tool similar to cowsay, dedicated to pnut.io")
let messageArgument = parser.add(positional: "\"some text\"", kind: String.self, optional: false, usage: "The text that will be included in the speech bubble")
let pnutprinterArgument = parser.add(option: "--pnutprinter", shortName: "-p", kind: Bool.self, usage: "Adds a tag which triggers pnut's printer")
let errrorMessage = "You need to pass \"some text\" to generate the message. Use --help to list arguments and options."
do {
let parsedArguments = try parser.parse(Array(arguments))
if let msg = parsedArguments.get(messageArgument) {
let ps = PnutSays(thoughts: msg)
if let _ = parsedArguments.get(pnutprinterArgument) {
print(ps.say(pnutPrinter: true))
} else {
print(ps.say())
}
} else {
print(errrorMessage)
}
} catch {
print("An error occured!\n`\(error)`.\n\(errrorMessage)")
}
Ensuite, la principale difficulté c'est d'adapter automatiquement la largeur de la bulle, ainsi que les angles, pour la bordure comme pour les retours à la ligne.
La manipulation des Strings avec Swift c'est pas la fête mais en décomposant bien ce que l'on veut achever en plus petites tâches on finit par rompre la complexité.
Donc déjà, une extension pour nous aider :
extension String {
func splitByLength(_ length: Int, seperator: String) -> [String] {
var result = [String]()
var collectedWords = [String]()
collectedWords.reserveCapacity(length)
var count = 0
let words = self.split { $0 == " " }.map(String.init)
for word in words {
count += word.count + 1 //add 1 to include space
if (count > length) {
result.append(collectedWords.map { String($0) }.joined(separator: seperator) )
collectedWords.removeAll(keepingCapacity: true)
count = word.count
collectedWords.append(word)
} else {
collectedWords.append(word)
}
}
if !collectedWords.isEmpty {
result.append(collectedWords.map { String($0) }.joined(separator: seperator))
}
return result
}
func components(withMaxLength length: Int) -> [String] {
return stride(from: 0, to: self.count, by: length).map {
let start = self.index(self.startIndex, offsetBy: $0)
let end = self.index(start, offsetBy: length, limitedBy: self.endIndex) ?? self.endIndex
return String(self[start..<end])
}
}
func removingLeadingSpaces() -> String {
guard let index = self.firstIndex(where: { String($0) != " " }) else {
return self
}
return String(self[index...])
}
}
Et voici la classe principale :
class PnutSays {
private let thoughts: String
private let maxWordLength: Int
private lazy var wrapped = computeWrapped()
private func computeWrapped() -> [String] {
let firstPass = thoughts.split { $0.isNewline }
var result = [String]()
for block in firstPass {
let cut = String(block).splitByLength(maxWordLength, seperator: " ")
for subBlock in cut {
let components = subBlock.components(withMaxLength: maxWordLength)
for element in components {
result.append(element.removingLeadingSpaces())
}
}
}
if result.isEmpty {
result.append("???")
}
return result
}
private lazy var longestLine = computeLongestLine()
private func computeLongestLine() -> Int {
let m = wrapped.max { (a, b) -> Bool in
return a.count < b.count
}
return m!.count
}
private func makePadding(with s: String) -> String {
let toAdd = longestLine - s.count
var base = s
for _ in 0 ... toAdd {
base.append(" ")
}
return base
}
private lazy var horizontalSeparator = makeHorizontalSeparator()
private func makeHorizontalSeparator() -> String {
var base = " "
for _ in 0 ... longestLine + 1 {
base.append("–")
}
base.append(" ")
return base
}
init(thoughts: String) {
self.thoughts = thoughts
self.maxWordLength = 15 // 15 is nice length for pnut cow
}
private let firstSeparatorStart = "/"
private let firstSeparatorEnd = "\\"
private let lastSeparatorStart = "\\"
private let lastSeparatorEnd = "/"
private let middleSeparator = "|"
private let cow =
"""
\\ ,__,
\\ (oo)____
(__) )\\
||--|| *
"""
func say(pnutPrinter: Bool = false) -> String {
let num = wrapped.count
if num == 1 {
return makeOneLine(pnutPrinter: pnutPrinter)
} else if num == 2 {
return makeTwoLines(pnutPrinter: pnutPrinter)
} else {
return makeMultipleLines(number: num, pnutPrinter: pnutPrinter)
}
}
private func makeOneLine(pnutPrinter: Bool) -> String {
var base = makeFirstLine(with: wrapped[0], unique: true)
base.append(horizontalSeparator + "\n" + cow)
if pnutPrinter {
base.append("\n\n" + "#pnutprinter*")
}
return base
}
private func makeTwoLines(pnutPrinter: Bool) -> String {
let base = makeFirstLine(with: wrapped[0], unique: false)
var last = makeLastLine(with: base, phrase: wrapped[1])
if pnutPrinter {
last.append("\n\n" + "#pnutprinter*")
}
return last
}
private func makeMultipleLines(number: Int, pnutPrinter: Bool) -> String {
var base = makeFirstLine(with: wrapped[0], unique: false)
for i in 1 ..< number - 1 {
base.append(middleSeparator + " " + makePadding(with: wrapped[i]) + middleSeparator + "\n")
}
var last = makeLastLine(with: base, phrase: wrapped[number - 1])
if pnutPrinter {
last.append("\n\n" + "#pnutprinter*")
}
return last
}
private func makeFirstLine(with phrase: String, unique: Bool) -> String {
var base = horizontalSeparator
if unique {
base.append("\n" + middleSeparator + " " + makePadding(with: phrase) + middleSeparator + "\n")
} else {
base.append("\n" + firstSeparatorStart + " " + makePadding(with: phrase) + firstSeparatorEnd + "\n")
}
return base
}
private func makeLastLine(with base: String, phrase: String) -> String {
var base = base
base.append(lastSeparatorStart + " " + makePadding(with: phrase) + lastSeparatorEnd + "\n")
base.append(horizontalSeparator + "\n" + cow)
return base
}
}
Pour le compiler, il vous faudra d'abord Swift, suivez leur guide au besoin.
Ensuite, cd
dans le dossier puis
swift build
Il n'y a plus qu'à le copier et le rendre exécutable :
cp -f .build/release/pnutsays /usr/local/bin/pnutsays