Upgrade to Pro — share decks privately, control downloads, hide ads and more …

[NSBarcelona/AppTalks Manchester] - Delightful Swift CLI applications

[NSBarcelona/AppTalks Manchester] - Delightful Swift CLI applications

Pol Piella Abadia

May 18, 2023
Tweet

More Decks by Pol Piella Abadia

Other Decks in Programming

Transcript

  1. Tooling + Swift ❤ • Familiar language • Low memory

    footprint • Type safety • Mature ecosystem • Great open-source community • Build for multiple platforms
  2. Some great examples built with Swift of course! ❤ •

    Sourcery - https://github.com/krzysztofzablocki/Sourcery • SwiftLint - https://github.com/realm/SwiftLint • Tuist - https://github.com/tuist/tuist • SwiftGen - https://github.com/SwiftGen/SwiftGen • Publish CLI - https://github.com/JohnSundell/Publish • Vapor toolbox - https://github.com/vapor/toolbox
  3. import ArgumentParser @main struct Repeat: AsyncParsableCommand { @Flag(help: "Include a

    counter with each repetition.") var includeCounter = false @Option(name: .shortAndLong, help: "The number of times to repeat 'phrase'.") var count: Int? = nil @Argument(help: "The phrase to repeat.") var phrase: String func run() async throws { let repeatCount = count ?? 2 for i in 1 ... repeatCount { if includeCounter { await someAsyncOperation() } else { print(phrase) } } } } https://github.com/apple/swift-argument-parser
  4. import ArgumentParser @main struct Repeat: AsyncParsableCommand { @Flag(help: "Include a

    counter with each repetition.") var includeCounter = false @Option(name: .shortAndLong, help: "The number of times to repeat 'phrase'.") var count: Int? = nil @Argument(help: "The phrase to repeat.") var phrase: String func run() async throws { let repeatCount = count ?? 2 for i in 1 ... repeatCount { if includeCounter { await someAsyncOperation() } else { print(phrase) } } } } https://github.com/apple/swift-argument-parser
  5. import ArgumentParser @main struct Repeat: AsyncParsableCommand { @Flag(help: "Include a

    counter with each repetition.") var includeCounter = false @Option(name: .shortAndLong, help: "The number of times to repeat 'phrase'.") var count: Int? = nil @Argument(help: "The phrase to repeat.") var phrase: String func run() async throws { let repeatCount = count ?? 2 for i in 1 ... repeatCount { if includeCounter { await someAsyncOperation() } else { print(phrase) } } } } https://github.com/apple/swift-argument-parser
  6. Making tooling that looks great is hard… • UX Developer

    experience is not great • Other languages and ecosystems are more advanced • Javascript is a great example! • We need to aim for delightful! ✨
  7. The ecosystem allows for delightful • Ora - https://github.com/sindresorhus/ora •

    Chalk - https://github.com/chalk/chalk • Prompts - https://github.com/terkelg/prompts • Commander - https://github.com/tj/commander.js • Clack - https://github.com/natemoo-re/clack
  8. Swift has some tools like that… • Chalk - https://github.com/mxcl/Chalk

    • Commander - https://github.com/kylef/Commander • TermKit - https://github.com/migueldeicaza/TermKit • ANSITerminal - https://github.com/pakLebah/ANSITerminal
  9. ANSI escape sequences • String sequences • Perform operations in

    the terminal • Control the cursor’s position, change colour of text, etc. • Supported by Unix operating systems • Support from Windows 10
  10. ▋"Hello, world!" Output Moving the cursor import ANSITerminal let startPosition

    = readCursorPos() write("Hello World!") moveTo(startPosition.row, startPosition.col)
  11. ▋"Hello, world!" Output Moving the cursor import ANSITerminal let startPosition

    = readCursorPos() write("Hello World!".foreColor(244)) moveTo(startPosition.row, startPosition.col)
  12. "Hello▋ world!" Output Moving the cursor import ANSITerminal let startPosition

    = readCursorPos() write("Hello World!".foreColor(244)) moveTo(startPosition.row, startPosition.col) write("Hello")
  13. Let’s build a Text Input component • Custom text entry

    • Placeholder • Validation • Support for secure text entry • Headless + UI
  14. import Foundation import ANSITerminal func readTextInput( validate: (String) -> Bool

    = { _ in true }, validationFailed: () -> Void = {}, onNewCharacter: (Character) -> Void, onDelete: (Int, Int) -> Void, removePlaceholder: () -> Void, showPlaceholder: () -> Void ) - > String { }
  15. import Foundation import ANSITerminal func readTextInput( validate: (String) -> Bool

    = { _ in true }, validationFailed: () -> Void = {}, onNewCharacter: (Character) -> Void, onDelete: (Int, Int) -> Void, removePlaceholder: () -> Void, showPlaceholder: () -> Void ) - > String { }
  16. import Foundation import ANSITerminal func readTextInput( validate: (String) -> Bool

    = { _ in true }, validationFailed: () -> Void = {}, onNewCharacter: (Character) -> Void, onDelete: (Int, Int) -> Void, removePlaceholder: () -> Void, showPlaceholder: () -> Void ) - > String { }
  17. import Foundation import ANSITerminal func readTextInput( validate: (String) -> Bool

    = { _ in true }, validationFailed: () -> Void = {}, onNewCharacter: (Character) -> Void, onDelete: (Int, Int) -> Void, removePlaceholder: () -> Void, showPlaceholder: () -> Void ) - > String { }
  18. var output = "" while true { clearBuffer() if keyPressed()

    { let char = readChar() if char == NonPrintableChar.enter.char() { if validate(output) { break } else { validationFailed() } } else if char == NonPrintableChar.del.char() { let cursorPosition = readCursorPos() if output.count > 0 { onDelete(cursorPosition.row, cursorPosition.col - 1) _ = output.removeLast() } if output.count == 0 { showPlaceholder() } } if !isNonPrintable(char: char) { if output.isEmpty { removePlaceholder() } onNewCharacter(char) output.append(char) } } } return output
  19. var output = "" while true { clearBuffer() if keyPressed()

    { let char = readChar() if char == NonPrintableChar.enter.char() { if validate(output) { break } else { validationFailed() } } else if char == NonPrintableChar.del.char() { let cursorPosition = readCursorPos() if output.count > 0 { onDelete(cursorPosition.row, cursorPosition.col - 1) _ = output.removeLast() } if output.count == 0 { showPlaceholder() } } if !isNonPrintable(char: char) { if output.isEmpty { removePlaceholder() } onNewCharacter(char) output.append(char) } } } return output
  20. var output = "" while true { clearBuffer() if keyPressed()

    { let char = readChar() if char == NonPrintableChar.enter.char() { if validate(output) { break } else { validationFailed() } } else if char == NonPrintableChar.del.char() { let cursorPosition = readCursorPos() if output.count > 0 { onDelete(cursorPosition.row, cursorPosition.col - 1) _ = output.removeLast() } if output.count == 0 { showPlaceholder() } } if !isNonPrintable(char: char) { if output.isEmpty { removePlaceholder() } onNewCharacter(char) output.append(char) } } } return output
  21. var output = "" while true { clearBuffer() if keyPressed()

    { let char = readChar() if char == NonPrintableChar.enter.char() { if validate(output) { break } else { validationFailed() } } else if char == NonPrintableChar.del.char() { let cursorPosition = readCursorPos() if output.count > 0 { onDelete(cursorPosition.row, cursorPosition.col - 1) _ = output.removeLast() } if output.count == 0 { showPlaceholder() } } if !isNonPrintable(char: char) { if output.isEmpty { removePlaceholder() } onNewCharacter(char) output.append(char) } } } return output
  22. var output = "" while true { clearBuffer() if keyPressed()

    { let char = readChar() if char == NonPrintableChar.enter.char() { if validate(output) { break } else { validationFailed() } } else if char == NonPrintableChar.del.char() { let cursorPosition = readCursorPos() if output.count > 0 { onDelete(cursorPosition.row, cursorPosition.col - 1) _ = output.removeLast() } if output.count == 0 { showPlaceholder() } } if !isNonPrintable(char: char) { if output.isEmpty { removePlaceholder() } onNewCharacter(char) output.append(char) } } } return output
  23. var output = "" while true { clearBuffer() if keyPressed()

    { let char = readChar() if char == NonPrintableChar.enter.char() { if validate(output) { break } else { validationFailed() } } else if char == NonPrintableChar.del.char() { let cursorPosition = readCursorPos() if output.count > 0 { onDelete(cursorPosition.row, cursorPosition.col - 1) _ = output.removeLast() } if output.count == 0 { showPlaceholder() } } if !isNonPrintable(char: char) { if output.isEmpty { removePlaceholder() } onNewCharacter(char) output.append(char) } } } return output
  24. var output = "" while true { clearBuffer() if keyPressed()

    { let char = readChar() if char == NonPrintableChar.enter.char() { if validate(output) { break } else { validationFailed() } } else if char == NonPrintableChar.del.char() { let cursorPosition = readCursorPos() if output.count > 0 { onDelete(cursorPosition.row, cursorPosition.col - 1) _ = output.removeLast() } if output.count == 0 { showPlaceholder() } } if !isNonPrintable(char: char) { if output.isEmpty { removePlaceholder() } onNewCharacter(char) output.append(char) } } } return output
  25. let textInput = readTextInput(validate: { !$0.isEmpty }, validationFailed: { validationFailed

    = true let currentPosition = readCursorPos() writeAt(promptStartLine, 0, ANSIChar.warn) updateBracketColor(fromLine: promptStartLine, toLine: bottomPos.row, withColor: 11) writeAt(bottomPos.row, bottomPos.col + 1, validator ?. failureString ?? "") moveTo(currentPosition.row, currentPosition.col) }, onNewCharacter: { char in if validationFailed { let currentPosition = readCursorPos() writeAt(promptStartLine, 0, "◆".foreColor(81).bold) updateBracketColor(fromLine: promptStartLine, toLine: bottomPos.row, withColor: 81) moveTo(bottomPos.row, bottomPos.col + 1) clearToEndOfLine() moveTo(currentPosition.row, currentPosition.col) validationFailed = false } write("\(isSecureEntry ? "▪" : char)") }, onDelete: { row, col in moveTo(row, col); deleteChar() }, removePlaceholder: { moveTo(initialCursorPosition.row, initialCursorPosition.col) clearToEndOfLine() }, showPlaceholder: { write(placeholder.foreColor(244)) moveTo(initialCursorPosition.row, initialCursorPosition.col) })
  26. let textInput = readTextInput(validate: { !$0.isEmpty }, validationFailed: { validationFailed

    = true let currentPosition = readCursorPos() writeAt(promptStartLine, 0, ANSIChar.warn) updateBracketColor(fromLine: promptStartLine, toLine: bottomPos.row, withColor: 11) writeAt(bottomPos.row, bottomPos.col + 1, validator ?. failureString ?? "") moveTo(currentPosition.row, currentPosition.col) }, onNewCharacter: { char in if validationFailed { let currentPosition = readCursorPos() writeAt(promptStartLine, 0, "◆".foreColor(81).bold) updateBracketColor(fromLine: promptStartLine, toLine: bottomPos.row, withColor: 81) moveTo(bottomPos.row, bottomPos.col + 1) clearToEndOfLine() moveTo(currentPosition.row, currentPosition.col) validationFailed = false } write("\(isSecureEntry ? "▪" : char)") }, onDelete: { row, col in moveTo(row, col); deleteChar() }, removePlaceholder: { moveTo(initialCursorPosition.row, initialCursorPosition.col) clearToEndOfLine() }, showPlaceholder: { write(placeholder.foreColor(244)) moveTo(initialCursorPosition.row, initialCursorPosition.col) })
  27. let textInput = readTextInput(validate: { !$0.isEmpty }, validationFailed: { validationFailed

    = true let currentPosition = readCursorPos() writeAt(promptStartLine, 0, ANSIChar.warn) updateBracketColor(fromLine: promptStartLine, toLine: bottomPos.row, withColor: 11) writeAt(bottomPos.row, bottomPos.col + 1, validator ?. failureString ?? "") moveTo(currentPosition.row, currentPosition.col) }, onNewCharacter: { char in if validationFailed { let currentPosition = readCursorPos() writeAt(promptStartLine, 0, "◆".foreColor(81).bold) updateBracketColor(fromLine: promptStartLine, toLine: bottomPos.row, withColor: 81) moveTo(bottomPos.row, bottomPos.col + 1) clearToEndOfLine() moveTo(currentPosition.row, currentPosition.col) validationFailed = false } write("\(isSecureEntry ? "▪" : char)") }, onDelete: { row, col in moveTo(row, col); deleteChar() }, removePlaceholder: { moveTo(initialCursorPosition.row, initialCursorPosition.col) clearToEndOfLine() }, showPlaceholder: { write(placeholder.foreColor(244)) moveTo(initialCursorPosition.row, initialCursorPosition.col) })
  28. let textInput = readTextInput(validate: { !$0.isEmpty }, validationFailed: { validationFailed

    = true let currentPosition = readCursorPos() writeAt(promptStartLine, 0, ANSIChar.warn) updateBracketColor(fromLine: promptStartLine, toLine: bottomPos.row, withColor: 11) writeAt(bottomPos.row, bottomPos.col + 1, validator ?. failureString ?? "") moveTo(currentPosition.row, currentPosition.col) }, onNewCharacter: { char in if validationFailed { let currentPosition = readCursorPos() writeAt(promptStartLine, 0, "◆".foreColor(81).bold) updateBracketColor(fromLine: promptStartLine, toLine: bottomPos.row, withColor: 81) moveTo(bottomPos.row, bottomPos.col + 1) clearToEndOfLine() moveTo(currentPosition.row, currentPosition.col) validationFailed = false } write("\(isSecureEntry ? "▪" : char)") }, onDelete: { row, col in moveTo(row, col); deleteChar() }, removePlaceholder: { moveTo(initialCursorPosition.row, initialCursorPosition.col) clearToEndOfLine() }, showPlaceholder: { write(placeholder.foreColor(244)) moveTo(initialCursorPosition.row, initialCursorPosition.col) })
  29. let textInput = readTextInput(validate: { !$0.isEmpty }, validationFailed: { validationFailed

    = true let currentPosition = readCursorPos() writeAt(promptStartLine, 0, ANSIChar.warn) updateBracketColor(fromLine: promptStartLine, toLine: bottomPos.row, withColor: 11) writeAt(bottomPos.row, bottomPos.col + 1, validator ?. failureString ?? "") moveTo(currentPosition.row, currentPosition.col) }, onNewCharacter: { char in if validationFailed { let currentPosition = readCursorPos() writeAt(promptStartLine, 0, "◆".foreColor(81).bold) updateBracketColor(fromLine: promptStartLine, toLine: bottomPos.row, withColor: 81) moveTo(bottomPos.row, bottomPos.col + 1) clearToEndOfLine() moveTo(currentPosition.row, currentPosition.col) validationFailed = false } write("\(isSecureEntry ? "▪" : char)") }, onDelete: { row, col in moveTo(row, col); deleteChar() }, removePlaceholder: { moveTo(initialCursorPosition.row, initialCursorPosition.col) clearToEndOfLine() }, showPlaceholder: { write(placeholder.foreColor(244)) moveTo(initialCursorPosition.row, initialCursorPosition.col) })
  30. let textInput = readTextInput(validate: { !$0.isEmpty }, validationFailed: { validationFailed

    = true let currentPosition = readCursorPos() writeAt(promptStartLine, 0, ANSIChar.warn) updateBracketColor(fromLine: promptStartLine, toLine: bottomPos.row, withColor: 11) writeAt(bottomPos.row, bottomPos.col + 1, validator ?. failureString ?? "") moveTo(currentPosition.row, currentPosition.col) }, onNewCharacter: { char in if validationFailed { let currentPosition = readCursorPos() writeAt(promptStartLine, 0, "◆".foreColor(81).bold) updateBracketColor(fromLine: promptStartLine, toLine: bottomPos.row, withColor: 81) moveTo(bottomPos.row, bottomPos.col + 1) clearToEndOfLine() moveTo(currentPosition.row, currentPosition.col) validationFailed = false } write("\(isSecureEntry ? "▪" : char)") }, onDelete: { row, col in moveTo(row, col); deleteChar() }, removePlaceholder: { moveTo(initialCursorPosition.row, initialCursorPosition.col) clearToEndOfLine() }, showPlaceholder: { write(placeholder.foreColor(244)) moveTo(initialCursorPosition.row, initialCursorPosition.col) })
  31. let textInput = readTextInput(validate: { !$0.isEmpty }, validationFailed: { validationFailed

    = true let currentPosition = readCursorPos() writeAt(promptStartLine, 0, ANSIChar.warn) updateBracketColor(fromLine: promptStartLine, toLine: bottomPos.row, withColor: 11) writeAt(bottomPos.row, bottomPos.col + 1, validator ?. failureString ?? "") moveTo(currentPosition.row, currentPosition.col) }, onNewCharacter: { char in if validationFailed { let currentPosition = readCursorPos() writeAt(promptStartLine, 0, "◆".foreColor(81).bold) updateBracketColor(fromLine: promptStartLine, toLine: bottomPos.row, withColor: 81) moveTo(bottomPos.row, bottomPos.col + 1) clearToEndOfLine() moveTo(currentPosition.row, currentPosition.col) validationFailed = false } write("\(isSecureEntry ? "▪" : char)") }, onDelete: { row, col in moveTo(row, col); deleteChar() }, removePlaceholder: { moveTo(initialCursorPosition.row, initialCursorPosition.col) clearToEndOfLine() }, showPlaceholder: { write("sk- ... ".foreColor(244)) moveTo(initialCursorPosition.row, initialCursorPosition.col) })
  32. struct Auth: AsyncParsableCommand { func run() async throws { intro(title:

    "Hi! Set your token to use Chatty!") let validator = Validator( validate: { !$0.isEmpty }, failureString: "The OpenAI API token can not be empty ... " ) let token = text( question: "Enter your OpenAPI token", placeholder: "sk- ... ", validator: validator, isSecureEntry: true ) let keychain = Keychain(service: "dev.polpiella.chatty") keychain["openaitoken"] = token outro( text: "You're all set! You can now run " + "`chatty`".magenta.onWhite + " to converse ... " ) } }
  33. struct Auth: AsyncParsableCommand { func run() async throws { intro(title:

    "Hi! 👋 Set your token to use Chatty!") let validator = Validator( validate: { !$0.isEmpty }, failureString: "The OpenAI API token can not be empty ... " ) let token = text( question: "Enter your OpenAPI token", placeholder: "sk- ... ", validator: validator, isSecureEntry: true ) let keychain = Keychain(service: "dev.polpiella.chatty") keychain["openaitoken"] = token outro( text: "You're all set! You can now run " + "`chatty`".magenta.onWhite + " to converse ... " ) } }
  34. struct Auth: AsyncParsableCommand { func run() async throws { intro(title:

    "Hi! 👋 Set your token to use Chatty!") let validator = Validator( validate: { !$0.isEmpty }, failureString: "The OpenAI API token can not be empty ... " ) let token = text( question: "Enter your OpenAPI token", placeholder: "sk- ... ", validator: validator, isSecureEntry: true ) let keychain = Keychain(service: "dev.polpiella.chatty") keychain["openaitoken"] = token outro( text: "You're all set! You can now run " + "`chatty`".magenta.onWhite + " to converse ... " ) } }
  35. struct Auth: AsyncParsableCommand { func run() async throws { intro(title:

    "Hi! 👋 Set your token to use Chatty!") let validator = Validator( validate: { !$0.isEmpty }, failureString: "The OpenAI API token can not be empty ... " ) let token = text( question: "Enter your OpenAPI token", placeholder: "sk- ... ", validator: validator, isSecureEntry: true ) let keychain = Keychain(service: "dev.polpiella.chatty") keychain["openaitoken"] = token outro( text: "You're all set! You can now run " + "`chatty`".magenta.onWhite + " to converse ... " ) } }
  36. struct Auth: AsyncParsableCommand { func run() async throws { intro(title:

    "Hi! 👋 Set your token to use Chatty!") let validator = Validator( validate: { !$0.isEmpty }, failureString: "The OpenAI API token can not be empty ... " ) let token = text( question: "Enter your OpenAPI token", placeholder: "sk- ... ", validator: validator, isSecureEntry: true ) let keychain = Keychain(service: "dev.polpiella.chatty") keychain["openaitoken"] = token outro( text: "✅ You're all set! You can now run " + "`chatty`".magenta.onWhite + " to converse ... " ) } }